Skip to content

Commit

Permalink
Merge pull request #27 from Terran-One/feat/persist
Browse files Browse the repository at this point in the history
Persistence Update
  • Loading branch information
Kiruse authored Dec 19, 2022
2 parents 49740c7 + 5591590 commit a4ed361
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 14 deletions.
31 changes: 29 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 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",
"description": "Mock blockchain environment for simulating CosmWasm interactions",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -39,6 +39,7 @@
"@cosmjs/amino": "^0.28.13",
"@cosmjs/crypto": "^0.28.13",
"@cosmjs/encoding": "^0.28.13",
"@kiruse/serde": "^0.6.3",
"@terran-one/cosmwasm-vm-js": "^0.2.16",
"immutable": "^4.1.0",
"lobyte": "^0.0.3",
Expand Down
35 changes: 35 additions & 0 deletions src/CWSimulateApp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { toBase64 } from '@cosmjs/encoding';
import fs from 'fs';
import { CWSimulateApp } from './CWSimulateApp';
import * as persist from './persist';
import { TestContract } from '../testing/wasm-util';

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 response = await ref.wasm.instantiateContract('alice', [], 1, {}, '');
const address = response.unwrap().events[0].attributes[0].value;

const bytes = persist.save(ref);
const clone = await persist.load(bytes);
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')));

let result = await clone.wasm.executeContract('alice', [], address, { debug: { msg: 'foobar' }});
expect(result.ok).toBeTruthy();
}
})
})
5 changes: 2 additions & 3 deletions src/CWSimulateApp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
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';

export interface CWSimulateAppOptions {
chainId: string;
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './CWSimulateApp';
export * from './types';
export * from './store';

import { save, load } from './persist';
export const persist = { save, load };
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
4 changes: 4 additions & 0 deletions src/modules/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export class WasmModule {
};
}

hasVM(contractAddress: string): boolean {
return !!this.vms[contractAddress];
}

async buildVM(contractAddress: string): Promise<CWSimulateVMInstance> {
if (!(contractAddress in this.vms)) {
const contractInfo = this.getContractInfo(contractAddress);
Expand Down
84 changes: 84 additions & 0 deletions src/persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import SerdeProtocol, { SERDE } from '@kiruse/serde';
import { Reference } from '@kiruse/serde/dist/types';
import { List, Map } from 'immutable';
import { Ok } from 'ts-results';
import { CWSimulateApp } from './CWSimulateApp';

export const serde = SerdeProtocol.standard()
.derive('immutable-list',
(list: List<any>, data) => {
return {
data: data(list.toArray()),
// ownerID is a unique object that should not even appear on
// other Immutable data structures. When present, it signifies
// that the Immutable should be mutated in-place rather than
// creating copies of its data.
mutable: !!(list as any).__ownerID,
};
},
({ data, mutable }, deref) => {
if (!data.length) return List();
const list = List().asMutable();
Reference.all(deref, data, values => {
for (const value of values) {
list.push(value);
}
!mutable && list.asImmutable();
});
return list;
},
)
.derive('immutable-map',
(map: Map<any, any>, data) => {
return {
data: data(map.toObject()),
// same as with List above
mutable: !!(map as any).__ownerID,
};
},
({ data, mutable }, deref) => {
const map = Map().asMutable();
const keys = Object.keys(data);
if (!keys.length) return Map();
Reference.all(deref, keys.map(k => data[k]), values => {
values.forEach((value, i) => {
const key = keys[i];
map.set(key, value);
});
!mutable && map.asImmutable();
});
return map;
},
)
.derive('cw-simulate-app',
(app: CWSimulateApp) => ({
chainId: app.chainId,
bech32Prefix: app.bech32Prefix,
store: app.store.db.data,
}),
({ chainId, bech32Prefix, store }, deref): CWSimulateApp => {
const app = new CWSimulateApp({
chainId,
bech32Prefix,
});
Reference.all(deref, [store], ([map]) => {
app.store.db.tx(update => {
update(() => map);
return Ok(undefined);
});
});
return app;
},
)

export const save = (app: CWSimulateApp) => serde.serializeAs('cw-simulate-app', app).compress().buffer;
export const load = async (bytes: Uint8Array) => {
const app = serde.deserializeAs('cw-simulate-app', bytes);
const contracts = [...app.wasm.store.get('contracts').keys()];
await Promise.all(contracts.map(address => app.wasm.buildVM(address)));
return app;
};

// Inject SERDE
Map.prototype[SERDE] = 'immutable-map';
List.prototype[SERDE] = 'immutable-list';
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+'';
3 changes: 2 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ 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 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 a4ed361

Please sign in to comment.