├── .gitignore ├── .node-version ├── .npmignore ├── .vscode └── settings.json ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── CWSimulateApp.CWSimulateApp.html │ ├── CWSimulateApp.Querier.html │ ├── SimulateCosmWasmClient.SimulateCosmWasmClient.html │ ├── fork.BufferCollection.html │ ├── fork.BufferIter.html │ ├── fork.BufferStream.html │ ├── fork.DownloadState.html │ ├── fork_old.BufferCollection.html │ ├── fork_old.BufferIter.html │ ├── fork_old.BufferStream.html │ ├── fork_old.DownloadState.html │ ├── index.BasicBackendApi.html │ ├── index.BasicKVIterStorage.html │ ├── index.BasicKVStorage.html │ ├── index.BasicQuerier.html │ ├── index.BinaryKVIterStorage.html │ ├── index.BinaryKVStorage.html │ ├── index.Environment.html │ ├── index.GasInfo.html │ ├── index.GenericError.html │ ├── index.QuerierBase.html │ ├── index.Record.html │ ├── index.Region.html │ ├── index.VMInstance.html │ ├── instrumentation_CWSimulateVMInstance.CWSimulateVMInstance.html │ ├── modules_bank.BankModule.html │ ├── modules_bank.ParsedCoin.html │ ├── modules_ibc.IbcModule.html │ ├── modules_wasm_contract.default.html │ ├── modules_wasm_error.ContractNotFoundError.html │ ├── modules_wasm_error.VmError.html │ ├── modules_wasm_module.WasmModule.html │ ├── store_transactional.Transactional.html │ ├── store_transactional.TransactionalLens.html │ └── sync.SyncState.html ├── enums │ ├── index.Order.html │ ├── index.ReplyOn.html │ ├── index.VoteOption.html │ └── types.IbcOrder.html ├── functions │ ├── benchmark.downloadState.html │ ├── benchmark.downloadStateOld.html │ ├── index.Err-1.html │ ├── index.Ok-1.html │ ├── index.Option.all.html │ ├── index.Option.any.html │ ├── index.Option.isOption.html │ ├── index.Result.all.html │ ├── index.Result.any.html │ ├── index.Result.isResult.html │ ├── index.Result.wrap.html │ ├── index.Result.wrapAsync.html │ ├── index.Some-1.html │ ├── index.SortedMap-2.html │ ├── index.SortedMap.isSortedMap.html │ ├── index.SortedMap.of.html │ ├── index.SortedMap.rawPack.html │ ├── index.SortedSet-2.html │ ├── index.SortedSet.fromKeys.html │ ├── index.SortedSet.isSortedSet.html │ ├── index.SortedSet.of.html │ ├── index.compare.html │ ├── index.decreaseBytes.html │ ├── index.increaseBytes.html │ ├── index.mergeUint8Array.html │ ├── index.toByteArray.html │ ├── index.toNumber.html │ ├── index.writeUInt32BE.html │ ├── modules_bank.lensFromSnapshot.html │ ├── modules_ibc.ibcDenom.html │ ├── modules_wasm_module.lensFromSnapshot.html │ ├── modules_wasm_wasm_util.buildAppResponse.html │ ├── modules_wasm_wasm_util.buildContractAddress.html │ ├── modules_wasm_wasm_util.wrapReplyResponse.html │ ├── persist.load.html │ ├── persist.save.html │ ├── store_transactional.fromImmutable.html │ ├── store_transactional.toImmutable.html │ ├── util.fromRustResult.html │ ├── util.getTransactionHash.html │ ├── util.isArrayLike.html │ ├── util.isRustResult.html │ ├── util.isTSResult.html │ ├── util.printDebug.html │ └── util.toRustResult.html ├── index.html ├── interfaces │ ├── CWSimulateApp.CWSimulateAppOptions.html │ ├── index.Attribute.html │ ├── index.BlockInfo.html │ ├── index.ContextData.html │ ├── index.ContractResponse.html │ ├── index.Event.html │ ├── index.Execute.html │ ├── index.GasState.html │ ├── index.IBackend.html │ ├── index.IBackendApi.html │ ├── index.IEnvironment.html │ ├── index.IGasInfo.html │ ├── index.IIterStorage.html │ ├── index.IQuerier.html │ ├── index.IStorage.html │ ├── index.IbcTimeout.html │ ├── index.IbcTimeoutBlock.html │ ├── index.Instantiate.html │ ├── index.Instantiate2.html │ ├── index.Iter.html │ ├── index.MessageInfo.html │ ├── index.Migrate.html │ ├── index.SortedMap-1.html │ ├── index.SortedSet-1.html │ ├── index.SubMsg.html │ ├── index.WeightedVoteOption.html │ ├── modules_wasm_module.CodeInfoQuery.html │ ├── modules_wasm_module.ContractInfoQuery.html │ ├── modules_wasm_module.RawQuery.html │ ├── modules_wasm_module.SmartQuery.html │ ├── types.AppResponse.html │ ├── types.CodeInfo.html │ ├── types.CodeInfoResponse.html │ ├── types.ContractInfo.html │ ├── types.ContractInfoResponse.html │ ├── types.DenomUnit.html │ ├── types.IbcAcknowledgement.html │ ├── types.IbcBasicResponse.html │ ├── types.IbcChannel.html │ ├── types.IbcChannelOpenResponse.html │ ├── types.IbcEndpoint.html │ ├── types.IbcPacket.html │ ├── types.IbcPacketAckMsg.html │ ├── types.IbcPacketReceiveMsg.html │ ├── types.IbcPacketTimeoutMsg.html │ ├── types.IbcReceiveResponse.html │ ├── types.Metadata.html │ └── types.PrintDebugLog.html ├── modules │ ├── CWSimulateApp.html │ ├── CWSimulateApp_spec.html │ ├── SimulateCosmWasmClient.html │ ├── SimulateCosmWasmClient_spec.html │ ├── benchmark.html │ ├── fork.html │ ├── fork_old.html │ ├── index.Option.html │ ├── index.Result.html │ ├── index.SortedMap.html │ ├── index.SortedSet.html │ ├── index.html │ ├── instrumentation_CWSimulateVMInstance.html │ ├── modules_bank.html │ ├── modules_bank_spec.html │ ├── modules_ibc.html │ ├── modules_ibc_spec.html │ ├── modules_wasm.html │ ├── modules_wasm_contract.html │ ├── modules_wasm_error.html │ ├── modules_wasm_module.html │ ├── modules_wasm_spec.html │ ├── modules_wasm_wasm_util.html │ ├── persist.html │ ├── store.html │ ├── store_transactional.html │ ├── sync.html │ ├── sync_test.html │ ├── types.html │ └── util.html ├── types │ ├── CWSimulateApp.ChainData.html │ ├── CWSimulateApp.HandleCustomMsgFunction.html │ ├── CWSimulateApp.KVIterStorageRegistry.html │ ├── CWSimulateApp.QueryCustomMsgFunction.html │ ├── CWSimulateApp.QueryMessage.html │ ├── index.Address.html │ ├── index.BankMsg.html │ ├── index.Binary.html │ ├── index.CosmosMsg.html │ ├── index.Decimal.html │ ├── index.DistributionMsg.html │ ├── index.Env.html │ ├── index.Err.html │ ├── index.GovMsg.html │ ├── index.IbcMsg.html │ ├── index.IbcMsgCloseChannel.html │ ├── index.IbcMsgSendPacket.html │ ├── index.IbcMsgTransfer.html │ ├── index.None.html │ ├── index.Ok.html │ ├── index.Option-1.html │ ├── index.Result-1.html │ ├── index.Some.html │ ├── index.StakingMsg.html │ ├── index.WasmMsg.html │ ├── instrumentation_CWSimulateVMInstance.DebugFunction.html │ ├── modules_bank.AllBalancesResponse.html │ ├── modules_bank.BalanceResponse.html │ ├── modules_bank.BankQuery.html │ ├── modules_bank.SupplyResponse.html │ ├── modules_ibc.IbcTransferData.html │ ├── modules_wasm_module.WasmQuery.html │ ├── store_transactional.NeverImmutify.html │ ├── sync.ChainConfig.html │ ├── sync.CustomWasmCodePaths.html │ ├── sync.MsgExecuteContractWithHeight.html │ ├── types.DebugLog.html │ ├── types.DistributionQuery.html │ ├── types.ExecuteTraceLog.html │ ├── types.IbcChannelCloseMsg.html │ ├── types.IbcChannelConnectMsg.html │ ├── types.IbcChannelOpenMsg.html │ ├── types.IbcQuery.html │ ├── types.ReplyMsg.html │ ├── types.ReplyTraceLog.html │ ├── types.RustResult.html │ ├── types.Snapshot.html │ ├── types.StakingQuery.html │ ├── types.TokenFactoryMsg.html │ ├── types.TokenFactoryMsgOptions.html │ ├── types.TokenFactoryQuery.html │ ├── types.TokenFactoryQueryEnum.html │ ├── types.TraceLog.html │ └── types.Uint128.html └── variables │ ├── index.CANONICAL_LENGTH.html │ ├── index.DEFAULT_GAS_LIMIT.html │ ├── index.EDDSA_PUBKEY_LEN.html │ ├── index.EXCESS_PADDING.html │ ├── index.GAS_COST_CANONICALIZE.html │ ├── index.GAS_COST_HUMANIZE.html │ ├── index.GAS_COST_LAST_ITERATION.html │ ├── index.GAS_COST_RANGE.html │ ├── index.GAS_MULTIPLIER.html │ ├── index.GAS_PER_OP.html │ ├── index.GAS_PER_US.html │ ├── index.MAX_LENGTH_CANONICAL_ADDRESS.html │ ├── index.MAX_LENGTH_DB_KEY.html │ ├── index.MAX_LENGTH_DB_VALUE.html │ ├── index.MAX_LENGTH_ED25519_MESSAGE.html │ ├── index.MAX_LENGTH_ED25519_SIGNATURE.html │ ├── index.MAX_LENGTH_HUMAN_ADDRESS.html │ ├── index.None-1.html │ ├── persist.serde.html │ └── store_transactional.NEVER_IMMUTIFY.html ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── cosmwasm-vm-js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── backend │ │ │ ├── backendApi.ts │ │ │ ├── index.ts │ │ │ ├── querier.ts │ │ │ ├── storage.bench.ts │ │ │ └── storage.ts │ │ ├── environment.ts │ │ ├── helpers │ │ │ └── byte-array.ts │ │ ├── index.ts │ │ ├── instance.ts │ │ ├── memory.ts │ │ └── types.ts │ ├── test │ │ ├── common │ │ │ ├── data.ts │ │ │ └── vm.ts │ │ ├── ibc.test.ts │ │ ├── imports.test.ts │ │ ├── integration │ │ │ ├── burner.test.ts │ │ │ ├── crypto-verify.test.ts │ │ │ ├── cyberpunk.test.ts │ │ │ ├── hackatom.test.ts │ │ │ └── queue.test.ts │ │ ├── sample.test.ts │ │ ├── vm.test.ts │ │ └── zk.test.ts │ ├── testdata │ │ ├── v1.0 │ │ │ ├── cosmwasm_vm_test.wasm │ │ │ ├── cw_machine-aarch64.wasm │ │ │ └── hackatom.wasm │ │ └── v1.1 │ │ │ ├── burner.wasm │ │ │ ├── crypto_verify.wasm │ │ │ ├── cyberpunk.wasm │ │ │ ├── floaty.wasm │ │ │ ├── hackatom.wasm │ │ │ ├── ibc_reflect.wasm │ │ │ ├── ibc_reflect_send.wasm │ │ │ ├── queue.wasm │ │ │ ├── reflect.wasm │ │ │ ├── staking.wasm │ │ │ └── zk.wasm │ ├── tsconfig.json │ └── webpack.config.js └── cw-simulate │ ├── README.md │ ├── bench │ └── snapshot.ts │ ├── package.json │ ├── src │ ├── CWSimulateApp.spec.ts │ ├── CWSimulateApp.ts │ ├── SimulateCosmWasmClient.spec.ts │ ├── SimulateCosmWasmClient.ts │ ├── benchmark.ts │ ├── fork-old.ts │ ├── fork.ts │ ├── index.ts │ ├── instrumentation │ │ └── CWSimulateVMInstance.ts │ ├── modules │ │ ├── bank.spec.ts │ │ ├── bank.ts │ │ ├── ibc.spec.ts │ │ ├── ibc.ts │ │ ├── wasm.spec.ts │ │ └── wasm │ │ │ ├── contract.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── module.ts │ │ │ └── wasm-util.ts │ ├── persist.ts │ ├── store │ │ ├── index.ts │ │ └── transactional.ts │ ├── sync-test.ts │ ├── sync.ts │ ├── types.ts │ └── util.ts │ ├── testing │ ├── cw_simulate_tests-aarch64.wasm │ ├── hello_world-aarch64.wasm │ ├── ibc_reflect.wasm │ ├── reflect.wasm │ └── wasm-util.ts │ ├── tsconfig.json │ └── webpack.config.js ├── patches ├── @cosmjs+cosmwasm-stargate+0.32.4.patch ├── @cosmjs+stargate+0.32.4.patch ├── @cosmjs+tendermint-rpc+0.32.4.patch └── typedoc+0.24.7.patch ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist 39 | 40 | # yarn artefacts 41 | .yarnrc.yml 42 | 43 | .npmrc 44 | 45 | data/ -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.10.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | src 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.singleQuote": true, 3 | "prettier.trailingComma": "none", 4 | "prettier.jsxSingleQuote": false, 5 | "prettier.printWidth": 120, 6 | "typescript.tsdk": "node_modules/typescript/lib" 7 | } -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #AF00DB; 11 | --dark-hl-4: #C586C0; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #008000; 15 | --dark-hl-6: #6A9955; 16 | --light-hl-7: #0070C1; 17 | --dark-hl-7: #4FC1FF; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #267F99; 21 | --dark-hl-9: #4EC9B0; 22 | --light-hl-10: #800000; 23 | --dark-hl-10: #808080; 24 | --light-hl-11: #800000; 25 | --dark-hl-11: #569CD6; 26 | --light-hl-12: #000000FF; 27 | --dark-hl-12: #D4D4D4; 28 | --light-hl-13: #E50000; 29 | --dark-hl-13: #9CDCFE; 30 | --light-hl-14: #0000FF; 31 | --dark-hl-14: #CE9178; 32 | --light-hl-15: #811F3F; 33 | --dark-hl-15: #D16969; 34 | --light-hl-16: #000000; 35 | --dark-hl-16: #C8C8C8; 36 | --light-code-background: #FFFFFF; 37 | --dark-code-background: #1E1E1E; 38 | } 39 | 40 | @media (prefers-color-scheme: light) { :root { 41 | --hl-0: var(--light-hl-0); 42 | --hl-1: var(--light-hl-1); 43 | --hl-2: var(--light-hl-2); 44 | --hl-3: var(--light-hl-3); 45 | --hl-4: var(--light-hl-4); 46 | --hl-5: var(--light-hl-5); 47 | --hl-6: var(--light-hl-6); 48 | --hl-7: var(--light-hl-7); 49 | --hl-8: var(--light-hl-8); 50 | --hl-9: var(--light-hl-9); 51 | --hl-10: var(--light-hl-10); 52 | --hl-11: var(--light-hl-11); 53 | --hl-12: var(--light-hl-12); 54 | --hl-13: var(--light-hl-13); 55 | --hl-14: var(--light-hl-14); 56 | --hl-15: var(--light-hl-15); 57 | --hl-16: var(--light-hl-16); 58 | --code-background: var(--light-code-background); 59 | } } 60 | 61 | @media (prefers-color-scheme: dark) { :root { 62 | --hl-0: var(--dark-hl-0); 63 | --hl-1: var(--dark-hl-1); 64 | --hl-2: var(--dark-hl-2); 65 | --hl-3: var(--dark-hl-3); 66 | --hl-4: var(--dark-hl-4); 67 | --hl-5: var(--dark-hl-5); 68 | --hl-6: var(--dark-hl-6); 69 | --hl-7: var(--dark-hl-7); 70 | --hl-8: var(--dark-hl-8); 71 | --hl-9: var(--dark-hl-9); 72 | --hl-10: var(--dark-hl-10); 73 | --hl-11: var(--dark-hl-11); 74 | --hl-12: var(--dark-hl-12); 75 | --hl-13: var(--dark-hl-13); 76 | --hl-14: var(--dark-hl-14); 77 | --hl-15: var(--dark-hl-15); 78 | --hl-16: var(--dark-hl-16); 79 | --code-background: var(--dark-code-background); 80 | } } 81 | 82 | :root[data-theme='light'] { 83 | --hl-0: var(--light-hl-0); 84 | --hl-1: var(--light-hl-1); 85 | --hl-2: var(--light-hl-2); 86 | --hl-3: var(--light-hl-3); 87 | --hl-4: var(--light-hl-4); 88 | --hl-5: var(--light-hl-5); 89 | --hl-6: var(--light-hl-6); 90 | --hl-7: var(--light-hl-7); 91 | --hl-8: var(--light-hl-8); 92 | --hl-9: var(--light-hl-9); 93 | --hl-10: var(--light-hl-10); 94 | --hl-11: var(--light-hl-11); 95 | --hl-12: var(--light-hl-12); 96 | --hl-13: var(--light-hl-13); 97 | --hl-14: var(--light-hl-14); 98 | --hl-15: var(--light-hl-15); 99 | --hl-16: var(--light-hl-16); 100 | --code-background: var(--light-code-background); 101 | } 102 | 103 | :root[data-theme='dark'] { 104 | --hl-0: var(--dark-hl-0); 105 | --hl-1: var(--dark-hl-1); 106 | --hl-2: var(--dark-hl-2); 107 | --hl-3: var(--dark-hl-3); 108 | --hl-4: var(--dark-hl-4); 109 | --hl-5: var(--dark-hl-5); 110 | --hl-6: var(--dark-hl-6); 111 | --hl-7: var(--dark-hl-7); 112 | --hl-8: var(--dark-hl-8); 113 | --hl-9: var(--dark-hl-9); 114 | --hl-10: var(--dark-hl-10); 115 | --hl-11: var(--dark-hl-11); 116 | --hl-12: var(--dark-hl-12); 117 | --hl-13: var(--dark-hl-13); 118 | --hl-14: var(--dark-hl-14); 119 | --hl-15: var(--dark-hl-15); 120 | --hl-16: var(--dark-hl-16); 121 | --code-background: var(--dark-code-background); 122 | } 123 | 124 | .hl-0 { color: var(--hl-0); } 125 | .hl-1 { color: var(--hl-1); } 126 | .hl-2 { color: var(--hl-2); } 127 | .hl-3 { color: var(--hl-3); } 128 | .hl-4 { color: var(--hl-4); } 129 | .hl-5 { color: var(--hl-5); } 130 | .hl-6 { color: var(--hl-6); } 131 | .hl-7 { color: var(--hl-7); } 132 | .hl-8 { color: var(--hl-8); } 133 | .hl-9 { color: var(--hl-9); } 134 | .hl-10 { color: var(--hl-10); } 135 | .hl-11 { color: var(--hl-11); } 136 | .hl-12 { color: var(--hl-12); } 137 | .hl-13 { color: var(--hl-13); } 138 | .hl-14 { color: var(--hl-14); } 139 | .hl-15 { color: var(--hl-15); } 140 | .hl-16 { color: var(--hl-16); } 141 | pre, code { background: var(--code-background); } 142 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+\\.ts?$': 'esbuild-runner/jest' 5 | }, 6 | transformIgnorePatterns: ['/node_modules/'] 7 | }; 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "npmClient": "yarn", 4 | "useNx": true, 5 | "version": "1.0.7", 6 | "command": { 7 | "publish": { 8 | "registry": "https://registry.npmjs.org/" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": "true", 4 | "license": "MIT", 5 | "scripts": { 6 | "deploy": "yarn publish --access public --patch", 7 | "docs": "typedoc --entryPointStrategy expand --readme README.md --name 'Cosmwasm Simulate SDK' packages/cw-simulate/src", 8 | "test": "jest", 9 | "build": "lerna run build", 10 | "postinstall": "patch-package" 11 | }, 12 | "workspaces": [ 13 | "packages/*" 14 | ], 15 | "engines": { 16 | "node": ">=18.18.0" 17 | }, 18 | "devDependencies": { 19 | "typedoc": "0.24.7", 20 | "esbuild": "^0.19.5", 21 | "esbuild-runner": "^2.2.2", 22 | "@types/jest": "^29.5.2", 23 | "@types/node": "^22.9.0", 24 | "dotenv": "^16.3.1", 25 | "prettier": "^2.7.1", 26 | "jest": "^29.5.0", 27 | "lerna": "^7.1.5", 28 | "nx": "16.10.0", 29 | "patch-package": "^7.0.0", 30 | "ts-loader": "^9.4.4", 31 | "tsx": "^4.7.2", 32 | "typescript": "5.0", 33 | "webpack": "^5.74.0", 34 | "webpack-cli": "^4.10.0", 35 | "tsconfig-paths-webpack-plugin": "^4.0.0", 36 | "webpack-node-externals": "^3.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/README.md: -------------------------------------------------------------------------------- 1 | # CosmWasm VM in JavaScript 2 | 3 | This package contains an implementation of the CosmWasm VM that is runnable on Node.js and web browsers that support 4 | WebAssembly (currently only tested on V8 browsers like Google Chrome). 5 | This allows you to run `.wasm` binaries intended for CosmWasm without the need for a backend blockchain or Rust 6 | toolchain, enabling new ways to instrument and test CosmWasm smart contracts. 7 | 8 | **NOTE:** This package is intended to work with contracts built for CosmWasm v1.0. 9 | 10 | **NOTE:** Although great care has been taken to match the behavior of the original CosmWasm VM (powered by Wasmer), 11 | this implementation may not provide identical results and should not be used as a drop-in replacement. Results obtained 12 | should be verified against the original implementation for critical use-cases. 13 | 14 | ## Setup 15 | 16 | Add the `cosmwasm-vm-js` package as a dependency in your `package.json`. 17 | 18 | ```sh 19 | npm install -S @oraichain/cosmwasm-vm-js 20 | ``` 21 | 22 | or 23 | 24 | ```sh 25 | yarn add @oraichain/cosmwasm-vm-js 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```ts 31 | import { readFileSync } from 'fs'; 32 | import { 33 | VMInstance, 34 | BasicBackendApi, 35 | BasicKVIterStorage, 36 | BasicQuerier, 37 | IBackend, 38 | } from '@oraichain/cosmwasm-vm-js'; 39 | 40 | const wasmBytecode = readFileSync('testdata/cosmwasm_vm_test.wasm'); 41 | const backend: IBackend = { 42 | backend_api: new BasicBackendApi('terra'), 43 | storage: new BasicKVIterStorage(), 44 | querier: new BasicQuerier(), 45 | }; 46 | 47 | const vm = new VMInstance(backend); 48 | const mockEnv = { 49 | block: { 50 | height: 1337, 51 | time: '2000000000', 52 | chain_id: 'columbus-5', 53 | }, 54 | contract: { 55 | address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76', 56 | }, 57 | }; 58 | 59 | const mockInfo = { 60 | sender: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 61 | funds: [], 62 | }; 63 | 64 | describe('CosmWasmVM', () => { 65 | it('instantiates', async () => { 66 | await vm.build(wasmBytecode); 67 | 68 | const region = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 69 | console.log(region.json); 70 | console.log(vm.backend); 71 | const actual = { 72 | ok: { 73 | attributes: [ 74 | { key: 'method', value: 'instantiate' }, 75 | { 76 | key: 'owner', 77 | value: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 78 | }, 79 | { key: 'count', value: '20' }, 80 | ], 81 | data: null, 82 | events: [], 83 | messages: [], 84 | }, 85 | }; 86 | expect(region.json).toEqual(actual); 87 | }); 88 | 89 | it('execute', async () => { 90 | await vm.build(wasmBytecode); 91 | 92 | let region = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 93 | region = vm.execute(mockEnv, mockInfo, { increment: {} }); 94 | console.log(region.json); 95 | console.log(vm.backend); 96 | const actual = { 97 | ok: { 98 | attributes: [{ key: 'method', value: 'try_increment' }], 99 | data: null, 100 | events: [], 101 | messages: [], 102 | }, 103 | }; 104 | expect(region.json).toEqual(actual); 105 | }); 106 | }); 107 | ``` 108 | 109 | ## How it works 110 | 111 | CosmWasm smart contracts are WebAssembly binaries that export certain function symbols called "entrypoints", such as 112 | the following: 113 | 114 | - `instantiate` 115 | - `execute` 116 | - `query` 117 | - `migrate` 118 | 119 | Users interact and invoke operations on the smart contract by calling the desired entrypoint with arguments. 120 | As these are exposed as WebAssembly functions, we should normally be able to call them directly. 121 | However, CosmWasm contracts carry some implicit requirements that must be met before we can interact with the contract's 122 | functions naturally. 123 | 124 | 1. Contracts expect certain symbols to be provided by the VM host (WASM imports). 125 | 2. Contracts need an environment with storage to which it can read and write data. 126 | 3. Contract entrypoints expect to be called with input arguments prepared and allocated into memory in a certain way. 127 | 4. The response of contract entrypoint invocations should be parsed. 128 | 129 | `cosmwasm-vm-js` provides a VM implementation that addresses all of these requirements and exposes a simulated execution 130 | environment that can be further customized to enable possibilities such as instrumentation, visualization, debugging, 131 | and more. 132 | 133 | ## WASM Imports 134 | 135 | The following WASM imports have been implemented according to `imports.rs` in `cosmwasm-vm`. 136 | 137 | | Import Name | Implemented? | Tested? | Notes | 138 | | -------------------------- | ------------------ | ------------------ | ------------------------------------------------------------ | 139 | | `db_read` | :white_check_mark: | :white_check_mark: | | 140 | | `db_write` | :white_check_mark: | :white_check_mark: | | 141 | | `db_remove` | :white_check_mark: | :white_check_mark: | | 142 | | `db_scan` | :white_check_mark: | :white_check_mark: | | 143 | | `db_next` | :white_check_mark: | :white_check_mark: | | 144 | | `addr_humanize` | :white_check_mark: | :white_check_mark: | | 145 | | `addr_canonicalize` | :white_check_mark: | :white_check_mark: | | 146 | | `addr_validate` | :white_check_mark: | :white_check_mark: | | 147 | | `secp256k1_verify` | :white_check_mark: | :white_check_mark: | | 148 | | `secp256k1_recover_pubkey` | :white_check_mark: | :white_check_mark: | | 149 | | `ed25519_verify` | :white_check_mark: | :white_check_mark: | | 150 | | `ed25519_batch_verify` | :white_check_mark: | :white_check_mark: | | 151 | | `debug` | :white_check_mark: | :white_check_mark: | Appends to a list of strings instead of printing to console. | 152 | | `query_chain` | :white_check_mark: | :white_check_mark: | | 153 | | `abort` | :white_check_mark: | :white_check_mark: | | 154 | 155 | ## Environment & Storage 156 | 157 | We provide a simple key-value store with bytes keys and bytes values in `BasicKVIterStorage`. 158 | 159 | ### WebAssembly Linear Memory 160 | 161 | A loaded CosmWasm contract module's linear memory is accessible as `WebAssembly.Memory`, which can be read as a 162 | bytearray through 163 | JavaScript's `Uint8Array` data type. 164 | 165 | ### Passing data from JavaScript to WASM 166 | 167 | To invoke entrypoint functions, we need to pass in arguments from JavaScript and load them into WebAssembly linear 168 | memory accessible by the contract. Although we can write directly to `WebAssembly.Memory`, doing this is considered 169 | unsafe as we don't know what we might be touching. 170 | Instead, we must use the contract's `allocate` entrypoint which gives us a pointer to a writeable region of linear 171 | memory which is recognized by the WASM code. 172 | 173 | `cosmwasm-vm-js` also provides the `Region` class, which is an analog of the `Region` type found in `cosmwasm-vm`. 174 | 175 | #### CosmWasm's `Region` type 176 | 177 | ```rust 178 | /// Describes some data allocated in Wasm's linear memory. 179 | /// A pointer to an instance of this can be returned over FFI boundaries. 180 | /// 181 | /// This is the same as `cosmwasm_std::memory::Region` 182 | /// but defined here to allow Wasmer specific implementation. 183 | #[repr(C)] 184 | #[derive(Default, Clone, Copy, Debug)] 185 | struct Region { 186 | /// The beginning of the region expressed as bytes from the beginning of the linear memory 187 | pub offset: u32, 188 | /// The number of bytes available in this region 189 | pub capacity: u32, 190 | /// The number of bytes used in this region 191 | pub length: u32, 192 | } 193 | ``` 194 | 195 | CosmWasm contract entrypoints expect their parameters to be pointers to `Region` structs, which point to the actual data 196 | via `offset`. 197 | 198 | ```text 199 | arg ---> Region ---> argument data 200 | ``` 201 | 202 | # License 203 | 204 | This software is licensed under the [MIT License](https://opensource.org/licenses/MIT). 205 | 206 | Copyright © 2022 Terran One LLC 207 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oraichain/cosmwasm-vm-js", 3 | "version": "0.2.91", 4 | "license": "MIT", 5 | "author": "TerranOne, Oraichain Labs", 6 | "main": "dist/index.js", 7 | "type": "commonjs", 8 | "files": [ 9 | "dist" 10 | ], 11 | "publishConfig": { 12 | "registry": "https://registry.npmjs.org" 13 | }, 14 | "scripts": { 15 | "build": "tsc --module commonjs && webpack --mode production", 16 | "//degit:contracts": "cd contracts && npx degit CosmWasm/cosmwasm/contracts/hackatom#0.16 hackatom" 17 | }, 18 | "prettier": { 19 | "printWidth": 80, 20 | "semi": true, 21 | "singleQuote": true, 22 | "trailingComma": "es5" 23 | }, 24 | "engines": { 25 | "node": ">=16" 26 | }, 27 | "devDependencies": { 28 | "@oraichain/cosmwasm-vm-zk": "^0.1.3", 29 | "@tsconfig/recommended": "^1.0.1", 30 | "@types/elliptic": "^6.4.14", 31 | "@types/secp256k1": "^4.0.3" 32 | }, 33 | "dependencies": { 34 | "@cosmjs/amino": "^0.32.4", 35 | "@cosmjs/crypto": "^0.32.4", 36 | "@cosmjs/encoding": "^0.32.4", 37 | "@oraichain/immutable": "^4.3.9", 38 | "@oraichain/wasm-json-toolkit": "^1.0.24", 39 | "bech32": "1.1.4", 40 | "elliptic": "^6.5.4", 41 | "secp256k1": "^4.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/backend/backendApi.ts: -------------------------------------------------------------------------------- 1 | import { fromBech32, normalizeBech32 } from '@cosmjs/encoding'; 2 | import bech32 from 'bech32'; 3 | import { MAX_LENGTH_HUMAN_ADDRESS } from '../instance'; 4 | 5 | export interface IGasInfo { 6 | cost: number; 7 | externally_used: number; 8 | } 9 | 10 | export class GasInfo implements IGasInfo { 11 | constructor(public cost: number, public externally_used: number) {} 12 | 13 | static with_cost(cost: number): IGasInfo { 14 | return new GasInfo(cost, 0); 15 | } 16 | 17 | static with_externally_used(externally_used: number): IGasInfo { 18 | return new GasInfo(0, externally_used); 19 | } 20 | 21 | static free(): IGasInfo { 22 | return new GasInfo(0, 0); 23 | } 24 | } 25 | 26 | export interface IBackendApi { 27 | bech32_prefix: string; 28 | canonical_address(human: string): Uint8Array; 29 | human_address(canonical: Uint8Array): string; 30 | poseidon_hash( 31 | left_input: Uint8Array, 32 | right_input: Uint8Array, 33 | curve: number 34 | ): Uint8Array; 35 | curve_hash(input: Uint8Array, curve: number): Uint8Array; 36 | groth16_verify( 37 | input: Uint8Array, 38 | proof: Uint8Array, 39 | vk: Uint8Array, 40 | curve: number 41 | ): boolean; 42 | keccak_256(input: Uint8Array): Uint8Array; 43 | sha256(input: Uint8Array): Uint8Array; 44 | } 45 | 46 | export const CANONICAL_LENGTH = 64; 47 | export const EXCESS_PADDING = 6; 48 | 49 | export class BasicBackendApi implements IBackendApi { 50 | constructor(public bech32_prefix: string = 'terra') {} 51 | 52 | poseidon_hash( 53 | left_input: Uint8Array, 54 | right_input: Uint8Array, 55 | curve: number 56 | ): Uint8Array { 57 | throw new Error('Method not implemented.'); 58 | } 59 | curve_hash(input: Uint8Array, curve: number): Uint8Array { 60 | throw new Error('Method not implemented.'); 61 | } 62 | groth16_verify( 63 | input: Uint8Array, 64 | proof: Uint8Array, 65 | vk: Uint8Array, 66 | curve: number 67 | ): boolean { 68 | throw new Error('Method not implemented.'); 69 | } 70 | keccak_256(input: Uint8Array): Uint8Array { 71 | throw new Error('Method not implemented.'); 72 | } 73 | sha256(input: Uint8Array): Uint8Array { 74 | throw new Error('Method not implemented.'); 75 | } 76 | 77 | public canonical_address(human: string): Uint8Array { 78 | if (human.length === 0) { 79 | throw new Error('Empty human address'); 80 | } 81 | 82 | const normalized = normalizeBech32(human); 83 | 84 | if (normalized.length < 3) { 85 | throw new Error(`canonical_address: Address too short: ${normalized}`); 86 | } 87 | 88 | if (normalized.length > CANONICAL_LENGTH) { 89 | throw new Error(`canonical_address: Address too long: ${normalized}`); 90 | } 91 | 92 | return fromBech32(normalized).data; 93 | } 94 | 95 | public human_address(canonical: Uint8Array): string { 96 | if (canonical.length === 0) { 97 | throw new Error('human_address: Empty canonical address'); 98 | } 99 | 100 | // Remove excess padding, otherwise bech32.encode will throw "Exceeds length limit" 101 | // error when normalized is greater than MAX_LENGTH_HUMAN_ADDRESS in length. 102 | const normalized = 103 | canonical.length >= CANONICAL_LENGTH 104 | ? canonical.slice(0, CANONICAL_LENGTH - EXCESS_PADDING) 105 | : canonical; 106 | 107 | return bech32.encode( 108 | this.bech32_prefix, 109 | bech32.toWords(normalized), 110 | MAX_LENGTH_HUMAN_ADDRESS 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/backend/index.ts: -------------------------------------------------------------------------------- 1 | import * as BackendApi from './backendApi'; 2 | import * as Querier from './querier'; 3 | import * as Storage from './storage'; 4 | 5 | export * from './backendApi'; 6 | export * from './querier'; 7 | export * from './storage'; 8 | 9 | export interface IBackend { 10 | backend_api: BackendApi.IBackendApi; 11 | querier: Querier.IQuerier; 12 | storage: Storage.IIterStorage; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/backend/querier.ts: -------------------------------------------------------------------------------- 1 | export interface IQuerier { 2 | query_raw(request: Uint8Array, gas_limit: number /* Uint64 */): Uint8Array; 3 | } 4 | 5 | /** Basic implementation of `IQuerier` with standardized `query_raw` 6 | * which delegates to a new, abstract `handleQuery` method. 7 | */ 8 | export abstract class QuerierBase implements IQuerier { 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | query_raw(request: Uint8Array, gas_limit: number): Uint8Array { 11 | const queryRequest = parseQuery(request); 12 | 13 | // The Ok(Ok(x)) represents SystemResult> 14 | const result = this.handleQuery(queryRequest); 15 | const contractResult = 16 | result instanceof Error 17 | ? { err: result.message } 18 | : // result may already in base64 format that starts with {" 19 | { 20 | ok: 21 | // if result is not primative type (number like) then just return 22 | typeof result === 'string' && Number.isNaN(parseFloat(result)) 23 | ? result 24 | : objectToBase64(result), 25 | }; 26 | 27 | return objectToUint8Array({ ok: contractResult }); 28 | } 29 | 30 | /** Handle a specific JSON query message. */ 31 | abstract handleQuery(queryRequest: any): any; 32 | } 33 | 34 | /** Basic implementation which does not actually implement `handleQuery`. Intended for testing. */ 35 | export class BasicQuerier extends QuerierBase { 36 | handleQuery(queryRequest: any): any { 37 | throw new Error( 38 | `Unimplemented - subclass BasicQuerier and provide handleQuery() implementation.` 39 | ); 40 | } 41 | } 42 | 43 | function parseQuery(bytes: Uint8Array): any { 44 | const query = JSON.parse(new TextDecoder().decode(bytes)); 45 | return query; 46 | } 47 | 48 | function objectToBase64(obj: object): string { 49 | return Buffer.from(JSON.stringify(obj)).toString('base64'); 50 | } 51 | 52 | function objectToUint8Array(obj: object): Uint8Array { 53 | return new TextEncoder().encode(JSON.stringify(obj)); 54 | } 55 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/backend/storage.bench.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { toAscii } from '@cosmjs/encoding'; 4 | import { toByteArray, toNumber } from '../helpers/byte-array'; 5 | import { BasicKVIterStorage, BinaryKVIterStorage, Order } from './storage'; 6 | 7 | let store = new BasicKVIterStorage(); 8 | let binaryStore = new BinaryKVIterStorage(); 9 | const n = 1000000; 10 | let start = toByteArray(n >> 1); 11 | let stop = toByteArray((n >> 1) + 10); 12 | 13 | console.time('BasicKVIterStorage Insert'); 14 | for (let i = 0; i < n; ++i) store.set(toByteArray(i), toAscii(i.toString())); 15 | console.timeEnd('BasicKVIterStorage Insert'); 16 | console.time('BinaryKVIterStorage Insert'); 17 | for (let i = 0; i < n; ++i) 18 | binaryStore.set(toByteArray(i), toAscii(i.toString())); 19 | console.timeEnd('BinaryKVIterStorage Insert'); 20 | 21 | let ret; 22 | console.time('BasicKVIterStorage Scan'); 23 | ret = store.all(store.scan(start, stop, Order.Ascending)); 24 | console.timeEnd('BasicKVIterStorage Scan'); 25 | console.log( 26 | ret.map((record) => [ 27 | toNumber(record.key), 28 | Buffer.from(record.value).toString(), 29 | ]) 30 | ); 31 | 32 | console.time('BinaryKVIterStorage Scan'); 33 | ret = binaryStore.all(binaryStore.scan(start, stop, Order.Ascending)); 34 | console.timeEnd('BinaryKVIterStorage Scan'); 35 | console.log( 36 | ret.map((record) => [ 37 | toNumber(record.key), 38 | Buffer.from(record.value).toString(), 39 | ]) 40 | ); 41 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/backend/storage.ts: -------------------------------------------------------------------------------- 1 | import { fromBase64, toBase64 } from '@cosmjs/encoding'; 2 | import { compare, toByteArray, toNumber } from '../helpers/byte-array'; 3 | import Immutable from '@oraichain/immutable'; 4 | 5 | export interface IStorage { 6 | dict?: Immutable.Map; 7 | 8 | get(key: Uint8Array): Uint8Array | null; 9 | 10 | set(key: Uint8Array, value: Uint8Array): void; 11 | 12 | remove(key: Uint8Array): void; 13 | 14 | keys(): Iterable; 15 | } 16 | 17 | export class Record { 18 | public key = Uint8Array.from([]); 19 | public value = Uint8Array.from([]); 20 | } 21 | 22 | export interface Iter { 23 | data: Array; 24 | position: number; 25 | } 26 | 27 | export enum Order { 28 | Ascending = 1, 29 | Descending = 2, 30 | } 31 | 32 | export interface IIterStorage extends IStorage { 33 | all(iterator_id: Uint8Array): Array; 34 | 35 | scan( 36 | start: Uint8Array | null, 37 | end: Uint8Array | null, 38 | order: Order 39 | ): Uint8Array; 40 | next(iterator_id: Uint8Array): Record | null; 41 | } 42 | 43 | export class BasicKVStorage implements IStorage { 44 | // TODO: Add binary uint / typed Addr maps for cw-storage-plus compatibility 45 | constructor(public dict: Immutable.Map = Immutable.Map()) {} 46 | 47 | *keys() { 48 | for (const key of this.dict.keys()) { 49 | yield fromBase64(key); 50 | } 51 | } 52 | 53 | get(key: Uint8Array): Uint8Array | null { 54 | const keyStr = toBase64(key); 55 | const value = this.dict.get(keyStr); 56 | if (value === undefined) { 57 | return null; 58 | } 59 | 60 | return fromBase64(value); 61 | } 62 | 63 | set(key: Uint8Array, value: Uint8Array): void { 64 | const keyStr = toBase64(key); 65 | this.dict = this.dict.set(keyStr, toBase64(value)); 66 | } 67 | 68 | remove(key: Uint8Array): void { 69 | this.dict = this.dict.remove(toBase64(key)); 70 | } 71 | } 72 | 73 | export class BasicKVIterStorage extends BasicKVStorage implements IIterStorage { 74 | constructor( 75 | public dict: Immutable.Map = Immutable.Map(), 76 | public iterators: Map = new Map() 77 | ) { 78 | super(dict); 79 | } 80 | 81 | all(iterator_id: Uint8Array): Array { 82 | const out: Array = []; 83 | 84 | while (true) { 85 | const record = this.next(iterator_id); 86 | if (record === null) { 87 | break; 88 | } 89 | out.push(record); 90 | } 91 | return out; 92 | } 93 | 94 | // Get next element of iterator with ID `iterator_id`. 95 | // Creates a region containing both key and value and returns its address. 96 | // Ownership of the result region is transferred to the contract. 97 | // The KV region uses the format value || valuelen || key || keylen, where valuelen and keylen are fixed-size big-endian u32 values. 98 | // An empty key (i.e. KV region ends with \0\0\0\0) means no more element, no matter what the value is. 99 | next(iterator_id: Uint8Array): Record | null { 100 | const iter = this.iterators.get(toNumber(iterator_id)); 101 | if (iter === undefined) { 102 | throw new Error(`Iterator not found.`); 103 | } 104 | const record = iter.data[iter.position]; 105 | if (!record) { 106 | return null; 107 | } 108 | 109 | iter.position += 1; 110 | return record; 111 | } 112 | 113 | scan( 114 | start: Uint8Array | null, 115 | end: Uint8Array | null, 116 | order: Order 117 | ): Uint8Array { 118 | if (!(order in Order)) { 119 | throw new Error(`Invalid order value ${order}.`); 120 | } 121 | const hasStart = start?.length; 122 | const hasEnd = end?.length; 123 | 124 | // if there is end namespace 125 | const filterKeyLength = 126 | hasStart && start[0] === 0 127 | ? start[1] 128 | : hasEnd && end[0] == 0 129 | ? end[1] 130 | : 0; 131 | 132 | const newId = this.iterators.size + 1; 133 | 134 | // if start > end, this represents an empty range 135 | if (hasStart && hasEnd && compare(start, end) === 1) { 136 | this.iterators.set(newId, { data: [], position: 0 }); 137 | return toByteArray(newId); 138 | } 139 | 140 | let data: Record[] = []; 141 | for (const key of this.dict.keys()) { 142 | let keyArr = fromBase64(key); 143 | 144 | // out of range 145 | if ( 146 | (hasStart && compare(keyArr, start) < 0) || 147 | (hasEnd && compare(keyArr, end) >= 0) 148 | ) 149 | continue; 150 | 151 | // different namespace 152 | if ( 153 | filterKeyLength !== 0 && 154 | keyArr[0] === 0 && 155 | filterKeyLength !== keyArr[1] 156 | ) { 157 | continue; 158 | } 159 | 160 | data.push({ key: keyArr, value: this.get(keyArr)! }); 161 | } 162 | 163 | data.sort((a, b) => 164 | order === Order.Descending ? compare(b.key, a.key) : compare(a.key, b.key) 165 | ); 166 | 167 | this.iterators.set(newId, { data, position: 0 }); 168 | return toByteArray(newId); 169 | } 170 | } 171 | 172 | export class BinaryKVStorage implements IStorage { 173 | constructor( 174 | public dict: Immutable.SortedMap< 175 | Uint8Array, 176 | Uint8Array 177 | > = Immutable.SortedMap(compare) 178 | ) {} 179 | 180 | *keys() { 181 | for (const key of this.dict.keys()) { 182 | yield key; 183 | } 184 | } 185 | 186 | get(key: Uint8Array): Uint8Array | null { 187 | const value = this.dict.get(key); 188 | if (value === undefined) { 189 | return null; 190 | } 191 | return value; 192 | } 193 | 194 | set(key: Uint8Array, value: Uint8Array): void { 195 | this.dict = this.dict.set(new Uint8Array(key), new Uint8Array(value)); 196 | } 197 | 198 | remove(key: Uint8Array): void { 199 | this.dict = this.dict.delete(new Uint8Array(key)); 200 | } 201 | } 202 | 203 | export class BinaryKVIterStorage 204 | extends BinaryKVStorage 205 | implements IIterStorage 206 | { 207 | constructor( 208 | dict?: Immutable.SortedMap, 209 | public iterators: Map = new Map() 210 | ) { 211 | super(dict); 212 | } 213 | 214 | all(iterator_id: Uint8Array): Array { 215 | const out: Array = []; 216 | 217 | while (true) { 218 | const record = this.next(iterator_id); 219 | if (record === null) { 220 | break; 221 | } 222 | out.push(record); 223 | } 224 | return out; 225 | } 226 | 227 | // Get next element of iterator with ID `iterator_id`. 228 | // Creates a region containing both key and value and returns its address. 229 | // Ownership of the result region is transferred to the contract. 230 | // The KV region uses the format value || valuelen || key || keylen, where valuelen and keylen are fixed-size big-endian u32 values. 231 | // An empty key (i.e. KV region ends with \0\0\0\0) means no more element, no matter what the value is. 232 | next(iterator_id: Uint8Array): Record | null { 233 | const iter = this.iterators.get(toNumber(iterator_id)); 234 | if (iter === undefined) { 235 | throw new Error(`Iterator not found.`); 236 | } 237 | const record = iter.data[iter.position]; 238 | if (!record) { 239 | return null; 240 | } 241 | 242 | iter.position += 1; 243 | return record; 244 | } 245 | 246 | scan( 247 | start: Uint8Array | null, 248 | end: Uint8Array | null, 249 | order: Order 250 | ): Uint8Array { 251 | if (!(order in Order)) { 252 | throw new Error(`Invalid order value ${order}.`); 253 | } 254 | 255 | const hasStart = start !== null && start.length; 256 | const hasEnd = end !== null && end.length; 257 | 258 | // if there is end namespace 259 | const filterKeyLength = 260 | hasStart && start[0] === 0 261 | ? start[1] 262 | : hasEnd && end[0] == 0 263 | ? end[1] 264 | : 0; 265 | const newId = this.iterators.size + 1; 266 | 267 | // if start > end, this represents an empty range 268 | if (hasStart && hasEnd && compare(start, end) === 1) { 269 | this.iterators.set(newId, { data: [], position: 0 }); 270 | return toByteArray(newId); 271 | } 272 | 273 | const data: Record[] = []; 274 | 275 | // we also create a temporary iterator so we just start from here 276 | let iter = hasStart ? this.dict.from(start) : this.dict; 277 | if (hasEnd) { 278 | iter = iter.takeUntil((_, key) => { 279 | return compare(key, end) >= 0; 280 | }); 281 | } 282 | 283 | // loop and filter 284 | iter.forEach((value, key) => { 285 | // different namespace 286 | if (filterKeyLength === 0 || key[0] !== 0 || filterKeyLength === key[1]) { 287 | data.push({ key, value }); 288 | } 289 | }); 290 | 291 | if (order === Order.Descending) data.reverse(); 292 | 293 | this.iterators.set(newId, { data, position: 0 }); 294 | return toByteArray(newId); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/environment.ts: -------------------------------------------------------------------------------- 1 | import { GasInfo, IBackendApi } from './backend'; 2 | 3 | export interface IEnvironment { 4 | processGasInfo(info: GasInfo): void; 5 | } 6 | 7 | export const GAS_PER_OP = 150_000; 8 | export const GAS_MULTIPLIER = 14_000_000; // convert to chain gas 9 | export const GAS_PER_US = 1_000_000_000; 10 | 11 | export const DEFAULT_GAS_LIMIT = 1_000_000_000_000; // ~1ms 12 | 13 | export interface GasState { 14 | gas_limit: number; 15 | externally_used_gas: number; 16 | } 17 | 18 | export interface ContextData { 19 | gas_state: GasState; 20 | storage_readonly: boolean; 21 | // wasmer_instance: any; 22 | } 23 | 24 | export class Environment { 25 | public backendApi: IBackendApi; 26 | public data: ContextData; 27 | public static gasConfig = { 28 | // ~154 us in crypto benchmarks 29 | secp256k1_verify_cost: 154 * GAS_PER_US, 30 | 31 | // ~6920 us in crypto benchmarks 32 | groth16_verify_cost: 6920 * GAS_PER_US, 33 | 34 | // ~43 us in crypto benchmarks 35 | poseidon_hash_cost: 43 * GAS_PER_US, 36 | 37 | // ~480 ns ~ 0.5 in crypto benchmarks 38 | keccak_256_cost: GAS_PER_US / 2, 39 | 40 | // ~968 ns ~ 1 us in crypto benchmarks 41 | sha256_cost: GAS_PER_US, 42 | 43 | // ~920 ns ~ 1 us in crypto benchmarks 44 | curve_hash_cost: GAS_PER_US, 45 | 46 | // ~162 us in crypto benchmarks 47 | secp256k1_recover_pubkey_cost: 162 * GAS_PER_US, 48 | // ~63 us in crypto benchmarks 49 | ed25519_verify_cost: 63 * GAS_PER_US, 50 | // Gas cost factors, relative to ed25519_verify cost 51 | // From https://docs.rs/ed25519-zebra/2.2.0/ed25519_zebra/batch/index.html 52 | ed25519_batch_verify_cost: (63 * GAS_PER_US) / 2, 53 | ed25519_batch_verify_one_pubkey_cost: (63 * GAS_PER_US) / 4, 54 | }; 55 | 56 | constructor(backendApi: IBackendApi, gasLimit: number = DEFAULT_GAS_LIMIT) { 57 | const data: ContextData = { 58 | gas_state: { 59 | gas_limit: gasLimit, 60 | externally_used_gas: 0, 61 | }, 62 | storage_readonly: false, // allow update 63 | // wasmer_instance: instance, 64 | }; 65 | 66 | this.backendApi = backendApi; 67 | this.data = data; 68 | } 69 | 70 | public get storageReadonly(): boolean { 71 | return this.data.storage_readonly; 72 | } 73 | 74 | public set storageReadonly(value: boolean) { 75 | this.data.storage_readonly = value; 76 | } 77 | 78 | public processGasInfo(info: GasInfo) { 79 | // accumulate externally used gas 80 | this.data.gas_state.externally_used_gas += 81 | info.externally_used + info.cost / GAS_MULTIPLIER; 82 | } 83 | 84 | public get gasUsed() { 85 | return Math.round(this.data.gas_state.externally_used_gas); 86 | } 87 | 88 | public get gasLimit() { 89 | return this.data.gas_state.gas_limit; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/helpers/byte-array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two byte arrays using the same logic as strcmp() 3 | * 4 | * @returns {number} bytes1 < bytes2 --> -1; bytes1 == bytes2 --> 0; bytes1 > bytes2 --> 1 5 | */ 6 | export function compare(bytes1: Uint8Array, bytes2: Uint8Array): number { 7 | const length = Math.max(bytes1.length, bytes2.length); 8 | for (let i = 0; i < length; i++) { 9 | if (bytes1.length < i) return -1; 10 | if (bytes2.length < i) return 1; 11 | 12 | if (bytes1[i] < bytes2[i]) return -1; 13 | if (bytes1[i] > bytes2[i]) return 1; 14 | } 15 | 16 | return 0; 17 | } 18 | 19 | export function toNumber(bigEndianByteArray: Uint8Array | number[]) { 20 | let value = 0; 21 | for (const num of bigEndianByteArray) { 22 | value = (value << 8) | num; 23 | } 24 | return value; 25 | } 26 | 27 | export function toByteArray( 28 | num: number, 29 | fixedLength: number = 4, 30 | offset: number = 0 31 | ) { 32 | if (num === 0) return new Uint8Array(fixedLength ?? 1); 33 | // log2(1) == 0, ceil(0) = 0 34 | const byteLength = fixedLength ?? (Math.ceil(Math.log2(num) / 8) || 1); 35 | const bytes = new Uint8Array(byteLength); 36 | writeUInt32BE(bytes, num, byteLength - offset); 37 | return bytes; 38 | } 39 | 40 | export function writeUInt32BE(bytes: Uint8Array, num: number, start: number) { 41 | while (num > 0) { 42 | bytes[--start] = num & 0b11111111; 43 | num >>= 8; 44 | } 45 | } 46 | 47 | export function mergeUint8Array(...array: Uint8Array[]) { 48 | let n = 0; 49 | for (const item of array) n += item.length; 50 | const bytes = new Uint8Array(n); 51 | n = 0; 52 | for (const item of array) { 53 | bytes.set(item, n); 54 | n += item.length; 55 | } 56 | return bytes; 57 | } 58 | 59 | export function decreaseBytes(bytes: Uint8Array) { 60 | for (let i = bytes.length - 1; i >= 0; --i) { 61 | if (bytes[i] === 0) { 62 | bytes[i] = 255; 63 | } else { 64 | bytes[i]--; 65 | break; 66 | } 67 | } 68 | } 69 | 70 | export function increaseBytes(bytes: Uint8Array) { 71 | for (let i = bytes.length - 1; i >= 0; --i) { 72 | if (bytes[i] === 255) { 73 | bytes[i] = 0; 74 | } else { 75 | bytes[i]++; 76 | break; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './memory'; 3 | export * from './backend'; 4 | export * from './instance'; 5 | export * from './environment'; 6 | export * from './helpers/byte-array'; 7 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/memory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper class for the Region data structure, which describes a region of 3 | * WebAssembly's linear memory that has been allocated by the VM. 4 | * 5 | * Note that this class is passed a pointer to the data structure, and the 6 | * Region's members (offset: u32, capacity: u32, length: u32) are read from 7 | * that pointer as they are laid out in the data structure. 8 | */ 9 | export class Region { 10 | /** 11 | * The region's data structure laid out in memory. 12 | */ 13 | public region_info: Uint32Array; 14 | 15 | /** 16 | * @param memory The WebAssembly.Memory object that this region is associated 17 | * @param ptr The offset of the region's data structure in memory 18 | */ 19 | constructor(public memory: WebAssembly.Memory, public ptr: number) { 20 | this.region_info = new Uint32Array(memory.buffer, ptr, 3); 21 | } 22 | 23 | public get offset(): number { 24 | return this.region_info[0]; 25 | } 26 | 27 | public set offset(val: number) { 28 | this.region_info[0] = val; 29 | } 30 | 31 | public set capacity(val: number) { 32 | this.region_info[1] = val; 33 | } 34 | 35 | public get capacity(): number { 36 | return this.region_info[1]; 37 | } 38 | 39 | public set length(val: number) { 40 | this.region_info[2] = val; 41 | } 42 | 43 | public get length(): number { 44 | return this.region_info[2]; 45 | } 46 | 47 | /** 48 | * Get a byte-slice of the region's data. 49 | */ 50 | public get data(): Uint8Array { 51 | return this.read(); 52 | } 53 | 54 | /** 55 | * Get a byte-slice of the entire writable region. 56 | */ 57 | public get slice(): Uint8Array { 58 | return new Uint8Array(this.memory.buffer, this.offset, this.capacity); 59 | } 60 | 61 | /** 62 | * Get a base64-encoded string of the region's data. 63 | */ 64 | public get b64(): string { 65 | return this.read_b64(); 66 | } 67 | 68 | /** 69 | * Get a string view of the region's data. 70 | */ 71 | public get str(): string { 72 | return this.read_str(); 73 | } 74 | 75 | /** 76 | * Parse the object of the region's data as JSON. 77 | */ 78 | public get json(): object { 79 | return this.read_json(); 80 | } 81 | 82 | /** 83 | * Write a byte-slice to the region. 84 | * @param bytes The bytes to write to the region 85 | */ 86 | public write(bytes: Uint8Array): void { 87 | this.slice.set(bytes); 88 | this.length = bytes.length; 89 | } 90 | 91 | /** 92 | * Write bytes encoded as base64 to the region. 93 | * @param b64 bytes encoded as base64 94 | */ 95 | public write_b64(b64: string): void { 96 | this.write(Buffer.from(b64, 'base64')); 97 | } 98 | 99 | /** 100 | * Write a string to the region. 101 | * @param str The string to write to the region 102 | */ 103 | public write_str(str: string): void { 104 | this.write(new TextEncoder().encode(str)); 105 | } 106 | 107 | /** 108 | * Write a JSON object to the region as a string. 109 | * @param obj The object to write to the region 110 | */ 111 | public write_json(obj: object): void { 112 | this.write_str(JSON.stringify(obj)); 113 | } 114 | 115 | /** 116 | * Reads the region's data as a Uint8Array. 117 | * @returns The byte-slice of the region's data. 118 | */ 119 | public read(): Uint8Array { 120 | return new Uint8Array(this.memory.buffer, this.offset, this.length); 121 | } 122 | 123 | public read_b64(): string { 124 | return Buffer.from(this.read()).toString('base64'); 125 | } 126 | 127 | /** 128 | * Reads the region's data as a String. 129 | * @returns The region's data as a string. 130 | */ 131 | public read_str(): string { 132 | return new TextDecoder().decode(this.read()); 133 | } 134 | 135 | /** 136 | * Parse the region's data as JSON. 137 | * @returns The region's data as a JSON object. 138 | */ 139 | public read_json(): object { 140 | return JSON.parse(this.read_str()); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Coin } from '@cosmjs/amino'; 2 | export type Address = string; 3 | export type Decimal = string; 4 | export type Binary = string; 5 | 6 | /** Port of [Env (Rust)](https://docs.rs/cosmwasm-std/1.1.4/cosmwasm_std/struct.Env.html) */ 7 | export type Env = 8 | | { 9 | block: BlockInfo; 10 | contract: { 11 | address: Address; 12 | }; 13 | } 14 | | { 15 | block: BlockInfo; 16 | transaction: { 17 | index: number | string; 18 | } | null; 19 | contract: { 20 | address: Address; 21 | }; 22 | }; 23 | 24 | export interface Attribute { 25 | key: string; 26 | value: string; 27 | } 28 | 29 | export interface Event { 30 | type: string; 31 | attributes: Attribute[]; 32 | } 33 | 34 | export interface SubMsg { 35 | id: number; 36 | msg: CosmosMsg; 37 | gas_limit: number | null; 38 | reply_on: ReplyOn; 39 | } 40 | 41 | export enum ReplyOn { 42 | Always = 'always', 43 | Never = 'never', 44 | Success = 'success', 45 | Error = 'error', 46 | } 47 | 48 | export interface BlockInfo { 49 | height: number | string; 50 | time: number | string; 51 | chain_id: string; 52 | } 53 | 54 | /** Port of [MessageInfo (Rust)](https://docs.rs/cosmwasm-std/1.1.4/cosmwasm_std/struct.MessageInfo.html) */ 55 | export interface MessageInfo { 56 | sender: Address; 57 | funds: Coin[]; 58 | } 59 | 60 | export type BankMsg = 61 | | { 62 | send: { 63 | to_address: Address; 64 | amount: Coin[]; 65 | }; 66 | } 67 | | { 68 | burn: { 69 | amount: Coin[]; 70 | }; 71 | }; 72 | 73 | export interface Execute { 74 | contract_addr: Address; 75 | msg: Binary; 76 | funds: Coin[]; 77 | } 78 | 79 | export interface Instantiate { 80 | admin: Address | null; 81 | code_id: number; 82 | msg: Binary; 83 | funds: Coin[]; 84 | label: string; 85 | } 86 | 87 | export interface Instantiate2 extends Instantiate { 88 | salt: Binary; 89 | } 90 | 91 | export interface Migrate { 92 | contract_addr: Address; 93 | new_code_id: number; 94 | msg: Binary; 95 | } 96 | 97 | export type WasmMsg = 98 | | { execute: Execute } 99 | | { instantiate: Instantiate } 100 | | { instantiate2: Instantiate2 } 101 | | { migrate: Migrate }; 102 | 103 | /// IBC types 104 | export interface IbcTimeoutBlock { 105 | revision: number; 106 | height: number; 107 | } 108 | 109 | export interface IbcTimeout { 110 | block?: IbcTimeoutBlock; 111 | timestamp?: string; 112 | } 113 | 114 | export type IbcMsgTransfer = { 115 | transfer: { 116 | channel_id: string; 117 | to_address: Address; 118 | amount: Coin; 119 | /// when packet times out, measured on remote chain 120 | timeout: IbcTimeout; 121 | }; 122 | }; 123 | 124 | export type IbcMsgSendPacket = { 125 | send_packet: { 126 | channel_id: string; 127 | data: Binary; 128 | /// when packet times out, measured on remote chain 129 | timeout: IbcTimeout; 130 | }; 131 | }; 132 | 133 | export type IbcMsgCloseChannel = { 134 | close_channel: { channel_id: string }; 135 | }; 136 | 137 | export type IbcMsg = IbcMsgTransfer | IbcMsgSendPacket | IbcMsgCloseChannel; 138 | 139 | export type StakingMsg = 140 | | { delegate: { validator: string; amount: Coin } } 141 | /// This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). 142 | /// `delegator_address` is automatically filled with the current contract's address. 143 | | { undelegate: { validator: string; amount: Coin } } 144 | /// This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). 145 | /// `delegator_address` is automatically filled with the current contract's address. 146 | | { 147 | redelegate: { 148 | src_validator: string; 149 | dst_validator: string; 150 | amount: Coin; 151 | }; 152 | }; 153 | 154 | export type DistributionMsg = 155 | | { 156 | /// This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). 157 | /// `delegator_address` is automatically filled with the current contract's address. 158 | set_withdraw_address: { 159 | /// The `withdraw_address` 160 | address: string; 161 | }; 162 | } 163 | /// This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). 164 | /// `delegator_address` is automatically filled with the current contract's address. 165 | | { 166 | withdraw_delegator_reward: { 167 | /// The `validator_address` 168 | validator: string; 169 | }; 170 | }; 171 | 172 | export enum VoteOption { 173 | Yes, 174 | No, 175 | Abstain, 176 | NoWithVeto, 177 | } 178 | 179 | export interface WeightedVoteOption { 180 | option: VoteOption; 181 | weight: Decimal; 182 | } 183 | 184 | export type GovMsg = 185 | | { 186 | /// This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address. 187 | vote: { 188 | proposal_id: number; 189 | /// The vote option. 190 | /// 191 | /// This should be called "option" for consistency with Cosmos SDK. Sorry for that. 192 | /// See . 193 | vote: VoteOption; 194 | }; 195 | } 196 | /// This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address. 197 | | { 198 | vote_weighted: { 199 | proposal_id: number; 200 | options: WeightedVoteOption[]; 201 | }; 202 | }; 203 | 204 | export type CosmosMsg = 205 | | { 206 | bank: BankMsg; 207 | } 208 | | { 209 | custom: T; 210 | } 211 | | { 212 | staking: StakingMsg; 213 | } 214 | | { 215 | distribution: DistributionMsg; 216 | } 217 | | { 218 | stargate: { 219 | type_url: string; 220 | value: Binary; 221 | }; 222 | } 223 | | { ibc: IbcMsg } 224 | | { wasm: WasmMsg } 225 | | { 226 | gov: GovMsg; 227 | }; 228 | 229 | /// response 230 | 231 | export interface ContractResponse { 232 | messages: SubMsg[]; 233 | events: Event[]; 234 | attributes: Attribute[]; 235 | data: Binary | null; 236 | } 237 | 238 | // general error like javascript error 239 | export class GenericError extends Error { 240 | constructor(msg: string) { 241 | super(`Generic error: ${msg}`); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/common/data.ts: -------------------------------------------------------------------------------- 1 | import { fromHex, toAscii } from '@cosmjs/encoding'; 2 | 3 | // Constants from https://github.com/cosmwasm/cosmwasm/blob/5e04c3c1aa7e278626196de43aa18e9bedbc6000/packages/vm/src/imports.rs#L499 4 | 5 | // In Rust, b"XXX" is the same as creating a bytestring of the ASCII-encoded string "XXX". 6 | export const KEY1 = toAscii('ant'); 7 | export const VALUE1 = toAscii('insect'); 8 | export const KEY2 = toAscii('tree'); 9 | export const VALUE2 = toAscii('plant'); 10 | 11 | export const ECDSA_HASH_HEX = fromHex( 12 | '5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0' 13 | ); 14 | export const ECDSA_SIG_HEX = fromHex( 15 | '207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4' 16 | ); 17 | export const ECDSA_PUBKEY_HEX = fromHex( 18 | '04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73' 19 | ); 20 | 21 | export const EDDSA_MSG_HEX = fromHex(''); 22 | export const EDDSA_SIG_HEX = fromHex( 23 | 'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b' 24 | ); 25 | export const EDDSA_PUBKEY_HEX = fromHex( 26 | 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a' 27 | ); 28 | 29 | export const SECP256K1_MSG_HEX = fromHex( 30 | '5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0' 31 | ); 32 | export const SECP256K1_SIG_HEX = fromHex( 33 | '45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788' 34 | ); 35 | export const RECOVER_PARAM = 1; 36 | export const SECP256K1_PUBKEY_HEX = fromHex( 37 | '044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595' 38 | ); 39 | 40 | export const ED25519_MSG_HEX = fromHex('72'); 41 | export const ED25519_SIG_HEX = fromHex( 42 | '92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00' 43 | ); 44 | export const ED25519_PUBKEY_HEX = fromHex( 45 | '3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c' 46 | ); 47 | 48 | export const SECP256K1_MESSAGE_HEX = fromHex('5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7'); 49 | export const ETHEREUM_MESSAGE = 'connect all the things'; 50 | export const ETHEREUM_SIGNATURE_HEX = fromHex('dada130255a447ecf434a2df9193e6fbba663e4546c35c075cd6eea21d8c7cb1714b9b65a4f7f604ff6aad55fba73f8c36514a512bbbba03709b37069194f8a41b'); 51 | export const ETHEREUM_SIGNER_ADDRESS = '0x12890D2cce102216644c59daE5baed380d84830c'; 52 | export const ED25519_MESSAGE_HEX = fromHex('af82'); 53 | export const ED25519_SIGNATURE_HEX = fromHex('6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a'); 54 | export const ED25519_PUBLIC_KEY_HEX = fromHex('fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025'); 55 | export const ED25519_MESSAGE2_HEX = fromHex('72'); 56 | export const ED25519_SIGNATURE2_HEX = fromHex('92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00'); 57 | export const ED25519_PUBLIC_KEY2_HEX = fromHex('3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c'); 58 | 59 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/common/vm.ts: -------------------------------------------------------------------------------- 1 | import { fromAscii, fromBase64 } from '@cosmjs/encoding'; 2 | import { readFileSync } from 'fs'; 3 | import { 4 | VMInstance, 5 | IBackend, 6 | BasicBackendApi, 7 | BinaryKVIterStorage, 8 | BasicQuerier, 9 | Region, 10 | } from '../../src'; 11 | import { KEY1, VALUE1, KEY2, VALUE2 } from './data'; 12 | import path from 'path'; 13 | 14 | export function wrapResult(res: any) { 15 | if (res instanceof Region) res = res.json; 16 | 17 | if (typeof res !== 'object') throw new Error('StdResult is not an object'); 18 | 19 | const isOk = !!res.ok; 20 | return { 21 | isOk, 22 | isErr: !isOk, 23 | unwrap: () => res.ok, 24 | unwrap_err: () => res.err, 25 | }; 26 | } 27 | 28 | export const createVM = async (): Promise => { 29 | const wasmByteCode = readFileSync( 30 | path.resolve(__dirname, '..', '..', 'testdata', 'v1.0', 'hackatom.wasm') 31 | ); 32 | const backend: IBackend = { 33 | backend_api: new BasicBackendApi('terra'), 34 | storage: new BinaryKVIterStorage(), 35 | querier: new BasicQuerier(), 36 | }; 37 | 38 | const vm = new VMInstance(backend); 39 | vm.backend.storage.set(KEY1, VALUE1); 40 | vm.backend.storage.set(KEY2, VALUE2); 41 | 42 | await vm.build(wasmByteCode); 43 | return vm; 44 | }; 45 | 46 | export const writeData = (vm: VMInstance, data: Uint8Array): Region => { 47 | return vm.allocate_bytes(data); 48 | }; 49 | 50 | export const writeObject = (vm: VMInstance, data: [Uint8Array]): Region => { 51 | return vm.allocate_json(data); 52 | }; 53 | 54 | export function parseBase64Response(data: string): any { 55 | let bytes: Uint8Array; 56 | try { 57 | bytes = fromBase64(data); 58 | } catch (_) { 59 | throw new Error( 60 | `Data value is not base64-encoded: ${JSON.stringify(data)}` 61 | ); 62 | } 63 | 64 | let str: string; 65 | try { 66 | str = fromAscii(bytes); 67 | } catch (_) { 68 | throw new Error( 69 | `Data value is not ASCII encoded: ${JSON.stringify(bytes)}` 70 | ); 71 | } 72 | 73 | try { 74 | return JSON.parse(str); 75 | } catch (_) { 76 | throw new Error(`Data value is not valid JSON: ${str}`); 77 | } 78 | } 79 | 80 | export function expectResponseToBeOk(json: object) { 81 | try { 82 | expect((json as { ok: string }).ok).toBeDefined(); 83 | } catch (_) { 84 | throw new Error( 85 | `Expected response to be ok; instead got: ${JSON.stringify(json)}` 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/ibc.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import path from 'path'; 3 | import { BinaryKVIterStorage, VMInstance } from '../src'; 4 | import { BasicBackendApi, BasicQuerier, IBackend } from '../src/backend'; 5 | 6 | const wasmByteCode = readFileSync( 7 | path.resolve(__dirname, '..', 'testdata', 'v1.1', 'ibc_reflect.wasm') 8 | ); 9 | const backend: IBackend = { 10 | backend_api: new BasicBackendApi('terra'), 11 | storage: new BinaryKVIterStorage(), 12 | querier: new BasicQuerier(), 13 | }; 14 | 15 | const vm = new VMInstance(backend); 16 | const mockEnv = { 17 | block: { 18 | height: 1337, 19 | time: '2000000000', 20 | chain_id: 'columbus-5', 21 | }, 22 | contract: { 23 | address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76', 24 | }, 25 | }; 26 | 27 | const mockInfo = { 28 | sender: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 29 | funds: [], 30 | }; 31 | 32 | describe('CosmWasmVM', () => { 33 | it('reply', async () => { 34 | await vm.build(wasmByteCode); 35 | 36 | let region = vm.instantiate(mockEnv, mockInfo, { reflect_code_id: 101 }); 37 | expect('ok' in region).toBeTruthy(); 38 | 39 | region = vm.ibc_channel_open(mockEnv, { 40 | open_init: { 41 | channel: { 42 | endpoint: { 43 | port_id: 'my_port', 44 | channel_id: 'channel-0', 45 | }, 46 | counterparty_endpoint: { 47 | port_id: 'their_port', 48 | channel_id: 'channel-7', 49 | }, 50 | order: 'ORDER_ORDERED', 51 | version: 'ibc-reflect-v1', 52 | connection_id: 'connection-2', 53 | }, 54 | }, 55 | }); 56 | 57 | expect('ok' in region).toBeTruthy(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/integration/burner.test.ts: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // Burner is an example contract for migration (introduced in CW 0.9). 3 | // It cannot be instantiated, but an existing contract can be migrated 4 | // to the Burner to permanently burn the contract and perform basic 5 | // cleanup. 6 | // ----- 7 | // Rust Sources: https://github.com/CosmWasm/cosmwasm/tree/main/contracts/burner 8 | import { readFileSync } from 'fs'; 9 | import { VMInstance } from '../../src/instance'; 10 | import { 11 | BasicBackendApi, 12 | BinaryKVIterStorage, 13 | BasicQuerier, 14 | IBackend, 15 | Order, 16 | } from '../../src/backend'; 17 | import { toAscii } from '@cosmjs/encoding'; 18 | import { ContractResponse, Env, MessageInfo } from '../../src/types'; 19 | import path from 'path'; 20 | 21 | class MockQuerier extends BasicQuerier { 22 | handleQuery(request: any): any { 23 | return { amount: [{ denom: 'earth', amount: '1000' }] }; 24 | } 25 | } 26 | 27 | const wasmBytecode = readFileSync( 28 | path.resolve(__dirname, '..', '..', 'testdata', 'v1.1', 'burner.wasm') 29 | ); 30 | const backend: IBackend = { 31 | backend_api: new BasicBackendApi('terra'), 32 | storage: new BinaryKVIterStorage(), 33 | querier: new MockQuerier(), 34 | }; 35 | 36 | const creator = 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3'; 37 | const payout = 'terra163u9pnx5sucsk537zpn82fzxjgdp44xehfdy4x'; 38 | 39 | const mockEnv: Env = { 40 | block: { 41 | height: 1337, 42 | time: '2000000000', 43 | chain_id: 'columbus-5', 44 | }, 45 | contract: { address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76' }, 46 | }; 47 | 48 | let vm: VMInstance; 49 | describe('burner', () => { 50 | beforeEach(async () => { 51 | vm = new VMInstance(backend); 52 | await vm.build(wasmBytecode); 53 | }); 54 | 55 | // port of https://github.com/CosmWasm/cosmwasm/blob/f6a0485088f1084379a5655bcc2956526290c09f/contracts/burner/tests/integration.rs#L32 56 | it('instantiate_fails', async () => { 57 | // Arrange 58 | const mockInfo: MessageInfo = { 59 | sender: creator, 60 | funds: [{ denom: 'earth', amount: '1000' }], 61 | }; 62 | 63 | // Act 64 | const instantiateResponse = vm.instantiate(mockEnv, mockInfo, {}); 65 | 66 | // Assert 67 | expect(instantiateResponse).toEqual({ 68 | error: 'Generic error: You can only use this contract for migrations', 69 | }); 70 | }); 71 | 72 | // port of https://github.com/CosmWasm/cosmwasm/blob/f6a0485088f1084379a5655bcc2956526290c09f/contracts/burner/tests/integration.rs#L47 73 | // TODO: querier not yet implemented 74 | // test verifies two things: 75 | // 1) remaining coins in storage (123456 gold) are sent to payout address 76 | // 2) storage is purged 77 | it('migrate_cleans_up_data', async () => { 78 | // Arrange 79 | // TODO: VM instance w/ coin data & Bank module 80 | // const vm = new VMInstance(backend, [{ denom: 'gold', amount: '123456' }]); 81 | const storage = vm.backend.storage; 82 | 83 | storage.set(toAscii('foo'), toAscii('bar')); 84 | storage.set(toAscii('key2'), toAscii('data2')); 85 | storage.set(toAscii('key3'), toAscii('cool stuff')); 86 | 87 | // TODO: support scan(null, null, Order) 88 | let iterId = storage.scan(null, null, Order.Ascending); 89 | let cnt = storage.all(iterId); 90 | expect(cnt.length).toStrictEqual(3); 91 | 92 | const migrateMsg = { payout }; 93 | 94 | // Act 95 | const res = vm.migrate(mockEnv, migrateMsg) as { ok: ContractResponse }; 96 | 97 | // Assert 98 | expect(res.ok.messages.length).toStrictEqual(1); 99 | expect(res.ok.messages[0]).toBeDefined(); 100 | // TODO: msg is SubMsg w/ BankMsg::Send to payout of all coins in contract 101 | iterId = storage.scan(null, null, Order.Ascending); 102 | cnt = storage.all(iterId); 103 | expect(cnt.length).toStrictEqual(0); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/integration/cyberpunk.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { VMInstance } from '../../src/instance'; 3 | import { 4 | BasicBackendApi, 5 | BinaryKVIterStorage, 6 | BasicQuerier, 7 | IBackend, 8 | } from '../../src/backend'; 9 | import type { Env, MessageInfo } from '../../src/types'; 10 | import { parseBase64Response, wrapResult } from '../common/vm'; 11 | import { Environment } from '../../src'; 12 | import path from 'path'; 13 | 14 | const wasmBytecode = readFileSync( 15 | path.resolve(__dirname, '..', '..', 'testdata', 'v1.1', 'cyberpunk.wasm') 16 | ); 17 | const backend: IBackend = { 18 | backend_api: new BasicBackendApi('terra'), 19 | storage: new BinaryKVIterStorage(), 20 | querier: new BasicQuerier(), 21 | }; 22 | 23 | const creator = 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3'; 24 | 25 | const mockEnv: Env = { 26 | block: { 27 | height: 1337, 28 | time: '2000000000', 29 | chain_id: 'columbus-5', 30 | }, 31 | contract: { address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76' }, 32 | transaction: null, 33 | }; 34 | 35 | const mockInfo: MessageInfo = { 36 | sender: creator, 37 | funds: [], 38 | }; 39 | 40 | let vm: VMInstance; 41 | describe('cyberpunk', () => { 42 | beforeEach(async () => { 43 | vm = new VMInstance(backend); 44 | await vm.build(wasmBytecode); 45 | }); 46 | 47 | // port of https://github.com/CosmWasm/cosmwasm/blob/f6a0485088f1084379a5655bcc2956526290c09f/contracts/cyberpunk/tests/integration.rs#L30 48 | it.skip('execute_argon2', async () => { 49 | // gas limit not implemented 50 | // Arrange 51 | const env = new Environment(backend.backend_api, 100_000_000_000_000); 52 | vm = new VMInstance(backend, env); 53 | const initRes = vm.instantiate(mockEnv, mockInfo, {}) as any; 54 | expect(initRes.messages.length).toStrictEqual(0); 55 | 56 | const gasBefore = vm.remainingGas; 57 | 58 | // Act 59 | const executeRes = vm.execute(mockEnv, mockInfo, { 60 | mem_cost: 256, 61 | time_cost: 5, 62 | }); 63 | 64 | // Assert 65 | // TODO 66 | }); 67 | 68 | it('test_env', async () => { 69 | // Arrange 70 | const initRes = wrapResult(vm.instantiate(mockEnv, mockInfo, {})).unwrap(); 71 | expect(initRes.messages.length).toStrictEqual(0); 72 | 73 | // Act 1 74 | const res = wrapResult( 75 | vm.execute(mockEnv, mockInfo, { mirror_env: {} }) 76 | ).unwrap(); 77 | 78 | // Assert 1 79 | expect(res.data).toBeDefined(); 80 | let receivedEnv = parseBase64Response(res.data); 81 | expect(receivedEnv).toEqual(mockEnv); 82 | 83 | // Act 2 84 | const data = wrapResult(vm.query(mockEnv, { mirror_env: {} })).unwrap(); 85 | receivedEnv = parseBase64Response(data); 86 | expect(receivedEnv).toEqual(mockEnv); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/integration/hackatom.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { VMInstance } from '../../src/instance'; 3 | import { 4 | BasicBackendApi, 5 | BinaryKVIterStorage, 6 | BasicQuerier, 7 | } from '../../src/backend'; 8 | import { fromBase64 } from '@cosmjs/encoding'; 9 | import { expectResponseToBeOk, parseBase64Response } from '../common/vm'; 10 | import path from 'path'; 11 | 12 | type HackatomQueryRequest = { 13 | bank: { 14 | all_balances: { 15 | address: string; 16 | }; 17 | }; 18 | }; 19 | class HackatomMockQuerier extends BasicQuerier { 20 | private balances: Map = 21 | new Map(); 22 | 23 | update_balance( 24 | addr: string, 25 | balance: { amount: string; denom: string }[] 26 | ): { amount: string; denom: string }[] { 27 | this.balances.set(addr, balance); 28 | return balance; 29 | } 30 | 31 | handleQuery(queryRequest: HackatomQueryRequest): any { 32 | if ('bank' in queryRequest) { 33 | if ('all_balances' in queryRequest.bank) { 34 | const { address } = queryRequest.bank.all_balances; 35 | return { amount: this.balances.get(address) || [] }; 36 | } 37 | } 38 | 39 | throw new Error(`unknown query: ${JSON.stringify(queryRequest)}`); 40 | } 41 | } 42 | 43 | const wasmBytecode = readFileSync( 44 | path.resolve(__dirname, '..', '..', 'testdata', 'v1.1', 'hackatom.wasm') 45 | ); 46 | 47 | const verifier = 'terra1kzsrgcktshvqe9p089lqlkadscqwkezy79t8y9'; 48 | const beneficiary = 'terra1zdpgj8am5nqqvht927k3etljyl6a52kwqup0je'; 49 | const creator = 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3'; 50 | const mockContractAddr = 'cosmos2contract'; 51 | 52 | const mockEnv = { 53 | block: { 54 | height: 12345, 55 | time: '1571797419879305533', 56 | chain_id: 'cosmos-testnet-14002', 57 | }, 58 | contract: { address: mockContractAddr }, 59 | }; 60 | 61 | const mockInfo: { sender: string; funds: { amount: string; denom: string }[] } = 62 | { 63 | sender: creator, 64 | funds: [], 65 | }; 66 | 67 | let vm: VMInstance; 68 | describe('hackatom', () => { 69 | let querier: HackatomMockQuerier; 70 | 71 | beforeEach(async () => { 72 | querier = new HackatomMockQuerier(); 73 | vm = new VMInstance({ 74 | backend_api: new BasicBackendApi('terra'), 75 | storage: new BinaryKVIterStorage(), 76 | querier, 77 | }); 78 | await vm.build(wasmBytecode); 79 | }); 80 | 81 | it('proper_initialization', async () => { 82 | // Act 83 | const instantiateResponse = vm.instantiate(mockEnv, mockInfo, { 84 | verifier, 85 | beneficiary, 86 | }); 87 | 88 | // Assert 89 | expect(instantiateResponse).toEqual({ 90 | ok: { 91 | attributes: [{ key: 'Let the', value: 'hacking begin' }], 92 | data: null, 93 | events: [], 94 | messages: [], 95 | }, 96 | }); 97 | expectVerifierToBe(verifier); 98 | }); 99 | 100 | it('instantiate_and_query', async () => { 101 | // Arrange 102 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 103 | 104 | // Act 105 | const queryResponse = vm.query(mockEnv, { verifier: {} }); 106 | 107 | // Assert 108 | expectResponseToBeOk(queryResponse); 109 | expect(parseBase64OkResponse(queryResponse)).toEqual({ verifier }); 110 | }); 111 | 112 | it('migrate_verifier', async () => { 113 | // Arrange 114 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 115 | 116 | // Act 117 | const newVerifier = 'terra1h8ljdmae7lx05kjj79c9ekscwsyjd3yr8wyvdn'; 118 | let response = vm.migrate(mockEnv, { verifier: newVerifier }); 119 | 120 | // Assert 121 | expectResponseToBeOk(response); 122 | expect((response as { ok: { messages: any[] } }).ok.messages.length).toBe( 123 | 0 124 | ); 125 | expectVerifierToBe(newVerifier); 126 | }); 127 | 128 | it('sudo_can_steal_tokens', async () => { 129 | const instantiateResponse = vm.instantiate(mockEnv, mockInfo, { 130 | verifier, 131 | beneficiary, 132 | }); 133 | 134 | // sudo takes any tax it wants 135 | const recipient = 'community-pool'; 136 | const amount = [{ amount: '700', denom: 'gold' }]; 137 | const sysMsg = { 138 | steal_funds: { 139 | recipient, 140 | amount, 141 | }, 142 | }; 143 | const sudoResponse = vm.sudo(mockEnv, sysMsg); 144 | // Assert 145 | expectResponseToBeOk(sudoResponse); 146 | 147 | expect((sudoResponse as any).ok.messages.length).toBe(1); 148 | expect((sudoResponse as any).ok.messages[0].msg).toEqual({ 149 | bank: { send: { to_address: recipient, amount } }, 150 | }); 151 | }); 152 | 153 | it('querier_callbacks_work', async () => { 154 | // Arrange 155 | const richAddress = 'foobar'; 156 | const richBalance = [{ amount: '10000', denom: 'gold' }]; 157 | querier.update_balance(richAddress, richBalance); 158 | 159 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 160 | 161 | // Act 162 | const queryResponse = vm.query(mockEnv, { 163 | other_balance: { address: richAddress }, 164 | }); 165 | const queryResponseWrongAddress = vm.query(mockEnv, { 166 | other_balance: { address: 'other address' }, 167 | }); 168 | 169 | // Assert 170 | expectResponseToBeOk(queryResponse); 171 | expect(parseBase64OkResponse(queryResponse).amount).toEqual(richBalance); 172 | 173 | expectResponseToBeOk(queryResponseWrongAddress); 174 | expect(parseBase64OkResponse(queryResponseWrongAddress).amount).toEqual([]); 175 | }); 176 | 177 | it('fails_on_bad_init', async () => { 178 | // Act 179 | const response = vm.instantiate( 180 | mockEnv, 181 | { funds: [{ amount: '1000', denom: 'earth' }] } as any, // invalid info message, missing sender field 182 | { verifier, beneficiary } 183 | ); 184 | 185 | // Assert 186 | expect((response as { error: string }).error.indexOf('Error parsing')).toBe( 187 | 0 188 | ); 189 | }); 190 | 191 | it('execute_release_works', async () => { 192 | // Arrange 193 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 194 | querier.update_balance(mockContractAddr, [ 195 | { amount: '1000', denom: 'earth' }, 196 | ]); 197 | 198 | // Act 199 | const execResponse = vm.execute( 200 | mockEnv, 201 | { sender: verifier, funds: [] }, 202 | { release: {} } 203 | ); 204 | 205 | // Assert 206 | expectResponseToBeOk(execResponse); 207 | 208 | expect((execResponse as any).ok.messages.length).toBe(1); 209 | expect((execResponse as any).ok.messages[0].msg.bank.send.to_address).toBe( 210 | beneficiary 211 | ); 212 | expect( 213 | (execResponse as any).ok.messages[0].msg.bank.send.amount 214 | ).toStrictEqual([{ amount: '1000', denom: 'earth' }]); 215 | 216 | expect((execResponse as any).ok.attributes[0]).toStrictEqual({ 217 | key: 'action', 218 | value: 'release', 219 | }); 220 | expect((execResponse as any).ok.attributes[1]).toStrictEqual({ 221 | key: 'destination', 222 | value: beneficiary, 223 | }); 224 | 225 | expect(fromBase64((execResponse as any).ok.data)[0]).toBe(240); // 0xF0 226 | expect(fromBase64((execResponse as any).ok.data)[1]).toBe(11); // 0x0B 227 | expect(fromBase64((execResponse as any).ok.data)[2]).toBe(170); // 0xAA 228 | }); 229 | 230 | it('execute_release_fails_for_wrong_sender', async () => { 231 | // Arrange 232 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 233 | querier.update_balance(mockContractAddr, [ 234 | { amount: '1000', denom: 'earth' }, 235 | ]); 236 | 237 | // Act 238 | const execResponse = vm.execute( 239 | mockEnv, 240 | { sender: beneficiary, funds: [] }, 241 | { release: {} } 242 | ); 243 | 244 | // Assert 245 | expect((execResponse as any).error).toBe('Unauthorized'); 246 | }); 247 | 248 | it('execute_panic', async () => { 249 | // Arrange 250 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 251 | 252 | // Act 253 | expect(() => vm.execute(mockEnv, mockInfo, { panic: {} })).toThrow(); 254 | }); 255 | 256 | it('execute_user_errors_in_api_calls', async () => { 257 | // Arrange 258 | vm.instantiate(mockEnv, mockInfo, { verifier, beneficiary }); 259 | 260 | // Act 261 | expect(() => 262 | vm.execute(mockEnv, mockInfo, { user_errors_in_api_calls: {} }) 263 | ).toThrow(); 264 | }); 265 | }); 266 | 267 | // Helpers 268 | 269 | function expectVerifierToBe(addr: string) { 270 | const queryResponse = vm.query(mockEnv, { verifier: {} }); 271 | const verifier = parseBase64OkResponse(queryResponse); 272 | expect(verifier).toEqual({ verifier: addr }); 273 | } 274 | 275 | function parseBase64OkResponse(json: object): any { 276 | const data = (json as { ok: string }).ok; 277 | if (!data) { 278 | throw new Error( 279 | `Response indicates an error state: ${JSON.stringify(json)}` 280 | ); 281 | } 282 | 283 | return parseBase64Response(data); 284 | } 285 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/integration/queue.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { VMInstance } from '../../src/instance'; 3 | import { 4 | BasicBackendApi, 5 | BasicQuerier, 6 | IBackend, 7 | BinaryKVIterStorage, 8 | } from '../../src/backend'; 9 | import { expectResponseToBeOk, parseBase64Response } from '../common/vm'; 10 | import { Environment } from '../../src'; 11 | import path from 'path'; 12 | 13 | const wasmBytecode = readFileSync( 14 | path.resolve(__dirname, '..', '..', 'testdata', 'v1.1', 'queue.wasm') 15 | ); 16 | 17 | const creator = 'creator'; 18 | const mockContractAddr = 'cosmos2contract'; 19 | 20 | const mockEnv = { 21 | block: { 22 | height: 12345, 23 | time: '1571797419879305533', 24 | chain_id: 'cosmos-testnet-14002', 25 | }, 26 | contract: { address: mockContractAddr }, 27 | }; 28 | 29 | const mockInfo: { sender: string; funds: { amount: string; denom: string }[] } = 30 | { 31 | sender: creator, 32 | funds: [], 33 | }; 34 | 35 | let vm: VMInstance; 36 | describe('queue', () => { 37 | beforeEach(async () => { 38 | const backend: IBackend = { 39 | backend_api: new BasicBackendApi('terra'), 40 | storage: new BinaryKVIterStorage(), 41 | querier: new BasicQuerier(), 42 | }; 43 | const env = new Environment(backend.backend_api, 100_000_000_000_000); 44 | vm = new VMInstance(backend, env); 45 | await vm.build(wasmBytecode); 46 | }); 47 | 48 | it('instantiate_and_query', async () => { 49 | // Arrange 50 | const instantiateResponse = vm.instantiate(mockEnv, mockInfo, {}); 51 | 52 | // Act 53 | const countResponse = vm.query(mockEnv, { count: {} }); 54 | const sumResponse = vm.query(mockEnv, { sum: {} }); 55 | 56 | // Assert 57 | expect((instantiateResponse as any).ok.messages.length).toBe(0); 58 | 59 | expectResponseToBeOk(countResponse); 60 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 0 }); 61 | 62 | expectResponseToBeOk(sumResponse); 63 | expect(parseBase64OkResponse(sumResponse)).toEqual({ sum: 0 }); 64 | }); 65 | 66 | it('push_and_query', () => { 67 | // Arrange 68 | vm.instantiate(mockEnv, mockInfo, {}); 69 | 70 | // Act 71 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 25 } }); 72 | 73 | // Assert 74 | const countResponse = vm.query(mockEnv, { count: {} }); 75 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 1 }); 76 | 77 | const sumResponse = vm.query(mockEnv, { sum: {} }); 78 | expect(parseBase64OkResponse(sumResponse)).toEqual({ sum: 25 }); 79 | }); 80 | 81 | it('multiple_push', () => { 82 | // Arrange 83 | vm.instantiate(mockEnv, mockInfo, {}); 84 | 85 | // Act 86 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 25 } }); 87 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 35 } }); 88 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 45 } }); 89 | 90 | // Assert 91 | const countResponse = vm.query(mockEnv, { count: {} }); 92 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 3 }); 93 | 94 | const sumResponse = vm.query(mockEnv, { sum: {} }); 95 | expect(parseBase64OkResponse(sumResponse)).toEqual({ sum: 105 }); 96 | }); 97 | 98 | it('push_and_pop', () => { 99 | // Arrange 100 | vm.instantiate(mockEnv, mockInfo, {}); 101 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 25 } }); 102 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 17 } }); 103 | 104 | // Act 105 | const dequeueResponse = vm.execute(mockEnv, mockInfo, { dequeue: {} }); 106 | 107 | // Assert 108 | expect(parseBase64Response((dequeueResponse as any).ok.data)).toEqual({ 109 | value: 25, 110 | }); 111 | 112 | const countResponse = vm.query(mockEnv, { count: {} }); 113 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 1 }); 114 | 115 | const sumResponse = vm.query(mockEnv, { sum: {} }); 116 | expect(parseBase64OkResponse(sumResponse)).toEqual({ sum: 17 }); 117 | }); 118 | 119 | it('push_and_reduce', () => { 120 | // Arrange 121 | vm.instantiate(mockEnv, mockInfo, {}); 122 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 40 } }); 123 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 15 } }); 124 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 85 } }); 125 | vm.execute(mockEnv, mockInfo, { enqueue: { value: -10 } }); 126 | 127 | // Act 128 | const reducerResponse = vm.query(mockEnv, { reducer: {} }); 129 | 130 | // Assert 131 | expect(parseBase64OkResponse(reducerResponse).counters).toStrictEqual([ 132 | [40, 85], 133 | [15, 125], 134 | [85, 0], 135 | [-10, 140], 136 | ]); 137 | }); 138 | 139 | it('migrate_works', () => { 140 | // Arrange 141 | vm.instantiate(mockEnv, mockInfo, {}); 142 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 25 } }); 143 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 17 } }); 144 | 145 | // Act 146 | const migrateResponse = vm.migrate(mockEnv, {}); 147 | 148 | // Assert 149 | expect((migrateResponse as any).ok.messages.length).toEqual(0); 150 | 151 | const countResponse = vm.query(mockEnv, { count: {} }); 152 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 3 }); 153 | 154 | const sumResponse = vm.query(mockEnv, { sum: {} }); 155 | expect(parseBase64OkResponse(sumResponse)).toEqual({ sum: 303 }); 156 | }); 157 | 158 | it('query_list', () => { 159 | // Arrange 160 | vm.instantiate(mockEnv, mockInfo, {}); 161 | 162 | for (let i = 0; i < 37; i++) { 163 | vm.execute(mockEnv, mockInfo, { enqueue: { value: 40 } }); 164 | } 165 | 166 | for (let i = 0; i < 25; i++) { 167 | vm.execute(mockEnv, mockInfo, { dequeue: {} }); 168 | } 169 | 170 | // Act 171 | const listResponse = vm.query(mockEnv, { list: {} }); 172 | 173 | // Assert 174 | const countResponse = vm.query(mockEnv, { count: {} }); 175 | expect(parseBase64OkResponse(countResponse)).toEqual({ count: 12 }); 176 | 177 | const list = parseBase64OkResponse(listResponse); 178 | 179 | expect(list.empty).toStrictEqual([]); 180 | expect(list.early).toStrictEqual([25, 26, 27, 28, 29, 30, 31]); 181 | expect(list.late).toStrictEqual([32, 33, 34, 35, 36]); 182 | }); 183 | 184 | it('query_open_iterators', async () => { 185 | // Arrange 186 | vm.instantiate(mockEnv, mockInfo, {}); 187 | 188 | // Act 189 | const response1 = vm.query(mockEnv, { open_iterators: { count: 1 } }); 190 | const response2 = vm.query(mockEnv, { open_iterators: { count: 2 } }); 191 | const response3 = vm.query(mockEnv, { open_iterators: { count: 321 } }); 192 | 193 | // Assert 194 | expectResponseToBeOk(response1); 195 | expectResponseToBeOk(response2); 196 | expectResponseToBeOk(response3); 197 | }); 198 | }); 199 | 200 | // Helpers 201 | 202 | function parseBase64OkResponse(json: object): any { 203 | const data = (json as { ok: string }).ok; 204 | if (!data) { 205 | throw new Error( 206 | `Response indicates an error state: ${JSON.stringify(json)}` 207 | ); 208 | } 209 | 210 | return parseBase64Response(data); 211 | } 212 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/sample.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import path from 'path'; 3 | import { VMInstance } from '../src/instance'; 4 | import { 5 | BasicBackendApi, 6 | BinaryKVIterStorage, 7 | BasicQuerier, 8 | IBackend, 9 | } from '../src/backend'; 10 | 11 | const wasmBytecode = readFileSync( 12 | path.resolve(__dirname, '..', 'testdata', 'v1.0', 'cosmwasm_vm_test.wasm') 13 | ); 14 | const backend: IBackend = { 15 | backend_api: new BasicBackendApi('terra'), 16 | storage: new BinaryKVIterStorage(), 17 | querier: new BasicQuerier(), 18 | }; 19 | 20 | const vm = new VMInstance(backend); 21 | const mockEnv = { 22 | block: { 23 | height: 1337, 24 | time: '2000000000', 25 | chain_id: 'columbus-5', 26 | }, 27 | contract: { 28 | address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76', 29 | }, 30 | }; 31 | 32 | const mockInfo = { 33 | sender: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 34 | funds: [], 35 | }; 36 | 37 | describe('CosmWasmVM', () => { 38 | it('instantiates', async () => { 39 | await vm.build(wasmBytecode); 40 | 41 | const json = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 42 | const actual = { 43 | ok: { 44 | attributes: [ 45 | { key: 'method', value: 'instantiate' }, 46 | { 47 | key: 'owner', 48 | value: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 49 | }, 50 | { key: 'count', value: '20' }, 51 | ], 52 | data: null, 53 | events: [], 54 | messages: [], 55 | }, 56 | }; 57 | expect(json).toEqual(actual); 58 | }); 59 | 60 | it('execute', async () => { 61 | await vm.build(wasmBytecode); 62 | 63 | let json = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 64 | let currentGasUsed = vm.gasUsed; 65 | json = vm.execute(mockEnv, mockInfo, { increment: {} }); 66 | console.log('gasUsed', vm.gasUsed - currentGasUsed); 67 | console.log(json); 68 | const actual = { 69 | ok: { 70 | attributes: [{ key: 'method', value: 'try_increment' }], 71 | data: null, 72 | events: [], 73 | messages: [], 74 | }, 75 | }; 76 | expect(json).toEqual(actual); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/vm.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { BinaryKVIterStorage, VMInstance } from '../src'; 3 | import { BasicBackendApi, BasicQuerier, IBackend } from '../src/backend'; 4 | import { writeData } from './common/vm'; 5 | import * as testData from './common/data'; 6 | import path from 'path'; 7 | 8 | const wasmByteCode = readFileSync( 9 | path.resolve(__dirname, '..', 'testdata', 'v1.0', 'cosmwasm_vm_test.wasm') 10 | ); 11 | 12 | const cwMachineBytecode = readFileSync( 13 | path.resolve(__dirname, '..', 'testdata', 'v1.0', 'cw_machine-aarch64.wasm') 14 | ); 15 | const backend: IBackend = { 16 | backend_api: new BasicBackendApi('terra'), 17 | storage: new BinaryKVIterStorage(), 18 | querier: new BasicQuerier(), 19 | }; 20 | 21 | const vm = new VMInstance(backend); 22 | const mockEnv = { 23 | block: { 24 | height: 1337, 25 | time: '2000000000', 26 | chain_id: 'columbus-5', 27 | }, 28 | contract: { 29 | address: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76', 30 | }, 31 | }; 32 | 33 | const mockInfo = { 34 | sender: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 35 | funds: [], 36 | }; 37 | 38 | describe('CosmWasmVM', () => { 39 | it('instantiates', async () => { 40 | await vm.build(wasmByteCode); 41 | 42 | const json = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 43 | const actual = { 44 | ok: { 45 | attributes: [ 46 | { key: 'method', value: 'instantiate' }, 47 | { 48 | key: 'owner', 49 | value: 'terra1337xewwfv3jdjuz8e0nea9vd8dpugc0k2dcyt3', 50 | }, 51 | { key: 'count', value: '20' }, 52 | ], 53 | data: null, 54 | events: [], 55 | messages: [], 56 | }, 57 | }; 58 | expect(json).toEqual(actual); 59 | }); 60 | 61 | it('execute', async () => { 62 | await vm.build(wasmByteCode); 63 | 64 | let json = vm.instantiate(mockEnv, mockInfo, { count: 20 }); 65 | json = vm.execute(mockEnv, mockInfo, { increment: {} }); 66 | const actual = { 67 | ok: { 68 | attributes: [{ key: 'method', value: 'try_increment' }], 69 | data: null, 70 | events: [], 71 | messages: [], 72 | }, 73 | }; 74 | expect(json).toEqual(actual); 75 | }); 76 | 77 | it('reply', async () => { 78 | await vm.build(cwMachineBytecode); 79 | 80 | let json = vm.instantiate(mockEnv, mockInfo, {}); 81 | json = vm.reply(mockEnv, { 82 | id: 1, 83 | result: { 84 | ok: { 85 | events: [{ type: 'wasm', attributes: [{ key: 'k', value: 'v' }] }], 86 | data: null, 87 | }, 88 | }, 89 | }); 90 | expect('ok' in json).toBeTruthy(); 91 | 92 | json = vm.reply(mockEnv, { 93 | id: 2, 94 | result: { 95 | ok: { 96 | events: [{ type: 'wasm', attributes: [{ key: 'k', value: 'v' }] }], 97 | data: null, 98 | }, 99 | }, 100 | }); 101 | expect('error' in json).toBeTruthy(); 102 | }); 103 | 104 | it('serializes', async () => { 105 | // Arrange 106 | await vm.build(wasmByteCode); 107 | vm.instantiate(mockEnv, mockInfo, { count: 20 }); 108 | 109 | // Act 110 | const json = JSON.stringify(vm); 111 | 112 | // Assert 113 | expect(json).toBeDefined(); 114 | }); 115 | 116 | it('serializes after edda usage', async () => { 117 | // Arrange 118 | await vm.build(wasmByteCode); 119 | vm.instantiate(mockEnv, mockInfo, { count: 20 }); 120 | 121 | const hashPtr = writeData(vm, testData.EDDSA_MSG_HEX); 122 | const sigPtr = writeData(vm, testData.EDDSA_SIG_HEX); 123 | const pubkeyPtr = writeData(vm, testData.EDDSA_PUBKEY_HEX); 124 | vm.do_ed25519_verify(hashPtr, sigPtr, pubkeyPtr); 125 | 126 | // Act 127 | const json = JSON.stringify(vm); 128 | 129 | // Assert 130 | expect(json).toBeDefined(); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/test/zk.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { fromAscii, fromBase64 } from '@cosmjs/encoding'; 3 | import { Env, MessageInfo } from '../src/types'; 4 | import { VMInstance } from '../src'; 5 | 6 | import { 7 | BasicBackendApi, 8 | BinaryKVIterStorage, 9 | BasicQuerier, 10 | IBackend, 11 | } from '../src/backend'; 12 | 13 | import { 14 | Poseidon, 15 | curve_hash, 16 | groth16_verify, 17 | keccak_256, 18 | sha256, 19 | } from '@oraichain/cosmwasm-vm-zk'; 20 | import path from 'path'; 21 | 22 | const poseidon = new Poseidon(); 23 | 24 | export class ZkBackendApi extends BasicBackendApi { 25 | poseidon_hash( 26 | left_input: Uint8Array, 27 | right_input: Uint8Array, 28 | curve: number 29 | ): Uint8Array { 30 | return poseidon.hash(left_input, right_input, curve); 31 | } 32 | curve_hash(input: Uint8Array, curve: number): Uint8Array { 33 | return curve_hash(input, curve); 34 | } 35 | groth16_verify( 36 | input: Uint8Array, 37 | proof: Uint8Array, 38 | vk: Uint8Array, 39 | curve: number 40 | ): boolean { 41 | return groth16_verify(input, proof, vk, curve); 42 | } 43 | keccak_256(input: Uint8Array): Uint8Array { 44 | return keccak_256(input); 45 | } 46 | sha256(input: Uint8Array): Uint8Array { 47 | return sha256(input); 48 | } 49 | } 50 | 51 | const wasmBytecode = readFileSync( 52 | path.resolve(__dirname, '..', 'testdata', 'v1.1', 'zk.wasm') 53 | ); 54 | const backend: IBackend = { 55 | backend_api: new ZkBackendApi('terra'), 56 | storage: new BinaryKVIterStorage(), 57 | querier: new BasicQuerier(), 58 | }; 59 | 60 | const vm = new VMInstance(backend); 61 | const mockEnv: Env = { 62 | block: { 63 | height: 1337, 64 | time: '2000000000', 65 | chain_id: 'Oraichain', 66 | }, 67 | contract: { 68 | address: 'terra1qxd52frq6jnd73nsw49jzp4xccal3g9v47pxwftzqy78ww02p75s62e94t', 69 | }, 70 | }; 71 | 72 | const mockInfo: MessageInfo = { 73 | sender: 'terra122qgjdfjm73guxjq0y67ng8jgex4w09ttguavj', 74 | funds: [], 75 | }; 76 | 77 | const PUBLIC_INPUTS = 78 | 't6CNWWLx3Fd4+fI4XCkc5vdvmwdeAo5lAPMnIDvrxh9SGQJVQa0SBUqvbA7oxa4J8jtpMGipzID9lg9mbNZRIeD7lRlesjw8ZdffRKet+Dhx7AOAGJ0+dXQXdl1Rrg0q'; 79 | const PROOF = 80 | 'Ig2y4hzjpMsVvHC96ppAv68XvyNrigWimFBtG3/ixK62J5Wk3EEMx2j7zwlWFV5KcftnhcaRTTtuqd5fp7SZJZH/uvmMWEdM0GKrmoE/oFoXrvh0eaTlxNjoteRLDQGGkkHa7zjdUgdKBndWTokOBXYaw2xsn/I9g1a5rW2a6y8='; 81 | const VK = 82 | 'qNKepAYpvnYvKhK9p8xFuZijTEOpbExnRMjHqQDo+Ape7Ob6V3FInLAwb0ma2Roz0BWfKXhjMteC24cKCYBECqEiWtI8DkdsfTa3luaptQJAhBtL6VXRPqVN2NoBEo4M+SCUtFn/iCeAq98+F4TAbfbIXAAG/X8ll+PpBS2SFSdPCPxPlNyBKfKaV43Bf16mDqhdLIingpS3ktvy+o0wlzuAymtWdGO2kLizqPcOtkaCJzWOXzFuuBUKkhUrdTUZxMqCf+wX9yg9FSKHZ7Vrc27JSY85/ltRGor1A7Y6GZcEAAAAAAAAALnIa74+XvNJDV20eL4KeTOTTktaFI4sAWArR1yD4lAIkdxEpv4vMx2ptm81YjmKdiZ397fJXTqCqWmODPYhISjW6yej8Rq7UqmKUJvxUC8JR+mrnB1yoIYUDA0xaGbWJMIqafjftZV+NdjT01CzyD7pXoiXx7dtQYgWg9JWHNkZ'; 83 | 84 | describe('CosmWasmVM', () => { 85 | it('full-flow', async () => { 86 | await vm.build(wasmBytecode); 87 | const instantiateRes = vm.instantiate(mockEnv, mockInfo, { 88 | curve: 'bn254', 89 | }); 90 | console.log(instantiateRes); 91 | 92 | const executeRes = vm.execute(mockEnv, mockInfo, { 93 | set_vk_raw: { 94 | vk_raw: VK, 95 | }, 96 | }); 97 | 98 | const queryRes = vm.query(mockEnv, { 99 | verify_proof_json: { 100 | proof_raw: PROOF, 101 | public_inputs: PUBLIC_INPUTS, 102 | }, 103 | }); 104 | 105 | const data = (queryRes as { ok: string }).ok; 106 | 107 | console.log(JSON.parse(fromAscii(fromBase64(data)))); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.0/cosmwasm_vm_test.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.0/cosmwasm_vm_test.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.0/cw_machine-aarch64.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.0/cw_machine-aarch64.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.0/hackatom.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.0/hackatom.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/burner.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/burner.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/crypto_verify.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/crypto_verify.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/cyberpunk.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/cyberpunk.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/floaty.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/floaty.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/hackatom.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/hackatom.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/ibc_reflect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/ibc_reflect.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/ibc_reflect_send.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/ibc_reflect_send.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/queue.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/queue.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/reflect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/reflect.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/staking.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/staking.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/testdata/v1.1/zk.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cosmwasm-vm-js/testdata/v1.1/zk.wasm -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src", 5 | "types", 6 | "test/common/zkbackendApi.ts" 7 | ], 8 | "exclude": [ 9 | "/node_modules/", 10 | "./src/**/*.spec.ts" 11 | ], 12 | "compilerOptions": { 13 | "outDir": "dist", 14 | "rootDir": "src", 15 | "baseUrl": "./" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/cosmwasm-vm-js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | 4 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 5 | 6 | const commonConfig = { 7 | mode: 'production', 8 | entry: './src/index.ts', 9 | devtool: 'source-map', 10 | output: { 11 | globalObject: 'this', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: ['.tsx', '.ts', '.js'], 24 | plugins: [new TsconfigPathsPlugin()], 25 | }, 26 | plugins: [ 27 | new webpack.IgnorePlugin({ 28 | resourceRegExp: 29 | /wordlists\/(french|spanish|italian|korean|chinese_simplified|chinese_traditional|japanese)\.json$/, 30 | }), 31 | ], 32 | }; 33 | 34 | module.exports = { 35 | ...commonConfig, 36 | target: 'node', 37 | output: { 38 | libraryTarget: 'commonjs', 39 | filename: 'bundle.node.js', 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/cw-simulate/bench/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { AppResponse, CWSimulateApp } from '../src'; 3 | import { List } from '@oraichain/immutable'; 4 | import _ from 'lodash'; 5 | import bytes from 'bytes'; 6 | 7 | const testBytecode = readFileSync('testing/cw_simulate_tests-aarch64.wasm'); 8 | 9 | function getContractAddress(res: AppResponse): string { 10 | return res.events[0].attributes[0].value; 11 | } 12 | 13 | const app = new CWSimulateApp({ 14 | chainId: 'phoenix-1', 15 | bech32Prefix: 'terra', 16 | }); 17 | 18 | let info = { 19 | sender: 'terra1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu', 20 | funds: [], 21 | }; 22 | 23 | const codeId = app.wasm.create(info.sender, Uint8Array.from(testBytecode)); 24 | 25 | async function main() { 26 | let snapshots = List(); 27 | 28 | // make 25 contracts 29 | console.time('calls'); 30 | for (let i = 0; i < 25; i++) { 31 | let res = await app.wasm.instantiateContract(info.sender, info.funds, codeId, {}, ''); 32 | if (res.err || typeof res.val === 'string') { 33 | throw new Error(res.val.toString()); 34 | } 35 | 36 | let contractAddress = getContractAddress(res.val); 37 | for (let j = 0; j < 1000; j++) { 38 | res = await app.wasm.executeContract(info.sender, info.funds, contractAddress, { 39 | push: { data: 'A'.repeat(100) }, 40 | }); 41 | snapshots = snapshots.push(app.store); 42 | } 43 | } 44 | console.timeEnd('calls'); 45 | 46 | console.log(_.mapValues(process.memoryUsage(), bytes)); 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /packages/cw-simulate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oraichain/cw-simulate", 3 | "version": "2.8.111", 4 | "description": "Mock blockchain environment for simulating CosmWasm interactions", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "engines": { 10 | "node": ">=18" 11 | }, 12 | "repository": "https://github.com/oraichain/cw-simulate", 13 | "author": "Terran One LLC, Oraichain Labs", 14 | "license": "MIT", 15 | "private": false, 16 | "devDependencies": { 17 | "@types/bytes": "^3.1.1", 18 | "@types/lodash": "^4.14.187", 19 | "benny": "^3.7.1", 20 | "buffer": "^6.0.3", 21 | "bufferutil": "^4.0.8", 22 | "bytes": "^3.1.2", 23 | "lodash": "^4.17.21", 24 | "path-browserify": "^1.0.1", 25 | "utf-8-validate": "^6.0.5" 26 | }, 27 | "dependencies": { 28 | "@cosmjs/amino": "^0.32.4", 29 | "@cosmjs/cosmwasm-stargate": "^0.32.4", 30 | "@cosmjs/crypto": "^0.32.4", 31 | "@cosmjs/encoding": "^0.32.4", 32 | "@cosmjs/stargate": "^0.32.4", 33 | "@kiruse/serde": "^0.8.0-rc.6", 34 | "@oraichain/common": "^1.2.0", 35 | "@oraichain/cosmwasm-vm-js": "^0.2.91", 36 | "eventemitter3": "^5.0.0", 37 | "protobufjs": "^7.2.3", 38 | "ts-results": "^3.3.0", 39 | "tslib": "^2.6.1" 40 | }, 41 | "scripts": { 42 | "build": "tsc --module commonjs && webpack --mode production", 43 | "bench": "tsx bench/snapshot.ts", 44 | "format": "prettier --check ./src/**/*.ts", 45 | "format:fix": "prettier --write ./src/**/*.ts" 46 | }, 47 | "lint-staged": { 48 | "./src/**/*.ts": [ 49 | "npm run lint:fix", 50 | "npm run format:fix" 51 | ] 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "pre-commit": "lint-staged", 56 | "post-checkout": "npm i" 57 | } 58 | }, 59 | "prettier": { 60 | "semi": true, 61 | "singleQuote": true, 62 | "trailingComma": "es5", 63 | "arrowParens": "avoid", 64 | "printWidth": 120 65 | }, 66 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 67 | } 68 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/CWSimulateApp.spec.ts: -------------------------------------------------------------------------------- 1 | import { toBase64 } from '@cosmjs/encoding'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { CWSimulateApp } from './CWSimulateApp'; 5 | import * as persist from './persist'; 6 | 7 | const bytecode = fs.readFileSync(path.resolve(__dirname, '..', 'testing', 'cw_simulate_tests-aarch64.wasm')); 8 | 9 | describe('de/serialize', () => { 10 | it('works', async () => { 11 | { 12 | const ref = new CWSimulateApp({ 13 | chainId: 'phoenix-1', 14 | bech32Prefix: 'terra1', 15 | }); 16 | ref.wasm.create('alice', bytecode); 17 | ref.wasm.create('bob', bytecode); 18 | 19 | const response = await ref.wasm.instantiateContract('alice', [], 1, {}, ''); 20 | const address = response.unwrap().events[0].attributes[0].value; 21 | 22 | const bytes = persist.save(ref); 23 | const clone = await persist.load(bytes); 24 | expect(clone.chainId).toStrictEqual(ref.chainId); 25 | expect(clone.bech32Prefix).toStrictEqual(ref.bech32Prefix); 26 | 27 | const code1 = clone.wasm.getCodeInfo(1)!; 28 | const code2 = clone.wasm.getCodeInfo(2)!; 29 | expect(code1.creator).toStrictEqual('alice'); 30 | expect(code2.creator).toStrictEqual('bob'); 31 | expect(toBase64(code1.wasmCode)).toStrictEqual(toBase64(ref.wasm.store.getObject('codes', 1, 'wasmCode'))); 32 | expect(toBase64(code2.wasmCode)).toStrictEqual(toBase64(ref.wasm.store.getObject('codes', 2, 'wasmCode'))); 33 | 34 | let result = await clone.wasm.executeContract('alice', [], address, { 35 | debug: { msg: 'foobar' }, 36 | }); 37 | expect(result.ok).toBeTruthy(); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/CWSimulateApp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasicBackendApi, 3 | CosmosMsg, 4 | Environment, 5 | IBackendApi, 6 | QuerierBase, 7 | BasicKVIterStorage, 8 | BinaryKVIterStorage, 9 | compare, 10 | Binary, 11 | } from '@oraichain/cosmwasm-vm-js'; 12 | import { Err, Ok, Result } from 'ts-results'; 13 | import { WasmModule, WasmQuery } from './modules/wasm'; 14 | import { BankModule, BankQuery } from './modules/bank'; 15 | import { Transactional, TransactionalLens } from './store/transactional'; 16 | import { AppResponse, DistributionQuery, IbcQuery, StakingQuery, TraceLog } from './types'; 17 | import { SERDE } from '@kiruse/serde'; 18 | import { IbcModule } from './modules/ibc'; 19 | import { DebugFunction } from './instrumentation/CWSimulateVMInstance'; 20 | import { printDebug } from './util'; 21 | import { Map, SortedMap } from '@oraichain/immutable'; 22 | 23 | export type HandleCustomMsgFunction = (sender: string, msg: CosmosMsg) => Promise>; 24 | export type QueryCustomMsgFunction = (query: QueryMessage) => any; 25 | 26 | export type KVIterStorageRegistry = typeof BasicKVIterStorage | typeof BinaryKVIterStorage; 27 | 28 | export interface CWSimulateAppOptions { 29 | chainId: string; 30 | bech32Prefix: string; 31 | backendApi?: IBackendApi; 32 | metering?: boolean; 33 | gasLimit?: number; 34 | debug?: DebugFunction; 35 | handleCustomMsg?: HandleCustomMsgFunction; 36 | queryCustomMsg?: QueryCustomMsgFunction; 37 | kvIterStorageRegistry?: KVIterStorageRegistry; 38 | } 39 | 40 | export type ChainData = { 41 | height: number; 42 | time: number; 43 | }; 44 | 45 | export class CWSimulateApp { 46 | [SERDE] = 'cw-simulate-app' as const; 47 | public chainId: string; 48 | public bech32Prefix: string; 49 | public backendApi: IBackendApi; 50 | public debug?: DebugFunction; 51 | public readonly env?: Environment; 52 | private readonly handleCustomMsg?: HandleCustomMsgFunction; // make sure can not re-assign it 53 | public readonly queryCustomMsg?: QueryCustomMsgFunction; // make sure can not re-assign it 54 | public store: TransactionalLens; 55 | public readonly kvIterStorageRegistry: KVIterStorageRegistry; 56 | 57 | public wasm: WasmModule; 58 | public bank: BankModule; 59 | public ibc: IbcModule; 60 | public querier: Querier; 61 | 62 | constructor(options: CWSimulateAppOptions) { 63 | this.chainId = options.chainId; 64 | this.bech32Prefix = options.bech32Prefix; 65 | this.backendApi = options.backendApi ?? new BasicBackendApi(this.bech32Prefix); 66 | if (options.metering) { 67 | this.env = new Environment(this.backendApi, options.gasLimit); 68 | } 69 | 70 | this.kvIterStorageRegistry = options.kvIterStorageRegistry ?? BinaryKVIterStorage; 71 | 72 | this.debug = options.debug ?? printDebug; 73 | this.handleCustomMsg = options.handleCustomMsg; 74 | this.queryCustomMsg = options.queryCustomMsg; 75 | this.store = new Transactional(this.kvIterStorageRegistry === BinaryKVIterStorage ? SortedMap(compare) : Map()) 76 | .lens() 77 | .initialize({ 78 | height: 1, 79 | time: Date.now() * 1e6, 80 | }); 81 | 82 | this.wasm = new WasmModule(this); 83 | this.bank = new BankModule(this); 84 | this.ibc = new IbcModule(this); 85 | this.querier = new Querier(this); 86 | } 87 | 88 | public get gasUsed() { 89 | return this.env?.gasUsed ?? 0; 90 | } 91 | 92 | public get gasLimit() { 93 | return this.env?.gasLimit ?? 0; 94 | } 95 | 96 | public async handleMsg( 97 | sender: string, 98 | msg: CosmosMsg, 99 | traces: TraceLog[] = [] 100 | ): Promise> { 101 | if ('wasm' in msg) { 102 | return await this.wasm.handleMsg(sender, msg.wasm, traces); 103 | } 104 | if ('bank' in msg) { 105 | return await this.bank.handleMsg(sender, msg.bank); 106 | } 107 | if ('ibc' in msg) { 108 | return await this.ibc.handleMsg(sender, msg.ibc); 109 | } 110 | // not yet implemented, so use custom fallback assignment 111 | if ('stargate' in msg || 'custom' in msg || 'gov' in msg || 'staking' in msg || 'distribution' in msg) { 112 | // make default response to keep app working 113 | if (!this.handleCustomMsg) return Err(`no custom handle found for: ${Object.keys(msg)[0]}`); 114 | return await this.handleCustomMsg(sender, msg); 115 | } 116 | 117 | return Err(`unknown message: ${JSON.stringify(msg)}`); 118 | } 119 | 120 | public pushBlock(callback: () => Result, sameBlock: boolean): Result; 121 | public pushBlock(callback: () => Promise>, sameBlock: boolean): Promise>; 122 | public pushBlock( 123 | callback: () => Result | Promise>, 124 | sameBlock: boolean 125 | ): Result | Promise> { 126 | //@ts-ignore 127 | return this.store.tx(setter => { 128 | // increase block height and time if new block 129 | if (!sameBlock) { 130 | setter('height')(this.height + 1); 131 | // if height or time are alredy increased, we will wait for it, this will help simulating future moment 132 | const current = Date.now() * 1e6; 133 | if (this.time < current) { 134 | setter('time')(current); // 1 millisecond = 1e6 nano seconds 135 | } 136 | } 137 | return callback(); 138 | }); 139 | } 140 | 141 | get height() { 142 | return this.store.get('height'); 143 | } 144 | get time() { 145 | return this.store.get('time'); 146 | } 147 | set time(nanoSeconds: number) { 148 | this.store.tx(setter => Ok(setter('time')(nanoSeconds))); 149 | } 150 | set height(blockHeight: number) { 151 | this.store.tx(setter => Ok(setter('height')(blockHeight))); 152 | } 153 | } 154 | 155 | export type QueryMessage = 156 | | { bank: BankQuery } 157 | | { wasm: WasmQuery } 158 | | { custom: T } 159 | | { staking: StakingQuery } 160 | | { distribution: DistributionQuery } 161 | | { 162 | stargate: { 163 | path: string; 164 | /// this is the expected protobuf message type (not any), binary encoded 165 | data: Binary; 166 | }; 167 | } 168 | | { ibc: IbcQuery } 169 | | { 170 | grpc: { 171 | path: string; 172 | /// The expected protobuf message type (not [Any](https://protobuf.dev/programming-guides/proto3/#any)), binary encoded 173 | data: Binary; 174 | }; 175 | }; 176 | 177 | export class Querier extends QuerierBase { 178 | constructor(public readonly app: CWSimulateApp) { 179 | super(); 180 | } 181 | 182 | handleQuery(query: QueryMessage): any { 183 | if ('bank' in query) { 184 | return this.app.bank.handleQuery(query.bank); 185 | } 186 | if ('wasm' in query) { 187 | return this.app.wasm.handleQuery(query.wasm); 188 | } 189 | if ( 190 | this.app.queryCustomMsg && 191 | ('stargate' in query || 192 | 'custom' in query || 193 | 'staking' in query || 194 | 'ibc' in query || 195 | 'distribution' in query || 196 | 'grpc' in query) 197 | ) { 198 | // make default response to keep app working 199 | if (!this.app.queryCustomMsg) return Err(`no custom query found for: ${Object.keys(query)[0]}`); 200 | return this.app.queryCustomMsg(query); 201 | } 202 | 203 | // not yet implemented, so use custom fallback assignment 204 | throw new Error('Unknown query message'); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/SimulateCosmWasmClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from '@cosmjs/crypto'; 2 | import { fromHex, toHex } from '@cosmjs/encoding'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { SimulateCosmWasmClient } from './SimulateCosmWasmClient'; 6 | import { instantiate2Address } from '@cosmjs/cosmwasm-stargate'; 7 | 8 | const bytecode = fs.readFileSync(path.resolve(__dirname, '..', 'testing', 'hello_world-aarch64.wasm')); 9 | const sender = 'orai12zyu8w93h0q2lcnt50g3fn0w3yqnhy4fvawaqz'; 10 | 11 | describe('SimulateCosmWasmClient', () => { 12 | it('works', async () => { 13 | { 14 | const client = new SimulateCosmWasmClient({ 15 | chainId: 'Oraichain', 16 | bech32Prefix: 'orai', 17 | metering: true, 18 | }); 19 | 20 | const { codeId } = await client.upload(sender, bytecode, 'auto'); 21 | 22 | const { contractAddress } = await client.instantiate(sender, codeId, { count: 10 }, '', 'auto'); 23 | console.log(contractAddress); 24 | 25 | const result = await client.execute( 26 | sender, 27 | contractAddress, 28 | { 29 | increment: {}, 30 | }, 31 | 'auto' 32 | ); 33 | 34 | console.log(result); 35 | 36 | expect(result.events[0].attributes[0].value).toEqual(contractAddress); 37 | 38 | expect(await client.queryContractSmart(contractAddress, { get_count: {} })).toEqual({ count: 11 }); 39 | 40 | const bytes = client.toBytes(); 41 | const clientRestore = await SimulateCosmWasmClient.from(bytes); 42 | 43 | const codes = await clientRestore.getCodes(); 44 | expect(codes).toEqual([ 45 | { 46 | id: codeId, 47 | creator: sender, 48 | checksum: toHex(sha256(bytecode)), 49 | }, 50 | ]); 51 | } 52 | }); 53 | 54 | it('instantiate2', async () => { 55 | { 56 | const client = new SimulateCosmWasmClient({ 57 | chainId: 'Oraichain', 58 | bech32Prefix: 'orai', 59 | metering: true, 60 | }); 61 | 62 | const { codeId } = await client.upload(sender, bytecode, 'auto'); 63 | const { checksum } = await client.getCodeDetails(codeId); 64 | const salt = Buffer.allocUnsafe(8); 65 | crypto.getRandomValues(salt); 66 | const predictContractAddress = instantiate2Address(fromHex(checksum), sender, salt, client.app.bech32Prefix); 67 | const { contractAddress } = await client.instantiate2(sender, codeId, salt, { count: 10 }, '', 'auto'); 68 | expect(contractAddress).toEqual(predictContractAddress); 69 | 70 | const result = await client.execute( 71 | sender, 72 | contractAddress, 73 | { 74 | increment: {}, 75 | }, 76 | 'auto' 77 | ); 78 | 79 | console.log(result); 80 | 81 | expect(result.events[0].attributes[0].value).toEqual(contractAddress); 82 | 83 | expect(await client.queryContractSmart(contractAddress, { get_count: {} })).toEqual({ count: 11 }); 84 | 85 | const bytes = client.toBytes(); 86 | const clientRestore = await SimulateCosmWasmClient.from(bytes); 87 | 88 | const codes = await clientRestore.getCodes(); 89 | expect(codes).toEqual([ 90 | { 91 | id: codeId, 92 | creator: sender, 93 | checksum: toHex(sha256(bytecode)), 94 | }, 95 | ]); 96 | } 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/SimulateCosmWasmClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SigningCosmWasmClient, 3 | ExecuteResult, 4 | InstantiateOptions, 5 | InstantiateResult, 6 | JsonObject, 7 | UploadResult, 8 | DeliverTxResponse, 9 | toBinary, 10 | Contract, 11 | CodeDetails, 12 | Code, 13 | ExecuteInstruction, 14 | MigrateResult, 15 | } from '@cosmjs/cosmwasm-stargate'; 16 | import { Account, SequenceResponse, Block } from '@cosmjs/stargate'; 17 | import { CWSimulateApp, CWSimulateAppOptions } from './CWSimulateApp'; 18 | import { sha256 } from '@cosmjs/crypto'; 19 | import { fromBase64, toHex } from '@cosmjs/encoding'; 20 | import { Map, SortedMap, isMap } from '@oraichain/immutable'; 21 | import { Coin, StdFee } from '@cosmjs/amino'; 22 | import { load, save } from './persist'; 23 | import { getTransactionHash } from './util'; 24 | import { ContractInfo } from './types'; 25 | import { BinaryKVIterStorage, compare } from '@oraichain/cosmwasm-vm-js'; 26 | import { WasmModule } from './modules/wasm'; 27 | 28 | export class SimulateCosmWasmClient extends SigningCosmWasmClient { 29 | // deserialize from bytes 30 | public static async from(bytes: Uint8Array | Buffer): Promise { 31 | const app = await load(Uint8Array.from(bytes)); 32 | return new SimulateCosmWasmClient(app); 33 | } 34 | 35 | public readonly app: CWSimulateApp; 36 | public constructor(appOrOptions: CWSimulateApp | CWSimulateAppOptions) { 37 | super(null, null, {}); 38 | if (appOrOptions instanceof CWSimulateApp) { 39 | this.app = appOrOptions; 40 | } else { 41 | this.app = new CWSimulateApp(appOrOptions); 42 | } 43 | } 44 | 45 | // serialize to bytes 46 | public toBytes(): Uint8Array { 47 | return save(this.app); 48 | } 49 | 50 | public async loadContract(address: string, info: ContractInfo, data: any) { 51 | this.app.wasm.setContractInfo(address, info); 52 | this.app.wasm.setContractStorage( 53 | address, 54 | isMap(data) ? data : this.app.kvIterStorageRegistry === BinaryKVIterStorage ? SortedMap(data, compare) : Map(data) 55 | ); 56 | await this.app.wasm.getContract(address).init(); 57 | } 58 | 59 | public getChainId(): Promise { 60 | return Promise.resolve(this.app.chainId); 61 | } 62 | public getHeight(): Promise { 63 | return Promise.resolve(this.app.height); 64 | } 65 | public getAccount(searchAddress: string): Promise { 66 | return Promise.resolve({ 67 | address: searchAddress, 68 | pubkey: null, 69 | accountNumber: 0, 70 | sequence: 0, 71 | }); 72 | } 73 | public getSequence(_address: string): Promise { 74 | return Promise.resolve({ 75 | accountNumber: 0, 76 | sequence: 0, 77 | }); 78 | } 79 | 80 | public getBlock(height?: number): Promise { 81 | return Promise.resolve({ 82 | id: '', 83 | header: { 84 | version: { 85 | app: 'simulate', 86 | block: 'simulate', 87 | }, 88 | height, 89 | chainId: this.app.chainId, 90 | time: new Date().toString(), 91 | }, 92 | txs: [], 93 | }); 94 | } 95 | public getBalance(address: string, searchDenom: string): Promise { 96 | // default return zero balance 97 | const coin = this.app.bank.getBalance(address).find(coin => coin.denom === searchDenom) ?? { 98 | denom: searchDenom, 99 | amount: '0', 100 | }; 101 | return Promise.resolve(coin); 102 | } 103 | 104 | getCodes(): Promise { 105 | const codes: Code[] = []; 106 | this.app.wasm.forEachCodeInfo((codeInfo, codeId) => { 107 | codes.push({ 108 | id: codeId, 109 | creator: codeInfo.creator, 110 | checksum: WasmModule.checksumCache[codeId], 111 | }); 112 | }); 113 | 114 | return Promise.resolve(codes); 115 | } 116 | 117 | public getCodeDetails(codeId: number): Promise { 118 | const codeInfo = this.app.wasm.getCodeInfo(codeId); 119 | const codeDetails = { 120 | id: codeId, 121 | creator: codeInfo.creator, 122 | checksum: WasmModule.checksumCache[codeId], 123 | data: codeInfo.wasmCode, 124 | }; 125 | return Promise.resolve(codeDetails); 126 | } 127 | 128 | public getContract(address: string): Promise { 129 | const contract = this.app.wasm.getContractInfo(address); 130 | 131 | return Promise.resolve({ 132 | address, 133 | codeId: contract.codeId, 134 | creator: contract.creator, 135 | admin: contract.admin, 136 | label: contract.label, 137 | ibcPortId: undefined, 138 | }); 139 | } 140 | 141 | public sendTokens( 142 | senderAddress: string, 143 | recipientAddress: string, 144 | amount: readonly Coin[], 145 | _fee: StdFee | 'auto' | number, 146 | _memo?: string 147 | ): Promise { 148 | const res = this.app.bank.send(senderAddress, recipientAddress, (amount as Coin[]) ?? []); 149 | return Promise.resolve({ 150 | height: this.app.height, 151 | txIndex: 0, 152 | code: res.ok ? 0 : 1, 153 | transactionHash: getTransactionHash(this.app.height, res), 154 | events: [], 155 | rawLog: typeof res.val === 'string' ? res.val : undefined, 156 | gasUsed: 66_000n, 157 | gasWanted: BigInt(this.app.gasLimit), 158 | msgResponses: [], // for cosmos sdk < 0.46 159 | }); 160 | } 161 | 162 | public upload( 163 | senderAddress: string, 164 | wasmCode: Uint8Array, 165 | _fee: StdFee | 'auto' | number, 166 | _memo?: string 167 | ): Promise { 168 | // import the wasm bytecode 169 | const checksum = toHex(sha256(wasmCode)); 170 | const codeId = this.app.wasm.create(senderAddress, wasmCode); 171 | WasmModule.checksumCache[codeId] = checksum; 172 | return Promise.resolve({ 173 | originalSize: wasmCode.length, 174 | compressedSize: wasmCode.length, 175 | checksum, 176 | codeId, 177 | logs: [], 178 | height: this.app.height, 179 | transactionHash: getTransactionHash(this.app.height, checksum), 180 | events: [], 181 | gasWanted: BigInt(this.app.gasLimit), 182 | gasUsed: BigInt(wasmCode.length * 10), 183 | }); 184 | } 185 | 186 | public async _instantiate( 187 | senderAddress: string, 188 | codeId: number, 189 | msg: JsonObject, 190 | label: string, 191 | salt: Uint8Array | null = null, 192 | options?: InstantiateOptions 193 | ): Promise { 194 | // instantiate the contract 195 | const contractGasUsed = this.app.gasUsed; 196 | // pass checksum to cache build 197 | const result = await this.app.wasm.instantiateContract( 198 | senderAddress, 199 | (options?.funds as Coin[]) ?? [], 200 | codeId, 201 | msg, 202 | label, 203 | options?.admin, 204 | salt 205 | ); 206 | 207 | if (result.err || typeof result.val === 'string') { 208 | throw new Error(result.val.toString()); 209 | } 210 | 211 | // pull out the contract address 212 | const contractAddress = result.val.events[0].attributes[0].value; 213 | return { 214 | contractAddress, 215 | logs: [], 216 | height: this.app.height, 217 | transactionHash: getTransactionHash(this.app.height, result), 218 | events: result.val.events, 219 | gasWanted: BigInt(this.app.gasLimit), 220 | gasUsed: BigInt(this.app.gasUsed - contractGasUsed), 221 | }; 222 | } 223 | 224 | public async instantiate( 225 | senderAddress: string, 226 | codeId: number, 227 | msg: JsonObject, 228 | label: string, 229 | _fee?: StdFee | 'auto' | number, 230 | options?: InstantiateOptions 231 | ): Promise { 232 | return this._instantiate(senderAddress, codeId, msg, label, null, options); 233 | } 234 | 235 | public async instantiate2( 236 | senderAddress: string, 237 | codeId: number, 238 | salt: Uint8Array, 239 | msg: JsonObject, 240 | label: string, 241 | _fee: StdFee | 'auto' | number, 242 | options?: InstantiateOptions 243 | ): Promise { 244 | return this._instantiate(senderAddress, codeId, msg, label, salt, options); 245 | } 246 | 247 | /** 248 | * Like `execute` but allows executing multiple messages in one transaction. 249 | */ 250 | public async executeMultiple( 251 | senderAddress: string, 252 | instructions: readonly ExecuteInstruction[], 253 | _fee: StdFee | 'auto' | number, 254 | _memo?: string 255 | ): Promise { 256 | const events = []; 257 | const contractGasUsed = this.app.gasUsed; 258 | const results = []; 259 | let ind = 0; 260 | for (const { contractAddress, funds, msg } of instructions) { 261 | // run in sequential, only last block will push new height 262 | const result = await this.app.wasm.executeContract( 263 | senderAddress, 264 | (funds as Coin[]) ?? [], 265 | contractAddress, 266 | msg, 267 | undefined, 268 | ++ind !== instructions.length 269 | ); 270 | 271 | if (result.err || typeof result.val === 'string') { 272 | throw new Error(result.val.toString()); 273 | } 274 | events.push(...result.val.events); 275 | results.push(result); 276 | } 277 | 278 | return { 279 | logs: [], 280 | height: this.app.height, 281 | transactionHash: getTransactionHash(this.app.height, results), 282 | events, 283 | gasWanted: BigInt(this.app.gasLimit), 284 | gasUsed: BigInt(this.app.gasUsed - contractGasUsed), 285 | }; 286 | } 287 | 288 | // keep the same interface so that we can switch to real environment 289 | public async execute( 290 | senderAddress: string, 291 | contractAddress: string, 292 | msg: JsonObject, 293 | fee: StdFee | 'auto' | number, 294 | memo?: string, 295 | funds?: readonly Coin[] 296 | ): Promise { 297 | return this.executeMultiple( 298 | senderAddress, 299 | [ 300 | { 301 | contractAddress, 302 | msg, 303 | funds, 304 | }, 305 | ], 306 | fee, 307 | memo 308 | ); 309 | } 310 | 311 | public async migrate( 312 | senderAddress: string, 313 | contractAddress: string, 314 | codeId: number, 315 | migrateMsg: JsonObject, 316 | _fee: StdFee | 'auto' | number, 317 | _memo?: string 318 | ): Promise { 319 | // only admin can migrate the contract 320 | 321 | const { admin } = this.app.wasm.getContractInfo(contractAddress); 322 | 323 | if (admin !== senderAddress) { 324 | throw new Error('unauthorized: can not migrate'); 325 | } 326 | 327 | const contractGasUsed = this.app.gasUsed; 328 | 329 | const result = await this.app.wasm.migrateContract(senderAddress, codeId, contractAddress, migrateMsg); 330 | 331 | if (result.err || typeof result.val === 'string') { 332 | throw new Error(result.val.toString()); 333 | } 334 | 335 | return { 336 | logs: [], 337 | height: this.app.height, 338 | transactionHash: getTransactionHash(this.app.height, result), 339 | events: result.val.events, 340 | gasWanted: BigInt(this.app.gasLimit), 341 | gasUsed: BigInt(this.app.gasUsed - contractGasUsed), 342 | }; 343 | } 344 | 345 | public async queryContractRaw(address: string, key: Uint8Array): Promise { 346 | const result = this.app.wasm.handleQuery({ raw: { contract_addr: address, key: toBinary(key) } }); 347 | return Promise.resolve(fromBase64(toBinary({ ok: result }))); 348 | } 349 | 350 | public async queryContractSmart(address: string, queryMsg: JsonObject): Promise { 351 | const result = this.app.wasm.query(address, queryMsg); 352 | // check is ok or err 353 | return result.ok ? Promise.resolve(result.val) : Promise.reject(new Error(result.val)); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { DownloadState } from './fork'; 2 | import { DownloadState as DownloadStateOld } from './fork-old'; 3 | import path from 'path'; 4 | import benchmark from 'benny'; 5 | 6 | export async function downloadState(contract: string, downloadPath: string) { 7 | const downloadState = new DownloadState('https://rpc.orai.io', downloadPath); 8 | await downloadState.saveState(contract); 9 | } 10 | 11 | export async function downloadStateOld(contract: string, downloadPath: string) { 12 | const downloadState = new DownloadStateOld('https://lcd.orai.io', downloadPath); 13 | await downloadState.saveState(contract); 14 | } 15 | 16 | const firstSuite = benchmark.suite( 17 | 'benchmark downloading states via RPC versus LCD', 18 | benchmark.add( 19 | 'download state RPC', 20 | async () => { 21 | await downloadState('orai1hur7m6wu7v79t6m3qal6qe0ufklw8uckrxk5lt', path.join(__dirname, './benchmark/new-data')); 22 | }, 23 | { maxTime: 1 } 24 | ), 25 | benchmark.add( 26 | 'download state LCD', 27 | async () => { 28 | await downloadStateOld('orai1hur7m6wu7v79t6m3qal6qe0ufklw8uckrxk5lt', path.join(__dirname, './benchmark/old-data')); 29 | }, 30 | { maxTime: 1 } 31 | ), 32 | benchmark.cycle(), 33 | benchmark.complete(), 34 | benchmark.save({ file: 'reduce', version: '1.0.0' }), 35 | benchmark.save({ file: 'reduce', format: 'chart.html' }) 36 | ); 37 | 38 | (async () => { 39 | await firstSuite; 40 | })(); 41 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/fork-old.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { compare, toNumber } from '@oraichain/cosmwasm-vm-js'; 4 | import { SimulateCosmWasmClient } from './SimulateCosmWasmClient'; 5 | import { SortedMap } from '@oraichain/immutable'; 6 | 7 | export class BufferStream { 8 | private readonly fd: number; 9 | private sizeBuf: Buffer; 10 | 11 | constructor(private readonly filePath: string, append: boolean) { 12 | if (!append || !fs.existsSync(filePath)) { 13 | this.sizeBuf = Buffer.alloc(4); 14 | fs.writeFileSync(filePath, this.sizeBuf); 15 | this.fd = fs.openSync(filePath, 'r+'); 16 | } else { 17 | this.fd = fs.openSync(filePath, 'r+'); 18 | this.sizeBuf = Buffer.allocUnsafe(4); 19 | fs.readSync(this.fd, this.sizeBuf, 0, 4, 0); 20 | } 21 | } 22 | 23 | private increaseSize() { 24 | for (let i = this.sizeBuf.length - 1; i >= 0; --i) { 25 | if (this.sizeBuf[i] === 255) { 26 | this.sizeBuf[i] = 0; 27 | } else { 28 | this.sizeBuf[i]++; 29 | break; 30 | } 31 | } 32 | } 33 | 34 | get size() { 35 | return this.sizeBuf.readUInt32BE(); 36 | } 37 | 38 | close() { 39 | fs.closeSync(this.fd); 40 | } 41 | 42 | write(entries: Array<[Uint8Array, Uint8Array]>) { 43 | let n = 0; 44 | for (const [k, v] of entries) { 45 | n += k.length + v.length + 4; 46 | } 47 | const outputBuffer = Buffer.allocUnsafe(n); 48 | let ind = 0; 49 | for (const [k, v] of entries) { 50 | outputBuffer[ind++] = k.length; 51 | outputBuffer.set(k, ind); 52 | ind += k.length; 53 | outputBuffer[ind++] = (v.length >> 16) & 0b11111111; 54 | outputBuffer[ind++] = (v.length >> 8) & 0b11111111; 55 | outputBuffer[ind++] = v.length & 0b11111111; 56 | outputBuffer.set(v, ind); 57 | ind += v.length; 58 | this.increaseSize(); 59 | } 60 | 61 | // update size 62 | fs.writeSync(this.fd, this.sizeBuf, 0, 4, 0); 63 | // append item 64 | fs.appendFileSync(this.filePath, outputBuffer); 65 | } 66 | } 67 | 68 | export class BufferIter { 69 | private ind: number = 0; 70 | private bufInd: number = 0; 71 | constructor(private readonly buf: Uint8Array, public readonly size: number) {} 72 | 73 | reset() { 74 | this.ind = 0; 75 | this.bufInd = 0; 76 | return this; 77 | } 78 | 79 | next() { 80 | if (this.ind === this.size) { 81 | return { 82 | done: true, 83 | }; 84 | } 85 | 86 | const keyLength = this.buf[this.bufInd++]; 87 | const k = this.buf.subarray(this.bufInd, (this.bufInd += keyLength)); 88 | const valueLength = (this.buf[this.bufInd++] << 16) | (this.buf[this.bufInd++] << 8) | this.buf[this.bufInd++]; 89 | const v = this.buf.subarray(this.bufInd, (this.bufInd += valueLength)); 90 | this.ind++; 91 | 92 | return { 93 | value: [k, v], 94 | }; 95 | } 96 | } 97 | 98 | export class BufferCollection { 99 | public readonly size: number; 100 | private readonly buf: Uint8Array; 101 | constructor(buf: Uint8Array) { 102 | // first 4 bytes is for uint32 be 103 | this.size = toNumber(buf.subarray(0, 4)); 104 | this.buf = buf.subarray(4); 105 | } 106 | 107 | entries() { 108 | return new BufferIter(this.buf, this.size); 109 | } 110 | } 111 | 112 | BufferCollection.prototype['@@__IMMUTABLE_KEYED__@@'] = true; 113 | 114 | // helper function 115 | const downloadState = async ( 116 | lcd: string, 117 | contractAddress: string, 118 | writeCallback: Function, 119 | endCallback: Function, 120 | startAfter?: string, 121 | limit = 5000, 122 | height?: number 123 | ) => { 124 | let nextKey = startAfter; 125 | 126 | let headers = new Headers(); 127 | if (height) headers.append('x-cosmos-block-height', height.toFixed()); 128 | 129 | while (true) { 130 | const url = new URL(`${lcd}/cosmwasm/wasm/v1/contract/${contractAddress}/state`); 131 | url.searchParams.append('pagination.limit', limit.toString()); 132 | if (nextKey) { 133 | url.searchParams.append('pagination.key', nextKey); 134 | console.log('nextKey', nextKey); 135 | } 136 | try { 137 | const { models, pagination } = await fetch(url.toString(), { 138 | signal: AbortSignal.timeout(30000), 139 | headers, 140 | }).then(res => res.json()); 141 | writeCallback(models); 142 | if (!(nextKey = pagination.next_key)) { 143 | return endCallback(); 144 | } 145 | } catch (ex) { 146 | await new Promise(r => setTimeout(r, 1000)); 147 | } 148 | } 149 | }; 150 | 151 | export class DownloadState { 152 | constructor(public readonly lcd: string, public readonly downloadPath: string, public readonly height?: number) {} 153 | 154 | // if there is nextKey then append, otherwise insert 155 | async saveState(contractAddress: string, nextKey?: string) { 156 | const bufStream = new BufferStream(path.join(this.downloadPath, `${contractAddress}.state`), !!nextKey); 157 | await new Promise(resolve => { 158 | downloadState( 159 | this.lcd, 160 | contractAddress, 161 | (chunks: any) => { 162 | const entries = chunks.map(({ key, value }) => [Buffer.from(key, 'hex'), Buffer.from(value, 'base64')]); 163 | bufStream.write(entries); 164 | }, 165 | resolve, 166 | nextKey, 167 | undefined, 168 | this.height 169 | ); 170 | }); 171 | bufStream.close(); 172 | 173 | // check contract code 174 | const contractFile = path.join(this.downloadPath, contractAddress); 175 | if (!fs.existsSync(contractFile)) { 176 | const { 177 | contract_info: { code_id }, 178 | } = await fetch(`${this.lcd}/cosmwasm/wasm/v1/contract/${contractAddress}`).then(res => res.json()); 179 | const { data } = await fetch(`${this.lcd}/cosmwasm/wasm/v1/code/${code_id}`).then(res => res.json()); 180 | fs.writeFileSync(contractFile, Buffer.from(data, 'base64')); 181 | } 182 | 183 | console.log('done'); 184 | } 185 | 186 | loadStateData(contractAddress: string): SortedMap { 187 | const buffer = fs.readFileSync(path.join(this.downloadPath, `${contractAddress}.state`)); 188 | 189 | // @ts-ignore 190 | return SortedMap.rawPack(new BufferCollection(buffer), compare); 191 | } 192 | 193 | async loadState( 194 | client: SimulateCosmWasmClient, 195 | senderAddress: string, 196 | contractAddress: string, 197 | label: string, 198 | data?: any 199 | ) { 200 | const { codeId } = await client.upload( 201 | senderAddress, 202 | fs.readFileSync(path.join(this.downloadPath, contractAddress)), 203 | 'auto' 204 | ); 205 | 206 | await client.loadContract( 207 | contractAddress, 208 | { 209 | codeId, 210 | admin: senderAddress, 211 | label, 212 | creator: senderAddress, 213 | created: 1, 214 | }, 215 | data ?? this.loadStateData(contractAddress) 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/fork.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { compare, toNumber } from '@oraichain/cosmwasm-vm-js'; 4 | import { SimulateCosmWasmClient } from './SimulateCosmWasmClient'; 5 | import { SortedMap } from '@oraichain/immutable'; 6 | import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'; 7 | 8 | export class BufferStream { 9 | private readonly fd: number; 10 | private sizeBuf: Buffer; 11 | 12 | constructor(private readonly filePath: string, append: boolean) { 13 | if (!append || !fs.existsSync(filePath)) { 14 | this.sizeBuf = Buffer.alloc(4); 15 | fs.writeFileSync(filePath, Uint8Array.from(this.sizeBuf)); 16 | this.fd = fs.openSync(filePath, 'r+'); 17 | } else { 18 | this.fd = fs.openSync(filePath, 'r+'); 19 | this.sizeBuf = Buffer.allocUnsafe(4); 20 | fs.readSync(this.fd, Uint8Array.from(this.sizeBuf), 0, 4, 0); 21 | } 22 | } 23 | 24 | private increaseSize() { 25 | for (let i = this.sizeBuf.length - 1; i >= 0; --i) { 26 | if (this.sizeBuf[i] === 255) { 27 | this.sizeBuf[i] = 0; 28 | } else { 29 | this.sizeBuf[i]++; 30 | break; 31 | } 32 | } 33 | } 34 | 35 | get size() { 36 | return this.sizeBuf.readUInt32BE(); 37 | } 38 | 39 | close() { 40 | fs.closeSync(this.fd); 41 | } 42 | 43 | write(entries: Array<[Uint8Array, Uint8Array]>) { 44 | let n = 0; 45 | for (const [k, v] of entries) { 46 | n += k.length + v.length + 4; 47 | } 48 | const outputBuffer = Buffer.allocUnsafe(n); 49 | let ind = 0; 50 | for (const [k, v] of entries) { 51 | outputBuffer[ind++] = k.length; 52 | outputBuffer.set(k, ind); 53 | ind += k.length; 54 | outputBuffer[ind++] = (v.length >> 16) & 0b11111111; 55 | outputBuffer[ind++] = (v.length >> 8) & 0b11111111; 56 | outputBuffer[ind++] = v.length & 0b11111111; 57 | outputBuffer.set(v, ind); 58 | ind += v.length; 59 | this.increaseSize(); 60 | } 61 | 62 | // update size 63 | fs.writeSync(this.fd, Uint8Array.from(this.sizeBuf), 0, 4, 0); 64 | // append item 65 | fs.appendFileSync(this.filePath, Uint8Array.from(outputBuffer)); 66 | } 67 | } 68 | 69 | export class BufferIter { 70 | private ind: number = 0; 71 | private bufInd: number = 0; 72 | constructor(private readonly buf: Uint8Array, public readonly size: number) {} 73 | 74 | reset() { 75 | this.ind = 0; 76 | this.bufInd = 0; 77 | return this; 78 | } 79 | 80 | next() { 81 | if (this.ind === this.size) { 82 | return { 83 | done: true, 84 | }; 85 | } 86 | 87 | const keyLength = this.buf[this.bufInd++]; 88 | const k = this.buf.subarray(this.bufInd, (this.bufInd += keyLength)); 89 | const valueLength = (this.buf[this.bufInd++] << 16) | (this.buf[this.bufInd++] << 8) | this.buf[this.bufInd++]; 90 | const v = this.buf.subarray(this.bufInd, (this.bufInd += valueLength)); 91 | this.ind++; 92 | 93 | return { 94 | value: [k, v], 95 | }; 96 | } 97 | } 98 | 99 | export class BufferCollection { 100 | public readonly size: number; 101 | private readonly buf: Uint8Array; 102 | constructor(buf: Uint8Array) { 103 | // first 4 bytes is for uint32 be 104 | this.size = toNumber(buf.subarray(0, 4)); 105 | this.buf = buf.subarray(4); 106 | } 107 | 108 | entries() { 109 | return new BufferIter(this.buf, this.size); 110 | } 111 | } 112 | 113 | BufferCollection.prototype['@@__IMMUTABLE_KEYED__@@'] = true; 114 | 115 | // helper function 116 | const downloadState = async ( 117 | rpc: string, 118 | contractAddress: string, 119 | writeCallback: Function, 120 | startAfter?: string, 121 | limit = 5000, 122 | height?: number 123 | ) => { 124 | let nextKey = startAfter ? Uint8Array.from(Buffer.from(startAfter, 'base64')) : undefined; 125 | const cosmwasmClient = await CosmWasmClient.connect(rpc, height); 126 | 127 | while (true) { 128 | try { 129 | const { models, pagination } = await cosmwasmClient.getAllContractState(contractAddress, nextKey, limit); 130 | writeCallback(models); 131 | console.log('next key: ', Buffer.from(pagination.nextKey).toString('base64')); 132 | if (!pagination.nextKey || pagination.nextKey.length === 0) { 133 | return; 134 | } 135 | nextKey = pagination.nextKey; 136 | } catch (ex) { 137 | console.log('ex downloading state: ', ex); 138 | await new Promise(r => setTimeout(r, 1000)); 139 | } 140 | } 141 | }; 142 | 143 | export class DownloadState { 144 | constructor(public readonly rpc: string, public readonly downloadPath: string, public readonly height?: number) {} 145 | 146 | // if there is nextKey then append, otherwise insert 147 | async saveState(contractAddress: string, nextKey?: string) { 148 | const bufStream = new BufferStream(path.join(this.downloadPath, `${contractAddress}.state`), !!nextKey); 149 | await downloadState( 150 | this.rpc, 151 | contractAddress, 152 | (chunks: any) => { 153 | const entries = chunks.map(({ key, value }) => [key, value]); 154 | bufStream.write(entries); 155 | }, 156 | nextKey, 157 | undefined, 158 | this.height 159 | ); 160 | 161 | bufStream.close(); 162 | 163 | // check contract code 164 | const contractFile = path.join(this.downloadPath, contractAddress); 165 | if (!fs.existsSync(contractFile)) { 166 | const client = await CosmWasmClient.connect(this.rpc, this.height); 167 | const { codeId } = await client.getContract(contractAddress); 168 | const { data } = await client.getCodeDetails(codeId); 169 | fs.writeFileSync(contractFile, Uint8Array.from(data)); 170 | } 171 | 172 | console.log('done'); 173 | } 174 | 175 | loadStateData(contractAddress: string): SortedMap { 176 | const buffer = fs.readFileSync(path.join(this.downloadPath, `${contractAddress}.state`)); 177 | 178 | // @ts-ignore 179 | return SortedMap.rawPack(new BufferCollection(buffer), compare); 180 | } 181 | 182 | async loadState( 183 | client: SimulateCosmWasmClient, 184 | senderAddress: string, 185 | contractAddress: string, 186 | label: string, 187 | data?: any, 188 | wasmCodePath?: string 189 | ) { 190 | const { codeId } = await client.upload( 191 | senderAddress, 192 | Uint8Array.from(fs.readFileSync(wasmCodePath ?? path.join(this.downloadPath, contractAddress))), 193 | 'auto' 194 | ); 195 | 196 | await client.loadContract( 197 | contractAddress, 198 | { 199 | codeId, 200 | admin: senderAddress, 201 | label, 202 | creator: senderAddress, 203 | created: 1, 204 | }, 205 | data ?? this.loadStateData(contractAddress) 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CWSimulateApp'; 2 | export * from './SimulateCosmWasmClient'; 3 | export * from './types'; 4 | export * from './store'; 5 | export * from './modules/wasm/error'; 6 | export * from './persist'; 7 | export * from './fork'; 8 | 9 | // re-export from vm-js 10 | export * from '@oraichain/cosmwasm-vm-js'; 11 | 12 | // re-export from ts-results 13 | export { Ok, Option, Err, Some, None, Result } from 'ts-results'; 14 | 15 | // export some extended Immutable structures 16 | export { SortedMap, SortedSet } from '@oraichain/immutable'; 17 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/instrumentation/CWSimulateVMInstance.ts: -------------------------------------------------------------------------------- 1 | import { VMInstance, Region, IBackend, Environment } from '@oraichain/cosmwasm-vm-js'; 2 | import { DebugLog } from '../types'; 3 | 4 | export type DebugFunction = (log: DebugLog) => void; 5 | 6 | export class CWSimulateVMInstance extends VMInstance { 7 | constructor( 8 | public logs: Array, 9 | private readonly debugFn: DebugFunction, 10 | backend: IBackend, 11 | env?: Environment 12 | ) { 13 | super(backend, env); 14 | } 15 | 16 | private processLog(log: DebugLog) { 17 | this.logs.push(log); 18 | this.debugFn(log); 19 | } 20 | 21 | do_db_read(key: Region): Region { 22 | const result = super.do_db_read(key); 23 | this.processLog({ 24 | type: 'call', 25 | fn: 'db_read', 26 | args: { 27 | key: key.str, 28 | }, 29 | result: result.str, 30 | }); 31 | 32 | return result; 33 | } 34 | 35 | do_db_write(key: Region, value: Region) { 36 | super.do_db_write(key, value); 37 | this.processLog({ type: 'call', fn: 'db_write', args: { key: key.str, value: value.str } }); 38 | } 39 | 40 | do_db_remove(key: Region) { 41 | super.do_db_remove(key); 42 | this.processLog({ 43 | type: 'call', 44 | fn: 'db_remove', 45 | args: { key: key.str }, 46 | }); 47 | } 48 | 49 | do_db_scan(start: Region, end: Region, order: number): Region { 50 | let result = super.do_db_scan(start, end, order); 51 | this.processLog({ 52 | type: 'call', 53 | fn: 'db_scan', 54 | args: { start: start.str, end: end.str, order }, 55 | result: result.str, 56 | }); 57 | 58 | return result; 59 | } 60 | 61 | do_db_next(iterator_id: Region): Region { 62 | let result = super.do_db_next(iterator_id); 63 | this.processLog({ 64 | type: 'call', 65 | fn: 'db_next', 66 | args: { iterator_id: iterator_id.str }, 67 | result: result.str, 68 | }); 69 | 70 | return result; 71 | } 72 | 73 | do_addr_humanize(source: Region, destination: Region): Region { 74 | let result = super.do_addr_humanize(source, destination); 75 | this.processLog({ 76 | type: 'call', 77 | fn: 'addr_humanize', 78 | args: { source: source.str }, 79 | result: result.str, 80 | }); 81 | 82 | return result; 83 | } 84 | 85 | do_addr_canonicalize(source: Region, destination: Region): Region { 86 | let result = super.do_addr_canonicalize(source, destination); 87 | this.processLog({ 88 | type: 'call', 89 | fn: 'addr_canonicalize', 90 | args: { source: source.str, destination: destination.str }, 91 | result: result.str, 92 | }); 93 | 94 | return result; 95 | } 96 | 97 | do_addr_validate(source: Region): Region { 98 | let result = super.do_addr_validate(source); 99 | this.processLog({ 100 | type: 'call', 101 | fn: 'addr_validate', 102 | args: { source: source.str }, 103 | result: result.str, 104 | }); 105 | 106 | return result; 107 | } 108 | 109 | do_secp256k1_verify(hash: Region, signature: Region, pubkey: Region): number { 110 | let result = super.do_secp256k1_verify(hash, signature, pubkey); 111 | this.processLog({ 112 | type: 'call', 113 | fn: 'secp256k1_verify', 114 | args: { 115 | hash: hash.str, 116 | signature: signature.str, 117 | pubkey: pubkey.str, 118 | }, 119 | result, 120 | }); 121 | 122 | return result; 123 | } 124 | 125 | do_secp256k1_recover_pubkey(msgHash: Region, signature: Region, recover_param: number): Region { 126 | let result = super.do_secp256k1_recover_pubkey(msgHash, signature, recover_param); 127 | this.processLog({ 128 | type: 'call', 129 | fn: 'secp256k1_recover_pubkey', 130 | args: { 131 | msgHash: msgHash.str, 132 | signature: signature.str, 133 | recover_param, 134 | }, 135 | result: result.str, 136 | }); 137 | 138 | return result; 139 | } 140 | 141 | do_abort(message: Region) { 142 | super.do_abort(message); 143 | this.processLog({ 144 | type: 'call', 145 | fn: 'abort', 146 | args: { message: message.read_str() }, 147 | }); 148 | } 149 | 150 | do_debug(message: Region) { 151 | const messageStr = message.read_str(); 152 | this.processLog({ 153 | type: 'call', 154 | fn: 'debug', 155 | args: { message: messageStr }, 156 | }); 157 | super.do_debug(message); 158 | // this help for implementing contract debug 159 | this.processLog({ 160 | type: 'print', 161 | message: messageStr, 162 | }); 163 | } 164 | 165 | do_ed25519_batch_verify(messages_ptr: Region, signatures_ptr: Region, public_keys_ptr: Region): number { 166 | let result = super.do_ed25519_batch_verify(messages_ptr, signatures_ptr, public_keys_ptr); 167 | this.processLog({ 168 | type: 'call', 169 | fn: 'ed25519_batch_verify', 170 | args: { 171 | messages_ptr: messages_ptr.str, 172 | signatures_ptr: signatures_ptr.str, 173 | pubkeys_ptr: public_keys_ptr.str, 174 | }, 175 | result, 176 | }); 177 | 178 | return result; 179 | } 180 | 181 | do_ed25519_verify(message: Region, signature: Region, pubkey: Region): number { 182 | let result = super.do_ed25519_verify(message, signature, pubkey); 183 | this.processLog({ 184 | type: 'call', 185 | fn: 'ed25519_verify', 186 | args: { 187 | message: message.str, 188 | signature: signature.str, 189 | pubkey: pubkey.str, 190 | }, 191 | result, 192 | }); 193 | 194 | return result; 195 | } 196 | 197 | do_query_chain(request: Region): Region { 198 | let result = super.do_query_chain(request); 199 | this.processLog({ 200 | type: 'call', 201 | fn: 'query_chain', 202 | args: { request: request.str }, 203 | result: result.str, 204 | }); 205 | return result; 206 | } 207 | 208 | /** Reset debug information such as debug messages & call history. 209 | * 210 | * These should be valid only for individual contract executions. 211 | */ 212 | resetDebugInfo() { 213 | this.debugMsgs = []; 214 | this.logs = []; 215 | return this; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/bank.spec.ts: -------------------------------------------------------------------------------- 1 | import { BankMsg } from '@oraichain/cosmwasm-vm-js'; 2 | import { cmd, exec, TestContract } from '../../testing/wasm-util'; 3 | import { CWSimulateApp } from '../CWSimulateApp'; 4 | import { BankQuery } from './bank'; 5 | 6 | type WrappedBankMsg = { 7 | bank: BankMsg; 8 | }; 9 | 10 | describe.only('BankModule', () => { 11 | let chain: CWSimulateApp; 12 | 13 | beforeEach(function () { 14 | chain = new CWSimulateApp({ 15 | chainId: 'test-1', 16 | bech32Prefix: 'terra', 17 | }); 18 | }); 19 | 20 | it('handle send', () => { 21 | // Arrange 22 | const bank = chain.bank; 23 | bank.setBalance('alice', [coin('foo', 1000)]); 24 | 25 | // Act 26 | bank.send('alice', 'bob', [coin('foo', 100)]).unwrap(); 27 | 28 | // Assert 29 | expect(bank.getBalance('alice')).toEqual([coin('foo', 900)]); 30 | expect(bank.getBalance('bob')).toEqual([coin('foo', 100)]); 31 | expect(bank.getBalances()).toEqual({ 32 | alice: [coin('foo', 900)], 33 | bob: [coin('foo', 100)], 34 | }); 35 | 36 | expect(bank.getSupply('foo')).toEqual('1000'); 37 | }); 38 | 39 | it('handle send failure', () => { 40 | // Arrange 41 | const bank = chain.bank; 42 | bank.setBalance('alice', [coin('foo', 100)]); 43 | 44 | // Act 45 | const res = bank.send('alice', 'bob', [coin('foo', 1000)]); 46 | 47 | // Assert 48 | expect(res.err).toBeDefined(); 49 | expect(bank.getBalances()).toEqual({ 50 | alice: [coin('foo', 100)], 51 | }); 52 | expect(bank.getBalance('alice')).toEqual([coin('foo', 100)]); 53 | }); 54 | 55 | it('handle burn', () => { 56 | // Arrange 57 | const bank = chain.bank; 58 | bank.setBalance('alice', [coin('foo', 1000)]); 59 | 60 | // Act 61 | bank.burn('alice', [coin('foo', 100)]); 62 | 63 | // Assert 64 | expect(bank.getBalance('alice')).toEqual([coin('foo', 900)]); 65 | expect(bank.getBalances()).toEqual({ 66 | alice: [coin('foo', 900)], 67 | }); 68 | }); 69 | 70 | it('handle burn failure', () => { 71 | // Arrange 72 | const bank = chain.bank; 73 | bank.setBalance('alice', [coin('foo', 100)]); 74 | 75 | // Act 76 | const res = bank.burn('alice', [coin('foo', 1000)]); 77 | 78 | // Assert 79 | expect(res.err).toBeDefined(); 80 | expect(bank.getBalance('alice')).toEqual([coin('foo', 100)]); 81 | expect(bank.getBalances()).toEqual({ 82 | alice: [coin('foo', 100)], 83 | }); 84 | }); 85 | 86 | it('handle msg', () => { 87 | // Arrange 88 | const bank = chain.bank; 89 | bank.setBalance('alice', [coin('foo', 1000)]); 90 | 91 | // Act 92 | let msg: WrappedBankMsg = { 93 | bank: { 94 | send: { 95 | to_address: 'bob', 96 | amount: [coin('foo', 100)], 97 | }, 98 | }, 99 | }; 100 | chain.handleMsg('alice', msg); 101 | 102 | // Assert 103 | expect(bank.getBalances()).toEqual({ 104 | alice: [coin('foo', 900)], 105 | bob: [coin('foo', 100)], 106 | }); 107 | }); 108 | 109 | it('contract integration', async () => { 110 | // Arrange 111 | const bank = chain.bank; 112 | const contract = await new TestContract(chain).instantiate(); 113 | bank.setBalance(contract.address, [coin('foo', 1000)]); 114 | 115 | // Act 116 | const msg = exec.run( 117 | cmd.bank({ 118 | send: { 119 | to_address: 'alice', 120 | amount: [coin('foo', 100)], 121 | }, 122 | }), 123 | cmd.bank({ 124 | send: { 125 | to_address: 'bob', 126 | amount: [coin('foo', 100)], 127 | }, 128 | }), 129 | cmd.bank({ 130 | burn: { 131 | amount: [coin('foo', 100)], 132 | }, 133 | }) 134 | ); 135 | const res = await contract.execute('alice', msg); 136 | 137 | // Assert 138 | expect(res.ok).toBeTruthy(); 139 | expect(bank.getBalances()).toMatchObject({ 140 | [contract.address]: [coin('foo', 700)], 141 | alice: [coin('foo', 100)], 142 | bob: [coin('foo', 100)], 143 | }); 144 | }); 145 | 146 | it('querier integration', () => { 147 | const bank = chain.bank; 148 | 149 | const queryBalance: BankQuery = { 150 | balance: { 151 | address: 'alice', 152 | denom: 'foo', 153 | }, 154 | }; 155 | 156 | const queryAllBalances: BankQuery = { 157 | all_balances: { 158 | address: 'bob', 159 | }, 160 | }; 161 | 162 | bank.setBalance('alice', [coin('foo', 100), coin('bar', 200)]); 163 | bank.setBalance('bob', [coin('foo', 200), coin('bar', 200)]); 164 | 165 | let res = chain.querier.handleQuery({ bank: queryBalance }); 166 | expect(res).toEqual({ amount: coin('foo', 100) }); 167 | 168 | res = chain.querier.handleQuery({ bank: queryAllBalances }); 169 | expect(res).toEqual({ 170 | amount: [coin('foo', 200), coin('bar', 200)], 171 | }); 172 | }); 173 | 174 | it('handle delete', () => { 175 | // Arrange 176 | const bank = chain.bank; 177 | bank.setBalance('alice', [coin('foo', 1000)]); 178 | bank.setBalance('bob', [coin('fizz', 900)]); 179 | 180 | // Act 181 | bank.deleteBalance('bob'); 182 | 183 | // Assert 184 | expect(bank.getBalance('alice')).toBeDefined(); 185 | expect(bank.getBalances()).toEqual({ 186 | alice: [coin('foo', 1000)], 187 | }); 188 | }); 189 | }); 190 | 191 | const coin = (denom: string, amount: string | number) => ({ denom, amount: `${amount}` }); 192 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/bank.ts: -------------------------------------------------------------------------------- 1 | import { Coin } from '@cosmjs/amino'; 2 | import { Err, Ok, Result } from 'ts-results'; 3 | import { BankMsg } from '@oraichain/cosmwasm-vm-js'; 4 | import { CWSimulateApp } from '../CWSimulateApp'; 5 | import { Transactional, TransactionalLens } from '../store/transactional'; 6 | import { AppResponse, Snapshot } from '../types'; 7 | 8 | type BankData = { 9 | balances: Record; 10 | }; 11 | 12 | export type BankQuery = 13 | | { 14 | balance: { 15 | address: string; 16 | denom: string; 17 | }; 18 | } 19 | | { 20 | all_balances: { 21 | address: string; 22 | }; 23 | } 24 | | { 25 | supply: { 26 | denom: string; 27 | }; 28 | }; 29 | 30 | export type BalanceResponse = { amount: Coin }; 31 | export type AllBalancesResponse = { amount: Coin[] }; 32 | export type SupplyResponse = { 33 | amount: Coin; 34 | }; 35 | 36 | export class BankModule { 37 | public readonly store: TransactionalLens; 38 | 39 | constructor(public readonly chain: CWSimulateApp) { 40 | this.store = this.chain.store.db.lens('bank').initialize({ 41 | balances: {}, 42 | }); 43 | } 44 | 45 | public send(sender: string, recipient: string, amount: Coin[]): Result { 46 | return this.store.tx(() => { 47 | let senderBalance = this.getBalance(sender) 48 | .map(ParsedCoin.fromCoin) 49 | .filter(c => c.amount > 0); 50 | const parsedCoins = amount.map(ParsedCoin.fromCoin).filter(c => c.amount > 0); 51 | 52 | // Deduct coins from sender 53 | for (const coin of parsedCoins) { 54 | const hasCoin = senderBalance.find(c => c.denom === coin.denom); 55 | 56 | if (hasCoin && hasCoin.amount >= coin.amount) { 57 | hasCoin.amount -= coin.amount; 58 | } else { 59 | return Err(`Sender ${sender} has ${hasCoin?.amount ?? 0} ${coin.denom}, needs ${coin.amount}`); 60 | } 61 | } 62 | senderBalance = senderBalance.filter(c => c.amount > 0); 63 | 64 | // Add amount to recipient 65 | const recipientBalance = this.getBalance(recipient).map(ParsedCoin.fromCoin); 66 | for (const coin of parsedCoins) { 67 | const hasCoin = recipientBalance.find(c => c.denom === coin.denom); 68 | 69 | if (hasCoin) { 70 | hasCoin.amount += coin.amount; 71 | } else { 72 | recipientBalance.push(coin); 73 | } 74 | } 75 | 76 | this.setBalance( 77 | sender, 78 | senderBalance.map(c => c.toCoin()) 79 | ); 80 | this.setBalance( 81 | recipient, 82 | recipientBalance.map(c => c.toCoin()) 83 | ); 84 | return Ok(undefined); 85 | }); 86 | } 87 | 88 | public burn(sender: string, amount: Coin[]): Result { 89 | return this.store.tx(() => { 90 | let balance = this.getBalance(sender).map(ParsedCoin.fromCoin); 91 | let parsedCoins = amount.map(ParsedCoin.fromCoin).filter(c => c.amount > 0); 92 | 93 | for (const coin of parsedCoins) { 94 | const hasCoin = balance.find(c => c.denom === coin.denom); 95 | 96 | if (hasCoin && hasCoin.amount >= coin.amount) { 97 | hasCoin.amount -= coin.amount; 98 | } else { 99 | return Err(`Sender ${sender} has ${hasCoin?.amount ?? 0} ${coin.denom}, needs ${coin.amount}`); 100 | } 101 | } 102 | balance = balance.filter(c => c.amount > 0); 103 | 104 | this.setBalance( 105 | sender, 106 | balance.map(c => c.toCoin()) 107 | ); 108 | return Ok(undefined); 109 | }); 110 | } 111 | 112 | public mint(sender: string, amount: Coin[]): Result { 113 | return this.store.tx(() => { 114 | let balance = this.getBalance(sender).map(ParsedCoin.fromCoin); 115 | let parsedCoins = amount.map(ParsedCoin.fromCoin).filter(c => c.amount > 0); 116 | 117 | for (const coin of parsedCoins) { 118 | const hasCoin = balance.find(c => c.denom === coin.denom); 119 | if (hasCoin) { 120 | hasCoin.amount += coin.amount; 121 | } else { 122 | balance.push(coin); 123 | } 124 | } 125 | balance = balance.filter(c => c.amount > 0); 126 | 127 | this.setBalance( 128 | sender, 129 | balance.map(c => c.toCoin()) 130 | ); 131 | return Ok(undefined); 132 | }); 133 | } 134 | 135 | public setBalance(address: string, amount: Coin[]) { 136 | this.store.tx((setter, deleter) => { 137 | setter('balances', address)(amount); 138 | return Ok(undefined); 139 | }); 140 | } 141 | 142 | public getBalance(address: string, storage?: Snapshot): Coin[] { 143 | return this.lens(storage).getObject('balances', address) ?? []; 144 | } 145 | 146 | public getBalances() { 147 | return this.store.getObject('balances'); 148 | } 149 | 150 | public deleteBalance(address: string) { 151 | this.store.tx((_, deleter) => { 152 | deleter('balances', address); 153 | return Ok(undefined); 154 | }); 155 | } 156 | 157 | public getSupply(denom: string): string { 158 | return Object.values(this.getBalances()) 159 | .flat() 160 | .filter(c => c.denom === denom) 161 | .reduce((total, c) => total + BigInt(c.amount), 0n) 162 | .toString(); 163 | } 164 | 165 | public async handleMsg(sender: string, msg: BankMsg): Promise> { 166 | if ('send' in msg) { 167 | const result = this.send(sender, msg.send.to_address, msg.send.amount); 168 | return result.andThen(() => 169 | Ok({ 170 | events: [ 171 | { 172 | type: 'transfer', 173 | attributes: [ 174 | { key: 'recipient', value: msg.send.to_address }, 175 | { key: 'sender', value: sender }, 176 | { key: 'amount', value: JSON.stringify(msg.send.amount) }, 177 | ], 178 | }, 179 | ], 180 | data: null, 181 | }) 182 | ); 183 | } 184 | 185 | if ('burn' in msg) { 186 | const result = this.burn(sender, msg.burn.amount); 187 | return result.andThen(() => 188 | Ok({ 189 | events: [ 190 | { 191 | type: 'burn', 192 | attributes: [ 193 | { key: 'sender', value: sender }, 194 | { key: 'amount', value: JSON.stringify(msg.burn.amount) }, 195 | ], 196 | }, 197 | ], 198 | data: null, 199 | }) 200 | ); 201 | } 202 | 203 | return Err('Unknown bank message'); 204 | } 205 | 206 | public handleQuery(query: BankQuery): BalanceResponse | AllBalancesResponse | SupplyResponse { 207 | let bankQuery = query; 208 | if ('balance' in bankQuery) { 209 | let { address, denom } = bankQuery.balance; 210 | const hasCoin = this.getBalance(address).find(c => c.denom === denom); 211 | return { 212 | amount: hasCoin ?? { denom, amount: '0' }, 213 | }; 214 | } 215 | 216 | if ('all_balances' in bankQuery) { 217 | let { address } = bankQuery.all_balances; 218 | return { 219 | amount: this.getBalance(address), 220 | }; 221 | } 222 | 223 | if ('supply' in bankQuery) { 224 | let { denom } = bankQuery.supply; 225 | return { 226 | amount: { denom, amount: this.getSupply(denom) }, 227 | }; 228 | } 229 | 230 | throw new Error('Unknown bank query'); 231 | } 232 | 233 | private lens(storage?: Snapshot) { 234 | return storage ? lensFromSnapshot(storage) : this.store; 235 | } 236 | } 237 | 238 | /** Essentially a `Coin`, but the `amount` is a `bigint` for more convenient use. */ 239 | export class ParsedCoin { 240 | constructor(public readonly denom: string, public amount: bigint) {} 241 | 242 | toCoin(): Coin { 243 | return { 244 | denom: this.denom, 245 | amount: this.amount.toString(), 246 | }; 247 | } 248 | 249 | static fromCoin(coin: Coin) { 250 | return new ParsedCoin(coin.denom, BigInt(coin.amount)); 251 | } 252 | } 253 | 254 | export function lensFromSnapshot(snapshot: Snapshot) { 255 | return new Transactional(snapshot).lens('bank'); 256 | } 257 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/ibc.spec.ts: -------------------------------------------------------------------------------- 1 | import { coin, coins } from '@cosmjs/amino'; 2 | import { fromBinary, toBinary } from '@cosmjs/cosmwasm-stargate'; 3 | import { fromBech32, toBech32 } from '@cosmjs/encoding'; 4 | import { CosmosMsg, IbcMsgTransfer } from '@oraichain/cosmwasm-vm-js'; 5 | import { readFileSync } from 'fs'; 6 | import path from 'path'; 7 | import { CWSimulateApp } from '../CWSimulateApp'; 8 | import { AppResponse, IbcOrder } from '../types'; 9 | import { ibcDenom } from './ibc'; 10 | 11 | const terraChain = new CWSimulateApp({ 12 | chainId: 'test-1', 13 | bech32Prefix: 'terra', 14 | }); 15 | const oraiChain = new CWSimulateApp({ 16 | chainId: 'Oraichain', 17 | bech32Prefix: 'orai', 18 | }); 19 | const oraiSenderAddress = 'orai1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejvfgs7g'; 20 | const bobAddress = 'orai1ur2vsjrjarygawpdwtqteaazfchvw4fg6uql76'; 21 | const terraSenderAddress = toBech32(terraChain.bech32Prefix, fromBech32(oraiSenderAddress).data); 22 | 23 | describe.only('IBCModule', () => { 24 | let oraiPort: string; 25 | let terraPort: string = 'transfer'; 26 | let contractAddress: string; 27 | beforeEach(async () => { 28 | const reflectCodeId = oraiChain.wasm.create( 29 | oraiSenderAddress, 30 | readFileSync(path.join(__dirname, '..', '..', 'testing', 'reflect.wasm')) 31 | ); 32 | const ibcReflectCodeId = oraiChain.wasm.create( 33 | oraiSenderAddress, 34 | readFileSync(path.join(__dirname, '..', '..', 'testing', 'ibc_reflect.wasm')) 35 | ); 36 | 37 | const oraiRet = await oraiChain.wasm.instantiateContract( 38 | oraiSenderAddress, 39 | [], 40 | ibcReflectCodeId, 41 | { reflect_code_id: reflectCodeId }, 42 | 'ibc-reflect' 43 | ); 44 | contractAddress = (oraiRet.val as AppResponse).events[0].attributes[0].value; 45 | oraiPort = 'wasm.' + contractAddress; 46 | }); 47 | 48 | it('handle-reflect', async () => { 49 | oraiChain.ibc.relay('channel-0', oraiPort, 'channel-0', terraPort, terraChain); 50 | expect(oraiPort).toEqual(oraiChain.ibc.getContractIbcPort(contractAddress)); 51 | const channelOpenRes = await terraChain.ibc.sendChannelOpen({ 52 | open_init: { 53 | channel: { 54 | counterparty_endpoint: { 55 | port_id: oraiPort, 56 | channel_id: 'channel-0', 57 | }, 58 | endpoint: { 59 | port_id: terraPort, 60 | channel_id: 'channel-0', 61 | }, 62 | order: IbcOrder.Ordered, 63 | version: 'ibc-reflect-v1', 64 | connection_id: 'connection-0', 65 | }, 66 | }, 67 | }); 68 | expect(channelOpenRes).toEqual({ version: 'ibc-reflect-v1' }); 69 | 70 | const channelConnectRes = await terraChain.ibc.sendChannelConnect({ 71 | open_ack: { 72 | channel: { 73 | counterparty_endpoint: { 74 | port_id: oraiPort, 75 | channel_id: 'channel-0', 76 | }, 77 | endpoint: { 78 | port_id: terraPort, 79 | channel_id: 'channel-0', 80 | }, 81 | order: IbcOrder.Ordered, 82 | version: 'ibc-reflect-v1', 83 | connection_id: 'connection-0', 84 | }, 85 | counterparty_version: 'ibc-reflect-v1', 86 | }, 87 | }); 88 | 89 | expect(channelConnectRes.attributes).toEqual([ 90 | { key: 'action', value: 'ibc_connect' }, 91 | { key: 'channel_id', value: 'channel-0' }, 92 | ]); 93 | 94 | // get reflect address 95 | let packetReceiveRes = await terraChain.ibc.sendPacketReceive({ 96 | packet: { 97 | data: toBinary({ 98 | who_am_i: {}, 99 | }), 100 | src: { 101 | port_id: terraPort, 102 | channel_id: 'channel-0', 103 | }, 104 | dest: { 105 | port_id: oraiPort, 106 | channel_id: 'channel-0', 107 | }, 108 | sequence: terraChain.ibc.sequence++, 109 | timeout: { 110 | block: { 111 | revision: 1, 112 | height: terraChain.height, 113 | }, 114 | }, 115 | }, 116 | relayer: terraSenderAddress, 117 | }); 118 | const res = fromBinary(packetReceiveRes.acknowledgement) as { ok: { account: string } }; 119 | const reflectContractAddress = res.ok.account; 120 | expect(reflectContractAddress).toEqual(oraiChain.wasm.getContracts()[1].address); 121 | // set some balance for reflect contract 122 | oraiChain.bank.setBalance(reflectContractAddress, coins('500000000000', 'orai')); 123 | 124 | // send message to bob on oraichain 125 | packetReceiveRes = await terraChain.ibc.sendPacketReceive({ 126 | packet: { 127 | data: toBinary({ 128 | dispatch: { 129 | msgs: [ 130 | { 131 | bank: { 132 | send: { 133 | to_address: bobAddress, 134 | amount: coins(123456789, 'orai'), 135 | }, 136 | }, 137 | }, 138 | ], 139 | }, 140 | }), 141 | src: { 142 | port_id: terraPort, 143 | channel_id: 'channel-0', 144 | }, 145 | dest: { 146 | port_id: oraiPort, 147 | channel_id: 'channel-0', 148 | }, 149 | sequence: terraChain.ibc.sequence++, 150 | timeout: { 151 | block: { 152 | revision: 1, 153 | height: terraChain.height, 154 | }, 155 | }, 156 | }, 157 | relayer: terraSenderAddress, 158 | }); 159 | 160 | const bobBalance = oraiChain.bank.getBalance(bobAddress); 161 | expect(bobBalance).toEqual(coins(123456789, 'orai')); 162 | 163 | const { val } = (await oraiChain.ibc.handleMsg(oraiSenderAddress, { 164 | close_channel: { channel_id: 'channel-0' }, 165 | })) as { val: AppResponse }; 166 | 167 | // call handle will merge all events from application module 168 | expect(val.events[3]).toEqual({ 169 | type: 'channel_close_init', 170 | attributes: [ 171 | { key: 'port_id', value: oraiPort }, 172 | { 173 | key: 'channel_id', 174 | value: oraiSenderAddress, 175 | }, 176 | { 177 | key: 'counterparty_port_id', 178 | value: terraPort, 179 | }, 180 | { 181 | key: 'counterparty_channel_id', 182 | value: 'channel-0', 183 | }, 184 | { 185 | key: 'connection_id', 186 | value: 'connection-0', 187 | }, 188 | { 189 | key: 'action', 190 | value: 'channel_close_init', 191 | }, 192 | { 193 | key: 'module', 194 | value: 'ibc_channel', 195 | }, 196 | ], 197 | }); 198 | }); 199 | 200 | it('ibc-handle-msg', async () => { 201 | // Arrange 202 | oraiChain.ibc.relay('channel-0', oraiPort, 'channel-0', terraPort, terraChain); 203 | 204 | // call transfer module, does not require wasm module 205 | let msg: IbcMsgTransfer = { 206 | transfer: { 207 | channel_id: 'channel-0', 208 | amount: coin('100000000', 'ust'), 209 | to_address: oraiSenderAddress, 210 | 211 | timeout: { 212 | timestamp: '123456', 213 | }, 214 | }, 215 | }; 216 | // to receive events and attributes we must call handleMsg, otherwise we only get response from sending message 217 | const ret = await terraChain.ibc.handleMsg(terraSenderAddress, msg); 218 | console.log(JSON.stringify(ret.val)); 219 | 220 | expect(oraiChain.bank.getBalance(oraiSenderAddress)).toEqual( 221 | coins('100000000', ibcDenom('transfer', 'channel-0', 'ust')) 222 | ); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/wasm/contract.ts: -------------------------------------------------------------------------------- 1 | import { Coin } from '@cosmjs/amino'; 2 | import { fromBinary } from '@cosmjs/cosmwasm-stargate'; 3 | import { ContractResponse, IBackend } from '@oraichain/cosmwasm-vm-js'; 4 | import { Map } from '@oraichain/immutable'; 5 | import { Err, Ok, Result } from 'ts-results'; 6 | import { CWSimulateVMInstance } from '../../instrumentation/CWSimulateVMInstance'; 7 | import { 8 | DebugLog, 9 | IbcBasicResponse, 10 | IbcChannelCloseMsg, 11 | IbcChannelConnectMsg, 12 | IbcChannelOpenMsg, 13 | IbcChannelOpenResponse, 14 | IbcPacketAckMsg, 15 | IbcPacketReceiveMsg, 16 | IbcPacketTimeoutMsg, 17 | IbcReceiveResponse, 18 | ReplyMsg, 19 | Snapshot, 20 | } from '../../types'; 21 | import { fromRustResult } from '../../util'; 22 | import { ContractNotFoundError } from './error'; 23 | import { WasmModule } from './module'; 24 | 25 | /** An interface to interact with CW SCs */ 26 | export default class Contract { 27 | private _vm: CWSimulateVMInstance | undefined; 28 | 29 | constructor(private _wasm: WasmModule, public readonly address: string) {} 30 | 31 | async init() { 32 | if (!this._vm) { 33 | const { _wasm: wasm, address } = this; 34 | const contractInfo = wasm.getContractInfo(address); 35 | if (!contractInfo) throw new Error(`Contract ${address} not found`); 36 | 37 | const { codeId } = contractInfo; 38 | const codeInfo = wasm.getCodeInfo(codeId); 39 | if (!codeInfo) throw new Error(`code ${codeId} not found`); 40 | 41 | const { wasmCode } = codeInfo; 42 | const contractState = this.getStorage(); 43 | 44 | // @ts-ignore 45 | const storage = new wasm.chain.kvIterStorageRegistry(contractState); 46 | 47 | const backend: IBackend = { 48 | backend_api: wasm.chain.backendApi, 49 | storage, 50 | querier: wasm.chain.querier, 51 | }; 52 | 53 | const logs: DebugLog[] = []; 54 | // pass debug reference from wasm.chain, if implemented, check metering when sharing env 55 | 56 | const vm = new CWSimulateVMInstance(logs, msg => wasm.chain.debug?.(msg), backend, wasm.chain.env); 57 | 58 | await vm.build(wasmCode, WasmModule.checksumCache[codeId]); 59 | 60 | this._vm = vm; 61 | } 62 | return this; 63 | } 64 | 65 | instantiate(sender: string, funds: Coin[], instantiateMsg: any, logs: DebugLog[]): Result { 66 | try { 67 | if (!this._vm) { 68 | return new ContractNotFoundError(this.address); 69 | } 70 | const vm = this._vm; 71 | 72 | const env = this.getExecutionEnv(); 73 | const info = { sender, funds }; 74 | 75 | const res = fromRustResult(vm.instantiate(env, info, instantiateMsg)); 76 | 77 | this.setStorage(vm.backend.storage.dict); 78 | 79 | logs.push(...vm.logs); 80 | 81 | return res; 82 | } catch (ex) { 83 | return Err((ex as Error).message ?? ex.toString()); 84 | } 85 | } 86 | 87 | execute(sender: string, funds: Coin[], executeMsg: any, logs: DebugLog[]): Result { 88 | try { 89 | if (!this._vm) { 90 | return new ContractNotFoundError(this.address); 91 | } 92 | const vm = this._vm; 93 | vm.resetDebugInfo(); 94 | const env = this.getExecutionEnv(); 95 | const info = { sender, funds }; 96 | const res = fromRustResult(vm.execute(env, info, executeMsg)); 97 | 98 | this.setStorage(vm.backend.storage.dict); 99 | 100 | logs.push(...vm.logs); 101 | 102 | return res; 103 | } catch (ex) { 104 | return Err((ex as Error).message ?? ex.toString()); 105 | } 106 | } 107 | 108 | migrate(migrateMsg: any, logs: DebugLog[]): Result { 109 | try { 110 | if (!this._vm) { 111 | return new ContractNotFoundError(this.address); 112 | } 113 | const vm = this._vm; 114 | const env = this.getExecutionEnv(); 115 | const res = fromRustResult(vm.migrate(env, migrateMsg)); 116 | 117 | this.setStorage(vm.backend.storage.dict); 118 | 119 | logs.push(...vm.logs); 120 | 121 | return res; 122 | } catch (ex) { 123 | return Err((ex as Error).message ?? ex.toString()); 124 | } 125 | } 126 | 127 | sudo(sudoMsg: any, logs: DebugLog[]): Result { 128 | try { 129 | if (!this._vm) { 130 | return new ContractNotFoundError(this.address); 131 | } 132 | const vm = this._vm; 133 | const env = this.getExecutionEnv(); 134 | const res = fromRustResult(vm.sudo(env, sudoMsg)); 135 | 136 | this.setStorage(vm.backend.storage.dict); 137 | 138 | logs.push(...vm.logs); 139 | 140 | return res; 141 | } catch (ex) { 142 | return Err((ex as Error).message ?? ex.toString()); 143 | } 144 | } 145 | 146 | reply(replyMsg: ReplyMsg, logs: DebugLog[]): Result { 147 | try { 148 | if (!this._vm) { 149 | return new ContractNotFoundError(this.address); 150 | } 151 | const vm = this._vm; 152 | const res = fromRustResult(vm.reply(this.getExecutionEnv(), replyMsg)); 153 | 154 | this.setStorage(vm.backend.storage.dict); 155 | 156 | logs.push(...vm.logs); 157 | 158 | return res; 159 | } catch (ex) { 160 | return Err((ex as Error).message ?? ex.toString()); 161 | } 162 | } 163 | 164 | ibc_channel_open(ibcChannelOpenMsg: IbcChannelOpenMsg, logs: DebugLog[]): Result { 165 | try { 166 | if (!this._vm) { 167 | return new ContractNotFoundError(this.address); 168 | } 169 | const vm = this._vm; 170 | const res = fromRustResult( 171 | vm.ibc_channel_open(this.getExecutionEnv(), ibcChannelOpenMsg) 172 | ); 173 | 174 | this.setStorage(vm.backend.storage.dict); 175 | 176 | logs.push(...vm.logs); 177 | 178 | return res; 179 | } catch (ex) { 180 | return Err((ex as Error).message ?? ex.toString()); 181 | } 182 | } 183 | 184 | ibc_channel_connect(ibcChannelConnectMsg: IbcChannelConnectMsg, logs: DebugLog[]): Result { 185 | try { 186 | if (!this._vm) { 187 | return new ContractNotFoundError(this.address); 188 | } 189 | const vm = this._vm; 190 | const res = fromRustResult( 191 | vm.ibc_channel_connect(this.getExecutionEnv(), ibcChannelConnectMsg) 192 | ); 193 | 194 | this.setStorage(vm.backend.storage.dict); 195 | 196 | logs.push(...vm.logs); 197 | 198 | return res; 199 | } catch (ex) { 200 | return Err((ex as Error).message ?? ex.toString()); 201 | } 202 | } 203 | 204 | ibc_channel_close(ibcChannelCloseMsg: IbcChannelCloseMsg, logs: DebugLog[]): Result { 205 | try { 206 | if (!this._vm) { 207 | return new ContractNotFoundError(this.address); 208 | } 209 | const vm = this._vm; 210 | const res = fromRustResult(vm.ibc_channel_close(this.getExecutionEnv(), ibcChannelCloseMsg)); 211 | 212 | this.setStorage(vm.backend.storage.dict); 213 | 214 | logs.push(...vm.logs); 215 | 216 | return res; 217 | } catch (ex) { 218 | return Err((ex as Error).message ?? ex.toString()); 219 | } 220 | } 221 | 222 | ibc_packet_receive(ibcPacketReceiveMsg: IbcPacketReceiveMsg, logs: DebugLog[]): Result { 223 | try { 224 | if (!this._vm) { 225 | return new ContractNotFoundError(this.address); 226 | } 227 | const vm = this._vm; 228 | const res = fromRustResult( 229 | vm.ibc_packet_receive(this.getExecutionEnv(), ibcPacketReceiveMsg) 230 | ); 231 | 232 | this.setStorage(vm.backend.storage.dict); 233 | 234 | logs.push(...vm.logs); 235 | 236 | return res; 237 | } catch (ex) { 238 | return Err((ex as Error).message ?? ex.toString()); 239 | } 240 | } 241 | 242 | ibc_packet_ack(ibcPacketAckMsg: IbcPacketAckMsg, logs: DebugLog[]): Result { 243 | try { 244 | if (!this._vm) { 245 | return new ContractNotFoundError(this.address); 246 | } 247 | const vm = this._vm; 248 | const res = fromRustResult(vm.ibc_packet_ack(this.getExecutionEnv(), ibcPacketAckMsg)); 249 | 250 | this.setStorage(vm.backend.storage.dict); 251 | 252 | logs.push(...vm.logs); 253 | 254 | return res; 255 | } catch (ex) { 256 | return Err((ex as Error).message ?? ex.toString()); 257 | } 258 | } 259 | 260 | ibc_packet_timeout(ibcPacketTimeoutMsg: IbcPacketTimeoutMsg, logs: DebugLog[]): Result { 261 | try { 262 | if (!this._vm) { 263 | return new ContractNotFoundError(this.address); 264 | } 265 | const vm = this._vm; 266 | const res = fromRustResult(vm.ibc_packet_timeout(this.getExecutionEnv(), ibcPacketTimeoutMsg)); 267 | 268 | this.setStorage(vm.backend.storage.dict); 269 | 270 | logs.push(...vm.logs); 271 | 272 | return res; 273 | } catch (ex) { 274 | return Err((ex as Error).message ?? ex.toString()); 275 | } 276 | } 277 | 278 | query(queryMsg: any, store?: Map): Result { 279 | if (!this._vm) { 280 | return new ContractNotFoundError(this.address); 281 | } 282 | 283 | const vm = this._vm; 284 | 285 | // time travel 286 | const currBackend = vm.backend; 287 | // @ts-ignore 288 | const storage = new this._wasm.chain.kvIterStorageRegistry(this.getStorage(store)); 289 | 290 | vm.backend = { 291 | ...vm.backend, 292 | storage, 293 | }; 294 | 295 | let env = this.getExecutionEnv(); 296 | try { 297 | return fromRustResult(vm.query(env, queryMsg)).andThen(v => Ok(fromBinary(v))); 298 | } catch (ex) { 299 | return Err((ex as Error).message ?? ex.toString()); 300 | } finally { 301 | // reset time travel 302 | this._vm.backend = currBackend; 303 | } 304 | } 305 | 306 | setStorage(value: Map) { 307 | this._wasm.setContractStorage(this.address, value); 308 | } 309 | 310 | getStorage(storage?: Snapshot): Map { 311 | return this._wasm.getContractStorage(this.address, storage); 312 | } 313 | 314 | getExecutionEnv() { 315 | return this._wasm.getExecutionEnv(this.address); 316 | } 317 | 318 | get vm() { 319 | return this._vm; 320 | } 321 | get valid() { 322 | return !!this._vm; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/wasm/error.ts: -------------------------------------------------------------------------------- 1 | import { ErrImpl } from 'ts-results'; 2 | 3 | export class VmError extends ErrImpl { 4 | constructor(msg: string) { 5 | super(`VmError: ${msg}`); 6 | } 7 | } 8 | 9 | export class ContractNotFoundError extends VmError { 10 | constructor(contractAddress: string) { 11 | super(`contract ${contractAddress} not found`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/wasm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/modules/wasm/wasm-util.ts: -------------------------------------------------------------------------------- 1 | import { Sha256 } from '@cosmjs/crypto'; 2 | import protobuf from 'protobufjs'; 3 | import { ContractResponse, Event, writeUInt32BE } from '@oraichain/cosmwasm-vm-js'; 4 | import { toBase64 } from '@cosmjs/encoding'; 5 | import { AppResponse } from '../../types'; 6 | 7 | const protobufRoot = protobuf.Root.fromJSON({ 8 | nested: { 9 | MsgInstantiateContractResponse: { 10 | fields: { 11 | address: { 12 | type: 'string', 13 | id: 1, 14 | }, 15 | data: { 16 | type: 'bytes', 17 | id: 2, 18 | }, 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | export function wrapReplyResponse(res: AppResponse): AppResponse { 25 | const MsgInstantiateContractResponse = protobufRoot.lookupType('MsgInstantiateContractResponse'); 26 | 27 | const payload = { 28 | data: res.data, 29 | address: null, 30 | }; 31 | 32 | for (const event of res.events) { 33 | const address = event.attributes.find(attr => attr.key === '_contract_address')?.value; 34 | if (address) { 35 | payload.address = address; 36 | break; 37 | } 38 | } 39 | 40 | const message = MsgInstantiateContractResponse.create(payload); //; 41 | return { 42 | events: res.events, 43 | data: toBase64(MsgInstantiateContractResponse.encode(message).finish()), 44 | }; 45 | } 46 | 47 | export function buildContractAddress(codeId: number, instanceId: number): Uint8Array { 48 | const payload = Buffer.alloc(21); // wasm0 + contractId = 5 + 16, and initialized to 0 by default 49 | payload.write('wasm'); 50 | // append code id 51 | writeUInt32BE(payload, codeId, 9); 52 | writeUInt32BE(payload, instanceId, 17); 53 | 54 | let hasher = new Sha256(); 55 | hasher.update(Buffer.from('module', 'utf-8')); 56 | let th = hasher.digest(); 57 | hasher = new Sha256(th); 58 | hasher.update(payload); 59 | let hash = hasher.digest(); 60 | return hash.slice(0, 20); 61 | } 62 | 63 | export function buildAppResponse(contract: string, customEvent: Event, response: ContractResponse): AppResponse { 64 | const appEvents: Event[] = []; 65 | // add custom event 66 | appEvents.push(customEvent); 67 | 68 | // add contract attributes under `wasm` event type 69 | if (response.attributes.length > 0) { 70 | appEvents.push({ 71 | type: 'wasm', 72 | attributes: [ 73 | { 74 | key: '_contract_address', 75 | value: contract, 76 | }, 77 | ...response.attributes, 78 | ], 79 | }); 80 | } 81 | 82 | // add events and prefix with `wasm-` 83 | for (const event of response.events) { 84 | appEvents.push({ 85 | type: `wasm-${event.type}`, 86 | attributes: [{ key: '_contract_address', value: contract }, ...event.attributes], 87 | }); 88 | } 89 | 90 | return { 91 | events: appEvents, 92 | data: response.data, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/persist.ts: -------------------------------------------------------------------------------- 1 | import Serde, { SERDE, StandardProtocolMap } from '@kiruse/serde'; 2 | import { Reference } from '@kiruse/serde/dist/types'; 3 | import { List, Map } from '@oraichain/immutable'; 4 | import { Ok } from 'ts-results'; 5 | import { CWSimulateApp } from './CWSimulateApp'; 6 | 7 | type Protocols = StandardProtocolMap & { 8 | 'immutable-list': List; 9 | 'immutable-map': Map; 10 | 'cw-simulate-app': CWSimulateApp; 11 | }; 12 | 13 | export const serde = Serde() 14 | .standard() 15 | .setSimple( 16 | 'immutable-list', 17 | (list: List, data) => { 18 | return { 19 | data: data(list.toArray()), 20 | // ownerID is a unique object that should not even appear on 21 | // other Immutable data structures. When present, it signifies 22 | // that the Immutable should be mutated in-place rather than 23 | // creating copies of its data. 24 | mutable: !!(list as any).__ownerID, 25 | }; 26 | }, 27 | ({ data, mutable }, deref) => { 28 | if (!data.length) return List(); 29 | const list = List().asMutable(); 30 | Reference.all(deref, data, values => { 31 | list.push(...values); 32 | !mutable && list.asImmutable(); 33 | }); 34 | return list; 35 | } 36 | ) 37 | .setSimple( 38 | 'immutable-map', 39 | (map: Map, data) => { 40 | return { 41 | data: data(map.toObject()), 42 | // same as with List above 43 | mutable: !!(map as any).__ownerID, 44 | }; 45 | }, 46 | ({ data, mutable }, deref) => { 47 | const map = Map().asMutable(); 48 | const keys = Object.keys(data); 49 | if (!keys.length) return Map(); 50 | Reference.all( 51 | deref, 52 | keys.map(k => data[k]), 53 | values => { 54 | values.forEach((value, i) => { 55 | const key = keys[i]; 56 | map.set(key, value); 57 | }); 58 | !mutable && map.asImmutable(); 59 | } 60 | ); 61 | return map; 62 | } 63 | ) 64 | .setSimple( 65 | 'cw-simulate-app', 66 | (app: CWSimulateApp) => ({ 67 | chainId: app.chainId, 68 | bech32Prefix: app.bech32Prefix, 69 | store: app.store.db.data, 70 | }), 71 | ({ chainId, bech32Prefix, store }, deref): CWSimulateApp => { 72 | // for sorted type, metering, need to update when restored succesfully 73 | const app = new CWSimulateApp({ 74 | chainId, 75 | bech32Prefix, 76 | }); 77 | Reference.all(deref, [store], ([map]) => { 78 | app.store.db.tx(update => { 79 | update(() => map); 80 | return Ok(undefined); 81 | }); 82 | }); 83 | return app; 84 | } 85 | ); 86 | 87 | export const save = (app: CWSimulateApp) => serde.serializeAs('cw-simulate-app', app).compress().buffer; 88 | 89 | export const load = async (bytes: Uint8Array) => { 90 | const app = serde.deserializeAs('cw-simulate-app', bytes); 91 | const contracts = [...app.wasm.store.get('contracts').keys()]; 92 | await Promise.all(contracts.map(address => app.wasm.getContract(address).init())); 93 | return app; 94 | }; 95 | 96 | // Inject SERDE 97 | Map.prototype[SERDE] = 'immutable-map'; 98 | List.prototype[SERDE] = 'immutable-list'; 99 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transactional'; 2 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/store/transactional.ts: -------------------------------------------------------------------------------- 1 | import { isCollection, isList, isMap, List, Map } from '@oraichain/immutable'; 2 | import { Ok, Result } from 'ts-results'; 3 | import { isArrayLike } from '../util'; 4 | 5 | // NEVER_IMMUTIFY is a string because that's easily serializable with different algorithms - symbols are not 6 | export type NeverImmutify = typeof NEVER_IMMUTIFY; 7 | export const NEVER_IMMUTIFY = '__NEVER_IMMUTIFY__'; 8 | 9 | type Primitive = boolean | number | bigint | string | null | undefined | symbol; 10 | type NoImmutify = Primitive | ArrayBuffer | ArrayBufferView | { [NEVER_IMMUTIFY]: any }; 11 | 12 | type Prefix = [P, ...T]; 13 | type First = T extends Prefix ? F : never; 14 | type Shift = T extends Prefix ? R : []; 15 | 16 | // god type 17 | type Lens = P extends Prefix 18 | ? First

