cw-simulate
This package combines cosmwasm-vm-js
with additional abstractions and state management to
more accurately simulate the effects of CosmWasm contracts on the blockchain environments on which
they are hosted.
Import the cw-simulate
library from NPM in your package.json
.
$ npm install "@oraichain/cw-simulate" --save-dev
If you're using Yarn:
$ yarn add "@oraichain/cw-simulate" -D
SimulateCosmWasmClient
object - this is a simulation environment describing a single chain that extends SigningCosmWasmClient.client.update
. This will register a new codeId
to reference the uploaded contract code.client.instantiate
, passing in the codeId
generated in the previous step.contractAddress
to refer to the contract instance.execute
and query
messages against the instance, and they should work as expected.Below are cw-simulate
examples that simulate CosmWasm, bank, and IBC modules. If you want us to support another type of module, please create an issue request!
import { sha256 } from '@cosmjs/crypto';
import { fromHex, toHex } from '@cosmjs/encoding';
import fs from 'fs';
import { SimulateCosmWasmClient } from '@oraichain/cw-simulate';
import { instantiate2Address } from '@cosmjs/cosmwasm-stargate';
// import the wasm bytecode
const bytecode = fs.readFileSync('./testing/hello_world-aarch64.wasm');
const sender = 'orai12zyu8w93h0q2lcnt50g3fn0w3yqnhy4fvawaqz';
describe('SimulateCosmWasmClient', () => {
it('works', async () => {
{
const client = new SimulateCosmWasmClient({
chainId: 'Oraichain',
bech32Prefix: 'orai',
metering: true
});
// deploy
const { codeId } = await client.upload(sender, bytecode, 'auto');
const { contractAddress } = await client.instantiate(sender, codeId, { count: 10 }, '', 'auto');
console.log(contractAddress);
// execute the contract
const result = await client.execute(
sender,
contractAddress,
{
increment: {}
},
'auto'
);
console.log(result);
expect(result.events[0].attributes[0].value).toEqual(contractAddress);
// query
expect(await client.queryContractSmart(contractAddress, { get_count: {} })).toEqual({ count: 11 });
}
});
});
import { coin, coins } from '@cosmjs/amino';
import { fromBinary, toBinary } from '@cosmjs/cosmwasm-stargate';
import { fromBech32, toBech32 } from '@cosmjs/encoding';
import { CosmosMsg, IbcMsgTransfer } from '@oraichain/cosmwasm-vm-js';
import { readFileSync } from 'fs';
import path from 'path';
import { CWSimulateApp } from '../CWSimulateApp';
import { AppResponse, IbcOrder } from '../types';
import { ibcDenom } from './ibc';
const terraChain = new CWSimulateApp({
chainId: 'test-1',
bech32Prefix: 'terra'
});
const oraiChain = new CWSimulateApp({
chainId: 'Oraichain',
bech32Prefix: 'orai'
});
const oraiSenderAddress = 'orai1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejvfgs7g';
const bobAddress = 'orai1ur2vsjrjarygawpdwtqteaazfchvw4fg6uql76';
const terraSenderAddress = toBech32(terraChain.bech32Prefix, fromBech32(oraiSenderAddress).data);
describe.only('IBCModule', () => {
let oraiPort: string;
let terraPort: string = 'transfer';
let contractAddress: string;
beforeEach(async () => {
const reflectCodeId = oraiChain.wasm.create(
oraiSenderAddress,
readFileSync(path.join(__dirname, '..', '..', 'testing', 'reflect.wasm'))
);
const ibcReflectCodeId = oraiChain.wasm.create(
oraiSenderAddress,
readFileSync(path.join(__dirname, '..', '..', 'testing', 'ibc_reflect.wasm'))
);
const oraiRet = await oraiChain.wasm.instantiateContract(
oraiSenderAddress,
[],
ibcReflectCodeId,
{ reflect_code_id: reflectCodeId },
'ibc-reflect'
);
contractAddress = (oraiRet.val as AppResponse).events[0].attributes[0].value;
oraiPort = 'wasm.' + contractAddress;
});
it('handle-reflect', async () => {
oraiChain.ibc.relay('channel-0', oraiPort, 'channel-0', terraPort, terraChain);
expect(oraiPort).toEqual(oraiChain.ibc.getContractIbcPort(contractAddress));
const channelOpenRes = await terraChain.ibc.sendChannelOpen({
open_init: {
channel: {
counterparty_endpoint: {
port_id: oraiPort,
channel_id: 'channel-0'
},
endpoint: {
port_id: terraPort,
channel_id: 'channel-0'
},
order: IbcOrder.Ordered,
version: 'ibc-reflect-v1',
connection_id: 'connection-0'
}
}
});
expect(channelOpenRes).toEqual({ version: 'ibc-reflect-v1' });
const channelConnectRes = await terraChain.ibc.sendChannelConnect({
open_ack: {
channel: {
counterparty_endpoint: {
port_id: oraiPort,
channel_id: 'channel-0'
},
endpoint: {
port_id: terraPort,
channel_id: 'channel-0'
},
order: IbcOrder.Ordered,
version: 'ibc-reflect-v1',
connection_id: 'connection-0'
},
counterparty_version: 'ibc-reflect-v1'
}
});
expect(channelConnectRes.attributes).toEqual([
{ key: 'action', value: 'ibc_connect' },
{ key: 'channel_id', value: 'channel-0' }
]);
// get reflect address
let packetReceiveRes = await terraChain.ibc.sendPacketReceive({
packet: {
data: toBinary({
who_am_i: {}
}),
src: {
port_id: terraPort,
channel_id: 'channel-0'
},
dest: {
port_id: oraiPort,
channel_id: 'channel-0'
},
sequence: terraChain.ibc.sequence++,
timeout: {
block: {
revision: 1,
height: terraChain.height
}
}
},
relayer: terraSenderAddress
});
const res = fromBinary(packetReceiveRes.acknowledgement) as { ok: { account: string } };
const reflectContractAddress = res.ok.account;
expect(reflectContractAddress).toEqual(oraiChain.wasm.getContracts()[1].address);
// set some balance for reflect contract
oraiChain.bank.setBalance(reflectContractAddress, coins('500000000000', 'orai'));
// send message to bob on oraichain
packetReceiveRes = await terraChain.ibc.sendPacketReceive({
packet: {
data: toBinary({
dispatch: {
msgs: [
<CosmosMsg>{
bank: {
send: {
to_address: bobAddress,
amount: coins(123456789, 'orai')
}
}
}
]
}
}),
src: {
port_id: terraPort,
channel_id: 'channel-0'
},
dest: {
port_id: oraiPort,
channel_id: 'channel-0'
},
sequence: terraChain.ibc.sequence++,
timeout: {
block: {
revision: 1,
height: terraChain.height
}
}
},
relayer: terraSenderAddress
});
});
});
import { BankMsg } from '@oraichain/cosmwasm-vm-js';
import { cmd, exec, TestContract } from '../../testing/wasm-util';
import { CWSimulateApp } from '../CWSimulateApp';
import { BankQuery } from './bank';
type WrappedBankMsg = {
bank: BankMsg;
};
const coin = (denom: string, amount: string | number) => ({ denom, amount: `${amount}` });
describe.only('BankModule', () => {
let chain: CWSimulateApp;
beforeEach(function () {
chain = new CWSimulateApp({
chainId: 'test-1',
bech32Prefix: 'terra'
});
});
it('handle send', () => {
// Arrange
const bank = chain.bank;
// Set balance to arbitrary address
bank.setBalance('alice', [coin('foo', 1000)]);
// Can also send to other addresses
bank.send('alice', 'bob', [coin('foo', 100)]).unwrap();
// Assert
expect(bank.getBalance('alice')).toEqual([coin('foo', 900)]);
expect(bank.getBalance('bob')).toEqual([coin('foo', 100)]);
expect(bank.getBalances()).toEqual({
alice: [coin('foo', 900)],
bob: [coin('foo', 100)]
});
});
});
import { DownloadState } from '@oraichain/cw-simulate';
const downloadState = new DownloadState('https://rpc.orai.io', path.resolve(__dirname, 'data'));
await downloadState.loadState(client, senderAddress, contractAddress, 'label');
Besides downloading production contract states, you can also download states from a custom chain height, called A, and apply cosmwasm txs from height A to B. Instead of spending hours forking the entire chain, it only takes a few seconds to replay your production transactions.
Below is an example from a demo file that demonstrates the power of cw-simulate:
import { resolve } from 'path';
import { SyncState } from './sync';
import dotenv from 'dotenv';
import { COSMOS_CHAIN_IDS, ORAI } from '@oraichain/common';
dotenv.config();
const SENDER = 'orai1hvr9d72r5um9lvt0rpkd4r75vrsqtw6yujhqs2';
(async () => {
const startHeight = 36975366;
const endHeight = 36975369;
const syncState = new SyncState(
SENDER,
{ rpc: process.env.RPC ?? 'https://rpc.orai.io', chainId: COSMOS_CHAIN_IDS.ORAICHAIN, bech32Prefix: ORAI },
resolve(__dirname, '../', 'data')
);
const relatedContracts = [
'orai12sxqkgsystjgd9faa48ghv3zmkfqc6qu05uy20mvv730vlzkpvls5zqxuz',
'orai1wuvhex9xqs3r539mvc6mtm7n20fcj3qr2m0y9khx6n5vtlngfzes3k0rq9',
'orai1rdykz2uuepxhkarar8ql5ajj5j37pq8h8d4zarvgx2s8pg0af37qucldna',
'orai1yglsm0u2x3xmct9kq3lxa654cshaxj9j5d9rw5enemkkkdjgzj7sr3gwt0'
];
const customWasmCodePaths = {
orai12sxqkgsystjgd9faa48ghv3zmkfqc6qu05uy20mvv730vlzkpvls5zqxuz: resolve(
__dirname,
'../',
'data',
startHeight.toString(),
'cw-app-bitcoin.wasm'
)
};
const { results, simulateClient } = await syncState.sync(
startHeight,
endHeight,
relatedContracts,
customWasmCodePaths
);
console.dir(results, { depth: null });
})();
SyncState
instance, passing several basic arguments, from contract admins, chain infos, and the location to store contract states.sync()
method, which allows us to fork and apply txs from startheight
to endHeight
. relatedContracts
are a set of related contracts that are used during the syncing process. customWasmCodePaths
is a Map, where key
is the contract address, and value
is the path to that contract's wasm code. If left empty, the contracts will use their wasm codes at startHeight
sync()
returns a list of tx results, and the simulateClient
, which holds all contract states at endHeight
after applying the txs.This is essentially a small-scaled fork, allowing developers to use their custom wasm codes to debug and gain more insights of what happened in the past.
There's only one catch: you need an archived node to retrieve history states and txs. This requirement is understandable because without a node keeping old blocks and states, there's no way to retrieve them.
We have applied cw-simulate
in almost every corner of Oraichain Labs' dApps, and they have worked wonders. See the following real test-suites:
Vite doesn't include shims for Node variables like Webpack 4 does, and cw-simulate currently relies on these. The following workaround exists:
buffer
package (npm add buffer
)index.html
(inside the body
tag, before your other js imports):<script>
window.global = window;
</script>
<script type="module">
import { Buffer } from 'buffer';
window.Buffer = Buffer;
</script>
See this github issue for more details.
©2020 - 2024 Oraichain Foundation