cw-simulateThis 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 startHeightsync() 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