Skip to content

Commit

Permalink
make CWSimulateApp state de/serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
Kiruse committed Dec 14, 2022
1 parent 49740c7 commit af3b07d
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@terran-one/cw-simulate",
"version": "2.7.5",
"version": "2.8.0-rc.1",
"description": "Mock blockchain environment for simulating CosmWasm interactions",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
26 changes: 26 additions & 0 deletions src/CWSimulateApp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { toBase64 } from '@cosmjs/encoding';
import fs from 'fs';
import { CWSimulateApp } from './CWSimulateApp';

const bytecode = fs.readFileSync('./testing/cw_simulate_tests-aarch64.wasm');

describe('de/serialize', () => {
it('works', async () => {
{
const ref = new CWSimulateApp({ chainId: 'phoenix-1', bech32Prefix: 'terra1' });
ref.wasm.create('alice', bytecode);
ref.wasm.create('bob', bytecode);

const clone = CWSimulateApp.deserialize(ref.serialize());
expect(clone.chainId).toStrictEqual(ref.chainId);
expect(clone.bech32Prefix).toStrictEqual(ref.bech32Prefix);

const code1 = clone.wasm.getCodeInfo(1)!;
const code2 = clone.wasm.getCodeInfo(2)!;
expect(code1.creator).toStrictEqual('alice');
expect(code2.creator).toStrictEqual('bob');
expect(toBase64(code1.wasmCode)).toStrictEqual(toBase64(ref.wasm.store.getObject('codes', 1, 'wasmCode')));
expect(toBase64(code2.wasmCode)).toStrictEqual(toBase64(ref.wasm.store.getObject('codes', 2, 'wasmCode')));
}
})
})
104 changes: 101 additions & 3 deletions src/CWSimulateApp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { fromBase64, toBase64 } from '@cosmjs/encoding';
import { QuerierBase } from '@terran-one/cosmwasm-vm-js';
import { Map } from 'immutable';
import { Err, Result } from 'ts-results';
import { Err, Ok, Result } from 'ts-results';
import { WasmModule, WasmQuery } from './modules/wasm';
import { BankModule, BankQuery } from './modules/bank';
import { fromImmutable, toImmutable, Transactional, TransactionalLens } from './store/transactional';
import { AppResponse, Binary } from './types';
import { Transactional, TransactionalLens } from './store/transactional';
import { getArrayBuffer, isArrayBufferLike, isArrayLike } from './util';

const TYPED_ARRAYS = [
null,
ArrayBuffer,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array,
]

export interface CWSimulateAppOptions {
chainId: string;
Expand Down Expand Up @@ -64,6 +81,30 @@ export class CWSimulateApp {
});
}

public serialize() {
return JSON.stringify({
chainId: this.chainId,
bech32Prefix: this.bech32Prefix,
data: toPersistable(fromImmutable(this.store.db.data)),
});
}

static deserialize(str: string) {
const json = JSON.parse(str);
const {
bech32Prefix,
chainId,
data,
} = json;

const inst = new CWSimulateApp({ chainId, bech32Prefix });
inst.store.db.tx(update => {
update(() => toImmutable(fromPersistable(data)));
return Ok(undefined);
});
return inst;
}

get height() { return this.store.get('height') }
get time() { return this.store.get('time') }
}
Expand All @@ -72,6 +113,13 @@ export type QueryMessage =
| { bank: BankQuery }
| { wasm: WasmQuery };

type PersistedTypedArray = {
/** Corresponds to TYPED_ARRAYS index */
__TYPEDARRAY__: number;
/** Base64 encoded binary data */
data: string;
}

export class Querier extends QuerierBase {
constructor(public readonly app: CWSimulateApp) {
super();
Expand All @@ -87,3 +135,53 @@ export class Querier extends QuerierBase {
}
}
}

/** Alter given data for optimized JSON stringification. Intended for internal use & testing only. */
export function toPersistable(obj: any): any {
if (!obj || typeof obj !== 'object') return obj;
if (isArrayLike(obj)) {
if (isArrayBufferLike(obj)) {
return toPersistedTypedArray(obj);
} else {
return obj.map(item => toPersistable(item));
}
} else {
return Object.fromEntries(Object.entries(obj).map(([prop, value]) => [prop, toPersistable(value)]));
}
}

/** Restore data from altered persistable representation. Inverse of `toPersistable`. Intended for internal use & testing only. */
export function fromPersistable(obj: any): any {
if (!obj || typeof obj !== 'object') return obj;
if ('__TYPEDARRAY__' in obj) {
return fromPersistedTypedArray(obj);
} else if (isArrayLike(obj)) {
return obj.map(item => fromPersistable(item));
} else {
return Object.fromEntries(Object.entries(obj).map(([prop, value]) => [prop, fromPersistable(value)]));
}
}

function toPersistedTypedArray(obj: ArrayBuffer | ArrayBufferView): PersistedTypedArray {
const data = getArrayBuffer(obj)!;
const idx = TYPED_ARRAYS.findIndex(constr => !!constr && obj instanceof constr);
if (idx === -1) throw new Error('Unknown TypedArray');
if (idx === 0) throw new Error('Contingency Error');
return {
__TYPEDARRAY__: idx,
data: toBase64(new Uint8Array(data)),
};
}

function fromPersistedTypedArray(obj: PersistedTypedArray): ArrayBuffer | ArrayBufferView {
const { __TYPEDARRAY__: idx, data } = obj;
if (idx < 1 || idx >= TYPED_ARRAYS.length) throw new Error(`Invalid TypedArray type ${idx}`);

const bytes = new Uint8Array(fromBase64(data));
if (idx === TYPED_ARRAYS.indexOf(ArrayBuffer)) {
return bytes.buffer;
} else {
//@ts-ignore b/c we handle the only two invalid cases above
return new TYPED_ARRAYS[idx](bytes.buffer);
}
}
1 change: 0 additions & 1 deletion src/modules/bank.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { fromJS } from 'immutable';
import { cmd, exec, TestContract } from '../../testing/wasm-util';
import { CWSimulateApp } from '../CWSimulateApp';
import { fromBinary } from '../util';
Expand Down
14 changes: 8 additions & 6 deletions src/store/transactional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class Transactional {
constructor(private _data = Map()) {}

lens<M extends object>(...path: PropertyKey[]) {
return new TransactionalLens<M>(this, path);
return new TransactionalLens<M>(this, path.map(stringify));
}

tx<R extends Result<any, any>>(cb: (update: TxUpdater) => Promise<R>): Promise<R>;
Expand Down Expand Up @@ -91,7 +91,7 @@ export class Transactional {
}

export class TransactionalLens<M extends object> {
constructor(public readonly db: Transactional, public readonly prefix: PropertyKey[]) {}
constructor(public readonly db: Transactional, public readonly prefix: string[]) {}

initialize(data: M) {
this.db.tx(update => {
Expand All @@ -104,7 +104,7 @@ export class TransactionalLens<M extends object> {
}

get<P extends PropertyKey[]>(...path: P): Immutify<Lens<M, P>> {
return this.db.data.getIn([...this.prefix, ...path]) as any;
return this.db.data.getIn([...this.prefix, ...path.map(stringify)]) as any;
}

getObject<P extends PropertyKey[]>(...path: P): Lens<M, P> {
Expand All @@ -118,17 +118,17 @@ export class TransactionalLens<M extends object> {
return this.db.tx(update => {
const setter: LensSetter<M> = <P extends PropertyKey[]>(...path: P) =>
(value: Lens<M, P> | Immutify<Lens<M, P>>) => {
update(curr => curr.setIn([...this.prefix, ...path], toImmutable(value)));
update(curr => curr.setIn([...this.prefix, ...path.map(stringify)], toImmutable(value)));
}
const deleter: LensDeleter = <P extends PropertyKey[]>(...path: P) => {
update(curr => curr.deleteIn([...this.prefix, ...path]));
update(curr => curr.deleteIn([...this.prefix, ...path.map(stringify)]));
}
return cb(setter, deleter);
});
}

lens<P extends PropertyKey[]>(...path: P): TransactionalLens<Lens<M, P>> {
return new TransactionalLens<Lens<M, P>>(this.db, [...this.prefix, ...path]);
return new TransactionalLens<Lens<M, P>>(this.db, [...this.prefix, ...path.map(stringify)]);
}

get data() { return this.db.data.getIn([...this.prefix]) as Immutify<M> }
Expand Down Expand Up @@ -195,3 +195,5 @@ export function fromImmutable(value: any): any {

return value;
}

const stringify = (v: any) => v+'';
11 changes: 10 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding";
import { Err, Ok, Result } from "ts-results";
import { Binary, RustResult } from "./types";

export const isArrayLike = (value: any): value is any[] => typeof value === 'object' && typeof value.length === 'number';
export const isArrayLike = (value: any): value is any[] =>
typeof value === 'object' && typeof value.length === 'number';
export const isArrayBufferLike = (value: any): value is ArrayBuffer | ArrayBufferView =>
value instanceof ArrayBuffer || ArrayBuffer.isView(value);
export const getArrayBuffer = (obj: any) =>
ArrayBuffer.isView(obj)
? obj.buffer
: obj instanceof ArrayBuffer
? obj
: undefined;

export const toBinary = (value: any): Binary => toBase64(toUtf8(JSON.stringify(value)));
export const fromBinary = (str: string): unknown => JSON.parse(fromUtf8(fromBase64(str)));
Expand Down

0 comments on commit af3b07d

Please sign in to comment.