Skip to content

Commit 352983e

Browse files
committed
fix copying raw bytes (ArrayBuffer & views);
also introduce NEVER_IMMUTIFY which prevents the storage system from converting specific objects to Immutable collections
1 parent a8677ff commit 352983e

File tree

5 files changed

+64
-12
lines changed

5 files changed

+64
-12
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@terran-one/cw-simulate",
3-
"version": "2.7.1",
3+
"version": "2.7.2",
44
"description": "Mock blockchain environment for simulating CosmWasm interactions",
55
"main": "dist/index.js",
66
"typings": "dist/index.d.ts",

src/modules/wasm.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import { Map } from 'immutable';
3232
import { Err, Ok, Result } from 'ts-results';
3333
import { fromBinary, fromRustResult, toBinary } from '../util';
34-
import { Transactional, TransactionalLens } from '../store/transactional';
34+
import { NEVER_IMMUTIFY, Transactional, TransactionalLens } from '../store/transactional';
3535

3636
function numberToBigEndianUint64(n: number): Uint8Array {
3737
const buffer = new ArrayBuffer(8);
@@ -310,6 +310,7 @@ export class WasmModule {
310310
if ('error' in response) {
311311
let result = Err(response.error);
312312
trace.push({
313+
[NEVER_IMMUTIFY]: true,
313314
type: 'instantiate' as 'instantiate',
314315
contractAddress,
315316
msg: instantiateMsg,
@@ -350,6 +351,7 @@ export class WasmModule {
350351
);
351352

352353
trace.push({
354+
[NEVER_IMMUTIFY]: true,
353355
type: 'instantiate' as 'instantiate',
354356
contractAddress,
355357
msg: instantiateMsg,
@@ -420,6 +422,7 @@ export class WasmModule {
420422
let result = Err(response.error);
421423

422424
trace.push({
425+
[NEVER_IMMUTIFY]: true,
423426
type: 'execute' as 'execute',
424427
contractAddress,
425428
msg: executeMsg,
@@ -462,6 +465,7 @@ export class WasmModule {
462465
);
463466

464467
trace.push({
468+
[NEVER_IMMUTIFY]: true,
465469
type: 'execute' as 'execute',
466470
contractAddress,
467471
msg: executeMsg,
@@ -613,6 +617,7 @@ export class WasmModule {
613617
let result = Err(response.error);
614618

615619
trace.push({
620+
[NEVER_IMMUTIFY]: true,
616621
type: 'reply' as 'reply',
617622
contractAddress,
618623
env: this.getExecutionEnv(contractAddress),
@@ -656,6 +661,7 @@ export class WasmModule {
656661
);
657662

658663
trace.push({
664+
[NEVER_IMMUTIFY]: true,
659665
type: 'reply' as 'reply',
660666
contractAddress,
661667
msg: replyMsg,

src/store/transactional.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { fromJS, isCollection, isList, isMap, List, Map } from "immutable";
1+
import { isCollection, isList, isMap, List, Map } from "immutable";
22
import { Ok, Result } from "ts-results";
3+
import { isArrayLike } from "../util";
4+
5+
// NEVER_IMMUTIFY is a string because that's easily serializable with different algorithms - symbols are not
6+
export type NeverImmutify = typeof NEVER_IMMUTIFY;
7+
export const NEVER_IMMUTIFY = '__NEVER_IMMUTIFY__';
38

49
type Primitive = boolean | number | bigint | string | null | undefined | symbol;
10+
type NoImmutify = Primitive | ArrayBuffer | ArrayBufferView | { [NEVER_IMMUTIFY]: any };
511

612
type Prefix<P, T extends any[]> = [P, ...T];
713
type First<T extends any[]> = T extends Prefix<infer F, any[]> ? F : never;
@@ -18,7 +24,7 @@ type Lens<T, P extends PropertyKey[]> =
1824
: T;
1925

2026
type Immutify<T> =
21-
T extends Primitive
27+
T extends NoImmutify
2228
? T
2329
: T extends ArrayLike<infer E>
2430
? List<Immutify<E>>
@@ -89,7 +95,7 @@ export class TransactionalLens<M extends object> {
8995

9096
initialize(data: M) {
9197
this.db.tx(update => {
92-
const coll = fromJS(data);
98+
const coll = toImmutable(data);
9399
if (!isCollection(coll)) throw new Error('Not an Immutable.Map');
94100
update(curr => curr.setIn([...this.prefix], coll));
95101
return Ok(undefined);
@@ -112,8 +118,7 @@ export class TransactionalLens<M extends object> {
112118
return this.db.tx(update => {
113119
const setter: LensSetter<M> = <P extends PropertyKey[]>(...path: P) =>
114120
(value: Lens<M, P> | Immutify<Lens<M, P>>) => {
115-
const v = isCollection(value) ? value : fromJS(value);
116-
update(curr => curr.setIn([...this.prefix, ...path], v));
121+
update(curr => curr.setIn([...this.prefix, ...path], toImmutable(value)));
117122
}
118123
const deleter: LensDeleter = <P extends PropertyKey[]>(...path: P) => {
119124
update(curr => curr.deleteIn([...this.prefix, ...path]));
@@ -129,14 +134,52 @@ export class TransactionalLens<M extends object> {
129134
get data() { return this.db.data.getIn([...this.prefix]) as Immutify<M> }
130135
}
131136

137+
function toImmutable(value: any): any {
138+
// passthru Immutable collections
139+
if (isCollection(value)) return value;
140+
141+
// don't touch ArrayBuffers & ArrayBufferViews - freeze them
142+
if (ArrayBuffer.isView(value)) {
143+
Object.freeze(value.buffer);
144+
return value;
145+
}
146+
if (value instanceof ArrayBuffer) {
147+
Object.freeze(value);
148+
return value;
149+
}
150+
151+
// recurse into arrays & objects, converting them to lists & maps
152+
// skip primitives & objects that don't want to be touched
153+
if (typeof value === 'object' && !(NEVER_IMMUTIFY in value)) {
154+
if (isArrayLike(value)) {
155+
return List(value.map(item => toImmutable(item)));
156+
} else {
157+
return Map(
158+
Object.entries(value).map(
159+
([key, value]) => [key, toImmutable(value)]
160+
)
161+
);
162+
}
163+
}
164+
165+
return value;
166+
}
167+
132168
function fromImmutable(value: any): any {
169+
// reverse Immutable maps & lists
133170
if (isMap(value)) {
134171
return fromImmutable(value.toObject());
135172
}
136-
else if (isList(value)) {
173+
if (isList(value)) {
137174
return fromImmutable(value.toArray());
138175
}
139-
else if (typeof value === 'object') {
176+
177+
// passthru ArrayBuffers & ArrayBufferViews
178+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
179+
180+
// revert objects & arrays
181+
// but: passthru objects w/ NEVER_IMMUTIFY
182+
if (typeof value === 'object' && !(NEVER_IMMUTIFY in value)) {
140183
if (typeof value.length === 'number' && 0 in value && value.length-1 in value) {
141184
for (let i = 0; i < value.length; ++i) {
142185
value[i] = fromImmutable(value[i]);
@@ -149,7 +192,6 @@ function fromImmutable(value: any): any {
149192
}
150193
return value;
151194
}
152-
else {
153-
return value;
154-
}
195+
196+
return value;
155197
}

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Immutable from 'immutable';
22
import { Result } from 'ts-results';
3+
import type { NEVER_IMMUTIFY } from './store/transactional';
34

45
export interface ContractResponse {
56
messages: SubMsg[];
@@ -120,6 +121,7 @@ type CallDebugLog<T extends keyof CosmWasmAPI = keyof CosmWasmAPI> = {
120121
export type Snapshot = Immutable.Map<unknown, unknown>;
121122

122123
interface TraceLogCommon {
124+
[NEVER_IMMUTIFY]: true;
123125
type: string;
124126
contractAddress: string;
125127
env: ExecuteEnv;

src/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding";
22
import { Err, Ok, Result } from "ts-results";
33
import { Binary, RustResult } from "./types";
44

5+
export const isArrayLike = (value: any): value is any[] => typeof value === 'object' && typeof value.length === 'number' && value[0] && value[value.length-1];
6+
57
export const toBinary = (value: any): Binary => toBase64(toUtf8(JSON.stringify(value)));
68
export const fromBinary = (str: string): unknown => JSON.parse(fromUtf8(fromBase64(str)));
79

0 commit comments

Comments
 (0)