extends keyof T 19 | ? Shift

extends Prefix 20 | ? Lens], Shift

> 21 | : T[First

] 22 | : never 23 | : T; 24 | 25 | type Immutify = T extends NoImmutify 26 | ? T 27 | : T extends ArrayLike 28 | ? List> 29 | : T extends Record 30 | ? Map> 31 | : T; 32 | 33 | type TxUpdater = (set: TxSetter) => void; 34 | type TxSetter = (current: Map) => Map; 35 | type LensSetter =

(...path: P) => (value: Lens | Immutify>) => void; 36 | type LensDeleter =

(...path: P) => void; 37 | 38 | /** Transactional database underlying multi-module chain storage. */ 39 | export class Transactional { 40 | constructor(private _data = Map()) {} 41 | 42 | lens(...path: PropertyKey[]) { 43 | return new TransactionalLens(this, path.map(stringify)); 44 | } 45 | 46 | tx>(cb: (update: TxUpdater) => Promise): Promise; 47 | tx>(cb: (update: TxUpdater) => R): R; 48 | tx>(cb: (update: TxUpdater) => R | Promise): R | Promise { 49 | let valid = true; 50 | const snapshot = this._data; 51 | const updater: TxUpdater = setter => { 52 | if (!valid) throw new Error('Attempted to set data outside tx'); 53 | this._data = setter(this._data); 54 | }; 55 | 56 | try { 57 | const result = cb(updater); 58 | if ('then' in result) { 59 | return result 60 | .then(r => { 61 | if (r.err) { 62 | this._data = snapshot; 63 | } 64 | return r; 65 | }) 66 | .catch(reason => { 67 | this._data = snapshot; 68 | throw reason; 69 | }); 70 | } else { 71 | if (result.err) { 72 | this._data = snapshot; 73 | } 74 | return result; 75 | } 76 | } catch (ex) { 77 | this._data = snapshot; 78 | throw ex; 79 | } finally { 80 | valid = false; 81 | } 82 | } 83 | 84 | get data() { 85 | return this._data; 86 | } 87 | } 88 | 89 | export class TransactionalLens { 90 | constructor(public readonly db: Transactional, public readonly prefix: string[]) {} 91 | 92 | initialize(data: M) { 93 | this.db 94 | .tx(update => { 95 | const coll = toImmutable(data); 96 | if (!isCollection(coll)) throw new Error('Not an Immutable.Map'); 97 | update(curr => curr.setIn([...this.prefix], coll)); 98 | return Ok(undefined); 99 | }) 100 | .unwrap(); 101 | return this; 102 | } 103 | 104 | get

(...path: P): Immutify> { 105 | return this.db.data.getIn([...this.prefix, ...path.map(stringify)]) as any; 106 | } 107 | 108 | getObject

(...path: P): Lens { 109 | return fromImmutable(this.get(...path)); 110 | } 111 | 112 | tx>(cb: (setter: LensSetter, deleter: LensDeleter) => Promise): Promise; 113 | tx>(cb: (setter: LensSetter, deleter: LensDeleter) => R): R; 114 | tx>(cb: (setter: LensSetter, deleter: LensDeleter) => R | Promise): R | Promise { 115 | //@ts-ignore 116 | return this.db.tx(update => { 117 | const setter: LensSetter = 118 |

(...path: P) => 119 | (value: Lens | Immutify>) => { 120 | update(curr => curr.setIn([...this.prefix, ...path.map(stringify)], toImmutable(value))); 121 | }; 122 | const deleter: LensDeleter =

(...path: P) => { 123 | update(curr => curr.deleteIn([...this.prefix, ...path.map(stringify)])); 124 | }; 125 | return cb(setter, deleter); 126 | }); 127 | } 128 | 129 | lens

(...path: P): TransactionalLens> { 130 | return new TransactionalLens>(this.db, [...this.prefix, ...path.map(stringify)]); 131 | } 132 | 133 | get data() { 134 | return this.db.data.getIn([...this.prefix]) as Immutify; 135 | } 136 | } 137 | 138 | export function toImmutable(value: any): any { 139 | // passthru Immutable collections 140 | if (isCollection(value)) return value; 141 | 142 | // don't touch ArrayBuffers & ArrayBufferViews - freeze them 143 | if (ArrayBuffer.isView(value)) { 144 | Object.freeze(value.buffer); 145 | return value; 146 | } 147 | if (value instanceof ArrayBuffer) { 148 | Object.freeze(value); 149 | return value; 150 | } 151 | 152 | // recurse into arrays & objects, converting them to lists & maps 153 | // skip primitives & objects that don't want to be touched 154 | if (value && typeof value === 'object' && !(NEVER_IMMUTIFY in value)) { 155 | if (isArrayLike(value)) { 156 | return List(value.map(item => toImmutable(item))); 157 | } else { 158 | return Map(Object.entries(value).map(([key, value]) => [key, toImmutable(value)])); 159 | } 160 | } 161 | 162 | return value; 163 | } 164 | 165 | export function fromImmutable(value: any): any { 166 | // reverse Immutable maps & lists 167 | if (isMap(value)) { 168 | return fromImmutable(value.toObject()); 169 | } 170 | if (isList(value)) { 171 | return fromImmutable(value.toArray()); 172 | } 173 | 174 | // passthru ArrayBuffers & ArrayBufferViews 175 | if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value; 176 | 177 | // revert objects & arrays 178 | // but: passthru objects w/ NEVER_IMMUTIFY 179 | if (value && typeof value === 'object' && !(NEVER_IMMUTIFY in value)) { 180 | if (typeof value.length === 'number' && 0 in value && value.length - 1 in value) { 181 | for (let i = 0; i < value.length; ++i) { 182 | value[i] = fromImmutable(value[i]); 183 | } 184 | } else { 185 | for (const prop in value) { 186 | value[prop] = fromImmutable(value[prop]); 187 | } 188 | } 189 | return value; 190 | } 191 | 192 | return value; 193 | } 194 | 195 | const stringify = (v: any) => v + ''; 196 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/sync-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { SyncState } from './sync'; 3 | import dotenv from 'dotenv'; 4 | import { COSMOS_CHAIN_IDS, ORAI } from '@oraichain/common'; 5 | dotenv.config(); 6 | 7 | const SENDER = 'orai1hvr9d72r5um9lvt0rpkd4r75vrsqtw6yujhqs2'; 8 | 9 | (async () => { 10 | const startHeight = 36975366; 11 | const endHeight = 36975369; 12 | const syncState = new SyncState( 13 | SENDER, 14 | { rpc: process.env.RPC ?? 'https://rpc.orai.io', chainId: COSMOS_CHAIN_IDS.ORAICHAIN, bech32Prefix: ORAI }, 15 | resolve(__dirname, '../', 'data') 16 | ); 17 | const relatedContracts = [ 18 | 'orai12sxqkgsystjgd9faa48ghv3zmkfqc6qu05uy20mvv730vlzkpvls5zqxuz', 19 | 'orai1wuvhex9xqs3r539mvc6mtm7n20fcj3qr2m0y9khx6n5vtlngfzes3k0rq9', 20 | 'orai1rdykz2uuepxhkarar8ql5ajj5j37pq8h8d4zarvgx2s8pg0af37qucldna', 21 | 'orai1yglsm0u2x3xmct9kq3lxa654cshaxj9j5d9rw5enemkkkdjgzj7sr3gwt0', 22 | ]; 23 | const { results, simulateClient } = await syncState.sync(startHeight, endHeight, relatedContracts, { 24 | orai12sxqkgsystjgd9faa48ghv3zmkfqc6qu05uy20mvv730vlzkpvls5zqxuz: resolve( 25 | __dirname, 26 | '../', 27 | 'data', 28 | startHeight.toString(), 29 | 'cw-app-bitcoin.wasm' 30 | ), 31 | }); 32 | console.dir(results, { depth: null }); 33 | })(); 34 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/sync.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { SimulateCosmWasmClient } from './SimulateCosmWasmClient'; 4 | import { DownloadState } from './fork'; 5 | import { parseTxToMsgExecuteContractMsgs, Tx, TxSearch } from '@oraichain/common'; 6 | import { StargateClient } from '@cosmjs/stargate'; 7 | import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'; 8 | import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; 9 | 10 | export type CustomWasmCodePaths = { [contractAddress: string]: string }; 11 | export type MsgExecuteContractWithHeight = MsgExecuteContract & { height: number; hash: string }; 12 | export type ChainConfig = { rpc: string; chainId: string; bech32Prefix: string }; 13 | 14 | /** 15 | * SyncState starts at a custom height 16 | * Then load all cosmwasm txs from that custom height to end height 17 | * Allow loading contracts using custom .wasm for debugging and testing 18 | * If any contract not found -> load state 19 | * If found -> apply tx -> much faster 20 | */ 21 | export class SyncState { 22 | private simulateClient: SimulateCosmWasmClient; 23 | private stargateClient: StargateClient; 24 | /** 25 | * 26 | * @param senderAddress admin of the contracts when loading contract states 27 | * @param chainConfig basic chain info - rpc, chainId, bech32Prefix 28 | * @param downloadPath path to store contract states. A new dir will be created matching the startHeight 29 | */ 30 | constructor(private senderAddress: string, private readonly chainConfig: ChainConfig, private downloadPath: string) { 31 | this.simulateClient = new SimulateCosmWasmClient({ 32 | chainId: chainConfig.chainId, 33 | bech32Prefix: chainConfig.bech32Prefix, 34 | metering: true, 35 | }); 36 | } 37 | 38 | /** 39 | * Download contract states from start height, and apply cosmwasm txs from start height to end height 40 | * @param startHeight start height to load contract states 41 | * @param endHeight end height to load txs from start to end 42 | * @param customContractsToDownload relevant contracts to download states 43 | * @param customWasmCodePaths wasm code paths that will be applied when executing txs for testing 44 | * @returns 45 | */ 46 | public async sync( 47 | startHeight: number, 48 | endHeight: number, 49 | customContractsToDownload: string[] = [], 50 | customWasmCodePaths: CustomWasmCodePaths = {} 51 | ) { 52 | // update download path to be a directory with name as 'startHeight' 53 | // this would help us re-run our tests based on different start heights -> fork states at different heights 54 | this.downloadPath = path.join(this.downloadPath, startHeight.toString()); 55 | if (!fs.existsSync(this.downloadPath)) { 56 | fs.mkdirSync(this.downloadPath); 57 | } 58 | 59 | console.info('Start forking at block ' + startHeight); 60 | 61 | const [_, txs] = await Promise.all([ 62 | this.downloadContractStates(startHeight, customContractsToDownload, customWasmCodePaths), 63 | this.searchTxs(startHeight, endHeight), 64 | ]); 65 | const results = await this.applyTxs(txs, startHeight, customWasmCodePaths); 66 | return { results, simulateClient: this.simulateClient, txs }; 67 | } 68 | 69 | private async downloadContractStates( 70 | height: number, 71 | contractsToDownload: string[], 72 | customWasmCodePaths: CustomWasmCodePaths 73 | ) { 74 | const downloadState = new DownloadState(this.chainConfig.rpc, this.downloadPath, height); 75 | for (const contract of contractsToDownload) { 76 | // if there's no already stored state path -> download state from height 77 | const statePath = path.join(this.downloadPath, `${contract}.state`); 78 | if (!fs.existsSync(statePath)) { 79 | await downloadState.saveState(contract); 80 | } 81 | const info = this.simulateClient.app.wasm.getContractInfo(contract); 82 | if (!info) { 83 | const customWasmCodeFound = fs.existsSync(customWasmCodePaths[contract]); 84 | // then try saving and loading state 85 | await downloadState.loadState( 86 | this.simulateClient, 87 | this.senderAddress, 88 | contract, 89 | contract, 90 | undefined, 91 | customWasmCodeFound ? customWasmCodePaths[contract] : undefined 92 | ); 93 | } 94 | } 95 | } 96 | 97 | private async searchTxs(startHeight: number, endHeight: number, totalThreads: number = 4) { 98 | if (!this.stargateClient) { 99 | this.stargateClient = await StargateClient.connect(this.chainConfig.rpc, { desiredHeight: startHeight }); 100 | } 101 | const txSearchInstance = new TxSearch(this.stargateClient, { 102 | startHeight, 103 | endHeight, 104 | maxThreadLevel: totalThreads, 105 | }); 106 | 107 | const txs = await txSearchInstance.txSearch(); 108 | return txs; 109 | } 110 | 111 | private async applyTxs(txs: Tx[], startheight: number, customWasmCodePaths: CustomWasmCodePaths = {}) { 112 | const msgExecuteContracts = txs.map(tx => this.parseTxToMsgExecuteContractMsgsWithTxData(tx)).flat(); 113 | 114 | // first, download states for all involved contracts at startHeight 115 | await this.downloadContractStates( 116 | startheight, 117 | // remove duplicates 118 | Array.from(new Set(msgExecuteContracts.map(msg => msg.contract))), 119 | customWasmCodePaths 120 | ); 121 | 122 | let simulateResults: ExecuteResult[] = []; 123 | 124 | for (const msgExecute of msgExecuteContracts) { 125 | // ignore txs that have the same startHeight since we already have their states stored 126 | if (msgExecute.height === startheight) continue; 127 | console.log(`Executing tx ${msgExecute.hash} at height ${msgExecute.height}...`); 128 | // only execute if the contract has some info already 129 | const res = await this.simulateClient.execute( 130 | msgExecute.sender, 131 | msgExecute.contract, 132 | JSON.parse(Buffer.from(msgExecute.msg).toString()), 133 | 'auto' 134 | ); 135 | simulateResults.push(res); 136 | } 137 | return simulateResults; 138 | } 139 | 140 | private parseTxToMsgExecuteContractMsgsWithTxData(tx: Tx): MsgExecuteContractWithHeight[] { 141 | const msgs = parseTxToMsgExecuteContractMsgs(tx); 142 | return msgs.map(msg => ({ ...msg, height: tx.height, hash: tx.hash })); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/cw-simulate/src/util.ts: -------------------------------------------------------------------------------- 1 | import { Err, Ok, Result } from 'ts-results'; 2 | import { RustResult, DebugLog } from './types'; 3 | import { sha256 } from '@cosmjs/crypto'; 4 | import { toHex } from '@cosmjs/encoding'; 5 | 6 | export const isArrayLike = (value: any): value is any[] => 7 | typeof value === 'object' && typeof value.length === 'number'; 8 | 9 | export function fromRustResult(res: RustResult): Result; 10 | export function fromRustResult(res: any): Result; 11 | export function fromRustResult(res: any): Result { 12 | if ('ok' in res) { 13 | return Ok(res.ok); 14 | } else if (typeof res.error === 'string') { 15 | return Err(res.error); 16 | } else throw new Error('Invalid RustResult type'); 17 | } 18 | export function toRustResult(res: Result): RustResult { 19 | if (res.ok) { 20 | return { ok: res.val }; 21 | } else { 22 | return { error: res.val as string }; 23 | } 24 | } 25 | 26 | export const isRustResult = (value: any): value is RustResult => 'ok' in value || 'err' in value; 27 | export const isTSResult = (value: any): value is Result => 28 | typeof value.ok === 'boolean' && typeof value.err === 'boolean' && 'val' in value; 29 | 30 | export const getTransactionHash = (height: number, data: any, encoding?: BufferEncoding) => { 31 | const buf = Buffer.from(JSON.stringify({ data, height }), encoding); 32 | return toHex(sha256(buf)); 33 | }; 34 | 35 | // debug debug print 36 | export const printDebug = (log: DebugLog) => { 37 | if (log.type === 'print') { 38 | console.log(log.message); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/cw-simulate/testing/cw_simulate_tests-aarch64.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cw-simulate/testing/cw_simulate_tests-aarch64.wasm -------------------------------------------------------------------------------- /packages/cw-simulate/testing/hello_world-aarch64.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cw-simulate/testing/hello_world-aarch64.wasm -------------------------------------------------------------------------------- /packages/cw-simulate/testing/ibc_reflect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cw-simulate/testing/ibc_reflect.wasm -------------------------------------------------------------------------------- /packages/cw-simulate/testing/reflect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraichain/cw-simulate/0eb9cc3ec8c5c880735a1c5822d1e77d21924e2b/packages/cw-simulate/testing/reflect.wasm -------------------------------------------------------------------------------- /packages/cw-simulate/testing/wasm-util.ts: -------------------------------------------------------------------------------- 1 | import { toBinary } from '@cosmjs/cosmwasm-stargate'; 2 | import { Coin } from '@cosmjs/amino'; 3 | import { readFileSync } from 'fs'; 4 | import { CWSimulateApp } from '../src/CWSimulateApp'; 5 | import { BankMsg, Event, ReplyOn } from '@oraichain/cosmwasm-vm-js'; 6 | import { TraceLog } from '../src/types'; 7 | 8 | export const DEFAULT_CREATOR = 'terra1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu'; 9 | const BYTECODE = readFileSync(`${__dirname}/cw_simulate_tests-aarch64.wasm`); 10 | 11 | interface MsgCommand { 12 | msg: any; 13 | } 14 | 15 | interface BankCommand { 16 | bank_msg: BankMsg; 17 | } 18 | 19 | interface SubCommand { 20 | sub: [number, any, ReplyOn]; 21 | } 22 | 23 | interface EvCommand { 24 | ev: [string, [string, string][]]; 25 | } 26 | 27 | interface AttrCommand { 28 | attr: [string, string]; 29 | } 30 | 31 | interface DataCommand { 32 | data: number[]; 33 | } 34 | 35 | interface ThrowCommand { 36 | throw: string; 37 | } 38 | 39 | type Command = MsgCommand | BankCommand | SubCommand | EvCommand | AttrCommand | DataCommand | ThrowCommand; 40 | 41 | interface InstantiateParams { 42 | codeId: number; 43 | admin?: string | null; 44 | msg: any; 45 | funds?: Coin[]; 46 | label: string; 47 | } 48 | 49 | export const exec = { 50 | run(...program: Command[]) { 51 | return { 52 | run: { 53 | program, 54 | }, 55 | }; 56 | }, 57 | debug: (msg: string) => ({ debug: { msg } }), 58 | push: (data: string) => ({ push: { data } }), 59 | pop: () => ({ pop: {} }), 60 | reset: () => ({ reset: {} }), 61 | instantiate({ codeId, admin, msg, funds = [], label }: InstantiateParams) { 62 | return { 63 | instantiate: { 64 | code_id: codeId, 65 | admin: admin || null, 66 | msg: toBinary(msg), 67 | funds, 68 | label, 69 | }, 70 | }; 71 | }, 72 | }; 73 | 74 | export const cmd = { 75 | msg(payload: any): MsgCommand { 76 | return { 77 | msg: payload, 78 | }; 79 | }, 80 | 81 | bank(msg: BankMsg): BankCommand { 82 | return { 83 | bank_msg: msg, 84 | }; 85 | }, 86 | 87 | sub(id: number, msg: any, reply_on: ReplyOn): SubCommand { 88 | return { 89 | sub: [id, msg, reply_on], 90 | }; 91 | }, 92 | 93 | ev(ty: string, attrs: [string, string][]): EvCommand { 94 | return { 95 | ev: [ty, attrs], 96 | }; 97 | }, 98 | 99 | attr(k: string, v: string): AttrCommand { 100 | return { 101 | attr: [k, v], 102 | }; 103 | }, 104 | 105 | data(v: number[]): DataCommand { 106 | return { 107 | data: v, 108 | }; 109 | }, 110 | 111 | push(data: string) { 112 | return { 113 | push: { data }, 114 | }; 115 | }, 116 | 117 | err(msg: string): ThrowCommand { 118 | return { 119 | throw: msg, 120 | }; 121 | }, 122 | }; 123 | 124 | export function event(ty: string, attrs: [string, string][]): Event { 125 | return { 126 | type: ty, 127 | attributes: attrs.map(([k, v]) => ({ key: k, value: v })), 128 | }; 129 | } 130 | 131 | type InstantiateOptions = { 132 | sender?: string; 133 | codeId?: number; 134 | funds?: Coin[]; 135 | }; 136 | 137 | /** Utility methods for registration, instantiation, and interaction 138 | * with our test contract. */ 139 | export class TestContract { 140 | constructor(public readonly app: CWSimulateApp, public readonly creator = DEFAULT_CREATOR) {} 141 | 142 | /** Register the test contract wasm code w/ the app */ 143 | register(creator?: string): number { 144 | return this.app.wasm.create(creator ?? this.creator, BYTECODE); 145 | } 146 | 147 | /** Instantiate test contract. */ 148 | async instantiate(opts: InstantiateOptions = {}) { 149 | const codeId = opts.codeId ?? this.register(opts.sender); 150 | const res = await this.app.wasm.instantiateContract( 151 | opts.sender ?? this.creator, 152 | opts.funds ?? [], 153 | codeId, 154 | {}, 155 | 'Test Contract' 156 | ); 157 | 158 | const addr = res.unwrap().events[0].attributes[0].value; 159 | return new TestContractInstance(this, addr); 160 | } 161 | } 162 | 163 | export class TestContractInstance { 164 | constructor(public readonly contract: TestContract, public readonly address: string) {} 165 | 166 | async execute(sender: string, msg: any, funds: Coin[] = [], trace: TraceLog[] = []) { 167 | return await this.app.wasm.executeContract(sender, funds, this.address, msg, trace); 168 | } 169 | 170 | get app() { 171 | return this.contract.app; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /packages/cw-simulate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "baseUrl": "./" 7 | }, 8 | "include": [ 9 | "src", 10 | "types" 11 | ], 12 | "exclude": [ 13 | "/node_modules/", 14 | "./src/**/*.spec.ts" 15 | ], 16 | } -------------------------------------------------------------------------------- /packages/cw-simulate/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | const path = require('path'); 4 | 5 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | const commonConfig = { 8 | mode: 'production', 9 | entry: './src/index.ts', 10 | devtool: 'source-map', 11 | output: { 12 | globalObject: 'this', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | resolve: { 24 | extensions: ['.tsx', '.ts', '.js'], 25 | plugins: [new TsconfigPathsPlugin({ baseUrl: path.resolve(__dirname, '.') })], 26 | }, 27 | plugins: [ 28 | new webpack.IgnorePlugin({ 29 | resourceRegExp: 30 | /wordlists\/(french|spanish|italian|korean|chinese_simplified|chinese_traditional|japanese)\.json$/, 31 | }), 32 | ], 33 | }; 34 | 35 | module.exports = { 36 | ...commonConfig, 37 | target: 'node', 38 | externals: [require('webpack-node-externals')()], 39 | output: { 40 | libraryTarget: 'commonjs', 41 | filename: 'bundle.node.js', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /patches/@cosmjs+cosmwasm-stargate+0.32.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts 2 | index a770785..fe2ea6b 100644 3 | --- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts 4 | +++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.d.ts 5 | @@ -1,5 +1,6 @@ 6 | import { Account, AuthExtension, BankExtension, Block, Coin, DeliverTxResponse, IndexedTx, QueryClient, SearchTxQuery, SequenceResponse, TxExtension } from "@cosmjs/stargate"; 7 | import { CometClient, HttpEndpoint } from "@cosmjs/tendermint-rpc"; 8 | +import { QueryAllContractStateResponse } from "cosmjs-types/cosmwasm/wasm/v1/query"; 9 | import { JsonObject, WasmExtension } from "./modules"; 10 | export interface Code { 11 | readonly id: number; 12 | @@ -49,7 +50,7 @@ export declare class CosmWasmClient { 13 | * This uses auto-detection to decide between a CometBFT 0.38, Tendermint 0.37 and 0.34 client. 14 | * To set the Comet client explicitly, use `create`. 15 | */ 16 | - static connect(endpoint: string | HttpEndpoint): Promise; 17 | + static connect(endpoint: string | HttpEndpoint, desiredHeight?: number): Promise 18 | /** 19 | * Creates an instance from a manually created Comet client. 20 | * Use this to use `Comet38Client` or `Tendermint37Client` instead of `Tendermint34Client`. 21 | @@ -136,5 +137,6 @@ export declare class CosmWasmClient { 22 | * Promise is rejected for invalid response format. 23 | */ 24 | queryContractSmart(address: string, queryMsg: JsonObject): Promise; 25 | + getAllContractState(address: string, paginationKey: Uint8Array, limit?: number): Promise; 26 | private txsQuery; 27 | } 28 | diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js 29 | index af8341d..118661d 100644 30 | --- a/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js 31 | +++ b/node_modules/@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js 32 | @@ -17,8 +17,8 @@ class CosmWasmClient { 33 | * This uses auto-detection to decide between a CometBFT 0.38, Tendermint 0.37 and 0.34 client. 34 | * To set the Comet client explicitly, use `create`. 35 | */ 36 | - static async connect(endpoint) { 37 | - const cometClient = await (0, tendermint_rpc_1.connectComet)(endpoint); 38 | + static async connect(endpoint, desiredHeight) { 39 | + const cometClient = await (0, tendermint_rpc_1.connectComet)(endpoint, desiredHeight); 40 | return CosmWasmClient.create(cometClient); 41 | } 42 | /** 43 | @@ -348,6 +348,33 @@ class CosmWasmClient { 44 | } 45 | } 46 | } 47 | + /** 48 | + * Makes a smart query on the contract, returns the parsed JSON document. 49 | + * 50 | + * Promise is rejected when contract does not exist. 51 | + * Promise is rejected for invalid query format. 52 | + * Promise is rejected for invalid response format. 53 | + */ 54 | + async getAllContractState(address, paginationKey, limit) { 55 | + try { 56 | + return await this.forceGetQueryClient().wasm.getAllContractState(address, paginationKey, limit); 57 | + } 58 | + catch (error) { 59 | + if (error instanceof Error) { 60 | + if (error.message.startsWith("not found: contract")) { 61 | + throw new Error(`No contract found at address "${address}"`); 62 | + } 63 | + else { 64 | + throw error; 65 | + } 66 | + } 67 | + else { 68 | + throw error; 69 | + } 70 | + } 71 | + } 72 | + 73 | + 74 | async txsQuery(query) { 75 | const results = await this.forceGetCometClient().txSearchAll({ query: query }); 76 | return results.txs.map((tx) => { 77 | diff --git a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js b/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js 78 | index e5bf448..58749d4 100644 79 | --- a/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js 80 | +++ b/node_modules/@cosmjs/cosmwasm-stargate/build/modules/wasm/queries.js 81 | @@ -46,10 +46,10 @@ function setupWasmExtension(base) { 82 | }; 83 | return queryService.ContractHistory(request); 84 | }, 85 | - getAllContractState: async (address, paginationKey) => { 86 | + getAllContractState: async (address, paginationKey, limit) => { 87 | const request = { 88 | address: address, 89 | - pagination: (0, stargate_1.createPagination)(paginationKey), 90 | + pagination: (0, stargate_1.createPagination)(paginationKey, limit), 91 | }; 92 | return queryService.AllContractState(request); 93 | }, 94 | -------------------------------------------------------------------------------- /patches/@cosmjs+stargate+0.32.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts b/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts 2 | index ec5b471..74f959b 100644 3 | --- a/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts 4 | +++ b/node_modules/@cosmjs/stargate/build/queryclient/utils.d.ts 5 | @@ -14,7 +14,7 @@ export declare function toAccAddress(address: string): Uint8Array; 6 | * Use this with a query response's pagination next key to 7 | * request the next page. 8 | */ 9 | -export declare function createPagination(paginationKey?: Uint8Array): PageRequest; 10 | +export declare function createPagination(paginationKey?: Uint8Array, limit?: number): PageRequest; 11 | export interface ProtobufRpcClient { 12 | request(service: string, method: string, data: Uint8Array): Promise; 13 | } 14 | diff --git a/node_modules/@cosmjs/stargate/build/queryclient/utils.js b/node_modules/@cosmjs/stargate/build/queryclient/utils.js 15 | index ea25080..c65c89c 100644 16 | --- a/node_modules/@cosmjs/stargate/build/queryclient/utils.js 17 | +++ b/node_modules/@cosmjs/stargate/build/queryclient/utils.js 18 | @@ -4,6 +4,7 @@ exports.decodeCosmosSdkDecFromProto = exports.longify = exports.createProtobufRp 19 | const encoding_1 = require("@cosmjs/encoding"); 20 | const math_1 = require("@cosmjs/math"); 21 | const pagination_1 = require("cosmjs-types/cosmos/base/query/v1beta1/pagination"); 22 | + 23 | /** 24 | * Takes a bech32 encoded address and returns the data part. The prefix is ignored and discarded. 25 | * This is called AccAddress in Cosmos SDK, which is basically an alias for raw binary data. 26 | @@ -20,15 +21,15 @@ exports.toAccAddress = toAccAddress; 27 | * Use this with a query response's pagination next key to 28 | * request the next page. 29 | */ 30 | -function createPagination(paginationKey) { 31 | - return paginationKey ? pagination_1.PageRequest.fromPartial({ key: paginationKey }) : pagination_1.PageRequest.fromPartial({}); 32 | +function createPagination(paginationKey, limit) { 33 | + return paginationKey ? pagination_1.PageRequest.fromPartial({ key: paginationKey, limit }) : pagination_1.PageRequest.fromPartial({}); 34 | } 35 | exports.createPagination = createPagination; 36 | function createProtobufRpcClient(base) { 37 | return { 38 | request: async (service, method, data) => { 39 | const path = `/${service}/${method}`; 40 | - const response = await base.queryAbci(path, data, undefined); 41 | + const response = await base.queryAbci(path, data, base.cometClient.desiredHeight); 42 | return response.value; 43 | }, 44 | }; 45 | diff --git a/node_modules/@cosmjs/stargate/build/stargateclient.d.ts b/node_modules/@cosmjs/stargate/build/stargateclient.d.ts 46 | index f4fd645..0119efc 100644 47 | --- a/node_modules/@cosmjs/stargate/build/stargateclient.d.ts 48 | +++ b/node_modules/@cosmjs/stargate/build/stargateclient.d.ts 49 | @@ -143,6 +143,7 @@ export interface PrivateStargateClient { 50 | } 51 | export interface StargateClientOptions { 52 | readonly accountParser?: AccountParser; 53 | + readonly desiredHeight?: number; 54 | } 55 | export declare class StargateClient { 56 | private readonly cometClient; 57 | diff --git a/node_modules/@cosmjs/stargate/build/stargateclient.js b/node_modules/@cosmjs/stargate/build/stargateclient.js 58 | index a6da130..bac8278 100644 59 | --- a/node_modules/@cosmjs/stargate/build/stargateclient.js 60 | +++ b/node_modules/@cosmjs/stargate/build/stargateclient.js 61 | @@ -68,7 +68,7 @@ class StargateClient { 62 | * To set the Comet client explicitly, use `create`. 63 | */ 64 | static async connect(endpoint, options = {}) { 65 | - const cometClient = await (0, tendermint_rpc_1.connectComet)(endpoint); 66 | + const cometClient = await (0, tendermint_rpc_1.connectComet)(endpoint, options.desiredHeight); 67 | return StargateClient.create(cometClient, options); 68 | } 69 | /** 70 | @@ -297,13 +297,12 @@ class StargateClient { 71 | return results.txs.map((tx) => { 72 | const txMsgData = abci_1.TxMsgData.decode(tx.result.data ?? new Uint8Array()); 73 | return { 74 | - height: tx.height, 75 | + ...tx, 76 | txIndex: tx.index, 77 | hash: (0, encoding_1.toHex)(tx.hash).toUpperCase(), 78 | code: tx.result.code, 79 | events: tx.result.events.map(events_1.fromTendermintEvent), 80 | rawLog: tx.result.log || "", 81 | - tx: tx.tx, 82 | msgResponses: txMsgData.msgResponses, 83 | gasUsed: tx.result.gasUsed, 84 | gasWanted: tx.result.gasWanted, 85 | -------------------------------------------------------------------------------- /patches/typedoc+0.24.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/typedoc/dist/lib/output/themes/default/partials/footer.js b/node_modules/typedoc/dist/lib/output/themes/default/partials/footer.js 2 | index d78f8a2..173bdfd 100644 3 | --- a/node_modules/typedoc/dist/lib/output/themes/default/partials/footer.js 4 | +++ b/node_modules/typedoc/dist/lib/output/themes/default/partials/footer.js 5 | @@ -7,7 +7,7 @@ function footer(context) { 6 | if (!hideGenerator) 7 | return (utils_1.JSX.createElement("div", { class: "tsd-generator" }, 8 | utils_1.JSX.createElement("p", null, 9 | - "Generated using ", 10 | - utils_1.JSX.createElement("a", { href: "https://typedoc.org/", target: "_blank" }, "TypeDoc")))); 11 | + `©2020 - ${new Date().getFullYear()} `, 12 | + utils_1.JSX.createElement("a", { href: "https://orai.io/", target: "_blank" }, "Oraichain Foundation")))); 13 | } 14 | exports.footer = footer; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "alwaysStrict": false, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "ESNext", 10 | "dom" 11 | ], 12 | "skipLibCheck": true, 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": false, 16 | "noImplicitAny": false, 17 | "noImplicitReturns": false, 18 | "noImplicitThis": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "sourceMap": true, 22 | "strict": false, 23 | "strictFunctionTypes": false, 24 | "strictNullChecks": false, 25 | "strictPropertyInitialization": false, 26 | "downlevelIteration": true, 27 | "target": "ESNext", 28 | }, 29 | "typeAcquisition": { 30 | "include": [ 31 | "jest" 32 | ] 33 | } 34 | } --------------------------------------------------------------------------------