@oraichain/cw-simulate v2.8.101
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.
Features
- configure multiple host chain environments with chain-specific settings / state
- multiple simultaneous contract instances can exist per chain
- chain modules can be simulated through custom user code
- Simulating block creation
- extensible for further instrumentation via custom middlewares
- Simulating IBC states
- Gas simulation
- Simulating address native balances.
- Download current contract states on the mainnet for simulation
Getting Started
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
Usage
- Create a
SimulateCosmWasmClient
object - this is a simulation environment describing a single chain that extends SigningCosmWasmClient. - As needed, per chain:
- Upload the WASM bytecode using
client.update
. This will register a newcodeId
to reference the uploaded contract code. - Create a new contract instance using
client.instantiate
, passing in thecodeId
generated in the previous step. - From the response, retrieve the
contractAddress
to refer to the contract instance.
- Upload the WASM bytecode using
- You can now run
execute
andquery
messages against the instance, and they should work as expected.
- As needed:
- Mint, burn, set native balances for addresses.
- Create IBC channels and invoke IBC receive messages.
- Fork contract states at a given height, called A, and apply cosmwasm txs from height A to height B for testing and debugging.
Examples
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!
CosmWasm Contract Interaction
import { sha256 } from '@cosmjs/crypto';
import { fromHex, toHex } from '@cosmjs/encoding';
import fs from 'fs';
import { SimulateCosmWasmClient } from './SimulateCosmWasmClient';
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 });
}
});
});
IBC mocks with opening channels and receiving IBC messages
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,
});
});
});
Manipulate native balances
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)],
});
});
});
Load fork state from mainnet
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');
Fork contract states and apply production cosmwasm txs for testing and debugging
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 take 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 });
})();
- Firstly, we initialize a
SyncState
instance, passing several basic arguments, from contract admins, chain infos, and the location to store contract states. - The next and final step is to call the
sync()
method, which allows us to fork and apply txs fromstartheight
toendHeight
.relatedContracts
are a set of related contracts that are used during the syncing process.customWasmCodePaths
is a Map, wherekey
is the contract address, andvalue
is the path to that contract's wasm code. If left empty, the contracts will use their wasm codes atstartHeight
sync()
returns a list of tx results, and thesimulateClient
, which holds all contract states atendHeight
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.
Real test-suites for production-grade dApps
We have applied cw-simulate
in almost every corner of Oraichain Labs' dApps, and they have worked wonders. See the following real test-suites:
Using with Vue.js and vite
Vite doesn't include shims for Node variables like Webpack 4 does, and cw-simulate currently relies on these. The following workaround exists:
- Add the
buffer
package (npm add buffer
) - Add the following to your
index.html
(inside thebody
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.
8 months ago
8 months ago
10 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
11 months ago
11 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago