├── .eslintrc.cjs ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .mocharc.json ├── .vscode └── settings.json ├── LICENSE ├── mgmt.did.js ├── package.json ├── readme.MD ├── spec_test ├── Cargo.lock ├── Cargo.toml └── src │ ├── account_identifier.rs │ ├── icp_ledger.rs │ ├── lib.rs │ ├── tests.rs │ └── types.rs ├── src ├── bls.ts ├── call_context.ts ├── canister.ts ├── hash_tree.ts ├── helpers │ ├── ledger_helper.ts │ └── nns_helper.ts ├── ic0.ts ├── idl_builder.ts ├── index.ts ├── instrumentation.ts ├── management_canister.did ├── management_canister.ts ├── mgmt.did.ts ├── mock_actor.ts ├── mock_agent.ts ├── replica_context.ts ├── server.ts ├── test_context.ts ├── tsconfig.json ├── utils.ts ├── wasm_canister.ts └── wasm_tools │ ├── .gitignore │ ├── .npmignore │ ├── Cargo.toml │ ├── package.json │ └── src │ ├── bls.rs │ ├── instrumentation.rs │ ├── lib.rs │ ├── target_json.rs │ └── wasm_transform │ ├── convert.rs │ └── mod.rs ├── status_test ├── test ├── bls.spec.ts ├── cmc.spec.ts ├── hashTree.spec.ts ├── idl.spec.ts ├── instrumentation.spec.ts ├── keeper.spec.ts ├── ledger.spec.ts ├── management.spec.ts ├── mockAgent.spec.ts ├── motoko.did ├── server.spec.ts ├── test.spec.ts └── utils.spec.ts ├── total_proposals.json ├── tsconfig-cjs.json ├── tsconfig.json └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | root: true, 6 | }; -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - name: wasm-pack-action 30 | # You may pin to the exact commit or the version. 31 | # uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa 32 | uses: jetli/wasm-pack-action@v0.4.0 33 | - run: yarn 34 | - run: npm run prePublish --if-present 35 | - run: npm test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target 3 | dist 4 | cache 5 | lib 6 | miracl -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | 2 | // This config file contains Mocha's defaults. 3 | // This same configuration could be provided in the `mocha` property of your 4 | // project's `package.json`. 5 | { 6 | "diff": true, 7 | "extension": ["js", "cjs", "mjs", "ts"], 8 | "package": "./package.json", 9 | "reporter": "spec", 10 | "slow": "75", 11 | "timeout": "2000", 12 | "ui": "bdd", 13 | "watch-files": ["lib/**/*.js", "test/**/*.ts"], 14 | "watch-ignore": ["lib/vendor"], 15 | "loader": "ts-node/esm" 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mochaExplorer.files": "test/**/*.ts", 3 | "mochaExplorer.require": "ts-node/register", 4 | "mochaExplorer.env": { "DEBUG": "lightic*", "NODE": "--cpu-prof" }, 5 | "cSpell.words": [ 6 | "Cbor", 7 | "dfinity", 8 | "ecid", 9 | "icrc", 10 | "lightic", 11 | "Prin", 12 | "webassemblyjs" 13 | ] 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 icopen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mgmt.did.js: -------------------------------------------------------------------------------- 1 | export const idlFactory = ({ IDL }) => { 2 | const bitcoin_network = IDL.Variant({ 3 | 'mainnet' : IDL.Null, 4 | 'testnet' : IDL.Null, 5 | }); 6 | const bitcoin_address = IDL.Text; 7 | const get_balance_request = IDL.Record({ 8 | 'network' : bitcoin_network, 9 | 'address' : bitcoin_address, 10 | 'min_confirmations' : IDL.Opt(IDL.Nat32), 11 | }); 12 | const satoshi = IDL.Nat64; 13 | const get_current_fee_percentiles_request = IDL.Record({ 14 | 'network' : bitcoin_network, 15 | }); 16 | const millisatoshi_per_byte = IDL.Nat64; 17 | const get_utxos_request = IDL.Record({ 18 | 'network' : bitcoin_network, 19 | 'filter' : IDL.Opt( 20 | IDL.Variant({ 21 | 'page' : IDL.Vec(IDL.Nat8), 22 | 'min_confirmations' : IDL.Nat32, 23 | }) 24 | ), 25 | 'address' : bitcoin_address, 26 | }); 27 | const block_hash = IDL.Vec(IDL.Nat8); 28 | const outpoint = IDL.Record({ 29 | 'txid' : IDL.Vec(IDL.Nat8), 30 | 'vout' : IDL.Nat32, 31 | }); 32 | const utxo = IDL.Record({ 33 | 'height' : IDL.Nat32, 34 | 'value' : satoshi, 35 | 'outpoint' : outpoint, 36 | }); 37 | const get_utxos_response = IDL.Record({ 38 | 'next_page' : IDL.Opt(IDL.Vec(IDL.Nat8)), 39 | 'tip_height' : IDL.Nat32, 40 | 'tip_block_hash' : block_hash, 41 | 'utxos' : IDL.Vec(utxo), 42 | }); 43 | const send_transaction_request = IDL.Record({ 44 | 'transaction' : IDL.Vec(IDL.Nat8), 45 | 'network' : bitcoin_network, 46 | }); 47 | const canister_id = IDL.Principal; 48 | const definite_canister_settings = IDL.Record({ 49 | 'freezing_threshold' : IDL.Nat, 50 | 'controllers' : IDL.Vec(IDL.Principal), 51 | 'memory_allocation' : IDL.Nat, 52 | 'compute_allocation' : IDL.Nat, 53 | }); 54 | const canister_settings = IDL.Record({ 55 | 'freezing_threshold' : IDL.Opt(IDL.Nat), 56 | 'controllers' : IDL.Opt(IDL.Vec(IDL.Principal)), 57 | 'memory_allocation' : IDL.Opt(IDL.Nat), 58 | 'compute_allocation' : IDL.Opt(IDL.Nat), 59 | }); 60 | const ecdsa_curve = IDL.Variant({ 'secp256k1' : IDL.Null }); 61 | const http_header = IDL.Record({ 'value' : IDL.Text, 'name' : IDL.Text }); 62 | const http_response = IDL.Record({ 63 | 'status' : IDL.Nat, 64 | 'body' : IDL.Vec(IDL.Nat8), 65 | 'headers' : IDL.Vec(http_header), 66 | }); 67 | const wasm_module = IDL.Vec(IDL.Nat8); 68 | return IDL.Service({ 69 | 'bitcoin_get_balance' : IDL.Func([get_balance_request], [satoshi], []), 70 | 'bitcoin_get_current_fee_percentiles' : IDL.Func( 71 | [get_current_fee_percentiles_request], 72 | [IDL.Vec(millisatoshi_per_byte)], 73 | [], 74 | ), 75 | 'bitcoin_get_utxos' : IDL.Func( 76 | [get_utxos_request], 77 | [get_utxos_response], 78 | [], 79 | ), 80 | 'bitcoin_send_transaction' : IDL.Func([send_transaction_request], [], []), 81 | 'canister_status' : IDL.Func( 82 | [IDL.Record({ 'canister_id' : canister_id })], 83 | [ 84 | IDL.Record({ 85 | 'status' : IDL.Variant({ 86 | 'stopped' : IDL.Null, 87 | 'stopping' : IDL.Null, 88 | 'running' : IDL.Null, 89 | }), 90 | 'memory_size' : IDL.Nat, 91 | 'cycles' : IDL.Nat, 92 | 'settings' : definite_canister_settings, 93 | 'idle_cycles_burned_per_day' : IDL.Nat, 94 | 'module_hash' : IDL.Opt(IDL.Vec(IDL.Nat8)), 95 | }), 96 | ], 97 | [], 98 | ), 99 | 'create_canister' : IDL.Func( 100 | [IDL.Record({ 'settings' : IDL.Opt(canister_settings) })], 101 | [IDL.Record({ 'canister_id' : canister_id })], 102 | [], 103 | ), 104 | 'delete_canister' : IDL.Func( 105 | [IDL.Record({ 'canister_id' : canister_id })], 106 | [], 107 | [], 108 | ), 109 | 'deposit_cycles' : IDL.Func( 110 | [IDL.Record({ 'canister_id' : canister_id })], 111 | [], 112 | [], 113 | ), 114 | 'ecdsa_public_key' : IDL.Func( 115 | [ 116 | IDL.Record({ 117 | 'key_id' : IDL.Record({ 'name' : IDL.Text, 'curve' : ecdsa_curve }), 118 | 'canister_id' : IDL.Opt(canister_id), 119 | 'derivation_path' : IDL.Vec(IDL.Vec(IDL.Nat8)), 120 | }), 121 | ], 122 | [ 123 | IDL.Record({ 124 | 'public_key' : IDL.Vec(IDL.Nat8), 125 | 'chain_code' : IDL.Vec(IDL.Nat8), 126 | }), 127 | ], 128 | [], 129 | ), 130 | 'http_request' : IDL.Func( 131 | [ 132 | IDL.Record({ 133 | 'url' : IDL.Text, 134 | 'method' : IDL.Variant({ 135 | 'get' : IDL.Null, 136 | 'head' : IDL.Null, 137 | 'post' : IDL.Null, 138 | }), 139 | 'max_response_bytes' : IDL.Opt(IDL.Nat64), 140 | 'body' : IDL.Opt(IDL.Vec(IDL.Nat8)), 141 | 'transform' : IDL.Opt( 142 | IDL.Record({ 143 | 'function' : IDL.Func( 144 | [ 145 | IDL.Record({ 146 | 'context' : IDL.Vec(IDL.Nat8), 147 | 'response' : http_response, 148 | }), 149 | ], 150 | [http_response], 151 | ['query'], 152 | ), 153 | 'context' : IDL.Vec(IDL.Nat8), 154 | }) 155 | ), 156 | 'headers' : IDL.Vec(http_header), 157 | }), 158 | ], 159 | [http_response], 160 | [], 161 | ), 162 | 'install_code' : IDL.Func( 163 | [ 164 | IDL.Record({ 165 | 'arg' : IDL.Vec(IDL.Nat8), 166 | 'wasm_module' : wasm_module, 167 | 'mode' : IDL.Variant({ 168 | 'reinstall' : IDL.Null, 169 | 'upgrade' : IDL.Null, 170 | 'install' : IDL.Null, 171 | }), 172 | 'canister_id' : canister_id, 173 | }), 174 | ], 175 | [], 176 | [], 177 | ), 178 | 'provisional_create_canister_with_cycles' : IDL.Func( 179 | [ 180 | IDL.Record({ 181 | 'settings' : IDL.Opt(canister_settings), 182 | 'specified_id' : IDL.Opt(canister_id), 183 | 'amount' : IDL.Opt(IDL.Nat), 184 | }), 185 | ], 186 | [IDL.Record({ 'canister_id' : canister_id })], 187 | [], 188 | ), 189 | 'provisional_top_up_canister' : IDL.Func( 190 | [IDL.Record({ 'canister_id' : canister_id, 'amount' : IDL.Nat })], 191 | [], 192 | [], 193 | ), 194 | 'raw_rand' : IDL.Func([], [IDL.Vec(IDL.Nat8)], []), 195 | 'sign_with_ecdsa' : IDL.Func( 196 | [ 197 | IDL.Record({ 198 | 'key_id' : IDL.Record({ 'name' : IDL.Text, 'curve' : ecdsa_curve }), 199 | 'derivation_path' : IDL.Vec(IDL.Vec(IDL.Nat8)), 200 | 'message_hash' : IDL.Vec(IDL.Nat8), 201 | }), 202 | ], 203 | [IDL.Record({ 'signature' : IDL.Vec(IDL.Nat8) })], 204 | [], 205 | ), 206 | 'start_canister' : IDL.Func( 207 | [IDL.Record({ 'canister_id' : canister_id })], 208 | [], 209 | [], 210 | ), 211 | 'stop_canister' : IDL.Func( 212 | [IDL.Record({ 'canister_id' : canister_id })], 213 | [], 214 | [], 215 | ), 216 | 'uninstall_code' : IDL.Func( 217 | [IDL.Record({ 'canister_id' : canister_id })], 218 | [], 219 | [], 220 | ), 221 | 'update_settings' : IDL.Func( 222 | [ 223 | IDL.Record({ 224 | 'canister_id' : IDL.Principal, 225 | 'settings' : canister_settings, 226 | }), 227 | ], 228 | [], 229 | [], 230 | ), 231 | }); 232 | }; 233 | export const init = ({ IDL }) => { return []; }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightic", 3 | "version": "0.3.1", 4 | "description": "", 5 | "main": "lib/esm/index.js", 6 | "bin": "lib/esm/server.js", 7 | "scripts": { 8 | "lint": "yarn prettier --check && yarn eslint", 9 | "lint:fix": "yarn prettier --write && yarn eslint --fix", 10 | "eslint": "eslint 'src/**/*.ts' 'test/**/*.ts'", 11 | "prettier": "prettier \"src/**/*.{js,md,json,ts}\"", 12 | "test": "yarn buildSpecTest && DEBUG=lightic* mocha --recursive \"test/**/*.ts\"", 13 | "build": "tsc --build ./src", 14 | "buildWasmTools": "cd ./src/wasm_tools && yarn build && cd ../../", 15 | "buildSpecTest": "cd ./spec_test && cargo build --target wasm32-unknown-unknown --release", 16 | "copyWasmTools": "[ -d ./lib/esm/wasm_tools ] || mkdir ./lib/esm/wasm_tools && cp -r ./src/wasm_tools/pkg ./lib/esm/wasm_tools/pkg && rm ./lib/esm/wasm_tools/pkg/.gitignore", 17 | "prePublish": "yarn buildWasmTools && yarn build && yarn copyWasmTools" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@types/chai": "^4.3.4", 23 | "@types/debug": "^4.1.7", 24 | "@types/mocha": "^10.0.1", 25 | "@types/node": "^18.13.0", 26 | "@typescript-eslint/eslint-plugin": "^5.59.2", 27 | "@typescript-eslint/parser": "^5.59.2", 28 | "chai": "^4.3.7", 29 | "eslint": "^8.39.0", 30 | "eslint-config-standard-with-typescript": "^34.0.1", 31 | "eslint-plugin-import": "^2.25.2", 32 | "eslint-plugin-n": "^15.0.0", 33 | "eslint-plugin-promise": "^6.0.0", 34 | "mocha": "^10.2.0", 35 | "prettier": "^2.8.4", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^5.0.4", 38 | "@dfinity/agent": "^0.15.4", 39 | "@dfinity/candid": "^0.15.3", 40 | "@dfinity/nns": "^0.14.0", 41 | "@dfinity/principal": "^0.15.3", 42 | "@dfinity/utils": "^0.0.12" 43 | }, 44 | "peerDependencies": { 45 | "@dfinity/agent": "^0.15.4", 46 | "@dfinity/candid": "^0.15.3", 47 | "@dfinity/nns": "^0.14.0", 48 | "@dfinity/principal": "^0.15.3", 49 | "@dfinity/utils": "^0.0.12" 50 | }, 51 | "dependencies": { 52 | "cbor": "^8.1.0", 53 | "commander": "^10.0.1", 54 | "debug": "^4.3.4", 55 | "express": "^4.18.2", 56 | "pako": "^2.1.0", 57 | "js-sha256": "0.9.0", 58 | "axios": "^0.24.0" 59 | }, 60 | "files": [ 61 | "lib/esm", 62 | "lib/esm/wasm_tools/pkg/wasm_tools_bg.wasm", 63 | "lib/esm/wasm_tools/pkg/wasm_tools.d.ts", 64 | "lib/esm/wasm_tools/pkg/wasm_tools.js" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /readme.MD: -------------------------------------------------------------------------------- 1 | # Project Description - Light Replica 2 | 3 | Ethereum and other EVM chains benefit from frameworks such as truffle or hardhat. They enable easy and fast creation, testing and deployment of solutions to EVM based chains. Their strength lies in usage of JS scripts to do everything: setup local node, make tests, make deployments and more. 4 | “Light Replica” is a project to startup creation of similar tool for the Internet Computer ecosystem. It will be composed from two elements: 5 | 6 | Light Node - it is a local node designed for development (fast startup and cleanup), it will replicate behavior of the real node with additional logging and functions to help testing. It will be able to run and interact with any IC compatible wasm file. 7 | Light Runner/Cli - a set of libraries written in JS (available in npm) that can be used in node.js environment to create and build projects, write tests, create deployments and arbitrary scripts that have access to project “context” right from the js code. 8 | 9 | Both tools are aimed at developers. To help them efficiently develop, test and deploy canisters to the IC. 10 | 11 | 12 | The replica mock is run directly in node.js environment (also works with all browsers that support WASM) and enables you to deploy and test and wasm file that is compatible with Internet Computer reference 13 | 14 | It can also be used as a local development replica (started by dfx start) replacement 15 | 16 | # How to start using, examples 17 | 18 | ``` 19 | npm i lightic 20 | ``` 21 | 22 | First you need to choose the js testing framework of your preference, it could be: mocha, jest, vitest or any other. 23 | 24 | For the mocha, you can check the examples in the folder tests 25 | 26 | ## Mocha tests setup 27 | 28 | ``` 29 | npm i @types/mocha @types/node typescript mocha chai ts-node 30 | ``` 31 | 32 | And create .mochars.json with content 33 | 34 | ```JSON 35 | // This config file contains Mocha's defaults. 36 | // This same configuration could be provided in the `mocha` property of your 37 | // project's `package.json`. 38 | { 39 | "diff": true, 40 | "extension": ["js", "cjs", "mjs", "ts"], 41 | "package": "./package.json", 42 | "reporter": "spec", 43 | "slow": "75", 44 | "timeout": "2000", 45 | "ui": "bdd", 46 | "watch-files": ["lib/**/*.js", "test/**/*.ts"], 47 | "watch-ignore": ["lib/vendor"], 48 | "loader": "ts-node/esm" 49 | } 50 | ``` 51 | 52 | ## Simple test 53 | 54 | In order to actually test your canister, you need to add to your test file 55 | 56 | ```TS 57 | import { TestContext, u64IntoPrincipalId } from 'lightic' 58 | 59 | const context = new TestContext() 60 | This will create a context to run tests, it is a harness that gives you a possibility to install and run canisters 61 | 62 | const canister = await context.deploy('./dfx/local/canisters/example/example.wasm') 63 | const caller = u64IntoPrincipalId(0n) 64 | const actor = Actor.createActor(canister.getIdlBuilder(), { 65 | agent: context.getAgent(caller), 66 | canisterId: canister.get_id() 67 | }) 68 | 69 | ``` 70 | 71 | 1. As you can see this works with a wasm file, so you need to first compile the project using dfx. 72 | ``` 73 | dfx canister build $CANISTER_NAME 74 | ``` 75 | 76 | In the future the test harness will also take care of compilation. 77 | 78 | 2. You also need to specify the identity principle (who is calling the canisters) and create an actor. There is a helper function `u64IntoPrincipalId` that creates a Principle based on supplied number, so you do not need to come up with fake principals. The created principal is not random. 79 | 80 | 3. Then you get an actor, which is the same type as regular dfinity actor. In this example the actual actor class from @dfinity package is used 81 | 82 | 4. In order to call canister: 83 | 84 | ```TS 85 | const result = await actor.test_caller() 86 | ``` 87 | 88 | Replace `test_caller()` with a function from you canister 89 | 90 | 91 | 92 | ## How to deploy Ledger canister in one step 93 | 94 | Lightic comes with builtin `LedgerHelper` that can download and deploy ledger canister. In order to start using ledger: 95 | 96 | ``` 97 | const minter = u64IntoPrincipalId(0n); 98 | const owner = u64IntoPrincipalId(1n); 99 | const ledgerCanister = await LedgerHelper.defaults(context, minter, owner) 100 | ``` 101 | 102 | Now you can call ledger both from agent and from other deployed canisters. If possible this will deploy ledger with the same principal as it is found in IC `ryjl3-tyaaa-aaaaa-aaaba-cai` 103 | 104 | 105 | Next call account_balance to check if the owner, has some ICP in its account 106 | 107 | ``` 108 | const balance = await ledgerCanister.balanceOf(minter) 109 | ``` 110 | 111 | If you need an account number from your principal there is a helper function `getAccount` the will do just that 112 | 113 | 114 | ## How to unit test your canisters 115 | 116 | 117 | You can find examples of lightic used for testing canisters here: [https://github.com/icopen/evm_utils_ic/blob/master/__tests__/_common.mjs]. More examples to come. 118 | 119 | ## HTTP Replica Endpoint 120 | It is possible to launch lightic in a standalone mode and call it from DFX or any other script that can work with DFX or mainnet. In order to do so: 121 | 122 | ``` 123 | npx lightic --p port 124 | ``` 125 | 126 | Where port is the desired TCP port on which the lightic should listen. 127 | 128 | # Building 129 | 130 | Most of the project was written in Type Script. 131 | 132 | 133 | ## Candid Util 134 | 135 | Util that can parse Candid compliant data and output it as a JSON formatted string 136 | 137 | ``` 138 | cd candid_util 139 | wasm-pack build --target nodejs 140 | ``` 141 | 142 | 143 | ## Specification test canister 144 | 145 | Canister that uses some of the most common features of the IC, used for testing the mock replica 146 | 147 | ``` 148 | cd spec_test 149 | cargo build --release --target wasm32-unknown-unknown 150 | ``` 151 | 152 | 153 | ## Whole Package 154 | 155 | ``` 156 | yarn prePublish 157 | ``` 158 | 159 | 160 | # What is Implemented 161 | 162 | ## IC0 Implementation 163 | Below is a list of IC0 functions exposed to WASM module on IC environment. Not all calls will be implemented as part of this project. 164 | 165 | - [x] - msg_arg_data_size 166 | - [x] - msg_arg_data_copy 167 | - [x] - msg_caller_size 168 | - [x] - msg_caller_copy 169 | - [x] - msg_reject_code 170 | - [x] - msg_reject_msg_size 171 | - [x] - msg_reject_msg_copy 172 | - [x] - msg_reply_data_append 173 | - [x] - msg_reply 174 | - [x] - msg_reject 175 | - [ ] - msg_cycles_available 176 | - [ ] - msg_cycles_available128 177 | - [ ] - msg_cycles_refunded 178 | - [ ] - msg_cycles_refunded128 179 | - [x] - msg_cycles_accept 180 | - [ ] - msg_cycles_accept128 181 | - [x] - canister_self_size 182 | - [x] - canister_self_copy 183 | - [x] - canister_cycle_balance 184 | - [ ] - canister_cycle_balance128 185 | - [ ] - canister_status 186 | - [ ] - canister_version 187 | - [ ] - msg_method_name_size 188 | - [ ] - msg_method_name_copy 189 | - [ ] - accept_message 190 | - [x] - call_new 191 | - [ ] - call_on_cleanup 192 | - [x] - call_data_append 193 | - [x] - call_cycles_add 194 | - [ ] - call_cycles_add128 195 | - [x] - call_perform 196 | - [x] - stable_size 197 | - [x] - stable_grow 198 | - [x] - stable_write 199 | - [x] - stable_read 200 | - [x] - stable64_size 201 | - [ ] - stable64_grow 202 | - [ ] - stable64_write 203 | - [ ] - stable64_read 204 | - [x] - certified_data_set 205 | - [ ] - data_certificate_present 206 | - [ ] - data_certificate_size 207 | - [ ] - data_certificate_copy 208 | - [x] - time 209 | - [x] - global_timer_set 210 | - [x] - performance_counter 211 | - [x] - debug_print 212 | - [x] - trap 213 | 214 | ## Not documented but still relevant 215 | - [x] - mint_cycles 216 | 217 | ## Management canister functions 218 | - [x] - create_canister 219 | - [ ] - update_settings 220 | - [x] - install_code 221 | - [ ] - uninstall_code 222 | - [ ] - canister_status 223 | - [ ] - stop_canister 224 | - [ ] - start_canisters 225 | - [ ] - delete_canister 226 | - [ ] - deposit_cycles 227 | - [x] - raw_rand 228 | - [ ] - ecdsa_public_key 229 | - [ ] - sign_with_ecdsa 230 | - [ ] - http_request 231 | - [x] - provisional_create_canister_with_cycles 232 | - [ ] - provisional_top_up_canister 233 | 234 | # TODO 235 | - [x] - Candid utils WASM-WASI module 236 | - [x] - WASM Module Loading 237 | - [x] - Assignment of Canister ID upon canister creation 238 | - [x] - Support for canister provided Candid Specs 239 | - [x] - Mocha/Jest integration 240 | - [x] - Canister Memory Rollback 241 | - [x] - Update call support 242 | - [x] - Stable Memory support 243 | - [x] - BLS Signatures 244 | - [x] - HTTP Server Implementation 245 | - [x] - npmjs package with helpers and runners 246 | 247 | 248 | # Further work 249 | - [ ] - Compatibility and cooperation with dfx 250 | - [ ] - Backup of wasm memory (normal and stable) for faster development cycles 251 | - [ ] - Log of all calls 252 | - [ ] - Support for other environments: local/dfx, production 253 | - [ ] - Cycles usage counting per every call 254 | - [ ] - Mocking of messages (ingress, egress and xnet) 255 | - [ ] - Support for canister upgrades (preupgrade and postupgrade) 256 | - [ ] - Limit cycle usage on calls 257 | - [ ] - Limit message size to subnet settings, allow for different subnet settings 258 | - [ ] - Support for multiple subnets 259 | - [ ] - Full compliance ic-ref-test [https://github.com/dfinity/ic-hs#ic-ref-test-an-acceptance-test-suite] 260 | -------------------------------------------------------------------------------- /spec_test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spec_test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | candid = "0.8.4" 13 | crc32fast = "1.3.2" 14 | hex = {version = "0.4.3", features = ["serde"] } 15 | ic-cdk = "0.7.0" 16 | serde = "1.0.152" 17 | sha2 = "0.10.6" 18 | 19 | [profile.release] 20 | lto = true 21 | -------------------------------------------------------------------------------- /spec_test/src/account_identifier.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use ic_cdk::export::candid::Principal; 3 | use sha2::Digest; 4 | use sha2::Sha224; 5 | 6 | use serde::{de, de::Error, Deserialize, Serialize}; 7 | 8 | use std::{ 9 | convert::TryInto, 10 | fmt::{Display, Formatter}, 11 | str::FromStr, 12 | }; 13 | 14 | /// While this is backed by an array of length 28, it's canonical representation 15 | /// is a hex string of length 64. The first 8 characters are the CRC-32 encoded 16 | /// hash of the following 56 characters of hex. Both, upper and lower case 17 | /// characters are valid in the input string and can even be mixed. 18 | /// 19 | /// When it is encoded or decoded it will always be as a string to make it 20 | /// easier to use from DFX. 21 | #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] 22 | pub struct AccountIdentifier { 23 | pub hash: [u8; 28], 24 | } 25 | 26 | impl AsRef<[u8]> for AccountIdentifier { 27 | fn as_ref(&self) -> &[u8] { 28 | &self.hash 29 | } 30 | } 31 | 32 | pub static SUB_ACCOUNT_ZERO: Subaccount = Subaccount([0; 32]); 33 | static ACCOUNT_DOMAIN_SEPERATOR: &[u8] = b"\x0Aaccount-id"; 34 | 35 | impl AccountIdentifier { 36 | ///Creates new Account identifier from Principal and sub_account id 37 | pub fn new(account: Principal, sub_account: Option) -> AccountIdentifier { 38 | let mut hash = Sha224::new(); 39 | hash.update(ACCOUNT_DOMAIN_SEPERATOR); 40 | hash.update(account.as_slice()); 41 | 42 | let sub_account = sub_account.unwrap_or(SUB_ACCOUNT_ZERO); 43 | hash.update(&sub_account.0[..]); 44 | 45 | AccountIdentifier { 46 | hash: hash.finalize().into(), 47 | } 48 | } 49 | 50 | pub fn from_hex(hex_str: &str) -> Result { 51 | let hex: Vec = hex::decode(hex_str).map_err(|e| e.to_string())?; 52 | Self::from_slice(&hex[..]).map_err(|err| match err { 53 | // Since the input was provided in hex, return an error that is hex-friendly. 54 | AccountIdParseError::InvalidLength(_) => format!( 55 | "{} has a length of {} but we expected a length of 64 or 56", 56 | hex_str, 57 | hex_str.len() 58 | ), 59 | AccountIdParseError::InvalidChecksum(err) => err.to_string(), 60 | }) 61 | } 62 | 63 | /// Converts a blob into an `AccountIdentifier`. 64 | /// 65 | /// The blob can be either: 66 | /// 67 | /// 1. The 32-byte canonical format (4 byte checksum + 28 byte hash). 68 | /// 2. The 28-byte hash. 69 | /// 70 | /// If the 32-byte canonical format is provided, the checksum is verified. 71 | pub fn from_slice(v: &[u8]) -> Result { 72 | // Try parsing it as a 32-byte blob. 73 | match v.try_into() { 74 | Ok(h) => { 75 | // It's a 32-byte blob. Validate the checksum. 76 | check_sum(h).map_err(AccountIdParseError::InvalidChecksum) 77 | } 78 | Err(_) => { 79 | // Try parsing it as a 28-byte hash. 80 | match v.try_into() { 81 | Ok(hash) => Ok(AccountIdentifier { hash }), 82 | Err(_) => Err(AccountIdParseError::InvalidLength(v.to_vec())), 83 | } 84 | } 85 | } 86 | } 87 | 88 | pub fn to_hex(&self) -> String { 89 | hex::encode(self.to_vec()) 90 | } 91 | 92 | /// Converts this account identifier into a binary "address". 93 | /// The address is CRC32(identifier) . identifier. 94 | #[allow(dead_code)] 95 | pub fn to_address(&self) -> [u8; 32] { 96 | let mut result = [0u8; 32]; 97 | result[0..4].copy_from_slice(&self.generate_checksum()); 98 | result[4..32].copy_from_slice(&self.hash); 99 | result 100 | } 101 | 102 | /// Tries to parse an account identifier from a binary address. 103 | #[allow(dead_code)] 104 | pub fn from_address(blob: [u8; 32]) -> Result { 105 | check_sum(blob) 106 | } 107 | 108 | pub fn to_vec(&self) -> Vec { 109 | [&self.generate_checksum()[..], &self.hash[..]].concat() 110 | } 111 | 112 | pub fn generate_checksum(&self) -> [u8; 4] { 113 | let mut hasher = crc32fast::Hasher::new(); 114 | hasher.update(&self.hash); 115 | hasher.finalize().to_be_bytes() 116 | } 117 | } 118 | 119 | impl Display for AccountIdentifier { 120 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 121 | self.to_hex().fmt(f) 122 | } 123 | } 124 | 125 | impl FromStr for AccountIdentifier { 126 | type Err = String; 127 | 128 | fn from_str(s: &str) -> Result { 129 | AccountIdentifier::from_hex(s) 130 | } 131 | } 132 | 133 | impl Serialize for AccountIdentifier { 134 | fn serialize(&self, serializer: S) -> Result 135 | where 136 | S: serde::Serializer, 137 | { 138 | self.to_hex().serialize(serializer) 139 | } 140 | } 141 | 142 | impl<'de> Deserialize<'de> for AccountIdentifier { 143 | // This is the canonical way to read a this from string 144 | fn deserialize(deserializer: D) -> Result 145 | where 146 | D: serde::Deserializer<'de>, 147 | D::Error: de::Error, 148 | { 149 | let hex: [u8; 32] = hex::serde::deserialize(deserializer)?; 150 | check_sum(hex).map_err(D::Error::custom) 151 | } 152 | } 153 | 154 | impl From for AccountIdentifier { 155 | fn from(pid: Principal) -> Self { 156 | AccountIdentifier::new(pid, None) 157 | } 158 | } 159 | 160 | fn check_sum(hex: [u8; 32]) -> Result { 161 | // Get the checksum provided 162 | let found_checksum = &hex[0..4]; 163 | 164 | // Copy the hash into a new array 165 | let mut hash = [0; 28]; 166 | hash.copy_from_slice(&hex[4..32]); 167 | 168 | let account_id = AccountIdentifier { hash }; 169 | let expected_checksum = account_id.generate_checksum(); 170 | 171 | // Check the generated checksum matches 172 | if expected_checksum == found_checksum { 173 | Ok(account_id) 174 | } else { 175 | Err(ChecksumError { 176 | input: hex, 177 | expected_checksum, 178 | found_checksum: found_checksum.try_into().unwrap(), 179 | }) 180 | } 181 | } 182 | 183 | impl CandidType for AccountIdentifier { 184 | // The type expected for account identifier is 185 | fn _ty() -> candid::types::Type { 186 | String::_ty() 187 | } 188 | 189 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 190 | where 191 | S: candid::types::Serializer, 192 | { 193 | self.to_hex().idl_serialize(serializer) 194 | } 195 | } 196 | 197 | /// Subaccounts are arbitrary 32-byte values. 198 | #[derive(Serialize, Deserialize, CandidType, Clone, Hash, Debug, PartialEq, Eq, Copy)] 199 | #[serde(transparent)] 200 | pub struct Subaccount(pub [u8; 32]); 201 | 202 | impl Subaccount { 203 | pub fn to_vec(&self) -> Vec { 204 | self.0.to_vec() 205 | } 206 | } 207 | 208 | impl From<&Principal> for Subaccount { 209 | fn from(principal_id: &Principal) -> Self { 210 | let mut subaccount = [0; std::mem::size_of::()]; 211 | let principal_id = principal_id.as_slice(); 212 | subaccount[0] = principal_id.len().try_into().unwrap(); 213 | subaccount[1..1 + principal_id.len()].copy_from_slice(principal_id); 214 | Subaccount(subaccount) 215 | } 216 | } 217 | 218 | impl From for Vec { 219 | fn from(val: Subaccount) -> Self { 220 | val.0.to_vec() 221 | } 222 | } 223 | 224 | impl Display for Subaccount { 225 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 226 | hex::encode(self.0).fmt(f) 227 | } 228 | } 229 | 230 | /// An error for reporting invalid checksums. 231 | #[derive(Debug, PartialEq, Eq)] 232 | pub struct ChecksumError { 233 | input: [u8; 32], 234 | expected_checksum: [u8; 4], 235 | found_checksum: [u8; 4], 236 | } 237 | 238 | impl Display for ChecksumError { 239 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 240 | write!( 241 | f, 242 | "Checksum failed for {}, expected check bytes {} but found {}", 243 | hex::encode(&self.input[..]), 244 | hex::encode(self.expected_checksum), 245 | hex::encode(self.found_checksum), 246 | ) 247 | } 248 | } 249 | 250 | /// An error for reporting invalid Account Identifiers. 251 | #[derive(Debug, PartialEq, Eq)] 252 | pub enum AccountIdParseError { 253 | InvalidChecksum(ChecksumError), 254 | InvalidLength(Vec), 255 | } 256 | 257 | impl Display for AccountIdParseError { 258 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 259 | match self { 260 | Self::InvalidChecksum(err) => write!(f, "{}", err), 261 | Self::InvalidLength(input) => write!( 262 | f, 263 | "Received an invalid AccountIdentifier with length {} bytes instead of the expected 28 or 32.", 264 | input.len() 265 | ), 266 | } 267 | } 268 | } 269 | 270 | #[test] 271 | fn check_from_principal() { 272 | let principal = Principal::from_str("i3oug-lyaaa-aaaah-qco3a-cai").unwrap(); 273 | let account_id = AccountIdentifier::new(principal, None); 274 | let account_id2 = AccountIdentifier::from_hex( 275 | "6749bfc99825a9e6a37521d00dabc44b20855c8666d87429cd63215490a6c611", 276 | ) 277 | .unwrap(); 278 | println!("{}", account_id.to_hex()); 279 | println!("{}", account_id2.to_hex()); 280 | } 281 | 282 | #[test] 283 | fn check_round_trip() { 284 | let ai = AccountIdentifier { hash: [7; 28] }; 285 | let res = ai.to_hex(); 286 | assert_eq!( 287 | res.parse(), 288 | Ok(ai), 289 | "The account identifier doesn't change after going back and forth between a string" 290 | ) 291 | } 292 | 293 | #[test] 294 | fn check_encoding() { 295 | let ai = AccountIdentifier { hash: [7; 28] }; 296 | 297 | let en1 = candid::encode_one(ai).unwrap(); 298 | let en2 = candid::encode_one(ai.to_string()).unwrap(); 299 | 300 | assert_eq!( 301 | &en1, &en2, 302 | "Candid encoding of an account identifier and a string should be identical" 303 | ); 304 | 305 | let de1: String = candid::decode_one(&en1[..]).unwrap(); 306 | let de2: AccountIdentifier = candid::decode_one(&en2[..]).unwrap(); 307 | 308 | assert_eq!( 309 | de1.parse(), 310 | Ok(de2), 311 | "The types are the same after decoding, even through a different type" 312 | ); 313 | 314 | assert_eq!(de2, ai, "And the value itself hasn't changed"); 315 | } 316 | 317 | #[test] 318 | fn test_account_id_from_slice() { 319 | let length_27 = b"123456789_123456789_1234567".to_vec(); 320 | assert_eq!( 321 | AccountIdentifier::from_slice(&length_27), 322 | Err(AccountIdParseError::InvalidLength(length_27)) 323 | ); 324 | 325 | let length_28 = b"123456789_123456789_12345678".to_vec(); 326 | assert_eq!( 327 | AccountIdentifier::from_slice(&length_28), 328 | Ok(AccountIdentifier { 329 | hash: length_28.try_into().unwrap() 330 | }) 331 | ); 332 | 333 | let length_29 = b"123456789_123456789_123456789".to_vec(); 334 | assert_eq!( 335 | AccountIdentifier::from_slice(&length_29), 336 | Err(AccountIdParseError::InvalidLength(length_29)) 337 | ); 338 | 339 | let length_32 = [0; 32].to_vec(); 340 | assert_eq!( 341 | AccountIdentifier::from_slice(&length_32), 342 | Err(AccountIdParseError::InvalidChecksum(ChecksumError { 343 | input: length_32.try_into().unwrap(), 344 | expected_checksum: [128, 112, 119, 233], 345 | found_checksum: [0, 0, 0, 0], 346 | })) 347 | ); 348 | 349 | // A 32-byte address with a valid checksum 350 | let length_32 = [ 351 | 128, 112, 119, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 352 | 0, 0, 0, 0, 353 | ] 354 | .to_vec(); 355 | assert_eq!( 356 | AccountIdentifier::from_slice(&length_32), 357 | Ok(AccountIdentifier { hash: [0; 28] }) 358 | ); 359 | } 360 | 361 | #[test] 362 | fn test_account_id_from_hex() { 363 | let length_56 = "00000000000000000000000000000000000000000000000000000000"; 364 | assert_eq!( 365 | AccountIdentifier::from_hex(length_56), 366 | Ok(AccountIdentifier { hash: [0; 28] }) 367 | ); 368 | 369 | let length_57 = "000000000000000000000000000000000000000000000000000000000"; 370 | assert!(AccountIdentifier::from_hex(length_57).is_err()); 371 | 372 | let length_58 = "0000000000000000000000000000000000000000000000000000000000"; 373 | assert_eq!( 374 | AccountIdentifier::from_hex(length_58), 375 | Err("0000000000000000000000000000000000000000000000000000000000 has a length of 58 but we expected a length of 64 or 56".to_string()) 376 | ); 377 | 378 | let length_64 = "0000000000000000000000000000000000000000000000000000000000000000"; 379 | assert!(AccountIdentifier::from_hex(length_64) 380 | .unwrap_err() 381 | .contains("Checksum failed")); 382 | 383 | // Try again with correct checksum 384 | let length_64 = "807077e900000000000000000000000000000000000000000000000000000000"; 385 | assert_eq!( 386 | AccountIdentifier::from_hex(length_64), 387 | Ok(AccountIdentifier { hash: [0; 28] }) 388 | ); 389 | } 390 | 391 | #[test] 392 | fn test_compare_accounts() { 393 | // Try again with correct checksum 394 | let length_64 = "807077e900000000000000000000000000000000000000000000000000000000"; 395 | 396 | let account_id = AccountIdentifier::from_hex(length_64).unwrap(); 397 | let account_id2 = AccountIdentifier::from_hex( 398 | "6749bfc99825a9e6a37521d00dabc44b20855c8666d87429cd63215490a6c611", 399 | ) 400 | .unwrap(); 401 | let account_id3 = AccountIdentifier::from_hex(length_64).unwrap(); 402 | 403 | println!("{}", account_id.to_hex()); 404 | println!("{}", account_id2.to_hex()); 405 | 406 | assert!(account_id != account_id2); 407 | assert!(account_id == account_id3); 408 | 409 | // assert_eq!( 410 | // account_id, 411 | // account_id2, 412 | // ); 413 | } 414 | -------------------------------------------------------------------------------- /spec_test/src/icp_ledger.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk::api::call::call_raw; 2 | use ic_cdk::export::candid::{encode_args, Decode, Principal}; 3 | use ic_cdk::print; 4 | 5 | use crate::types::{NotifyArgs, SendArgs}; 6 | 7 | pub async fn call_send_dfx(canister: Principal, args: &SendArgs) -> Result { 8 | //Encode args in candid 9 | let event_raw = 10 | encode_args((args,)).map_err(|_| String::from("Cannot serialize Transaction Args"))?; 11 | 12 | print(format!("array size: {}", event_raw.len())); 13 | 14 | //Inter container call to ledger canister 15 | let raw_res = call_raw(canister, "send_dfx", &event_raw, 0) 16 | .await 17 | .map_err(|(_, s)| format!("Error invoking Ledger Canister, {}", &s))?; 18 | 19 | // //Todo: deserialize send_dfx result to get block height! 20 | let res = Decode!(&raw_res, u64) 21 | .map_err(|_| String::from("Error decoding response from Ledger canister"))?; 22 | 23 | Ok(res) 24 | } 25 | 26 | #[allow(dead_code)] 27 | pub async fn call_notify_dfx(canister: Principal, args: &NotifyArgs) -> Result { 28 | //Encode args in candid 29 | let event_raw = 30 | encode_args((args,)).map_err(|_| String::from("Cannot serialize Transaction Args"))?; 31 | 32 | //Inter container call to ledger canister 33 | let raw_res = call_raw(canister, "notify_dfx", &event_raw, 0) 34 | .await 35 | .map_err(|(_, s)| format!("Error invoking Ledger Canister, {}", &s))?; 36 | 37 | // //Todo: deserialize send_dfx result to get block height! 38 | let res = Decode!(&raw_res, u64) 39 | .map_err(|_| String::from("Error decoding response from Ledger canister"))?; 40 | 41 | Ok(res) 42 | } 43 | -------------------------------------------------------------------------------- /spec_test/src/lib.rs: -------------------------------------------------------------------------------- 1 | use candid::export_service; 2 | use ic_cdk::export::Principal; 3 | use ic_cdk::query; 4 | 5 | mod account_identifier; 6 | mod icp_ledger; 7 | mod types; 8 | 9 | mod tests; 10 | 11 | #[query(name = "__get_candid_interface_tmp_hack")] 12 | fn export_candid() -> String { 13 | export_service!(); 14 | __export_service() 15 | } 16 | -------------------------------------------------------------------------------- /spec_test/src/tests.rs: -------------------------------------------------------------------------------- 1 | use candid::{candid_method, Principal}; 2 | use ic_cdk::{ 3 | api::{ 4 | canister_balance, canister_balance128, canister_version, data_certificate, 5 | instruction_counter, 6 | stable::{stable64_size, stable_grow, stable_read, stable_size}, 7 | }, 8 | caller, id, init, post_upgrade, pre_upgrade, query, update, print, trap 9 | }; 10 | 11 | use crate::{ 12 | icp_ledger::call_send_dfx, 13 | types::{ICPTs, SendArgs}, 14 | }; 15 | 16 | #[query] 17 | #[candid_method(query)] 18 | pub fn test_caller() -> Principal { 19 | let caller_id = caller(); 20 | print(format!("Caller Id: {caller_id}")); 21 | caller_id 22 | } 23 | 24 | #[query] 25 | #[candid_method(query)] 26 | pub fn test_id() -> Principal { 27 | let canister_id = id(); 28 | print(format!("Canister Id: {canister_id}")); 29 | canister_id 30 | } 31 | 32 | #[query] 33 | #[candid_method(query)] 34 | pub fn test_balance() -> u64 { 35 | canister_balance() 36 | } 37 | 38 | #[query] 39 | #[candid_method(query)] 40 | pub fn test_balance128() -> u128 { 41 | canister_balance128() 42 | } 43 | 44 | #[query] 45 | #[candid_method(query)] 46 | pub fn test_data_certificate() -> Option> { 47 | data_certificate() 48 | } 49 | 50 | #[query] 51 | #[candid_method(query)] 52 | pub fn test_instruction_counter() -> u64 { 53 | instruction_counter() 54 | } 55 | 56 | #[query] 57 | #[candid_method(query)] 58 | pub fn test_stable_size() -> u32 { 59 | stable_size() 60 | } 61 | 62 | #[update] 63 | #[candid_method(update)] 64 | pub fn test_stable_grow() -> Result { 65 | stable_grow(1).map_err(|x| format!("{x}")) 66 | } 67 | 68 | #[query] 69 | #[candid_method(query)] 70 | pub fn test_stable64_size() -> u64 { 71 | stable64_size() 72 | } 73 | 74 | #[query] 75 | #[candid_method(query)] 76 | pub fn test_canister_version() -> u64 { 77 | canister_version() 78 | } 79 | 80 | #[update] 81 | #[candid_method(update)] 82 | pub fn test_trap() -> Result { 83 | trap("This is a trap"); 84 | } 85 | 86 | #[update] 87 | #[candid_method(update)] 88 | pub fn test_updates() -> Result { 89 | stable_grow(1).map_err(|x| format!("{x}"))?; 90 | // stable_ 91 | 92 | let mut buf = vec![64]; 93 | stable_read(0, &mut buf); 94 | 95 | Ok(0) 96 | } 97 | 98 | #[update] 99 | #[candid_method(update)] 100 | pub async fn test_inter_canister(target: Principal, to: String, amount: u64) -> Result { 101 | let args = SendArgs { 102 | amount: ICPTs { e8s: amount }, 103 | memo: 0, 104 | fee: ICPTs { e8s: 10_000 }, 105 | from_subaccount: None, 106 | to: to, 107 | created_at_time: None, 108 | }; 109 | 110 | call_send_dfx(target, &args).await?; 111 | 112 | Ok(0) 113 | } 114 | 115 | #[pre_upgrade] 116 | pub fn test_pre_upgrade() {} 117 | 118 | #[post_upgrade] 119 | pub fn test_post_upgrade() {} 120 | 121 | #[init] 122 | #[candid_method(init)] 123 | pub fn test_init() {} 124 | -------------------------------------------------------------------------------- /spec_test/src/types.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk::export::candid::{CandidType, Deserialize, Principal}; 2 | use serde::Serialize; 3 | 4 | use crate::account_identifier::Subaccount; 5 | 6 | #[derive(Clone, CandidType, Deserialize, Serialize)] 7 | pub struct Property { 8 | pub name: String, 9 | pub value: String, 10 | } 11 | 12 | #[derive(Clone, CandidType, Deserialize)] 13 | pub struct MintRequest { 14 | pub name: String, 15 | pub url: String, 16 | pub desc: String, 17 | pub properties: Vec, 18 | pub data: Vec, 19 | pub content_type: String, 20 | pub owner: Principal, 21 | } 22 | 23 | #[derive(CandidType, Deserialize, Clone, Serialize)] 24 | #[allow(non_camel_case_types)] 25 | pub enum Operation { 26 | delist, 27 | init, 28 | list, 29 | mint, 30 | burn, 31 | purchase, 32 | transfer, 33 | approval, 34 | } 35 | 36 | impl Default for Operation { 37 | fn default() -> Self { 38 | Operation::init 39 | } 40 | } 41 | 42 | #[derive(Clone, CandidType, Deserialize)] 43 | pub struct Record { 44 | pub caller: Principal, 45 | pub op: Operation, 46 | pub from: Option, 47 | pub to: Option, 48 | pub token_id: u128, 49 | pub price: u64, 50 | pub timestamp: u128, 51 | } 52 | 53 | #[derive(Clone, CandidType, Deserialize, Serialize)] 54 | pub struct ICPTs { 55 | pub e8s: u64, 56 | } 57 | 58 | #[derive(Clone, CandidType, Deserialize, Serialize)] 59 | pub struct TransactionNotification { 60 | pub amount: ICPTs, 61 | pub block_height: u64, 62 | pub from: Principal, 63 | pub from_subaccount: Option, 64 | pub memo: u64, 65 | pub to: Principal, 66 | pub to_subaccount: Option, 67 | } 68 | 69 | #[derive(Clone, CandidType, Deserialize, Serialize)] 70 | pub struct TransactionResponse { 71 | pub block: u64, 72 | pub creators_fee: u64, 73 | pub seller: Principal, 74 | } 75 | 76 | #[derive(Clone, CandidType, Deserialize, Serialize)] 77 | pub struct TimeStamp { 78 | pub timestamp_nanos: u64, 79 | } 80 | 81 | // #[derive(Copy, Clone, CandidType, Deserialize, Serialize)] 82 | // pub struct Subaccount(pub [u8; 32]); 83 | 84 | #[derive(Clone, CandidType, Serialize, Deserialize)] 85 | pub struct SendArgs { 86 | pub memo: u64, 87 | pub amount: ICPTs, 88 | pub fee: ICPTs, 89 | pub from_subaccount: Option, 90 | pub to: String, 91 | pub created_at_time: Option, 92 | } 93 | 94 | #[derive(Clone, CandidType, Serialize, Deserialize)] 95 | pub struct NotifyArgs { 96 | pub block_height: u64, 97 | pub max_fee: ICPTs, 98 | pub from_subaccount: Option, 99 | pub to_canister: Principal, 100 | pub to_subaccount: Option, 101 | } 102 | -------------------------------------------------------------------------------- /src/bls.ts: -------------------------------------------------------------------------------- 1 | import { fromHex } from '@dfinity/agent' 2 | import { concat } from '@dfinity/candid' 3 | 4 | import { bls_sign, bls_init, bls_get_key_pair } from './wasm_tools/pkg/wasm_tools.js' 5 | 6 | export const DER_PREFIX = fromHex( 7 | '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100' 8 | ) 9 | 10 | export class Bls { 11 | derPublicKey: Buffer 12 | 13 | // ctx: any 14 | // private key 15 | S: Uint8Array 16 | // public key 17 | publicKey: Uint8Array 18 | 19 | async init() { 20 | //miracl embedded in wasm 21 | bls_init() 22 | 23 | const keys = bls_get_key_pair() 24 | 25 | this.S = keys.slice(0,48) 26 | // this.W = keys.slice(48) 27 | 28 | this.publicKey = keys.slice(48) 29 | 30 | const tmp = new Uint8Array(DER_PREFIX.byteLength + 96) 31 | tmp.set(Buffer.from(DER_PREFIX), 0) 32 | tmp.set(this.publicKey, DER_PREFIX.byteLength) 33 | 34 | this.derPublicKey = Buffer.from(tmp) 35 | 36 | // await this.generateKey() 37 | } 38 | 39 | // async generateKey() { 40 | // // //miracl/core 41 | // this.ctx = new CTX("BLS12381"); 42 | // this.ctx.ECP.ALLOW_ALT_COMPRESS = true 43 | 44 | // const RAW: number[] = [] 45 | // const rng = new this.ctx.RAND(); 46 | 47 | // rng.clean(); 48 | // for (let i = 0; i < 100; i++) RAW[i] = i; 49 | 50 | // rng.seed(100, RAW); 51 | 52 | // const IKM: any[] = []; 53 | // for (let i = 0; i < 32; i++) 54 | // //IKM[i]=i+1; 55 | // IKM[i] = rng.getByte(); 56 | 57 | // if (this.ctx.BLS.init() !== 0) { 58 | // throw new Error('Cannot initialize BLS') 59 | // } 60 | 61 | // this.W = [] 62 | // this.S = [] 63 | 64 | // const res = this.ctx.BLS.KeyPairGenerate(IKM, this.S, this.W); 65 | // if (res != 0) { 66 | // console.log("Failed to Generate Keys"); 67 | // return; 68 | // } 69 | 70 | // console.log("Private key : 0x" + this.ctx.BLS.bytestostring(this.S)); 71 | // console.log("Public key : 0x" + this.ctx.BLS.bytestostring(this.W)); 72 | 73 | // this.publicKey = new Uint8Array(this.W) 74 | 75 | // const tmp = new Uint8Array(DER_PREFIX.byteLength + 96) 76 | // tmp.set(Buffer.from(DER_PREFIX), 0) 77 | // tmp.set(this.W, DER_PREFIX.byteLength) 78 | 79 | // this.derPublicKey = Buffer.from(tmp) 80 | // } 81 | 82 | 83 | domain_sep(s: string): ArrayBuffer { 84 | const len = new Uint8Array([s.length]); 85 | const str = new TextEncoder().encode(s); 86 | return concat(len, str); 87 | } 88 | 89 | async sign(msg: ArrayBuffer): Promise { 90 | const message = new Uint8Array(msg) 91 | // const SIG: any[]=[]; 92 | 93 | // const sigResult = this.ctx.BLS.core_sign(SIG, message, this.S) 94 | // const sig = new Uint8Array(SIG) 95 | 96 | const sig = bls_sign(message, this.S) 97 | 98 | return sig 99 | } 100 | } -------------------------------------------------------------------------------- /src/call_context.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid' 2 | import { Principal } from '@dfinity/principal' 3 | 4 | // More info is in file subnet_config.rs about subnet parameters and costs 5 | export enum SubnetType { 6 | Unspecified = 0, 7 | /// A normal subnet where no restrictions are applied. 8 | Application = 1, 9 | /// A more privileged subnet where certain restrictions are applied, 10 | /// like not charging for cycles or restricting who can create and 11 | /// install canisters on it. 12 | System = 2, 13 | /// A subnet type that is like application subnets but can have some 14 | /// additional features. 15 | VerifiedApplication = 4, 16 | } 17 | 18 | export enum CallSource { 19 | Ingress = 'Ingress', 20 | InterCanister = 'InterCanister', 21 | XNet = 'XNet', 22 | Internal = 'Internal', 23 | } 24 | 25 | export enum CallType { 26 | Init = 'I', 27 | PreUpgrade = 'G', 28 | Update = 'U', 29 | Query = 'Q', 30 | ReplyCallback = 'Ry', 31 | RejectCallback = 'Rt', 32 | Cleanup = 'C', 33 | Start = 's', 34 | InspectMessage = 'F', 35 | SystemTask = 'T', 36 | } 37 | 38 | export enum RejectionCode { 39 | NoError = 0, 40 | 41 | SysFatal = 1, 42 | SysTransient = 2, 43 | DestinationInvalid = 3, 44 | CanisterReject = 4, 45 | CanisterError = 5, 46 | 47 | Unknown, 48 | } 49 | 50 | export class CallContext { 51 | type: CallType 52 | } 53 | 54 | export enum CallStatus { 55 | New = 'New', 56 | Processing = 'Processing', 57 | Error = 'Error', 58 | Ok = 'Ok', 59 | CanisterNotFound = 'CanisterNotFound' 60 | } 61 | export class Message { 62 | id?: string 63 | type: CallType 64 | source: CallSource 65 | 66 | target: Principal 67 | sender: Principal 68 | 69 | method: string 70 | args_raw?: ArrayBuffer 71 | cycles: bigint 72 | 73 | status: CallStatus 74 | rejectionCode: RejectionCode 75 | rejectionMessage?: ArrayBuffer 76 | 77 | result?: ArrayBuffer 78 | 79 | //For inter canister calls 80 | replyFun: number 81 | replyEnv: number 82 | rejectFun: number 83 | rejectEnv: number 84 | replyContext: Message 85 | 86 | relatedMessages: Message[] 87 | 88 | constructor (source: Partial) { 89 | this.status = CallStatus.New 90 | this.cycles = 0n 91 | this.relatedMessages = [] 92 | Object.assign(this, source) 93 | } 94 | 95 | getMethodName (): string { 96 | if (this.type === CallType.Update) { 97 | return 'canister_update ' + this.method 98 | } 99 | if (this.type === CallType.Query) { 100 | return 'canister_query ' + this.method 101 | } 102 | if (this.type === CallType.Init) { 103 | return 'canister_init' 104 | } 105 | 106 | return this.method 107 | } 108 | 109 | static init (canister: Principal, sender: Principal, args: ArrayBuffer): Message { 110 | return new Message({ 111 | type: CallType.Init, 112 | source: CallSource.Internal, 113 | target: Principal.fromText(canister.toString()), 114 | sender: Principal.fromText(sender.toString()), 115 | method: '', 116 | args_raw: args 117 | }) 118 | } 119 | 120 | static candidHack (canister: Principal): Message { 121 | return new Message({ 122 | type: CallType.Query, 123 | source: CallSource.Internal, 124 | target: Principal.fromText(canister.toString()), 125 | sender: Principal.anonymous(), 126 | method: '__get_candid_interface_tmp_hack', 127 | args_raw: IDL.encode([], []), 128 | }) 129 | } 130 | 131 | static query ( 132 | canister: Principal, name: string, sender: Principal, args: ArrayBuffer 133 | ): Message { 134 | return new Message({ 135 | type: CallType.Query, 136 | source: CallSource.Ingress, 137 | target: Principal.fromText(canister.toString()), 138 | sender: Principal.fromText(sender.toString()), 139 | method: name, 140 | args_raw: args, 141 | }) 142 | } 143 | 144 | static update ( 145 | canister: Principal, name: string, sender: Principal, args: ArrayBuffer 146 | ): Message { 147 | return new Message({ 148 | type: CallType.Update, 149 | source: CallSource.Ingress, 150 | target: Principal.fromText(canister.toString()), 151 | sender: Principal.fromText(sender.toString()), 152 | method: name, 153 | args_raw: args, 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/canister.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid' 2 | import { Message } from "./call_context"; 3 | import { Principal } from '@dfinity/principal'; 4 | 5 | export enum CanisterStatus { 6 | Running, 7 | Stopping, 8 | Stopped, 9 | } 10 | 11 | export interface WasmModule { 12 | module: WebAssembly.Module, 13 | hash: string 14 | } 15 | 16 | export interface Canister { 17 | // timestamp at which given canister was created 18 | created: bigint 19 | 20 | getIdlBuilder(): IDL.InterfaceFactory; 21 | get_id(): Principal; 22 | get_idl(): IDL.ConstructType; 23 | 24 | process_message(msg: Message): Promise; 25 | 26 | get_module_hash(): Buffer | undefined; 27 | } -------------------------------------------------------------------------------- /src/hash_tree.ts: -------------------------------------------------------------------------------- 1 | import { HashTree } from "@dfinity/agent" 2 | import { lebEncode } from "@dfinity/candid" 3 | 4 | interface TreeLabel { 5 | name: ArrayBuffer, 6 | value: TreeNode 7 | } 8 | 9 | interface TreeNode { 10 | value?: ArrayBuffer 11 | nodes: TreeLabel[] 12 | // nodes: Record 13 | } 14 | 15 | export class Tree { 16 | node: TreeNode 17 | 18 | constructor() { 19 | this.node = {nodes: []} 20 | } 21 | 22 | insertValue(path: (Buffer | string)[], val: Buffer | string | number | bigint | ArrayBuffer) { 23 | const node = this.traverseTree(this.node, path) 24 | 25 | if (typeof val === 'string') { 26 | val = Buffer.from(new TextEncoder().encode(val)) 27 | } 28 | if (typeof val === 'number') { 29 | val = Buffer.from([val]) 30 | } 31 | 32 | if (typeof val === 'bigint') { 33 | const tmp = lebEncode(val) 34 | val = Buffer.from(tmp) 35 | } 36 | 37 | node.value = val 38 | } 39 | 40 | traverseTree(node: TreeNode, path: (Buffer | string)[]): TreeNode { 41 | if (path.length === 0) { 42 | return node 43 | } 44 | 45 | let name = path[0] 46 | if (typeof name === 'string') { 47 | name = Buffer.from(new TextEncoder().encode(name)) 48 | } 49 | 50 | // let res: TreeNode = node.nodes[name] 51 | 52 | const match = node.nodes.filter((x) => Buffer.compare(Buffer.from(x.name), name as Buffer) == 0) 53 | let nextNode: TreeNode 54 | 55 | if (match.length === 0) { 56 | const label: TreeLabel = { 57 | name: name, 58 | value: { nodes: [] } 59 | } 60 | node.nodes.push(label) 61 | nextNode = label.value 62 | } else { 63 | nextNode = match[0].value 64 | } 65 | 66 | return this.traverseTree(nextNode, path.slice(1)) 67 | } 68 | 69 | getHashTree(): HashTree { 70 | return makeHashTree(this.node) 71 | } 72 | } 73 | 74 | 75 | function makeHashTree(node: TreeNode): HashTree { 76 | if (node.nodes.length > 0) { 77 | const items: HashTree[] = node.nodes.map((x) => [2, x.name, makeHashTree(x.value)]); 78 | 79 | return fork(items) 80 | } 81 | if (node.value !== undefined) { 82 | return [3, node.value] 83 | } 84 | return [0] 85 | 86 | } 87 | function fork(items: HashTree[]): HashTree { 88 | if (items.length === 1) { 89 | return items[0] 90 | } 91 | if (items.length === 2) { 92 | return [1, items[0], items[1]] 93 | } 94 | if (items.length === 3) { 95 | return [1, items[0], fork(items.slice(1))] 96 | } 97 | 98 | return items[0] 99 | } 100 | 101 | // Builds tree based on path and value 102 | export function makeHashTreeOld(path: (Buffer | string)[], val: Buffer | string): HashTree { 103 | if (typeof val === 'string') { 104 | val = Buffer.from(new TextEncoder().encode(val)) 105 | } 106 | let prev: HashTree = [3, val] 107 | 108 | for (let item of path.reverse()) { 109 | if (typeof item === 'string') { 110 | item = Buffer.from(new TextEncoder().encode(item)) 111 | } 112 | 113 | const treeItem = [2, item, prev] 114 | prev = treeItem 115 | } 116 | 117 | return prev 118 | } 119 | 120 | // function clone(tree: HashTree): HashTree { 121 | // let item: HashTree = [...tree] 122 | 123 | // return item 124 | // } 125 | 126 | export function mergeTrees(tree1: HashTree, tree2: HashTree): HashTree { 127 | let item: HashTree = [0] 128 | 129 | if (tree1[0] === 2) { 130 | if (tree2[0] === 2) { 131 | if (Buffer.compare(tree1[1] as Buffer, tree2[1] as Buffer) === 0) { 132 | //the same label 133 | const val = mergeTrees(tree1[2], tree2[2]) 134 | item = [2, tree1[1], val] 135 | } else { 136 | //label is different 137 | item = [1, tree1, tree2] 138 | } 139 | } 140 | } 141 | 142 | return item 143 | } 144 | -------------------------------------------------------------------------------- /src/helpers/ledger_helper.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { type TestContext } from '../test_context' 3 | import fs from 'fs' 4 | import https from 'node:https' 5 | import type http from 'node:http' 6 | import url from 'url' 7 | import pako from 'pako' 8 | import { type WasmCanister } from '../wasm_canister' 9 | import { getAccount } from '../utils' 10 | import { sha256 } from 'js-sha256' 11 | import { MockAgent } from '../mock_agent' 12 | 13 | export const latestRelease = 'f02cc38677905e24a9016637fddc697039930808' 14 | 15 | export class HttpPromise { 16 | async get (url): Promise { 17 | const [, , content] = await this._makeRequest('GET', url, {}) 18 | 19 | // if (code === 301) { 20 | // response.headers.location 21 | // } 22 | 23 | return content 24 | } 25 | 26 | // eslint-disable-next-line @typescript-eslint/promise-function-async 27 | _makeRequest (method, urlString, options): Promise<[number, http.IncomingMessage, any]> { 28 | // create a new Promise 29 | return new Promise<[number, http.IncomingMessage, any]>((resolve, reject) => { 30 | /* Node's URL library allows us to create a 31 | * URL object from our request string, so we can build 32 | * our request for http.get */ 33 | const parsedUrl = new url.URL(urlString) 34 | 35 | // const requestOptions = this._createOptions(method, parsedUrl) 36 | const request = https.get(parsedUrl, res => { 37 | this._onResponse(res, resolve, reject) 38 | }) 39 | 40 | /* if there's an error, then reject the Promise 41 | * (can be handled with Promise.prototype.catch) */ 42 | request.on('error', reject) 43 | 44 | request.end() 45 | }) 46 | } 47 | 48 | // // the options that are required by http.get 49 | // _createOptions (method, url: url.URL): http.RequestOptions { 50 | // http.Req 51 | // return { 52 | // hostname: url.hostname, 53 | // path: url.pathname, 54 | // protocol: url.protocol, 55 | // port: url.port, 56 | // method 57 | // } 58 | // } 59 | 60 | /* once http.get returns a response, build it and 61 | * resolve or reject the Promise */ 62 | _onResponse (response: http.IncomingMessage, resolve: any, reject: any): void { 63 | if (response.statusCode === undefined) return 64 | const hasResponseFailed = response.statusCode >= 400 65 | 66 | const chunks: Uint8Array[] = [] 67 | 68 | if (hasResponseFailed) { 69 | reject(`Request to ${response.url ?? ''} failed with HTTP ${response.statusCode}`) 70 | } 71 | 72 | /* the response stream's (an instance of Stream) current data. See: 73 | * https://nodejs.org/api/stream.html#stream_event_data */ 74 | response.on('data', chunk => { 75 | chunks.push(chunk) 76 | }) 77 | 78 | // once all the data has been read, resolve the Promise 79 | response.on('end', () => { 80 | // Get the total length of all arrays. 81 | let length = 0 82 | for (const item of chunks) { 83 | length += item.length 84 | } 85 | 86 | // Create a new array with total length and merge all source arrays. 87 | const mergedArray = new Uint8Array(length) 88 | let offset = 0 89 | for (const item of chunks) { 90 | mergedArray.set(item, offset) 91 | offset += item.length 92 | } 93 | 94 | resolve([response.statusCode, response, mergedArray]) 95 | }) 96 | } 97 | } 98 | 99 | export class LedgerHelper { 100 | public ledger: WasmCanister 101 | public minter: Principal 102 | public owner: Principal 103 | 104 | constructor (ledger: WasmCanister, minter: Principal, owner: Principal) { 105 | this.ledger = ledger 106 | this.minter = minter 107 | this.owner = owner 108 | } 109 | 110 | async faucet(target: Principal, amount: number): Promise { 111 | // const args = LedgerHelper.getSendArgs(target, amount) 112 | // await this.ledger.update('send_dfx', args) 113 | } 114 | 115 | async balanceOf(target: Principal): Promise { 116 | const args = { 117 | account: getAccount(target, 0).toUint8Array() 118 | } 119 | 120 | const agent = new MockAgent(this.ledger.state.replica, Principal.anonymous()) 121 | const actor = agent.getActor(this.ledger) 122 | 123 | const result = await actor.account_balance(args) as any 124 | return result.e8s 125 | } 126 | 127 | static async checkAndDownload (commit: string): Promise { 128 | if (!fs.existsSync('./cache')) { 129 | fs.mkdirSync('./cache') 130 | } 131 | if (!fs.existsSync('./cache/ledger.wasm')) { 132 | const prom = new HttpPromise() 133 | 134 | console.log('Downloading latest ledger package, commit: ' + commit) 135 | 136 | const url = 'https://download.dfinity.systems/ic/' + commit + '/canisters/ledger-canister_notify-method.wasm.gz' 137 | const res = await prom.get(url) 138 | 139 | const hash = sha256(res) 140 | console.log('Hash: ' + hash); 141 | 142 | const inflated = pako.inflate(res) 143 | 144 | console.log('Ledger module downloaded') 145 | 146 | fs.writeFileSync('./cache/ledger.wasm', inflated) 147 | } 148 | } 149 | 150 | static getSendArgs (target: Principal, amount: number, memo = 0): any { 151 | const canisterAccount = getAccount(target, 0) 152 | const args = { 153 | amount: { e8s: amount }, 154 | memo: memo, 155 | fee: { e8s: 10_000 }, 156 | from_subaccount: [], 157 | to: canisterAccount.toUint8Array(), 158 | created_at_time: [] 159 | } 160 | return args 161 | } 162 | 163 | static async defaults (context: TestContext, minter: Principal, owner: Principal): Promise { 164 | await LedgerHelper.checkAndDownload(latestRelease) 165 | 166 | const mintingAccount = getAccount(minter, 0) 167 | const invokingAccount = getAccount(owner, 0) 168 | 169 | const ledger = await context.deploy('./cache/ledger.wasm', { 170 | initArgs: [{ 171 | minting_account: mintingAccount.toHex(), 172 | initial_values: [[invokingAccount.toHex(), { e8s: 100_000_000_000 }]], 173 | send_whitelist: [], 174 | token_symbol: [], 175 | token_name: [], 176 | transfer_fee: [{ e8s: 10_000 }], 177 | transaction_window: [], 178 | max_message_size_bytes: [], 179 | icrc1_minting_account: [], 180 | archive_options: [] 181 | }], 182 | id: 'ryjl3-tyaaa-aaaaa-aaaba-cai' 183 | }) as WasmCanister 184 | 185 | const helper: LedgerHelper = new LedgerHelper(ledger, minter, owner); 186 | 187 | return helper 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/helpers/nns_helper.ts: -------------------------------------------------------------------------------- 1 | import { type Principal } from '@dfinity/principal' 2 | import { type TestContext } from '../test_context' 3 | import fs from 'fs' 4 | import pako from 'pako' 5 | import { type WasmCanister } from '../wasm_canister' 6 | import { getAccount } from '../utils' 7 | import { HttpPromise } from './ledger_helper' 8 | 9 | const latestRelease = 'f02cc38677905e24a9016637fddc697039930808' 10 | 11 | export class NNSHelper { 12 | public cmc: WasmCanister 13 | public ledger: Principal 14 | public owner: Principal 15 | 16 | static async checkAndDownload (): Promise { 17 | if (!fs.existsSync('./cache')) { 18 | fs.mkdirSync('./cache') 19 | } 20 | if (!fs.existsSync('./cache/cycles-minting-canister.wasm')) { 21 | const prom = new HttpPromise() 22 | 23 | console.log('Downloading latest cycles minting canister package, commit: ' + latestRelease) 24 | 25 | const url = 'https://download.dfinity.systems/ic/' + latestRelease + '/canisters/cycles-minting-canister.wasm.gz' 26 | const res = await prom.get(url) 27 | 28 | const inflated = pako.inflate(res) 29 | 30 | console.log('Cycles minting canister module downloaded') 31 | 32 | fs.writeFileSync('./cache/cycles-minting-canister.wasm', inflated) 33 | } 34 | } 35 | 36 | static getSendArgs (target: Principal, amount: number): any { 37 | const canisterAccount = getAccount(target, 0) 38 | const args = { 39 | amount: { e8s: amount }, 40 | memo: 0, 41 | fee: { e8s: 10_000 }, 42 | from_subaccount: [], 43 | to: canisterAccount.toUint8Array(), 44 | created_at_time: [] 45 | } 46 | return args 47 | } 48 | 49 | static async defaults (context: TestContext, ledger: Principal, owner: Principal): Promise { 50 | await NNSHelper.checkAndDownload() 51 | 52 | const cmc = await context.deploy('./cache/cycles-minting-canister.wasm', { 53 | initArgs: [{ 54 | ledger_canister_id: ledger, 55 | governance_canister_id: owner, 56 | minting_account_id: [], 57 | last_purged_notification: [], 58 | exchange_rate_canister: [], 59 | }] 60 | }) as WasmCanister 61 | 62 | const helper: NNSHelper = { 63 | cmc, 64 | ledger, 65 | owner 66 | } 67 | 68 | return helper 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ic0.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug" 2 | import { CallSource, CallStatus, CallType, Message, RejectionCode } from "./call_context" 3 | import { Principal } from "@dfinity/principal" 4 | import { canisterIdIntoU64 } from "./utils" 5 | import { CanisterState } from "./wasm_canister" 6 | 7 | const log = debug('lightic:canister') 8 | const ic0log = log.extend('ic0') 9 | 10 | 11 | export class Ic0 { 12 | public getImports(state: CanisterState, importList: string[]) { 13 | const importObject = {} 14 | 15 | for (const item of importList) { 16 | if (this[item] !== undefined) { 17 | importObject[item] = (...args) => this[item](state, ...args) 18 | } else { 19 | importObject[item] = () => console.log("Call to ic0 not implemented function: "+item) 20 | } 21 | } 22 | 23 | return {ic0: importObject} 24 | } 25 | 26 | // Return length of args, called from canister 27 | msg_arg_data_size(cntx: CanisterState): number { 28 | ic0log('msg_arg_data_size: %o', cntx.args_buffer?.byteLength) 29 | return cntx.args_buffer?.byteLength ?? 0 30 | } 31 | 32 | // Copy args data to WASM memory, called from canister 33 | msg_arg_data_copy(cntx: CanisterState, dst: number, offset: number, size: number): void { 34 | const view = new Uint8Array(cntx.memory.buffer) 35 | ic0log('msg_arg_data_copy: %o %o %o', dst, offset, size) 36 | 37 | if (cntx.args_buffer !== null && cntx.args_buffer !== undefined) { 38 | for (let i = 0; i < size; i++) { 39 | const val = cntx.args_buffer[i] 40 | view[dst + i] = val 41 | } 42 | } 43 | } 44 | 45 | // Return length of args, called from canister 46 | msg_caller_size(cntx: CanisterState): number | undefined { 47 | return cntx.message?.sender?.toUint8Array().byteLength 48 | } 49 | 50 | // Copy args data to WASM memory, called from canister 51 | msg_caller_copy(cntx: CanisterState, dst: number, offset: number, size: number): void { 52 | const view = new Uint8Array(cntx.memory.buffer) 53 | 54 | if (cntx.message?.sender !== null && cntx.message?.sender !== undefined) { 55 | const buf = cntx.message.sender.toUint8Array() 56 | 57 | for (let i = 0; i < size; i++) { 58 | const val = buf[i] 59 | view[dst + i] = val 60 | } 61 | } 62 | } 63 | 64 | msg_reject_code(cntx: CanisterState): number { 65 | return cntx.message?.rejectionCode ?? 0 66 | } 67 | 68 | msg_reject_msg_size(cntx: CanisterState): number { 69 | return cntx.message?.rejectionMessage?.byteLength ?? 0 70 | } 71 | 72 | // Copy rejection msg data to WASM memory, called from canister 73 | msg_reject_msg_copy(cntx: CanisterState, dst: number, offset: number, size: number): void { 74 | if (cntx.message === undefined) return 75 | if (cntx.message.rejectionMessage === null) return 76 | 77 | const view = new Uint8Array(cntx.memory.buffer) 78 | 79 | if (cntx.message !== undefined && cntx.message.rejectionMessage !== undefined) { 80 | const buf = new Uint8Array(cntx.message.rejectionMessage) 81 | 82 | for (let i = 0; i < size; i++) { 83 | const val = buf[i] 84 | view[dst + i] = val 85 | } 86 | } 87 | } 88 | 89 | 90 | // Return length of args, called from canister 91 | canister_self_size(cntx: CanisterState): number { 92 | return cntx.canister.get_id().toUint8Array().byteLength 93 | } 94 | 95 | // Copy args data to WASM memory, called from canister 96 | canister_self_copy(cntx: CanisterState, dst: number, offset: number, size: number): void { 97 | const view = new Uint8Array(cntx.memory.buffer) 98 | 99 | const buf = cntx.canister.get_id().toUint8Array() 100 | 101 | for (let i = 0; i < size; i++) { 102 | const val = buf[i] 103 | view[dst + i] = val 104 | } 105 | } 106 | 107 | canister_cycle_balance(cntx: CanisterState): bigint { 108 | return cntx.cycles 109 | } 110 | 111 | // Called from canister, info about part of response 112 | msg_reply_data_append(cntx: CanisterState, src: number, size: number): void { 113 | const view = new Uint8Array(cntx.memory.buffer, src, size) 114 | 115 | cntx.reply_buffer.set(view, cntx.reply_size) 116 | cntx.reply_size += size 117 | } 118 | 119 | msg_reply(cntx: CanisterState): void { 120 | ic0log('msg_reply') 121 | 122 | if (cntx.message !== undefined) { 123 | if (cntx.message.status === CallStatus.Ok) { 124 | throw new Error('Message already replied') 125 | } 126 | 127 | ic0log(cntx.message.id + ' ' + cntx.message.method) 128 | cntx.message.status = CallStatus.Ok 129 | cntx.message.result = cntx.reply_buffer.subarray(0, cntx.reply_size) 130 | } 131 | } 132 | 133 | msg_reject(cntx: CanisterState, src: number, size: number): void { 134 | ic0log('msg_reject') 135 | 136 | if (cntx.message !== undefined) { 137 | // ic0log(cntx.message.id + ' ' + cntx.message.method) 138 | cntx.message.status = CallStatus.Error 139 | cntx.message.rejectionCode = RejectionCode.CanisterReject 140 | 141 | const view = new Uint8Array(cntx.memory.buffer, src, size) 142 | cntx.reply_buffer.set(view, 0) 143 | const msg = cntx.reply_buffer.subarray(0, size) 144 | 145 | cntx.message.rejectionMessage = msg 146 | } 147 | } 148 | 149 | msg_cycles_accept(cntx: CanisterState, maxAmount: bigint): bigint { 150 | if (cntx.message === undefined) return 0n 151 | 152 | let amount = maxAmount 153 | if (amount > cntx.message.cycles) { 154 | amount = cntx.message.cycles 155 | } 156 | 157 | cntx.message.cycles -= amount 158 | cntx.cycles += BigInt(amount) 159 | 160 | return amount 161 | } 162 | 163 | call_new( 164 | cntx: CanisterState, 165 | calleeSrc: number, 166 | calleeSize: number, 167 | nameSrc: number, 168 | nameSize: number, 169 | replyFun: number, 170 | replyEnv: number, 171 | rejectFun: number, 172 | rejectEnv: number): void { 173 | const msg = new Message({ 174 | source: CallSource.InterCanister 175 | }) 176 | 177 | msg.sender = Principal.fromText(cntx.canister.get_id().toString()) 178 | 179 | const view = new Uint8Array(cntx.memory.buffer, calleeSrc, calleeSize) 180 | const target = Principal.fromUint8Array(view) 181 | 182 | msg.target = Principal.fromText(target.toString()) 183 | 184 | const view2 = new Uint8Array(cntx.memory.buffer, nameSrc, nameSize) 185 | const name = new TextDecoder().decode(view2) 186 | 187 | msg.method = name 188 | 189 | if (cntx.message !== undefined) { 190 | msg.replyContext = cntx.message 191 | } 192 | msg.replyFun = replyFun 193 | msg.replyEnv = replyEnv 194 | msg.rejectFun = rejectFun 195 | msg.rejectEnv = rejectEnv 196 | 197 | cntx.newMessage = msg 198 | cntx.newMessageReplySize = 0 199 | 200 | ic0log('call_new: %o %o', target.toString(), name) 201 | } 202 | 203 | call_data_append(cntx: CanisterState, src: number, size: number): void { 204 | const view = new Uint8Array(cntx.memory.buffer, src, size) 205 | 206 | cntx.newMessageArgs.set(view, cntx.newMessageReplySize) 207 | cntx.newMessageReplySize += size 208 | ic0log('call_data_append: %o %o', src, size) 209 | } 210 | 211 | call_cycles_add(cntx: CanisterState, amount: bigint): void { 212 | if (cntx.newMessage !== undefined) { 213 | cntx.newMessage.cycles += amount 214 | } 215 | } 216 | 217 | call_cycles_add128(cntx: CanisterState, amountHigh: bigint, amountLow: bigint): void { 218 | if (amountHigh > 0n) throw new Error('Amount high is not implemented in call_cycles_add128!') 219 | if (cntx.newMessage !== undefined) { 220 | cntx.newMessage.cycles += amountLow 221 | } 222 | } 223 | 224 | call_perform(cntx: CanisterState): number { 225 | if (cntx.newMessage == null) return 1 226 | 227 | const args = cntx.newMessageArgs.subarray(0, cntx.newMessageReplySize) 228 | 229 | cntx.newMessage.args_raw = args 230 | 231 | 232 | if (cntx.message !== undefined) { 233 | cntx.message.relatedMessages.push(cntx.newMessage) 234 | } 235 | 236 | // Store inter canister message to be processed after cntx call is completed 237 | cntx.replica.store_message(cntx.newMessage) 238 | 239 | return 0 240 | } 241 | 242 | stable_size(cntx: CanisterState): number { 243 | return cntx.stableMemory.buffer.byteLength/65536 244 | } 245 | 246 | stable_grow(cntx: CanisterState, newPages: number): number { 247 | return cntx.stableMemory.grow(newPages) 248 | } 249 | 250 | stable_write(cntx: CanisterState, offset: number, src: number, size: number): void { 251 | const stableView = new Uint8Array(cntx.stableMemory.buffer) 252 | const canisterView = new Uint8Array(cntx.memory.buffer, src, size) 253 | 254 | stableView.set(canisterView, offset) 255 | } 256 | 257 | stable_read(cntx: CanisterState, dst: number, offset: number, size: number): void { 258 | const stableView = new Uint8Array(cntx.stableMemory.buffer, offset, size) 259 | const canisterView = new Uint8Array(cntx.memory.buffer, dst, size) 260 | 261 | canisterView.set(stableView) 262 | } 263 | 264 | stable64_size(cntx: CanisterState): bigint { 265 | return BigInt(cntx.stableMemory.buffer.byteLength)/65536n 266 | } 267 | 268 | certified_data_set(cntx: CanisterState, src: number, size: number): void { 269 | const view = new Uint8Array(cntx.memory.buffer, src, size) 270 | 271 | cntx.certified_data.set(view, 0) 272 | ic0log('certified_data_set: %o', cntx.certified_data) 273 | } 274 | 275 | // time(cntx: CanisterState): bigint { 276 | time(): bigint { 277 | const t = process.hrtime.bigint() 278 | ic0log('time: %o', t) 279 | return t 280 | } 281 | 282 | // global_timer_set(cntx: CanisterState, timestamp: bigint): bigint { 283 | global_timer_set(): bigint { 284 | return 0n 285 | } 286 | 287 | // performance_counter(cntx: CanisterState, counterType: number): bigint { 288 | performance_counter(): bigint { 289 | return 0n 290 | } 291 | 292 | debug_print(cntx: CanisterState, src: number, size: number): void { 293 | const data = cntx.memory.buffer.slice(src, src + size) 294 | const text = new TextDecoder().decode(data) 295 | 296 | log('Canister debug: %s', text) 297 | } 298 | 299 | trap(cntx: CanisterState, src: number, size: number): void { 300 | const data = cntx.memory.buffer.slice(src, src + size) 301 | const text = new TextDecoder().decode(data) 302 | 303 | log('Canister trap!: %s', text) 304 | 305 | // Revert canister memory to pre trap 306 | log('Reverting memory') 307 | const view = new Uint8Array(cntx.memory.buffer) 308 | view.set(new Uint8Array(cntx.memoryCopy)) 309 | 310 | throw new Error('Canister trap!: ' + text) 311 | } 312 | 313 | mint_cycles(cntx: CanisterState, amount: bigint): bigint { 314 | const canId = canisterIdIntoU64(cntx.canister.get_id()) 315 | 316 | if (canId !== 4n) { 317 | throw new Error('ic0.mint_cycles can only be executed on Cycles Minting Canister:') 318 | } 319 | 320 | cntx.cycles += amount 321 | 322 | return amount 323 | } 324 | } -------------------------------------------------------------------------------- /src/idl_builder.ts: -------------------------------------------------------------------------------- 1 | import { type IDL } from '@dfinity/candid' 2 | 3 | export class IdlResult { 4 | idl: IDL.ServiceClass 5 | init_args?: IDL.ConstructType[] 6 | 7 | constructor (idl: IDL.ServiceClass, initArgs?: IDL.ConstructType[]) { 8 | this.idl = idl 9 | this.init_args = initArgs 10 | } 11 | } 12 | 13 | class IdlBuilder { 14 | private readonly data: any 15 | private types: Record 16 | 17 | private readonly fill: any[] 18 | 19 | constructor (data: any) { 20 | this.data = data 21 | this.types = {} 22 | this.fill = [] 23 | } 24 | 25 | public build_idl (IDL: any): IdlResult { 26 | for (const name of Object.keys(this.data.types)) { 27 | this.get_type(IDL, name) 28 | } 29 | 30 | for (const item of this.fill) { 31 | item() 32 | } 33 | 34 | const idl = this.get_idl(IDL, this.data.actor.Spec) as IDL.ServiceClass 35 | 36 | let init: IDL.ConstructType[] | undefined 37 | if (this.data.actor.Init !== undefined) { 38 | init = this.get_idls(IDL, this.data.actor.Init) 39 | } 40 | 41 | return new IdlResult(idl, init) 42 | } 43 | 44 | get_service (IDL: any, data: any): IDL.ServiceClass { 45 | const func = {} 46 | 47 | for (const [name, type] of Object.entries(data)) { 48 | const idl = this.get_idl(IDL, type) 49 | func[name] = idl 50 | } 51 | 52 | const idl = IDL.Service(func) 53 | 54 | return idl 55 | } 56 | 57 | get_type (IDL: any, name: string): IDL.ConstructType | null { 58 | if (this.types[name] !== undefined) return this.types[name] 59 | 60 | // console.log("Decoding type:", name); 61 | 62 | if (this.data.types[name] !== undefined) { 63 | const type = this.data.types[name] 64 | const idl = this.get_idl(IDL, type) 65 | 66 | this.types[name] = idl 67 | 68 | return idl 69 | } 70 | 71 | return null 72 | } 73 | 74 | get_idls (IDL: any, data: any[]): IDL.ConstructType[] { 75 | const res: IDL.ConstructType[] = [] 76 | 77 | for (const i of data) { 78 | res.push(this.get_idl(IDL, i)) 79 | } 80 | 81 | return res 82 | } 83 | 84 | get_idl (IDL: any, data: any): IDL.ConstructType { 85 | let idl: any = null 86 | 87 | if (data === null) { 88 | idl = IDL.Null 89 | } else if (data.Record !== undefined) { 90 | idl = this.get_record(IDL, data.Record) 91 | } else if (data.Tuple !== undefined) { 92 | idl = this.get_tuple(IDL, data.Tuple) 93 | } else if (data.Func !== undefined) { 94 | const args = this.get_idls(IDL, data.Func.args) 95 | const rets = this.get_idls(IDL, data.Func.rets) 96 | const modes = data.Func.modes.map((x: string) => x.toLowerCase()) 97 | 98 | idl = IDL.Func(args, rets, modes) 99 | } else if (data.Vec !== undefined) { 100 | idl = IDL.Vec(this.get_idl(IDL, data.Vec)) 101 | } else if (data.Variant !== undefined) { 102 | idl = this.get_variant(IDL, data.Variant) 103 | } else if (data.Opt !== undefined) { 104 | idl = IDL.Opt(this.get_idl(IDL, data.Opt)) 105 | } else if (data.Var !== undefined) { 106 | if (this.types[data.Var] !== undefined) { 107 | idl = this.types[data.Var] 108 | } else { 109 | const temp = IDL.Rec() 110 | idl = temp 111 | 112 | this.fill.push(() => { 113 | temp.fill(this.get_type(IDL, data.Var)) 114 | }) 115 | } 116 | 117 | // idl = this.get_type(data.Var); 118 | } else if (data.Service !== undefined) { 119 | idl = this.get_service(IDL, data.Service) 120 | } else if (typeof data === 'string' || data instanceof String) { 121 | switch (data) { 122 | case 'nat': 123 | idl = IDL.Nat 124 | break 125 | case 'nat8': 126 | idl = IDL.Nat8 127 | break 128 | case 'nat32': 129 | idl = IDL.Nat32 130 | break 131 | case 'nat64': 132 | idl = IDL.Nat64 133 | break 134 | case 'text': 135 | idl = IDL.Text 136 | break 137 | case 'principal': 138 | idl = IDL.Principal 139 | break 140 | case 'int': 141 | idl = IDL.Int 142 | break 143 | case 'bool': 144 | idl = IDL.Bool 145 | break 146 | default: 147 | break 148 | } 149 | } 150 | 151 | return idl 152 | } 153 | 154 | get_variant (IDL: any, data: any): any { 155 | const variants = {} 156 | for (const [name, type] of Object.entries(data)) { 157 | const idl = this.get_idl(IDL, type) 158 | variants[name] = idl 159 | } 160 | 161 | return IDL.Variant(variants) 162 | } 163 | 164 | get_record (IDL: any, data: any): any { 165 | const item = {} 166 | 167 | for (const [field, ty] of Object.entries(data)) { 168 | const idl = this.get_idl(IDL, ty) 169 | 170 | item[field] = idl 171 | } 172 | 173 | return IDL.Record(item) 174 | } 175 | 176 | // IDL.Tuple(IDL.Text, Tokens), 177 | get_tuple (IDL: any, data: any): any { 178 | const items = this.get_idls(IDL, data) 179 | return IDL.Tuple(...items) 180 | } 181 | } 182 | 183 | export function buildIdl (IDL: any, jsonCandid: any): IdlResult { 184 | const builder = new IdlBuilder(jsonCandid) 185 | return builder.build_idl(IDL) 186 | } 187 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { CallContext, Message, CallType, CallSource, CallStatus } from './call_context' 2 | export { WasmCanister } from './wasm_canister' 3 | export { Canister } from './canister' 4 | export { ReplicaContext } from './replica_context' 5 | export { TestContext, getGlobalTestContext } from './test_context' 6 | export { LedgerHelper } from './helpers/ledger_helper' 7 | export { ActorSubclass } from './mock_actor' 8 | 9 | export { getAccount, hexToBytes, u64IntoCanisterId, canisterIdIntoU64, u64IntoPrincipalId } from './utils' 10 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import debug from 'debug' 3 | import { wasm_instrument } from './wasm_tools/pkg/wasm_tools' 4 | import { sha256 } from 'js-sha256'; 5 | import { WasmModule } from './canister'; 6 | 7 | const log = debug('lightic:instrumentation') 8 | 9 | //Really simple cache, uses wasm module length as identifier 10 | // const moduleCache: Record = {} 11 | const moduleCache: Record = {} 12 | 13 | function getCodeHash(code: Buffer) { 14 | const hash = sha256(code) 15 | return hash 16 | } 17 | 18 | export async function loadWasmFromFile (file: string): Promise { 19 | const wasmBuffer = fs.readFileSync(file) 20 | return await loadWasm(wasmBuffer) 21 | } 22 | 23 | export async function loadWasm(wasmBuffer: Buffer): Promise { 24 | const hash = getCodeHash(wasmBuffer) 25 | 26 | if (moduleCache[hash] !== undefined) { 27 | return moduleCache[hash] 28 | } 29 | 30 | log('Instrumenting WASM ') 31 | const instrumented = wasm_instrument(wasmBuffer) 32 | log('Compiling WASM') 33 | const compiled = await WebAssembly.compile(instrumented) 34 | 35 | const item: WasmModule = { module: compiled, hash: hash } 36 | moduleCache[hash] = item 37 | 38 | return item 39 | } -------------------------------------------------------------------------------- /src/management_canister.did: -------------------------------------------------------------------------------- 1 | type canister_id = principal; 2 | type user_id = principal; 3 | type wasm_module = blob; 4 | 5 | type canister_settings = record { 6 | controllers : opt vec principal; 7 | compute_allocation : opt nat; 8 | memory_allocation : opt nat; 9 | freezing_threshold : opt nat; 10 | }; 11 | 12 | type definite_canister_settings = record { 13 | controllers : vec principal; 14 | compute_allocation : nat; 15 | memory_allocation : nat; 16 | freezing_threshold : nat; 17 | }; 18 | 19 | type http_header = record { name: text; value: text }; 20 | 21 | type http_response = record { 22 | status: nat; 23 | headers: vec http_header; 24 | body: blob; 25 | }; 26 | 27 | type ecdsa_curve = variant { secp256k1; }; 28 | 29 | type satoshi = nat64; 30 | 31 | type bitcoin_network = variant { 32 | mainnet; 33 | testnet; 34 | }; 35 | 36 | type bitcoin_address = text; 37 | 38 | type block_hash = blob; 39 | 40 | type outpoint = record { 41 | txid : blob; 42 | vout : nat32 43 | }; 44 | 45 | type utxo = record { 46 | outpoint: outpoint; 47 | value: satoshi; 48 | height: nat32; 49 | }; 50 | 51 | type get_utxos_request = record { 52 | address : bitcoin_address; 53 | network: bitcoin_network; 54 | filter: opt variant { 55 | min_confirmations: nat32; 56 | page: blob; 57 | }; 58 | }; 59 | 60 | type get_current_fee_percentiles_request = record { 61 | network: bitcoin_network; 62 | }; 63 | 64 | type get_utxos_response = record { 65 | utxos: vec utxo; 66 | tip_block_hash: block_hash; 67 | tip_height: nat32; 68 | next_page: opt blob; 69 | }; 70 | 71 | type get_balance_request = record { 72 | address : bitcoin_address; 73 | network: bitcoin_network; 74 | min_confirmations: opt nat32; 75 | }; 76 | 77 | type send_transaction_request = record { 78 | transaction: blob; 79 | network: bitcoin_network; 80 | }; 81 | 82 | type millisatoshi_per_byte = nat64; 83 | 84 | service ic : { 85 | create_canister : (record { 86 | settings : opt canister_settings 87 | }) -> (record {canister_id : canister_id}); 88 | update_settings : (record { 89 | canister_id : principal; 90 | settings : canister_settings 91 | }) -> (); 92 | install_code : (record { 93 | mode : variant {install; reinstall; upgrade}; 94 | canister_id : canister_id; 95 | wasm_module : wasm_module; 96 | arg : blob; 97 | }) -> (); 98 | uninstall_code : (record {canister_id : canister_id}) -> (); 99 | start_canister : (record {canister_id : canister_id}) -> (); 100 | stop_canister : (record {canister_id : canister_id}) -> (); 101 | canister_status : (record {canister_id : canister_id}) -> (record { 102 | status : variant { running; stopping; stopped }; 103 | settings: definite_canister_settings; 104 | module_hash: opt blob; 105 | memory_size: nat; 106 | cycles: nat; 107 | idle_cycles_burned_per_day: nat; 108 | }); 109 | delete_canister : (record {canister_id : canister_id}) -> (); 110 | deposit_cycles : (record {canister_id : canister_id}) -> (); 111 | raw_rand : () -> (blob); 112 | http_request : (record { 113 | url : text; 114 | max_response_bytes: opt nat64; 115 | method : variant { get; head; post }; 116 | headers: vec http_header; 117 | body : opt blob; 118 | transform : opt record { 119 | function : func (record {response : http_response; context : blob}) -> (http_response) query; 120 | context : blob 121 | }; 122 | }) -> (http_response); 123 | 124 | // Threshold ECDSA signature 125 | ecdsa_public_key : (record { 126 | canister_id : opt canister_id; 127 | derivation_path : vec blob; 128 | key_id : record { curve: ecdsa_curve; name: text }; 129 | }) -> (record { public_key : blob; chain_code : blob; }); 130 | sign_with_ecdsa : (record { 131 | message_hash : blob; 132 | derivation_path : vec blob; 133 | key_id : record { curve: ecdsa_curve; name: text }; 134 | }) -> (record { signature : blob }); 135 | 136 | // bitcoin interface 137 | bitcoin_get_balance: (get_balance_request) -> (satoshi); 138 | bitcoin_get_utxos: (get_utxos_request) -> (get_utxos_response); 139 | bitcoin_send_transaction: (send_transaction_request) -> (); 140 | bitcoin_get_current_fee_percentiles: (get_current_fee_percentiles_request) -> (vec millisatoshi_per_byte); 141 | 142 | // provisional interfaces for the pre-ledger world 143 | provisional_create_canister_with_cycles : (record { 144 | amount: opt nat; 145 | settings : opt canister_settings; 146 | specified_id: opt canister_id; 147 | }) -> (record {canister_id : canister_id}); 148 | provisional_top_up_canister : 149 | (record { canister_id: canister_id; amount: nat }) -> (); 150 | } -------------------------------------------------------------------------------- /src/management_canister.ts: -------------------------------------------------------------------------------- 1 | import { ConstructType, InterfaceFactory } from "@dfinity/candid/lib/cjs/idl" 2 | import { CallStatus, Message, RejectionCode } from "./call_context" 3 | import { Canister } from "./canister" 4 | import { InstallCanisterArgs, ReplicaContext } from "./replica_context" 5 | import { Principal } from "@dfinity/principal" 6 | 7 | import { idlFactory } from './mgmt.did' 8 | import { IDL } from "@dfinity/candid" 9 | 10 | import crypto from 'crypto' 11 | import { CanisterInstallMode } from "@dfinity/agent" 12 | import { WasmCanister } from "./wasm_canister" 13 | import { loadWasm } from "./instrumentation" 14 | 15 | const { OptClass, Rec } = require("@dfinity/candid/lib/cjs/idl") 16 | const leb128_1 = require('@dfinity/candid/lib/cjs/utils/leb128') 17 | 18 | OptClass.prototype.decodeValue = function (b, t) { 19 | const opt = this.checkType(t); 20 | if (!(opt instanceof OptClass)) { 21 | const type = Rec() 22 | type._type = opt 23 | 24 | return [this._type.decodeValue(b, type)]; 25 | // throw new Error('Not an option type'); 26 | } 27 | switch ((0, leb128_1.safeReadUint8)(b)) { 28 | case 0: 29 | return []; 30 | case 1: 31 | return [this._type.decodeValue(b, opt._type)]; 32 | default: 33 | throw new Error('Not an option value'); 34 | } 35 | } 36 | 37 | interface CanisterSettings { 38 | controllers: [], 39 | compute_allocation: [], 40 | memory_allocation: [], 41 | freezing_threshold: [] 42 | } 43 | 44 | interface ProvisionalArgs { 45 | amount: [], 46 | settings: CanisterSettings[], 47 | specified_id: string[] 48 | } 49 | 50 | // interface CreateCanisterArgs { 51 | // settings: CanisterSettings[], 52 | // } 53 | 54 | interface CanisterCreateResult { 55 | canister_id: Principal 56 | } 57 | 58 | class CustomError implements Error { 59 | name: string 60 | message: string 61 | stack?: string | undefined 62 | code: number 63 | } 64 | 65 | export class ManagementCanister implements Canister { 66 | private context: ReplicaContext 67 | 68 | private idl: ConstructType 69 | readonly created: bigint 70 | 71 | constructor(context: ReplicaContext) { 72 | this.context = context 73 | this.idl = idlFactory({ IDL }) 74 | this.created = 0n 75 | } 76 | get_module_hash(): Buffer | undefined { 77 | throw new Error("Method not implemented.") 78 | } 79 | getIdlBuilder(): InterfaceFactory { 80 | throw new Error("Method not implemented.") 81 | } 82 | get_id(): Principal { 83 | throw new Error("Method not implemented.") 84 | } 85 | get_idl(): ConstructType { 86 | return this.idl 87 | } 88 | async process_message(msg: Message): Promise { 89 | const fun = this[msg.method] 90 | 91 | if (fun !== undefined) { 92 | for (const field of (this.idl as any)._fields) { 93 | if (field[0] === msg.method) { 94 | const argTypes = field[1].argTypes 95 | const retTypes = field[1].retTypes 96 | 97 | if (msg.args_raw !== undefined) { 98 | try { 99 | const args = IDL.decode(argTypes, msg.args_raw) 100 | let result = fun.apply(this, [msg, ...args]) 101 | 102 | if (result instanceof Promise) { 103 | result = await result 104 | } 105 | 106 | if (result === undefined) { 107 | result = [] 108 | } else { 109 | result = [result] 110 | } 111 | 112 | const ret = IDL.encode(retTypes, result) 113 | 114 | msg.result = ret 115 | msg.status = CallStatus.Ok 116 | } catch (e) { 117 | msg.status = CallStatus.Error 118 | msg.rejectionCode = e.code ?? RejectionCode.CanisterError 119 | msg.rejectionMessage = new TextEncoder().encode(e.message) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | else { 127 | console.log(`Management Canister: Invalid function ${msg.method}`) 128 | 129 | msg.status = CallStatus.Error 130 | msg.rejectionCode = RejectionCode.CanisterReject 131 | msg.rejectionMessage = new TextEncoder().encode(`Management Canister: Invalid function ${msg.method}`) 132 | } 133 | 134 | } 135 | 136 | raw_rand(): Uint8Array { 137 | const view = new Uint8Array(32) 138 | crypto.webcrypto.getRandomValues(view) 139 | 140 | // const result = IDL.encode([IDL.Vec(IDL.Nat8)], [view]) 141 | // return result 142 | 143 | return view 144 | } 145 | 146 | // create_canister(msg: Message, args: CreateCanisterArgs | null): CanisterCreateResult { 147 | create_canister(msg: Message): CanisterCreateResult { 148 | const params: InstallCanisterArgs = { 149 | caller: msg.sender, 150 | } 151 | 152 | try { 153 | const canister = this.context.create_canister(params) as WasmCanister 154 | canister.state.cycles = msg.cycles 155 | 156 | return { canister_id: canister.get_id() } 157 | } catch (e) { 158 | const err = new CustomError() 159 | err.message = e.message 160 | err.code = RejectionCode.DestinationInvalid 161 | 162 | throw err 163 | } 164 | } 165 | 166 | provisional_create_canister_with_cycles(msg: Message, args: ProvisionalArgs | null): CanisterCreateResult { 167 | const params: InstallCanisterArgs = { 168 | caller: msg.sender, 169 | } 170 | 171 | if (args !== undefined && args !== null && args.specified_id.length === 1) { 172 | params.id = args.specified_id[0].toString() 173 | } 174 | 175 | try { 176 | const canister = this.context.create_canister(params) as WasmCanister 177 | canister.state.cycles = 10_000_000_000n 178 | 179 | return { canister_id: canister.get_id() } 180 | } catch (e) { 181 | const err = new CustomError() 182 | err.message = e.message 183 | err.code = RejectionCode.DestinationInvalid 184 | 185 | throw err 186 | } 187 | } 188 | 189 | async install_code(msg: Message, arg: { arg: ArrayBuffer, wasm_module: ArrayBuffer, mode: CanisterInstallMode, canister_id: Principal }): Promise { 190 | const canister = this.context.get_canister(arg.canister_id) as WasmCanister 191 | if (canister !== undefined) { 192 | const module = await loadWasm(Buffer.from(arg.wasm_module)) 193 | 194 | await canister.install_module(module) 195 | await canister.initialize(arg.arg, msg.sender ?? Principal.anonymous()) 196 | } else { 197 | throw new Error('Canister not found') 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /src/mgmt.did.ts: -------------------------------------------------------------------------------- 1 | export const idlFactory = ({ IDL }) => { 2 | const bitcoin_network = IDL.Variant({ 3 | 'mainnet' : IDL.Null, 4 | 'testnet' : IDL.Null, 5 | }); 6 | const bitcoin_address = IDL.Text; 7 | const get_balance_request = IDL.Record({ 8 | 'network' : bitcoin_network, 9 | 'address' : bitcoin_address, 10 | 'min_confirmations' : IDL.Opt(IDL.Nat32), 11 | }); 12 | const satoshi = IDL.Nat64; 13 | const get_current_fee_percentiles_request = IDL.Record({ 14 | 'network' : bitcoin_network, 15 | }); 16 | const millisatoshi_per_byte = IDL.Nat64; 17 | const get_utxos_request = IDL.Record({ 18 | 'network' : bitcoin_network, 19 | 'filter' : IDL.Opt( 20 | IDL.Variant({ 21 | 'page' : IDL.Vec(IDL.Nat8), 22 | 'min_confirmations' : IDL.Nat32, 23 | }) 24 | ), 25 | 'address' : bitcoin_address, 26 | }); 27 | const block_hash = IDL.Vec(IDL.Nat8); 28 | const outpoint = IDL.Record({ 29 | 'txid' : IDL.Vec(IDL.Nat8), 30 | 'vout' : IDL.Nat32, 31 | }); 32 | const utxo = IDL.Record({ 33 | 'height' : IDL.Nat32, 34 | 'value' : satoshi, 35 | 'outpoint' : outpoint, 36 | }); 37 | const get_utxos_response = IDL.Record({ 38 | 'next_page' : IDL.Opt(IDL.Vec(IDL.Nat8)), 39 | 'tip_height' : IDL.Nat32, 40 | 'tip_block_hash' : block_hash, 41 | 'utxos' : IDL.Vec(utxo), 42 | }); 43 | const send_transaction_request = IDL.Record({ 44 | 'transaction' : IDL.Vec(IDL.Nat8), 45 | 'network' : bitcoin_network, 46 | }); 47 | const canister_id = IDL.Principal; 48 | const definite_canister_settings = IDL.Record({ 49 | 'freezing_threshold' : IDL.Nat, 50 | 'controllers' : IDL.Vec(IDL.Principal), 51 | 'memory_allocation' : IDL.Nat, 52 | 'compute_allocation' : IDL.Nat, 53 | }); 54 | const canister_settings = IDL.Record({ 55 | 'freezing_threshold' : IDL.Opt(IDL.Nat), 56 | 'controllers' : IDL.Opt(IDL.Vec(IDL.Principal)), 57 | 'memory_allocation' : IDL.Opt(IDL.Nat), 58 | 'compute_allocation' : IDL.Opt(IDL.Nat), 59 | }); 60 | const ecdsa_curve = IDL.Variant({ 'secp256k1' : IDL.Null }); 61 | const http_header = IDL.Record({ 'value' : IDL.Text, 'name' : IDL.Text }); 62 | const http_response = IDL.Record({ 63 | 'status' : IDL.Nat, 64 | 'body' : IDL.Vec(IDL.Nat8), 65 | 'headers' : IDL.Vec(http_header), 66 | }); 67 | const wasm_module = IDL.Vec(IDL.Nat8); 68 | return IDL.Service({ 69 | 'bitcoin_get_balance' : IDL.Func([get_balance_request], [satoshi], []), 70 | 'bitcoin_get_current_fee_percentiles' : IDL.Func( 71 | [get_current_fee_percentiles_request], 72 | [IDL.Vec(millisatoshi_per_byte)], 73 | [], 74 | ), 75 | 'bitcoin_get_utxos' : IDL.Func( 76 | [get_utxos_request], 77 | [get_utxos_response], 78 | [], 79 | ), 80 | 'bitcoin_send_transaction' : IDL.Func([send_transaction_request], [], []), 81 | 'canister_status' : IDL.Func( 82 | [IDL.Record({ 'canister_id' : canister_id })], 83 | [ 84 | IDL.Record({ 85 | 'status' : IDL.Variant({ 86 | 'stopped' : IDL.Null, 87 | 'stopping' : IDL.Null, 88 | 'running' : IDL.Null, 89 | }), 90 | 'memory_size' : IDL.Nat, 91 | 'cycles' : IDL.Nat, 92 | 'settings' : definite_canister_settings, 93 | 'idle_cycles_burned_per_day' : IDL.Nat, 94 | 'module_hash' : IDL.Opt(IDL.Vec(IDL.Nat8)), 95 | }), 96 | ], 97 | [], 98 | ), 99 | 'create_canister' : IDL.Func( 100 | [IDL.Record({ 'settings' : IDL.Opt(canister_settings) })], 101 | [IDL.Record({ 'canister_id' : canister_id })], 102 | [], 103 | ), 104 | 'delete_canister' : IDL.Func( 105 | [IDL.Record({ 'canister_id' : canister_id })], 106 | [], 107 | [], 108 | ), 109 | 'deposit_cycles' : IDL.Func( 110 | [IDL.Record({ 'canister_id' : canister_id })], 111 | [], 112 | [], 113 | ), 114 | 'ecdsa_public_key' : IDL.Func( 115 | [ 116 | IDL.Record({ 117 | 'key_id' : IDL.Record({ 'name' : IDL.Text, 'curve' : ecdsa_curve }), 118 | 'canister_id' : IDL.Opt(canister_id), 119 | 'derivation_path' : IDL.Vec(IDL.Vec(IDL.Nat8)), 120 | }), 121 | ], 122 | [ 123 | IDL.Record({ 124 | 'public_key' : IDL.Vec(IDL.Nat8), 125 | 'chain_code' : IDL.Vec(IDL.Nat8), 126 | }), 127 | ], 128 | [], 129 | ), 130 | 'http_request' : IDL.Func( 131 | [ 132 | IDL.Record({ 133 | 'url' : IDL.Text, 134 | 'method' : IDL.Variant({ 135 | 'get' : IDL.Null, 136 | 'head' : IDL.Null, 137 | 'post' : IDL.Null, 138 | }), 139 | 'max_response_bytes' : IDL.Opt(IDL.Nat64), 140 | 'body' : IDL.Opt(IDL.Vec(IDL.Nat8)), 141 | 'transform' : IDL.Opt( 142 | IDL.Record({ 143 | 'function' : IDL.Func( 144 | [ 145 | IDL.Record({ 146 | 'context' : IDL.Vec(IDL.Nat8), 147 | 'response' : http_response, 148 | }), 149 | ], 150 | [http_response], 151 | ['query'], 152 | ), 153 | 'context' : IDL.Vec(IDL.Nat8), 154 | }) 155 | ), 156 | 'headers' : IDL.Vec(http_header), 157 | }), 158 | ], 159 | [http_response], 160 | [], 161 | ), 162 | 'install_code' : IDL.Func( 163 | [ 164 | IDL.Record({ 165 | 'arg' : IDL.Vec(IDL.Nat8), 166 | 'wasm_module' : wasm_module, 167 | 'mode' : IDL.Variant({ 168 | 'reinstall' : IDL.Null, 169 | 'upgrade' : IDL.Null, 170 | 'install' : IDL.Null, 171 | }), 172 | 'canister_id' : canister_id, 173 | }), 174 | ], 175 | [], 176 | [], 177 | ), 178 | 'provisional_create_canister_with_cycles' : IDL.Func( 179 | [ 180 | IDL.Record({ 181 | 'settings' : IDL.Opt(canister_settings), 182 | 'specified_id' : IDL.Opt(canister_id), 183 | 'amount' : IDL.Opt(IDL.Nat), 184 | }), 185 | ], 186 | [IDL.Record({ 'canister_id' : canister_id })], 187 | [], 188 | ), 189 | 'provisional_top_up_canister' : IDL.Func( 190 | [IDL.Record({ 'canister_id' : canister_id, 'amount' : IDL.Nat })], 191 | [], 192 | [], 193 | ), 194 | 'raw_rand' : IDL.Func([], [IDL.Vec(IDL.Nat8)], []), 195 | 'sign_with_ecdsa' : IDL.Func( 196 | [ 197 | IDL.Record({ 198 | 'key_id' : IDL.Record({ 'name' : IDL.Text, 'curve' : ecdsa_curve }), 199 | 'derivation_path' : IDL.Vec(IDL.Vec(IDL.Nat8)), 200 | 'message_hash' : IDL.Vec(IDL.Nat8), 201 | }), 202 | ], 203 | [IDL.Record({ 'signature' : IDL.Vec(IDL.Nat8) })], 204 | [], 205 | ), 206 | 'start_canister' : IDL.Func( 207 | [IDL.Record({ 'canister_id' : canister_id })], 208 | [], 209 | [], 210 | ), 211 | 'stop_canister' : IDL.Func( 212 | [IDL.Record({ 'canister_id' : canister_id })], 213 | [], 214 | [], 215 | ), 216 | 'uninstall_code' : IDL.Func( 217 | [IDL.Record({ 'canister_id' : canister_id })], 218 | [], 219 | [], 220 | ), 221 | 'update_settings' : IDL.Func( 222 | [ 223 | IDL.Record({ 224 | 'canister_id' : IDL.Principal, 225 | 'settings' : canister_settings, 226 | }), 227 | ], 228 | [], 229 | [], 230 | ), 231 | }); 232 | }; 233 | export const init = ({ IDL }) => { return []; }; -------------------------------------------------------------------------------- /src/mock_actor.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid' 2 | import { type Principal } from '@dfinity/principal' 3 | 4 | import debug from 'debug' 5 | import { 6 | UpdateCallRejectedError, 7 | QueryResponseStatus, 8 | QueryCallRejectedError 9 | } from '@dfinity/agent' 10 | import { type MockAgent } from './mock_agent' 11 | 12 | const log = debug('lightic:actor') 13 | 14 | export type ActorMethod = ( 15 | ...args: Args 16 | ) => Promise 17 | 18 | export type ActorSubclass> = MockActor & T 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 21 | export class MockActor { 22 | // canister 23 | // caller 24 | 25 | // constructor (canister, prin) { 26 | // this.canister = canister 27 | // this.caller = prin 28 | // } 29 | 30 | public static createActor>( 31 | agent: MockAgent, 32 | canister_id: Principal, 33 | idl: any | undefined = undefined 34 | ): ActorSubclass { 35 | let service: IDL.ConstructType | undefined 36 | if (idl !== undefined) { 37 | service = idl({ IDL }) 38 | } else { 39 | const canister = agent.replica.get_canister(canister_id); 40 | service = canister.get_idl() 41 | } 42 | // const service = idl ?? canister.get_idl() 43 | 44 | class CanisterActor extends MockActor { 45 | [x: string]: ActorMethod; 46 | 47 | constructor (agent: MockAgent, canister_id: Principal, service: any) { 48 | super() 49 | 50 | for (const [methodName, func] of service._fields) { 51 | this[methodName] = _createActorMethod( 52 | agent, 53 | canister_id, 54 | methodName, 55 | func 56 | ) 57 | } 58 | } 59 | } 60 | 61 | const item = new CanisterActor(agent, canister_id, service) 62 | 63 | return item as ActorSubclass 64 | } 65 | } 66 | 67 | function _createActorMethod ( 68 | agent: MockAgent, 69 | 70 | canisterId: Principal, 71 | methodName: string, 72 | func: IDL.FuncClass 73 | ): ActorMethod { 74 | let caller: (...args: unknown[]) => Promise 75 | if (func.annotations.includes('query')) { 76 | caller = async (...args) => { 77 | log('Calling query %s', methodName) 78 | 79 | const arg = IDL.encode(func.argTypes, args) 80 | 81 | const cid = canisterId 82 | 83 | const result = await agent.query(cid, { methodName, arg }) 84 | 85 | switch (result.status) { 86 | case QueryResponseStatus.Rejected: 87 | throw new QueryCallRejectedError(cid, methodName, result) 88 | 89 | case QueryResponseStatus.Replied: 90 | return decodeReturnValue(func.retTypes, result.reply.arg) 91 | } 92 | } 93 | } else { 94 | // THIS IS UPDATE CALL, WE DO NOT HANDLE IT NOW!!!!! 95 | caller = async (...args) => { 96 | log('Calling update %s', methodName) 97 | 98 | const arg = IDL.encode(func.argTypes, args) 99 | 100 | const cid = canisterId 101 | const ecid = canisterId 102 | 103 | const { requestId, response } = await agent.call(cid, { 104 | methodName, 105 | arg, 106 | effectiveCanisterId: ecid 107 | }) 108 | 109 | if (!response.ok) { 110 | throw new UpdateCallRejectedError(cid, methodName, requestId, response) 111 | } 112 | 113 | // const pollStrategy = strategy.defaultStrategy() 114 | // const responseBytes = await pollForResponse(agent, ecid, requestId, pollStrategy) 115 | 116 | const responseBytes = await agent.waitForResponse(requestId) 117 | 118 | if (responseBytes !== undefined) { 119 | return decodeReturnValue(func.retTypes, responseBytes) 120 | } else if (func.retTypes.length === 0) { 121 | return undefined 122 | } else { 123 | throw new Error(`Call was returned undefined, but type [${func.retTypes.join(',')}].`) 124 | } 125 | } 126 | } 127 | 128 | const handler = async (...args: unknown[]): Promise => 129 | await caller(...args) 130 | return handler as ActorMethod 131 | } 132 | 133 | function decodeReturnValue (types: IDL.Type[], responseBytes: ArrayBuffer): any { 134 | const returnValues = IDL.decode(types, responseBytes) 135 | switch (returnValues.length) { 136 | case 0: 137 | return undefined 138 | case 1: 139 | return returnValues[0] 140 | default: 141 | return returnValues 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/mock_agent.ts: -------------------------------------------------------------------------------- 1 | import { type JsonObject, IDL } from '@dfinity/candid' 2 | import { Principal } from '@dfinity/principal' 3 | 4 | import { 5 | type Identity, type QueryFields, type QueryResponse, 6 | type QueryResponseReplied, 7 | QueryResponseStatus, type Agent, type CallOptions, 8 | type SubmitResponse, type ReadStateOptions, type ReadStateResponse, 9 | type RequestId, 10 | Cbor, 11 | } from '@dfinity/agent' 12 | import { type ActorSubclass, MockActor } from './mock_actor' 13 | import { type ReplicaContext } from './replica_context' 14 | import { CallSource, CallStatus, CallType, Message } from './call_context' 15 | import { Canister } from './canister' 16 | import { DER_PREFIX } from './bls' 17 | 18 | 19 | export class MockAgent implements Agent { 20 | readonly rootKey: ArrayBuffer | null 21 | 22 | readonly replica: ReplicaContext 23 | readonly caller: Principal 24 | 25 | constructor (replica: ReplicaContext, identity: Principal) { 26 | this.replica = replica 27 | this.caller = identity 28 | 29 | const rootKey = new Uint8Array(133) 30 | rootKey.set(new Uint8Array(DER_PREFIX)) 31 | this.rootKey = rootKey 32 | } 33 | 34 | async getPrincipal (): Promise { 35 | return this.caller 36 | } 37 | 38 | getActor (canister: Canister | string, idl: any | undefined = undefined): ActorSubclass { 39 | if (canister['get_id'] !== undefined) { 40 | return MockActor.createActor(this, (canister as Canister).get_id(), idl) 41 | } else { 42 | return MockActor.createActor(this, Principal.from(canister), idl) 43 | } 44 | } 45 | 46 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 47 | async createReadStateRequest? (options: ReadStateOptions, identity?: Identity): Promise { 48 | return '' 49 | } 50 | 51 | async readState (effectiveCanisterId: Principal | string, options: ReadStateOptions, identity?: Identity, request?: any): Promise { 52 | const name = new TextDecoder().decode(options.paths[0][0]) 53 | const msg = this.replica.get_message(options.paths[0][1] as any as string) 54 | if (msg === undefined) throw new Error('Message was not found!') 55 | 56 | if (name === 'request_status') { 57 | const cert = Cbor.encode({ 58 | tree: [2, options.paths[0][0], [2, new TextEncoder().encode(msg.id), [1, [2, new TextEncoder().encode('reply'), [3, msg.result]], [2, new TextEncoder().encode('status'), [3, new TextEncoder().encode('replied')]]]]], 59 | signature: {}, 60 | delegation: false 61 | }) 62 | 63 | const resp: ReadStateResponse = { 64 | certificate: cert 65 | } 66 | 67 | return resp 68 | } 69 | 70 | throw new Error('Not implemented path: ' + name) 71 | } 72 | 73 | async call (canisterId: Principal | string, fields: CallOptions): Promise { 74 | let target: Principal 75 | 76 | if (canisterId instanceof Principal) { 77 | target = canisterId 78 | } else { 79 | target = Principal.from(canisterId) 80 | } 81 | 82 | const msg = new Message({ 83 | type: CallType.Update, 84 | source: CallSource.Ingress, 85 | 86 | target: Principal.fromText(target.toString()), 87 | sender: Principal.fromText(this.caller.toString()), 88 | 89 | method: fields.methodName, 90 | args_raw: fields.arg 91 | }) 92 | 93 | // Store and process message 94 | this.replica.store_message(msg) 95 | await this.replica.process_messages() 96 | 97 | const requestId = msg.id as any as RequestId 98 | 99 | return { 100 | requestId, 101 | response: { 102 | ok: true, 103 | status: 0, 104 | statusText: '' 105 | } 106 | } 107 | } 108 | 109 | async waitForResponse (requestId: RequestId): Promise { 110 | const msg = this.replica.get_message(requestId as any as string) 111 | 112 | if (msg === undefined) throw new Error('Message was not found!') 113 | // await this.replica.process_pending_messages() 114 | 115 | if (msg.status === CallStatus.Ok && msg.result !== undefined) { 116 | return msg.result 117 | } else if (msg.status === CallStatus.Error) { 118 | const rejectionMessage = new TextDecoder().decode(msg.rejectionMessage as Uint8Array) 119 | 120 | throw new Error('Error while processing message, code:' + msg.rejectionCode+ ', text: '+rejectionMessage); 121 | } 122 | 123 | throw new Error('Message was not fully processed!') 124 | } 125 | 126 | async status (): Promise { 127 | const res: JsonObject = { res: {} } 128 | 129 | return res 130 | } 131 | 132 | async query (canisterId: Principal | string, options: QueryFields): Promise { 133 | let target: Principal 134 | if (canisterId instanceof Principal) { 135 | target = canisterId 136 | } else { 137 | target = Principal.from(canisterId) 138 | } 139 | 140 | const msg = new Message({ 141 | type: CallType.Query, 142 | source: CallSource.Ingress, 143 | 144 | target: Principal.fromText(target.toString()), 145 | sender: Principal.fromText(this.caller.toString()), 146 | 147 | method: options.methodName, 148 | args_raw: options.arg 149 | }) 150 | 151 | // Store and process message 152 | this.replica.store_message(msg) 153 | await this.replica.process_messages() 154 | 155 | const response: QueryResponseReplied = { 156 | status: QueryResponseStatus.Replied, 157 | reply: { 158 | arg: msg.result as ArrayBuffer 159 | } 160 | } 161 | 162 | return response 163 | } 164 | 165 | async fetchRootKey (): Promise { 166 | return this.rootKey ?? new Uint8Array(133) 167 | } 168 | 169 | // eslint-disable-next-line @typescript-eslint/no-empty-function 170 | invalidateIdentity? (): void { 171 | } 172 | 173 | // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars 174 | replaceIdentity? (identity: Identity): void { 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/replica_context.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { WasmCanister } from './wasm_canister' 3 | 4 | import debug from 'debug' 5 | import { CallSource, CallStatus, CallType, Message, RejectionCode } from './call_context' 6 | import { u64IntoCanisterId } from './utils' 7 | import { ManagementCanister } from './management_canister' 8 | import { Canister, WasmModule } from './canister' 9 | const log = debug('lightic:replica') 10 | 11 | export interface InstallCanisterArgs { 12 | initArgs?: ArrayBuffer, 13 | candid?: string, 14 | id?: string, 15 | caller?: Principal 16 | } 17 | 18 | export class ReplicaContext { 19 | private last_id: bigint 20 | private canisters: Record 21 | // private msg_log: Message[] 22 | 23 | private messages: Record 24 | 25 | //Maximum number of canister before force dealocation 26 | private maxCanisterNumber: number 27 | //How many canisters at once should be freed 28 | private freeCanisters: number 29 | 30 | constructor() { 31 | this.canisters = {} 32 | this.last_id = 0n 33 | this.messages = {} 34 | 35 | this.canisters['aaaaa-aa'] = new ManagementCanister(this) 36 | 37 | this.maxCanisterNumber = 80 38 | this.freeCanisters = 30 39 | 40 | // this.msg_log = [] 41 | } 42 | 43 | // Processes messages 44 | // Todo: add correct replica errors 45 | private async process_message(msg: Message): Promise { 46 | const canister = this.canisters[msg.target.toString()] 47 | 48 | if (canister !== undefined) { 49 | await canister.process_message(msg) 50 | 51 | 52 | } else { 53 | msg.status = CallStatus.Error 54 | msg.rejectionCode = RejectionCode.DestinationInvalid 55 | msg.rejectionMessage = new TextEncoder().encode("Canister not found: " + msg.target.toString()) 56 | } 57 | 58 | return msg.result 59 | } 60 | 61 | // Store message for processing 62 | store_message(msg: Message): void { 63 | // const canister = this.canisters[msg.target.toString()] 64 | 65 | // if (canister === undefined) { 66 | // throw new Error('Canister not found! ' + msg.target.toString()) 67 | // } 68 | 69 | if (msg.id === undefined) { 70 | msg.id = (Object.keys(this.messages).length + 1).toString() 71 | } 72 | 73 | this.messages[msg.id] = msg 74 | } 75 | 76 | // Process all waiting messages 77 | async process_messages(): Promise { 78 | while (Object.values(this.messages).some((x) => x.status === CallStatus.New)) { 79 | // Take first message from list for processing 80 | const msg = Object.values(this.messages).filter((x) => x.status === CallStatus.New)[0] 81 | await this.process_message(msg) 82 | 83 | if (msg.source === CallSource.InterCanister) { 84 | if (msg.status === CallStatus.Ok) { 85 | const reply: Message = new Message({ 86 | source: CallSource.Internal, 87 | type: CallType.ReplyCallback, 88 | 89 | target: Principal.fromText(msg.sender.toString()), 90 | sender: Principal.fromText(msg.target.toString()), 91 | 92 | result: msg.result, 93 | args_raw: msg.result, 94 | replyFun: msg.replyFun, 95 | replyEnv: msg.replyEnv, 96 | rejectFun: msg.rejectFun, 97 | rejectEnv: msg.rejectEnv, 98 | replyContext: msg.replyContext 99 | }) 100 | this.store_message(reply) 101 | } 102 | if (msg.status === CallStatus.Error) { 103 | const reply: Message = new Message({ 104 | source: CallSource.InterCanister, 105 | type: CallType.RejectCallback, 106 | rejectionCode: msg.rejectionCode, 107 | 108 | target: Principal.fromText(msg.sender.toString()), 109 | sender: Principal.fromText(msg.target.toString()), 110 | 111 | result: msg.result, 112 | args_raw: msg.result, 113 | replyFun: msg.replyFun, 114 | replyEnv: msg.replyEnv, 115 | rejectFun: msg.rejectFun, 116 | rejectEnv: msg.rejectEnv, 117 | replyContext: msg.replyContext 118 | }) 119 | this.store_message(reply) 120 | } 121 | } 122 | } 123 | } 124 | 125 | get_message(id: string): Message | undefined { 126 | return this.messages[id] 127 | } 128 | 129 | get_canisters(): Canister[] { 130 | return Object.values(this.canisters) 131 | } 132 | 133 | get_module_hash(canisterId: Principal): Buffer | undefined { 134 | const canister = this.get_canister(canisterId); 135 | if (canister === undefined) { 136 | return undefined; 137 | } 138 | 139 | return canister.get_module_hash(); 140 | } 141 | 142 | get_canister_id(): Principal { 143 | const id = u64IntoCanisterId(this.last_id) 144 | this.last_id += 1n 145 | return id 146 | } 147 | 148 | create_canister(params?: InstallCanisterArgs): Canister { 149 | let idPrin: Principal | undefined 150 | 151 | if (params === undefined || params.id === undefined) { 152 | idPrin = this.get_canister_id() 153 | } else { 154 | if (this.canisters[params.id] !== undefined) { 155 | throw new Error('Canister with id ' + params.id + ' is already installed') 156 | } 157 | idPrin = Principal.from(params.id) 158 | } 159 | 160 | if (idPrin === undefined) { 161 | throw new Error('Could not establish id for canister') 162 | } 163 | 164 | this.free_memory() 165 | 166 | const canister = new WasmCanister(this, idPrin) 167 | 168 | this.canisters[idPrin.toString()] = canister 169 | log('Created canister with id: %s', idPrin.toString()) 170 | 171 | return canister 172 | } 173 | 174 | // Installs code as a canister in replica, assigns ID in similar fashion as replica 175 | async install_canister( 176 | code: WasmModule, 177 | params: InstallCanisterArgs 178 | ): Promise { 179 | let idPrin: Principal | undefined 180 | 181 | if (params.id === undefined) { 182 | idPrin = this.get_canister_id() 183 | } else { 184 | if (this.canisters[params.id] !== undefined) { 185 | throw new Error('Canister with id ' + params.id + ' is already installed') 186 | } 187 | idPrin = Principal.from(params.id) 188 | } 189 | 190 | if (idPrin === undefined) { 191 | throw new Error('Could not establish id for canister') 192 | } 193 | 194 | this.free_memory() 195 | 196 | const canister = new WasmCanister(this, idPrin) 197 | await canister.install_module_candid(code, params.initArgs, params.caller ?? Principal.anonymous(), params.candid) 198 | 199 | this.canisters[idPrin.toString()] = canister 200 | 201 | log('Installed canister with id: %s', idPrin.toString()) 202 | 203 | return canister 204 | } 205 | 206 | 207 | //If the there are more canisters than configured threshold, the oldest ones will be deleted 208 | free_memory() { 209 | const canisters = Object.values(this.canisters).filter(x => x.created > 0).sort((x, y) => x.created < y.created ? -1 : 1) 210 | if (canisters.length > this.maxCanisterNumber) { 211 | 212 | console.log("Trying to free up memory") 213 | 214 | for (let i = 0; i < this.freeCanisters; i++) { 215 | const toDelete = canisters[i]; 216 | 217 | //Remove canister from list, it should free memory 218 | delete this.canisters[toDelete.get_id().toString()] 219 | } 220 | } 221 | } 222 | 223 | // Returns canister with given principal 224 | get_canister(id: Principal): Canister { 225 | return this.canisters[id.toString()] 226 | } 227 | 228 | // Removes all canisters from replica 229 | clean(): void { 230 | this.canisters = {} 231 | this.last_id = 0n 232 | this.messages = {} 233 | this.canisters['aaaaa-aa'] = new ManagementCanister(this) 234 | 235 | // this.msg_log = [] 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import express from 'express' 4 | import fs from 'fs' 5 | 6 | import cbor, { Tagged } from 'cbor' 7 | import { ReplicaContext } from './replica_context'; 8 | import { CallSource, CallStatus, CallType, Message } from './call_context'; 9 | import { Principal } from '@dfinity/principal'; 10 | import { ReadStateResponse, requestIdOf, toHex, concat, HashTree, reconstruct } from '@dfinity/agent'; 11 | 12 | import { Tree } from './hash_tree'; 13 | 14 | import { Bls } from './bls'; 15 | 16 | import {Command} from 'commander' 17 | import path from 'path'; 18 | 19 | const program = new Command(); 20 | program.option('--p, --port ','Specifies port on which http server will be started'); 21 | program.option('--c, --clean','Cleans up the DFX state before starting the server'); 22 | program.parse(process.argv); 23 | 24 | const options = program.opts(); 25 | 26 | const bls = new Bls() 27 | 28 | const context = new ReplicaContext(); 29 | 30 | const app = express(); 31 | const port = options.port ?? 8001; 32 | 33 | interface CallContent { 34 | request_type: string, 35 | sender: Buffer 36 | nonce: Buffer, 37 | ingress_expiry: bigint, 38 | canister_id: Buffer, 39 | method_name: string, 40 | arg: Buffer, 41 | } 42 | 43 | interface ReadStateContent { 44 | request_type: string, 45 | ingress_expiry: bigint, 46 | paths: any[] 47 | sender: Buffer 48 | } 49 | 50 | interface QueryContent { 51 | request_type: string, 52 | arg: Buffer, 53 | canister_id: Buffer, 54 | ingress_expiry: bigint, 55 | method_name: string, 56 | sender: Buffer 57 | } 58 | 59 | interface CallRequest { 60 | content: T 61 | sender_pubkey: Buffer, 62 | sender_sig: Buffer 63 | } 64 | 65 | app.use(express.raw()) 66 | app.use(express.json()) 67 | 68 | app.all('/api/v2/*', function (req, res, next) { 69 | console.log(`[server]: URL Request: ${req.method} ${req.url}`) 70 | // console.log('General Validations'); 71 | next(); 72 | }); 73 | 74 | app.post('/api/v2/canister/:canisterId/query', (req, res) => { 75 | const body: any[] = []; 76 | req.on('data', (chunk: any) => { 77 | body.push(chunk) 78 | }).on('end', async () => { 79 | 80 | const tag = cbor.decode(body[0] as Buffer) 81 | const data = tag.value as CallRequest 82 | const content = data.content 83 | 84 | if (content.sender === undefined) { 85 | res.status(400) 86 | res.send('Missing sender') 87 | 88 | return 89 | } 90 | 91 | if (content.arg === undefined) { 92 | res.status(400) 93 | res.send('Missing arg') 94 | 95 | return 96 | } 97 | 98 | if (content.method_name === undefined) { 99 | res.status(400) 100 | res.send('Missing arg') 101 | 102 | return 103 | } 104 | 105 | 106 | if (content.canister_id === undefined) { 107 | res.status(400) 108 | res.send('Missing canister_id') 109 | 110 | return 111 | } 112 | 113 | if (data.content.request_type !== 'query') { 114 | res.status(400) 115 | res.send('Invalid request type') 116 | 117 | return 118 | } 119 | 120 | 121 | 122 | const sender = Principal.fromUint8Array(content.sender) 123 | const canister_id = Principal.fromUint8Array(content.canister_id) 124 | const reqId = toHex(requestIdOf(data.content)) 125 | 126 | const msg = new Message({ 127 | id: reqId, 128 | type: CallType.Query, 129 | source: CallSource.Ingress, 130 | args_raw: content.arg, 131 | method: content.method_name, 132 | 133 | target: Principal.fromText(canister_id.toString()), 134 | sender: Principal.fromText(sender.toString()), 135 | }) 136 | context.store_message(msg); 137 | try { 138 | await context.process_messages() 139 | 140 | if (msg.status !== CallStatus.Ok) { 141 | throw new Error() 142 | } 143 | 144 | if (msg.result !== undefined && msg.result !== null) { 145 | const resp = { 146 | status: 'replied', 147 | reply: { 148 | arg: Buffer.from(msg.result) 149 | } 150 | } 151 | const tagged = new Tagged(55799, resp); 152 | const cborResp = cbor.encode(tagged) 153 | 154 | res.status(202) 155 | res.send(cborResp) 156 | } else { 157 | res.status(202) 158 | res.send() 159 | } 160 | } catch { 161 | const tagged = new Tagged(55799, { 162 | status: 'rejected', 163 | reject_code: msg.rejectionCode, 164 | }); 165 | if (msg.rejectionMessage !== null && msg.rejectionMessage !== undefined) { 166 | tagged.value.reject_message = new TextDecoder().decode(Buffer.from(msg.rejectionMessage)) 167 | } 168 | 169 | const cborResp = cbor.encode(tagged) 170 | 171 | // console.log("Error: " + e) 172 | res.send(cborResp) 173 | } 174 | 175 | }) 176 | }); 177 | 178 | app.post('/api/v2/canister/:canisterId/call', (req, res) => { 179 | const canisterId = req.params.canisterId 180 | const canister = context.get_canister(Principal.from(canisterId)) 181 | 182 | 183 | const body: Buffer[] = []; 184 | req.on('data', (chunk: any) => { 185 | body.push(chunk) 186 | }).on('end', async () => { 187 | // fs.writeFileSync('request', body[0]) 188 | 189 | const bodySize = body.map(x => x.byteLength).reduce((x, y) => x + y) 190 | const rawData = new Uint8Array(bodySize) 191 | 192 | let pos = 0 193 | for (const item of body) { 194 | rawData.set(item, pos) 195 | pos += item.byteLength 196 | } 197 | 198 | const tag = cbor.decode(rawData) 199 | const data = tag.value as CallRequest 200 | const content = data.content 201 | 202 | if (content.sender === undefined) { 203 | res.status(400) 204 | res.send('Missing sender') 205 | 206 | return 207 | } 208 | 209 | if (content.method_name === undefined) { 210 | res.status(400) 211 | res.send('Missing method name') 212 | 213 | return 214 | } 215 | 216 | if (content.canister_id === undefined) { 217 | res.status(400) 218 | res.send('Missing canister_id') 219 | 220 | return 221 | } 222 | 223 | if (data.content.request_type !== 'call') { 224 | res.status(400) 225 | res.send('Invalid request type') 226 | 227 | return 228 | } 229 | 230 | if (tag.tag !== 55799) { 231 | res.send({}); 232 | } else { 233 | const content = data.content 234 | const sender_pubkey = data.sender_pubkey 235 | const sender_sig = data.sender_sig 236 | 237 | const sender = Principal.fromUint8Array(content.sender) 238 | const canister_id = Principal.fromUint8Array(content.canister_id) 239 | 240 | if (canister === undefined && !(canister_id.toString() === 'aaaaa-aa' && content.method_name === 'provisional_create_canister_with_cycles')) { 241 | res.status(404) 242 | res.send('Canister not found: ' + canisterId) 243 | } else { 244 | const reqId = toHex(requestIdOf(data.content)) 245 | 246 | if (content.request_type === 'call') { 247 | const msg = new Message({ 248 | id: reqId, 249 | type: CallType.Update, 250 | sender: Principal.fromText(sender.toString()), 251 | source: CallSource.Ingress, 252 | args_raw: content.arg, 253 | method: content.method_name, 254 | target: Principal.fromText(canister_id.toString()) 255 | }) 256 | 257 | context.store_message(msg); 258 | try { 259 | await context.process_messages(); 260 | 261 | res.status(202) 262 | } catch (e) { 263 | console.log("Error: " + e) 264 | // res.send({}) 265 | } 266 | res.send() 267 | } else { 268 | res.send({}) 269 | } 270 | } 271 | } 272 | 273 | 274 | }) 275 | 276 | }); 277 | 278 | export async function getReadResponse(bls: Bls, hashTree: HashTree): Promise { 279 | const rootHash = await reconstruct(hashTree) 280 | 281 | const msg2 = concat(bls.domain_sep('ic-state-root'), rootHash); 282 | const sig = await bls.sign(msg2) 283 | 284 | const cert = { 285 | tree: hashTree, 286 | signature: Buffer.from(sig) 287 | } 288 | const certTagged = new Tagged(55799, cert); 289 | const certEncoded = cbor.encode(certTagged); 290 | 291 | const resp: ReadStateResponse = { 292 | certificate: certEncoded 293 | } 294 | 295 | const tagged = new Tagged(55799, resp); 296 | const serializedAsBuffer = cbor.encode(tagged); 297 | return serializedAsBuffer 298 | } 299 | 300 | app.post('/api/v2/canister/:canisterId/read_state', (req, res) => { 301 | const body: any[] = []; 302 | req.on('data', (chunk: any) => { 303 | body.push(chunk) 304 | }).on('end', async () => { 305 | 306 | const tag = cbor.decode(body[0] as Buffer) 307 | const data = tag.value as CallRequest 308 | 309 | if (data.content.request_type !== 'read_state') { 310 | res.status(400) 311 | res.send('Invalid request type') 312 | 313 | return 314 | } 315 | 316 | const paths = data.content.paths 317 | 318 | if (data.content.sender === undefined) { 319 | res.status(400) 320 | res.send('Missing sender') 321 | 322 | return 323 | } 324 | 325 | if (paths === undefined || paths === null) { 326 | res.status(400) 327 | res.send('Missing paths') 328 | 329 | return 330 | } 331 | 332 | const tree = new Tree(); 333 | 334 | for (const path of paths) { 335 | const start = new TextDecoder().decode(path[0]) 336 | 337 | if (start === 'request_status') { 338 | const msgId = path[1] 339 | const msgIdStr = toHex(msgId) 340 | 341 | const msg = context.get_message(msgIdStr) 342 | if (msg === undefined) { 343 | res.status(404) 344 | res.send("Message not found") 345 | 346 | return 347 | } 348 | 349 | if (msg.status === CallStatus.Ok) { 350 | tree.insertValue(['request_status', msgId, 'reply'], Buffer.from(msg.result ?? new Uint8Array(0))) 351 | tree.insertValue(['request_status', msgId, 'status'], 'replied') 352 | } else if (msg.status === CallStatus.Error) { 353 | tree.insertValue(['request_status', msgId, 'reject_code'], msg.rejectionCode) 354 | if (msg.rejectionMessage !== null && msg.rejectionMessage !== undefined) { 355 | tree.insertValue(['request_status', msgId, 'reject_message'], Buffer.from(msg.rejectionMessage)) 356 | } 357 | tree.insertValue(['request_status', msgId, 'status'], 'rejected') 358 | } else { 359 | console.log("Unsupported request for status for message with status: " + msg.status) 360 | } 361 | } 362 | 363 | else if (start === 'canister') { 364 | const canisterId = Principal.fromUint8Array(path[1]) 365 | const end = new TextDecoder().decode(path[2]) 366 | 367 | if (end === 'module_hash') { 368 | const moduleHash = context.get_module_hash(canisterId) 369 | 370 | if (moduleHash !== undefined) { 371 | tree.insertValue(path, moduleHash) 372 | } 373 | } 374 | } 375 | } 376 | 377 | // Every tree will have time entry 378 | tree.insertValue(['time'], process.hrtime.bigint()) 379 | 380 | const treeHash = tree.getHashTree() 381 | const resp = await getReadResponse(bls, treeHash) 382 | res.send(resp) 383 | }) 384 | }); 385 | 386 | app.get('/api/v2/status', (req, res) => { 387 | // let root_key = new Uint8Array(133) 388 | const status = { 389 | impl_source: 'https://github.com/icopen/lightic', 390 | ic_api_version: '0.18.0', 391 | root_key: bls.derPublicKey, 392 | impl_version: '0.1.0', 393 | } 394 | 395 | const tagged = new Tagged(55799, status); 396 | 397 | const serializedAsBuffer = cbor.encode(tagged); 398 | 399 | res.send(serializedAsBuffer); 400 | }); 401 | 402 | 403 | 404 | async function run() { 405 | await bls.init() 406 | 407 | if (options.clean) { 408 | //remove file if exists ~/.local/share/dfx/network/local/wallets.json 409 | const walletPath = path.join('/home/adam', '.local/share/dfx/network/local/wallets.json') 410 | if (fs.existsSync(walletPath)) { 411 | fs.unlinkSync(walletPath) 412 | } 413 | 414 | //delete all file in folder .dfx/local 415 | const localPath = path.join('~/', '.dfx/local') 416 | if (fs.existsSync(localPath)) { 417 | fs.unlinkSync(localPath) 418 | } 419 | } 420 | 421 | app.listen(port, () => { 422 | console.log(`[server]: Server is running at http://localhost:${port}`); 423 | }); 424 | } 425 | 426 | run() -------------------------------------------------------------------------------- /src/test_context.ts: -------------------------------------------------------------------------------- 1 | import { type Principal } from '@dfinity/principal' 2 | import { MockAgent } from './mock_agent' 3 | import { loadWasmFromFile } from './instrumentation' 4 | import { ReplicaContext } from './replica_context' 5 | import { Canister } from './canister' 6 | import fs from 'fs' 7 | 8 | export interface DeployOptions { 9 | initArgs?: any 10 | candid?: string 11 | id?: string 12 | caller?: Principal 13 | } 14 | 15 | export function getGlobalTestContext(): TestContext { 16 | if (global.testContext === undefined) { 17 | global.testContext = new TestContext(); 18 | } 19 | 20 | return global.testContext as TestContext 21 | } 22 | 23 | export class TestContext { 24 | replica: ReplicaContext 25 | 26 | constructor () { 27 | this.replica = new ReplicaContext() 28 | } 29 | 30 | clean (): void { 31 | this.replica.clean() 32 | } 33 | 34 | getAgent (identity: Principal): MockAgent { 35 | const agent = new MockAgent(this.replica, identity) 36 | 37 | return agent 38 | } 39 | 40 | async deploy (filename: string, opts?: DeployOptions): Promise { 41 | if (opts?.candid !== undefined) { 42 | if (fs.existsSync(opts.candid)) { 43 | opts.candid = fs.readFileSync(opts.candid).toString() 44 | } 45 | } 46 | 47 | const module = await loadWasmFromFile(filename) 48 | 49 | const result = await this.replica.install_canister(module, { 50 | initArgs: opts?.initArgs, 51 | candid: opts?.candid, 52 | id: opts?.id, 53 | caller: opts?.caller 54 | }) 55 | 56 | return result 57 | } 58 | 59 | // async deployWithId (filename: string, id: Principal, initArgs: any = null): Promise { 60 | // throw new Error('Not implemented') 61 | // } 62 | } 63 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "ES2020", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "../lib/esm", 9 | "strictNullChecks": true, 10 | "declaration": true 11 | }, 12 | "lib": ["es2015"], 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AccountIdentifier, SubAccount } from '@dfinity/nns' 2 | import { Principal } from '@dfinity/principal' 3 | 4 | export function canisterIdIntoU64 (canisterId: Principal): bigint { 5 | const bytes = canisterId.toUint8Array().slice(0, 8) 6 | 7 | let num = 0n 8 | 9 | for (const byte of bytes) { 10 | num = num << 4n 11 | num += BigInt(byte) 12 | } 13 | 14 | return num 15 | } 16 | 17 | export function u64IntoPrincipalId (num: bigint): Principal { 18 | const bytes = new Uint8Array(29) 19 | 20 | bytes[0] = Number(num) 21 | bytes[28] = 2 22 | 23 | return Principal.fromUint8Array(bytes) 24 | } 25 | 26 | export function u64IntoCanisterId (num: bigint): Principal { 27 | const bytes = new Uint8Array(10) 28 | 29 | // todo: conversion of number to byte array 30 | 31 | bytes[7] = Number(num) 32 | bytes[8] = 1 33 | bytes[9] = 1 34 | 35 | return Principal.fromUint8Array(bytes) 36 | } 37 | 38 | // Convert a hex string to a byte array 39 | export function hexToBytes (hex: string): number[] { 40 | const bytes: number[] = [] 41 | for (let c = 0; c < hex.length; c += 2) { 42 | bytes.push(parseInt(hex.substr(c, 2), 16)) 43 | } 44 | return bytes 45 | } 46 | 47 | export function getAccount (principal: Principal, no: number): AccountIdentifier { 48 | const subAccount = SubAccount.fromID(no) 49 | const accountIdentifier = AccountIdentifier.fromPrincipal({ 50 | principal, 51 | subAccount 52 | }) 53 | 54 | return accountIdentifier 55 | } 56 | -------------------------------------------------------------------------------- /src/wasm_canister.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid' 2 | import { Principal } from '@dfinity/principal' 3 | import { buildIdl, type IdlResult } from './idl_builder' 4 | import { CallSource, CallStatus, CallType, Message, RejectionCode } from './call_context' 5 | import { type ReplicaContext } from './replica_context' 6 | import { parse_candid } from './wasm_tools/pkg/wasm_tools' 7 | import { Canister, WasmModule } from './canister' 8 | import debug from 'debug' 9 | import { Ic0 } from './ic0' 10 | import { hexToBytes } from './utils' 11 | 12 | const log = debug('lightic:canister') 13 | 14 | export class CanisterState { 15 | replica: ReplicaContext 16 | canister: Canister 17 | 18 | memory: WebAssembly.Memory 19 | memoryCopy: ArrayBuffer 20 | cycles: bigint 21 | 22 | message?: Message 23 | 24 | args_buffer?: ArrayBuffer 25 | 26 | reply_buffer: Uint8Array 27 | reply_size: number 28 | 29 | newMessage?: Message 30 | newMessageArgs: Uint8Array 31 | newMessageReplySize: number 32 | certified_data: Uint8Array 33 | 34 | stableMemory: WebAssembly.Memory 35 | 36 | constructor(item: Partial) { 37 | this.certified_data = new Uint8Array(32) 38 | 39 | this.reply_buffer = new Uint8Array(102400) 40 | this.reply_size = 0 41 | 42 | this.newMessageArgs = new Uint8Array(102400) 43 | this.newMessageReplySize = 0 44 | 45 | this.stableMemory = new WebAssembly.Memory({ 46 | initial: 0 47 | }) 48 | 49 | Object.assign(this, item) 50 | } 51 | } 52 | 53 | 54 | /// Implementation of Imports used by canisters according to The Internet Computer Interface Specification 55 | export class WasmCanister implements Canister { 56 | private readonly id: Principal 57 | 58 | readonly created: bigint 59 | 60 | private module?: WasmModule 61 | 62 | private instance: WebAssembly.Instance 63 | 64 | private candid: any 65 | private idl: IdlResult 66 | 67 | private methods: Record 68 | 69 | readonly state: CanisterState 70 | readonly ic0: Ic0 71 | 72 | constructor(replica: ReplicaContext, id: Principal) { 73 | this.id = id 74 | this.created = process.hrtime.bigint() 75 | 76 | this.ic0 = new Ic0() 77 | this.state = new CanisterState({ 78 | canister: this, 79 | replica: replica, 80 | cycles: 1_000_000_000_000n 81 | }) 82 | this.methods = {} 83 | } 84 | 85 | get_module_hash(): Buffer | undefined { 86 | return this.module !== undefined ? Buffer.from(hexToBytes(this.module.hash)) : undefined 87 | } 88 | 89 | async install_module(code: WasmModule) { 90 | this.module = code 91 | 92 | const imports = WebAssembly.Module.imports(code.module) 93 | const importObject = this.ic0.getImports(this.state, imports.map(x => x.name)) 94 | 95 | if (this.module === undefined) return 96 | 97 | this.instance = await WebAssembly.instantiate(this.module.module, importObject) 98 | this.state.memory = this.instance.exports.memory as WebAssembly.Memory ?? this.instance.exports.mem as WebAssembly.Memory 99 | 100 | for (const obj of Object.keys(this.instance.exports)) { 101 | if (obj.startsWith('canister_')) { 102 | const [type, name] = obj.split(' ') 103 | if (name === undefined) continue; 104 | this.methods[name] = { 105 | type: type, 106 | name: name, 107 | func: this.instance.exports[obj] 108 | } 109 | } 110 | } 111 | } 112 | 113 | async initialize(initArgs: ArrayBuffer, sender: Principal) { 114 | //todo: add warning if canister_init is exported, and there are no initArgs 115 | if (initArgs !== undefined && initArgs !== null && initArgs.byteLength > 0 && this.instance.exports['canister_init'] !== undefined) { 116 | // Initialize canister 117 | const msg = Message.init(this.id, sender, initArgs) 118 | await this.process_message(msg) 119 | } 120 | } 121 | 122 | async install_module_candid(module: WasmModule, initArgs: any, sender: Principal, candidSpec?: string) { 123 | // this.module = module 124 | 125 | // const imports = WebAssembly.Module.imports(module.module) 126 | // const importObject = this.ic0.getImports(this.state, imports.map(x => x.name)) 127 | 128 | // if (this.module === undefined) return 129 | 130 | // this.instance = await WebAssembly.instantiate(this.module.module, importObject) 131 | // this.state.memory = this.instance.exports.memory as WebAssembly.Memory ?? this.instance.exports.mem as WebAssembly.Memory 132 | 133 | await this.install_module(module); 134 | 135 | if (candidSpec === undefined) { 136 | candidSpec = await this.get_candid() 137 | } 138 | 139 | const jsonCandid = parse_candid(candidSpec) 140 | const candid = JSON.parse(jsonCandid) 141 | this.candid = candid 142 | this.idl = buildIdl(IDL, candid) 143 | 144 | if (this.idl.init_args !== undefined && this.idl.init_args !== null && this.idl.init_args.length > 0) { 145 | initArgs = IDL.encode(this.idl.init_args, initArgs) 146 | } 147 | 148 | this.initialize(initArgs, sender) 149 | // if (this.idl.init_args !== undefined && this.idl.init_args !== null && this.idl.init_args.length > 0 && this.instance.exports['canister_init'] !== undefined) { 150 | // const args = IDL.encode(this.idl.init_args, initArgs) 151 | 152 | // // Initialize canister 153 | // const msg = Message.init(this.id, sender, args) 154 | // await this.process_message(msg) 155 | // } 156 | } 157 | 158 | get_id(): Principal { 159 | return this.id 160 | } 161 | 162 | get_instance(): WebAssembly.Instance { 163 | return this.instance 164 | } 165 | 166 | getIdlBuilder(): IDL.InterfaceFactory { 167 | return (IDL) => buildIdl(IDL.IDL, this.candid).idl 168 | } 169 | 170 | public async process_message(msg: Message): Promise { 171 | msg.status = CallStatus.Processing 172 | 173 | if (msg.type === CallType.ReplyCallback) { 174 | const table = this.instance.exports.table as WebAssembly.Table 175 | 176 | const replyEnv = msg.replyEnv 177 | const fun = table.get(msg.replyFun) 178 | this.state.message = msg.replyContext 179 | 180 | this.state.args_buffer = msg.result 181 | this.state.reply_size = 0 182 | try { 183 | fun(replyEnv) 184 | msg.status = CallStatus.Ok 185 | } catch (e) { 186 | msg.status = CallStatus.Error 187 | msg.rejectionCode = RejectionCode.CanisterError 188 | msg.rejectionMessage = new TextEncoder().encode(e.message) 189 | 190 | msg.replyContext.status = CallStatus.Error 191 | msg.replyContext.rejectionCode = RejectionCode.CanisterError 192 | msg.replyContext.rejectionMessage = new TextEncoder().encode("ReplyCallback: "+e.message) 193 | 194 | log('Error on ReplyCallback of {}', e) 195 | } 196 | } else if (msg.type === CallType.RejectCallback) { 197 | const table = this.instance.exports.table as WebAssembly.Table 198 | 199 | const replyEnv = msg.rejectEnv 200 | const fun = table.get(msg.rejectFun) 201 | this.state.message = msg.replyContext 202 | 203 | this.state.args_buffer = msg.result 204 | this.state.reply_size = 0 205 | try { 206 | fun(replyEnv) 207 | msg.status = CallStatus.Ok 208 | } catch (e) { 209 | msg.status = CallStatus.Error 210 | msg.rejectionCode = RejectionCode.CanisterError 211 | msg.rejectionMessage = new TextEncoder().encode(e.message) 212 | 213 | msg.replyContext.status = CallStatus.Error 214 | msg.replyContext.rejectionCode = RejectionCode.CanisterError 215 | msg.replyContext.rejectionMessage = new TextEncoder().encode("RejectCallback: "+e.message) 216 | 217 | log('Error on RejectCallback of {}', e) 218 | } 219 | } else { 220 | const method = msg.getMethodName() 221 | let func = this.instance.exports[method] as any 222 | 223 | if (msg.source === CallSource.InterCanister) { 224 | if (func === undefined) { 225 | func = this.methods[method]?.func; 226 | } 227 | } 228 | 229 | // Check if function was found in wasm, this should be 4 Canister Reject 230 | if (func === undefined) { 231 | msg.status = CallStatus.Error 232 | msg.rejectionCode = RejectionCode.DestinationInvalid 233 | msg.rejectionMessage = new TextEncoder().encode('Function not found ' + method) 234 | 235 | throw new Error('Function not found ' + method) 236 | } 237 | 238 | log(this.id.toString() + ': Calling ' + msg.method) 239 | 240 | this.state.message = msg 241 | 242 | this.state.args_buffer = msg.args_raw 243 | this.state.reply_size = 0 244 | 245 | // Copy canister memory, for possible restore on trap 246 | this.state.memoryCopy = new ArrayBuffer(this.state.memory.buffer.byteLength) 247 | new Uint8Array(this.state.memoryCopy).set(new Uint8Array(this.state.memory.buffer)) 248 | 249 | try { 250 | func() 251 | 252 | if (msg.type === CallType.Init) { 253 | msg.status = CallStatus.Ok 254 | } 255 | 256 | } catch (e) { 257 | msg.status = CallStatus.Error 258 | msg.rejectionCode = RejectionCode.CanisterError 259 | msg.rejectionMessage = new TextEncoder().encode(e.message) 260 | 261 | log('Error on execution of {} {}', method, e) 262 | throw e 263 | } 264 | } 265 | 266 | // If call was an query, revert canister state 267 | if (msg.type === CallType.Query || msg.status === CallStatus.Error) { 268 | new Uint8Array(this.state.memory.buffer).set(new Uint8Array(this.state.memoryCopy)) 269 | } 270 | 271 | //This is a problem only if there are no related messages, otherwise this is fine 272 | if (msg.status === CallStatus.Processing && msg.relatedMessages.length === 0) { 273 | msg.status = CallStatus.Error 274 | msg.rejectionCode = RejectionCode.CanisterError 275 | msg.rejectionMessage = new TextEncoder().encode('Invalid processing, no response') 276 | } 277 | } 278 | 279 | public async get_candid(): Promise { 280 | let candidHackRaw: string | undefined 281 | 282 | try { 283 | const msg = Message.candidHack(this.id) 284 | await this.process_message(msg) 285 | 286 | if (msg.result !== null && msg.result !== undefined) { 287 | const decoded = IDL.decode([IDL.Text], msg.result) 288 | candidHackRaw = decoded[0] as string 289 | 290 | log('Found candid via hack') 291 | return candidHackRaw 292 | } 293 | } catch (e) { 294 | log('Error when trying to get candid via hack') 295 | } 296 | 297 | if (this.module === undefined) { 298 | throw new Error('Cannot get candid for canister with no module installed') 299 | } 300 | 301 | const candidRaw = WebAssembly.Module.customSections(this.module, 'icp:public candid:service') 302 | if (candidRaw.length === 1) { 303 | log('Found candid via custom section') 304 | const candid = new TextDecoder().decode(candidRaw[0]) 305 | 306 | return candid 307 | } 308 | 309 | throw new Error('Could not execute get candid hack') 310 | } 311 | 312 | public get_idl(): IDL.ConstructType { 313 | return this.idl.idl 314 | } 315 | 316 | public get_init_args(): IDL.ConstructType[] | undefined { 317 | return this.idl.init_args 318 | } 319 | 320 | } 321 | -------------------------------------------------------------------------------- /src/wasm_tools/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | pkg -------------------------------------------------------------------------------- /src/wasm_tools/.npmignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /src/wasm_tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_tools" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [features] 12 | default = ['std', 'allow_alt_compress'] 13 | allow_alt_compress = [] 14 | fallback_separator = [] 15 | std = [] 16 | 17 | [dependencies] 18 | candid = "0.8.4" 19 | getrandom = { version = "0.2.9", features=['js'] } 20 | miracl_core_bls12381 = { version="4.2.2", default-features=false, features= ['wasm-bindgen', 'allow_alt_compress'] } 21 | serde_json = "1.0.96" 22 | wasm-bindgen = "0.2.84" 23 | wasm-encoder = "0.20.0" 24 | wasmparser = "0.95.0" 25 | 26 | [dev-dependencies] 27 | hex-literal = "0.4.1" 28 | 29 | [profile.release] 30 | lto = true 31 | opt-level = 'z' 32 | -------------------------------------------------------------------------------- /src/wasm_tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lightic/candid_util", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "pkg/candid_util.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "wasm-pack build --target nodejs" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | }, 14 | "files": [ 15 | "pkg/candid_util_bg.wasm", 16 | "pkg/candid_util.js", 17 | "pkg/candid_util.d.ts" 18 | ], 19 | "types": "pkg/candid_util.d.ts" 20 | } 21 | -------------------------------------------------------------------------------- /src/wasm_tools/src/bls.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | use miracl_core_bls12381::*; 4 | 5 | #[wasm_bindgen] 6 | pub fn bls_get_key_pair() -> Result, String> { 7 | const BFS: usize = bls12381::bls::BFS; 8 | const BGS: usize = bls12381::bls::BGS; 9 | 10 | const G1S: usize = BFS; /* Group 1 Size - compressed */ 11 | const G2S: usize = 2 * BFS; /* Group 2 Size - compressed */ 12 | 13 | let mut r: [u8; BGS + G2S] = [0; BGS + G2S]; 14 | 15 | let (s, w) = r[..].split_at_mut(BGS); 16 | let mut ikm: [u8; 32] = [0; 32]; 17 | 18 | getrandom::getrandom(&mut ikm).map_err(|e| format!("{e}"))?; 19 | 20 | bls12381::bls::key_pair_generate(&ikm, s, w); 21 | 22 | Ok(r.to_vec()) 23 | } 24 | 25 | #[wasm_bindgen] 26 | pub fn bls_sign(m: &[u8], s: &[u8]) -> Vec { 27 | let mut sig: [u8; 48] = [0; 48]; 28 | 29 | bls12381::bls::core_sign(&mut sig, m, s); 30 | 31 | sig.to_vec() 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use std::error::Error; 37 | 38 | use super::*; 39 | use miracl_core_bls12381::bls12381; 40 | 41 | #[test] 42 | fn bls_verify() { 43 | use bls12381::bls::{core_verify, init, BLS_FAIL, BLS_OK}; 44 | use hex_literal::hex; 45 | let pk = hex!("a7623a93cdb56c4d23d99c14216afaab3dfd6d4f9eb3db23d038280b6d5cb2caaee2a19dd92c9df7001dede23bf036bc0f33982dfb41e8fa9b8e96b5dc3e83d55ca4dd146c7eb2e8b6859cb5a5db815db86810b8d12cee1588b5dbf34a4dc9a5"); 46 | let sig = hex!("b89e13a212c830586eaa9ad53946cd968718ebecc27eda849d9232673dcd4f440e8b5df39bf14a88048c15e16cbcaabe"); 47 | assert_eq!(init(), BLS_OK); 48 | assert_eq!(core_verify(&sig, b"hello".as_ref(), &pk), BLS_OK); 49 | assert_eq!(core_verify(&sig, b"hallo".as_ref(), &pk), BLS_FAIL); 50 | } 51 | 52 | #[test] 53 | fn generate_keys() -> Result<(), Box> { 54 | use bls12381::bls::{core_verify, init, BLS_OK}; 55 | assert_eq!(init(), BLS_OK); 56 | 57 | let keys = bls_get_key_pair()?; 58 | 59 | let private = &keys[..48]; 60 | let public = &keys[48..]; 61 | 62 | let sig = bls_sign(b"hello".as_ref(), private); 63 | 64 | assert_eq!(core_verify(&sig, b"hello".as_ref(), public), BLS_OK); 65 | 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/wasm_tools/src/target_json.rs: -------------------------------------------------------------------------------- 1 | use candid::{ 2 | parser::types::FuncMode, 3 | types::{Field, Type}, 4 | TypeEnv, 5 | }; 6 | 7 | // The definition of tuple is language specific. 8 | pub(crate) fn is_tuple(t: &Type) -> bool { 9 | match t { 10 | Type::Record(ref fs) => { 11 | if fs.is_empty() { 12 | return false; 13 | } 14 | for (i, field) in fs.iter().enumerate() { 15 | if field.id.get_id() != (i as u32) { 16 | return false; 17 | } 18 | } 19 | true 20 | } 21 | _ => false, 22 | } 23 | } 24 | 25 | pub fn print_modes(mo: &Vec) -> String { 26 | let mut str = String::new(); 27 | str.push('['); 28 | 29 | let mut first = true; 30 | for p in mo { 31 | if !first { 32 | str.push(','); 33 | } 34 | first = false; 35 | 36 | str.push_str(&format!("\"{p:?}\"")); 37 | } 38 | 39 | str.push(']'); 40 | str 41 | } 42 | 43 | pub fn print_array(ty: &Vec) -> String { 44 | let mut str = String::new(); 45 | 46 | str.push('['); 47 | 48 | let mut first = true; 49 | for p in ty { 50 | if !first { 51 | str.push(','); 52 | } 53 | first = false; 54 | 55 | str.push_str(&print_type(p)); 56 | } 57 | 58 | str.push(']'); 59 | 60 | str 61 | } 62 | 63 | pub fn print_type(ty: &Type) -> String { 64 | let mut str = String::new(); 65 | 66 | match ty { 67 | Type::Null => str.push_str(&format!("{ty}")), 68 | Type::Bool 69 | | Type::Nat 70 | | Type::Int 71 | | Type::Nat8 72 | | Type::Nat16 73 | | Type::Nat32 74 | | Type::Nat64 75 | | Type::Int8 76 | | Type::Int16 77 | | Type::Int32 78 | | Type::Int64 79 | | Type::Float32 80 | | Type::Float64 81 | | Type::Text 82 | | Type::Principal => str.push_str(&format!("\"{ty}\"")), 83 | 84 | Type::Opt(s) => { 85 | str.push_str(&format!("{{ \"Opt\": {} }}", print_type(s))); 86 | } 87 | Type::Vec(s) => { 88 | str.push_str(&format!("{{ \"Vec\": {} }}", print_type(s))); 89 | } 90 | Type::Var(s) => { 91 | str.push_str(&format!("{{ \"Var\": \"{s}\" }}")); 92 | } 93 | Type::Record(r) => { 94 | if is_tuple(ty) { 95 | str.push_str("{ \"Tuple\":"); 96 | // str.push_str(&print_array(r)); 97 | str.push_str(&print_array(&r.iter().map(|f| f.ty.clone()).collect())); 98 | str.push('}'); 99 | 100 | // str.push_str(&format!("{{ \"Tuple\": {} }}", print_fields(r))); 101 | } else { 102 | str.push_str(&format!("{{ \"Record\": {{ {} }} }}", print_fields(r))); 103 | } 104 | } 105 | Type::Variant(r) => { 106 | str.push_str(&format!("{{ \"Variant\": {{ {} }} }}", print_fields(r))); 107 | } 108 | Type::Service(s) => { 109 | str.push_str("{ \"Service\": {"); 110 | let mut first = true; 111 | 112 | for p in s { 113 | if !first { 114 | str.push(','); 115 | } 116 | first = false; 117 | str.push_str(&format!("\"{}\": {}", p.0, print_type(&p.1))); 118 | } 119 | 120 | str.push_str("}}"); 121 | } 122 | Type::Func(f) => { 123 | str.push_str(&format!( 124 | "{{ \"Func\": {{ \"args\": {}, \"rets\": {}, \"modes\": {} }} }}", 125 | print_array(&f.args), 126 | print_array(&f.rets), 127 | print_modes(&f.modes) 128 | )); 129 | } 130 | Type::Empty | Type::Reserved | Type::Unknown => {} 131 | Type::Knot(f) => { 132 | str.push_str(&format!("{{ \"Knot\": {f} }}")); 133 | } 134 | Type::Class(_args, b) => { 135 | str.push_str("\"Init\": "); 136 | str.push_str(&print_array(_args)); 137 | str.push_str(", \"Spec\": "); 138 | str.push_str(&print_type(b)); 139 | 140 | // str.push_str(&format!("\"Not handled, {}\"", ty)) 141 | } // _ => { 142 | // str.push_str(&format!("\"Not handled, {}\"", ty)) 143 | // } 144 | }; 145 | 146 | str 147 | } 148 | 149 | pub fn print_fields(fields: &Vec) -> String { 150 | let mut str = String::new(); 151 | let mut first = true; 152 | 153 | for f in fields { 154 | if !first { 155 | str.push(','); 156 | } 157 | first = false; 158 | 159 | str.push_str(&format!("\"{}\": {}", f.id, &print_type(&f.ty))); 160 | } 161 | 162 | str 163 | } 164 | 165 | pub fn compile(env: &TypeEnv, actor: &Option) -> String { 166 | let mut result = String::new(); 167 | 168 | result.push_str("{ \"types\": {"); 169 | let mut first = true; 170 | 171 | for i in env.0.iter() { 172 | if !first { 173 | result.push(','); 174 | } 175 | first = false; 176 | 177 | result.push_str(&format!("\"{}\": {}", i.0, &print_type(i.1))); 178 | 179 | // match i.1 { 180 | // Type::Service(_) => { 181 | // result.push_str( &format!("\"{}\": {{ {} }}", i.0, &print_type(i.1))); 182 | // } 183 | // _ => { 184 | // result.push_str( &format!("\"{}\": {}", i.0, &print_type(i.1))); 185 | // } 186 | // } 187 | } 188 | 189 | result.push_str("}, \"actor\": {"); 190 | // result.push('}'); 191 | 192 | match actor { 193 | None => {} 194 | Some(actor) => match actor { 195 | Type::Service(_) => { 196 | result.push_str(&format!("\"Spec\": {}", &print_type(actor))); 197 | } 198 | _ => { 199 | result.push_str(&print_type(actor)); 200 | } 201 | }, 202 | } 203 | 204 | result.push_str("}}"); 205 | 206 | result 207 | } 208 | -------------------------------------------------------------------------------- /src/wasm_tools/src/wasm_transform/convert.rs: -------------------------------------------------------------------------------- 1 | //! This module contains functions to convert between [`wasmparser`] types and 2 | //! [`wasm_encoder`] types. In most cases the types are almost exactly the same 3 | //! for the two crates, but they occasionally vary slightly in terms of 4 | //! structure or ownership of the contained data. 5 | 6 | use wasm_encoder::{BlockType, DataSegment, DataSegmentMode, EntityType, Instruction}; 7 | use wasmparser::{ 8 | BinaryReaderError, ConstExpr, ExternalKind, GlobalType, MemArg, MemoryType, Operator, 9 | TableType, TagKind, TagType, TypeRef, ValType, 10 | }; 11 | 12 | use super::ElementItems; 13 | 14 | pub(super) fn block_type(ty: &wasmparser::BlockType) -> BlockType { 15 | match ty { 16 | wasmparser::BlockType::Empty => BlockType::Empty, 17 | wasmparser::BlockType::Type(ty) => BlockType::Result(val_type(ty)), 18 | wasmparser::BlockType::FuncType(f) => BlockType::FunctionType(*f), 19 | } 20 | } 21 | 22 | pub(super) fn val_type(v: &ValType) -> wasm_encoder::ValType { 23 | match v { 24 | ValType::I32 => wasm_encoder::ValType::I32, 25 | ValType::I64 => wasm_encoder::ValType::I64, 26 | ValType::F32 => wasm_encoder::ValType::F32, 27 | ValType::F64 => wasm_encoder::ValType::F64, 28 | ValType::V128 => wasm_encoder::ValType::V128, 29 | ValType::FuncRef => wasm_encoder::ValType::FuncRef, 30 | ValType::ExternRef => wasm_encoder::ValType::ExternRef, 31 | } 32 | } 33 | 34 | pub(super) fn table_type(t: TableType) -> wasm_encoder::TableType { 35 | wasm_encoder::TableType { 36 | element_type: val_type(&t.element_type), 37 | minimum: t.initial, 38 | maximum: t.maximum, 39 | } 40 | } 41 | 42 | pub(super) fn memory_type(m: MemoryType) -> wasm_encoder::MemoryType { 43 | wasm_encoder::MemoryType { 44 | memory64: m.memory64, 45 | shared: m.shared, 46 | minimum: m.initial, 47 | maximum: m.maximum, 48 | } 49 | } 50 | 51 | pub(super) fn global_type(g: GlobalType) -> wasm_encoder::GlobalType { 52 | wasm_encoder::GlobalType { 53 | val_type: val_type(&g.content_type), 54 | mutable: g.mutable, 55 | } 56 | } 57 | 58 | fn tag_kind(k: TagKind) -> wasm_encoder::TagKind { 59 | match k { 60 | TagKind::Exception => wasm_encoder::TagKind::Exception, 61 | } 62 | } 63 | 64 | fn tag_type(t: TagType) -> wasm_encoder::TagType { 65 | wasm_encoder::TagType { 66 | kind: tag_kind(t.kind), 67 | func_type_idx: t.func_type_idx, 68 | } 69 | } 70 | 71 | pub(super) fn import_type(ty: TypeRef) -> EntityType { 72 | match ty { 73 | TypeRef::Func(f) => EntityType::Function(f), 74 | TypeRef::Table(t) => EntityType::Table(table_type(t)), 75 | TypeRef::Memory(m) => EntityType::Memory(memory_type(m)), 76 | TypeRef::Global(g) => EntityType::Global(global_type(g)), 77 | TypeRef::Tag(t) => EntityType::Tag(tag_type(t)), 78 | } 79 | } 80 | 81 | pub(super) fn op_to_const_expr( 82 | operator: &Operator, 83 | ) -> Result { 84 | use wasm_encoder::Encode; 85 | let mut bytes: Vec = Vec::new(); 86 | op(operator)?.encode(&mut bytes); 87 | Ok(wasm_encoder::ConstExpr::raw(bytes)) 88 | } 89 | 90 | pub(super) fn const_expr(expr: ConstExpr) -> Result { 91 | let mut reader = expr.get_binary_reader(); 92 | let size = reader.bytes_remaining(); 93 | // The const expression should end in a `End` instruction, but the encoder 94 | // doesn't expect that instruction to be part of its input so we drop it. 95 | let bytes = reader.read_bytes(size - 1)?.to_vec(); 96 | match reader.read_operator()? { 97 | Operator::End => {} 98 | _ => { 99 | panic!("const expr didn't end with `End` instruction"); 100 | } 101 | } 102 | Ok(wasm_encoder::ConstExpr::raw(bytes)) 103 | } 104 | 105 | pub(super) struct DerefBytesIterator<'a> { 106 | data: &'a [u8], 107 | current: usize, 108 | } 109 | 110 | impl<'a> DerefBytesIterator<'a> { 111 | fn new(data: &'a [u8]) -> Self { 112 | Self { data, current: 0 } 113 | } 114 | } 115 | 116 | impl<'a> Iterator for DerefBytesIterator<'a> { 117 | type Item = u8; 118 | 119 | fn next(&mut self) -> Option { 120 | if self.current < self.data.len() { 121 | let next = self.data[self.current]; 122 | self.current += 1; 123 | Some(next) 124 | } else { 125 | None 126 | } 127 | } 128 | 129 | fn size_hint(&self) -> (usize, Option) { 130 | let remaining = self.data.len() - self.current; 131 | (remaining, Some(remaining)) 132 | } 133 | } 134 | 135 | impl<'a> ExactSizeIterator for DerefBytesIterator<'a> {} 136 | 137 | pub(super) fn data_segment<'a>( 138 | segment: super::DataSegment<'a>, 139 | temp_const_expr: &'a mut wasm_encoder::ConstExpr, 140 | ) -> Result>, BinaryReaderError> { 141 | let mode = match segment.kind { 142 | super::DataSegmentKind::Passive => DataSegmentMode::Passive, 143 | super::DataSegmentKind::Active { 144 | memory_index, 145 | offset_expr, 146 | } => { 147 | *temp_const_expr = op_to_const_expr(&offset_expr)?; 148 | DataSegmentMode::Active { 149 | memory_index, 150 | offset: temp_const_expr, 151 | } 152 | } 153 | }; 154 | 155 | Ok(DataSegment { 156 | mode, 157 | data: DerefBytesIterator::new(segment.data), 158 | }) 159 | } 160 | 161 | pub(super) fn export_kind(export_kind: ExternalKind) -> wasm_encoder::ExportKind { 162 | match export_kind { 163 | ExternalKind::Func => wasm_encoder::ExportKind::Func, 164 | ExternalKind::Table => wasm_encoder::ExportKind::Table, 165 | ExternalKind::Memory => wasm_encoder::ExportKind::Memory, 166 | ExternalKind::Global => wasm_encoder::ExportKind::Global, 167 | ExternalKind::Tag => wasm_encoder::ExportKind::Tag, 168 | } 169 | } 170 | 171 | fn memarg(memarg: &MemArg) -> wasm_encoder::MemArg { 172 | wasm_encoder::MemArg { 173 | offset: memarg.offset, 174 | align: memarg.align as u32, 175 | memory_index: memarg.memory, 176 | } 177 | } 178 | 179 | pub(super) fn element_items(element_items: &ElementItems) -> wasm_encoder::Elements<'_> { 180 | match element_items { 181 | ElementItems::Functions(funcs) => wasm_encoder::Elements::Functions(funcs), 182 | ElementItems::ConstExprs(exprs) => wasm_encoder::Elements::Expressions(exprs), 183 | } 184 | } 185 | 186 | /// Convert [`wasmparser::Operator`] to [`wasm_encoder::Instruction`]. A 187 | /// simplified example of the conversion done in wasm-mutate 188 | /// [here](https://github.com/bytecodealliance/wasm-tools/blob/a8c4fddd239b0cb8978c76e6dfd856d5bd29b860/crates/wasm-mutate/src/mutators/translate.rs#L279). 189 | #[allow(unused_variables)] 190 | pub(super) fn op(op: &Operator<'_>) -> Result, BinaryReaderError> { 191 | use wasm_encoder::Instruction as I; 192 | 193 | macro_rules! convert { 194 | ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident)*) => { 195 | match op { 196 | $( 197 | wasmparser::Operator::$op $({ $($arg),* })? => { 198 | $( 199 | $(let $arg = convert!(map $arg $arg);)* 200 | )? 201 | convert!(build $op $($($arg)*)?) 202 | } 203 | )* 204 | } 205 | }; 206 | 207 | // Mapping the arguments from wasmparser to wasm-encoder types. 208 | 209 | // Arguments which need to be explicitly converted or ignored. 210 | (map $arg:ident blockty) => (block_type($arg)); 211 | (map $arg:ident targets) => (( 212 | $arg 213 | .targets() 214 | .collect::, wasmparser::BinaryReaderError>>()? 215 | .into(), 216 | $arg.default(), 217 | )); 218 | (map $arg:ident ty) => (val_type($arg)); 219 | (map $arg:ident memarg) => (memarg($arg)); 220 | (map $arg:ident table_byte) => (()); 221 | (map $arg:ident mem_byte) => (()); 222 | (map $arg:ident flags) => (()); 223 | 224 | // All other arguments are just dereferenced. 225 | (map $arg:ident $_:ident) => (*$arg); 226 | 227 | // Construct the wasm-encoder Instruction from the arguments of a 228 | // wasmparser instruction. There are a few special cases for where the 229 | // structure of a wasmparser instruction differs from that of 230 | // wasm-encoder. 231 | 232 | // Single operators are directly converted. 233 | (build $op:ident) => (Ok(I::$op)); 234 | 235 | // Special cases with a single argument. 236 | (build BrTable $arg:ident) => (Ok(I::BrTable($arg.0, $arg.1))); 237 | (build F32Const $arg:ident) => (Ok(I::F32Const(f32::from_bits($arg.bits())))); 238 | (build F64Const $arg:ident) => (Ok(I::F64Const(f64::from_bits($arg.bits())))); 239 | (build V128Const $arg:ident) => (Ok(I::V128Const($arg.i128()))); 240 | 241 | // Standard case with a single argument. 242 | (build $op:ident $arg:ident) => (Ok(I::$op($arg))); 243 | 244 | // Special case of multiple arguments. 245 | (build CallIndirect $ty:ident $table:ident $_:ident) => (Ok(I::CallIndirect { 246 | ty: $ty, 247 | table: $table, 248 | })); 249 | (build ReturnCallIndirect $ty:ident $table:ident) => (Ok(I::ReturnCallIndirect { 250 | ty: $ty, 251 | table: $table, 252 | })); 253 | (build MemoryGrow $mem:ident $_:ident) => (Ok(I::MemoryGrow($mem))); 254 | (build MemorySize $mem:ident $_:ident) => (Ok(I::MemorySize($mem))); 255 | 256 | // Standard case of multiple arguments. 257 | (build $op:ident $($arg:ident)*) => (Ok(I::$op { $($arg),* })); 258 | } 259 | 260 | wasmparser::for_each_operator!(convert) 261 | } 262 | -------------------------------------------------------------------------------- /status_test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icopen/lightic/54b5efb736562a95f57edd926a72c4cd30689e7f/status_test -------------------------------------------------------------------------------- /test/bls.spec.ts: -------------------------------------------------------------------------------- 1 | import { blsVerify, fromHex } from '@dfinity/agent' 2 | import { fromHexString } from '@dfinity/candid' 3 | import { assert } from 'chai' 4 | import { Bls } from '../src/bls' 5 | 6 | 7 | describe('BLS 12 381', function () { 8 | it('sign message', async function () { 9 | console.profile() 10 | 11 | const bls = new Bls() 12 | await bls.init() 13 | 14 | const message = new Uint8Array(fromHexString('0ed555d9bfa07404c59b17116793348fdea037856fe57d835ba81b5ad16211fd')) 15 | 16 | const sign = await bls.sign(message) 17 | 18 | const verify = await blsVerify(bls.publicKey, new Uint8Array(sign), message) 19 | console.log("verified " + verify) 20 | assert.isTrue(verify) 21 | 22 | console.profileEnd() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/cmc.spec.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { assert } from 'chai' 3 | import { LedgerHelper, TestContext, getAccount, hexToBytes } from '../src' 4 | import { NNSHelper } from '../src/helpers/nns_helper' 5 | 6 | const context = new TestContext() 7 | 8 | describe('CMC', function () { 9 | afterEach(function () { 10 | // Clean LightIc 11 | context.clean() 12 | }) 13 | 14 | it('install ledger canister', async function () { 15 | // const principal = Principal.fromText('dkzjk-sxlxb-cdh5x-rtexw-7y54l-yfwbq-rhayo-ufw34-lugle-j4s23-4ae') 16 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 17 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 18 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 19 | 20 | const ledger = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 21 | 22 | const cmc = await NNSHelper.defaults(context, ledger.ledger.get_id(), invokingPrincipal) 23 | 24 | 25 | }) 26 | 27 | // it('check account balance', async function () { 28 | // const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 29 | // const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 30 | // const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 31 | 32 | // const mintingAccount = getAccount(mintingPrincipal, 0) 33 | // const invokingAccount = getAccount(invokingPrincipal, 0) 34 | // const targetAccount = getAccount(targetPrincipal, 0) 35 | 36 | // const canister = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 37 | 38 | // // const hexAccount = hexToBytes(invokingAccount) 39 | 40 | // const actor = context.getAgent(Principal.anonymous()).getActor(canister.ledger) 41 | 42 | // const result = await actor.account_balance({ account: invokingAccount.toUint8Array() }) as any 43 | 44 | // console.log(result) 45 | 46 | // assert.equal(result.e8s, 100_000_000_000n) 47 | // }) 48 | 49 | // it('transfer ICP', async function () { 50 | // const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 51 | // const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 52 | // const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 53 | 54 | // const mintingAccount = getAccount(mintingPrincipal, 0) 55 | // const invokingAccount = getAccount(invokingPrincipal, 0) 56 | // const targetAccount = getAccount(targetPrincipal, 0) 57 | 58 | // const canister = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 59 | 60 | // const actor = context.getAgent(invokingPrincipal).getActor(canister.ledger) 61 | // const args = { 62 | // amount: { e8s: 100_000 }, 63 | // memo: 0, 64 | // fee: { e8s: 10_000 }, 65 | // from_subaccount: [], 66 | // to: targetAccount.toUint8Array(), 67 | // created_at_time: [] 68 | // } 69 | // const result = await actor.transfer(args) as any 70 | 71 | // console.log(result) 72 | 73 | // assert.equal(result.Ok, 1n) 74 | // }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/hashTree.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromHexString } from '@dfinity/candid' 2 | import { assert } from 'chai' 3 | import { Tree, makeHashTreeOld, mergeTrees } from '../src/hash_tree' 4 | import { lookup_path } from '@dfinity/agent' 5 | import cbor, { Tagged } from 'cbor' 6 | 7 | 8 | describe('Hash Tree', function () { 9 | it('create', async function () { 10 | console.profile() 11 | const message = new Uint8Array(fromHexString('0ed555d9bfa07404c59b17116793348fdea037856fe57d835ba81b5ad16211fd')) 12 | const msgId = '1' 13 | 14 | const tree = new Tree() 15 | tree.insertValue(['request_status', msgId, 'status'], 'replied') 16 | tree.insertValue(['request_status', msgId, 'reply'], Buffer.from(message)) 17 | 18 | const tree1 = makeHashTreeOld(['request_status', msgId, 'status'], 'replied') 19 | const tree2 = makeHashTreeOld(['request_status', msgId, 'reply'], Buffer.from(message)) 20 | 21 | const tree3 = mergeTrees(tree1, tree2) 22 | const tree4 = tree.getHashTree() 23 | 24 | const encoded1 = cbor.encode(tree3) 25 | const encoded2 = cbor.encode(tree4) 26 | console.profileEnd() 27 | 28 | }) 29 | 30 | it('get hash tree', async function () { 31 | const message = new Uint8Array(fromHexString('0ed555d9bfa07404c59b17116793348fdea037856fe57d835ba81b5ad16211fd')) 32 | const msgId = '1' 33 | 34 | const tree = new Tree() 35 | tree.insertValue(['request_status', msgId, 'status'], 'replied') 36 | tree.insertValue(['request_status', msgId, 'reply'], Buffer.from(message)) 37 | tree.insertValue(['request_status', msgId, 'reply_message'], 'some example') 38 | 39 | const hashTree = tree.getHashTree(); 40 | 41 | const value = lookup_path(['request_status', msgId, 'reply_message'], hashTree) 42 | assert.isTrue(value !== undefined) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/idl.spec.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid' 2 | 3 | import fs from 'fs' 4 | import { TestContext } from '../src' 5 | import { buildIdl } from '../src/idl_builder' 6 | import { parse_candid } from '../src/wasm_tools/pkg/wasm_tools' 7 | 8 | describe('IDL Build', function () { 9 | it('from motoko', async function () { 10 | const candidSpec = fs.readFileSync('./test/motoko.did').toString() 11 | const jsonCandid = parse_candid(candidSpec) 12 | const candid = JSON.parse(jsonCandid) 13 | buildIdl(IDL, candid) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/instrumentation.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { TestContext } from '../src' 3 | import { WasmCanister } from '../src/wasm_canister' 4 | 5 | const context = new TestContext() 6 | 7 | describe('Instrumentation', function () { 8 | afterEach(function () { 9 | // Clean LightIc 10 | context.clean() 11 | }) 12 | 13 | it('export func table', async function () { 14 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 15 | 16 | assert.exists((canister as WasmCanister).get_instance().exports.table) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/keeper.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { TestContext } from '../src' 3 | import { WasmCanister } from '../src/wasm_canister' 4 | import { Principal } from '@dfinity/principal' 5 | 6 | const context = new TestContext() 7 | 8 | describe('Keeper', function () { 9 | afterEach(function () { 10 | // Clean LightIc 11 | context.clean() 12 | }) 13 | 14 | it('test set ledger', async function () { 15 | 16 | const owner = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 17 | 18 | const canister = await context.deploy('./cache/keeper.wasm') 19 | const actor = context.getAgent(owner).getActor(canister); 20 | 21 | await actor.set_ledger_canister(owner); 22 | 23 | }) 24 | 25 | it('test set token', async function () { 26 | 27 | const owner = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 28 | 29 | const canister = await context.deploy('./cache/keeper.wasm') 30 | const actor = context.getAgent(owner).getActor(canister); 31 | 32 | await actor.set_token_canister(owner); 33 | 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/ledger.spec.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { assert } from 'chai' 3 | import { LedgerHelper, TestContext, getAccount, hexToBytes } from '../src' 4 | 5 | const context = new TestContext() 6 | 7 | describe('Ledger', function () { 8 | afterEach(function () { 9 | // Clean LightIc 10 | context.clean() 11 | }) 12 | 13 | it('install ledger canister', async function () { 14 | // const principal = Principal.fromText('dkzjk-sxlxb-cdh5x-rtexw-7y54l-yfwbq-rhayo-ufw34-lugle-j4s23-4ae') 15 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 16 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 17 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 18 | 19 | await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 20 | }) 21 | 22 | it('check account balance', async function () { 23 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 24 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 25 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 26 | 27 | const mintingAccount = getAccount(mintingPrincipal, 0) 28 | const invokingAccount = getAccount(invokingPrincipal, 0) 29 | const targetAccount = getAccount(targetPrincipal, 0) 30 | 31 | const canister = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 32 | 33 | // const hexAccount = hexToBytes(invokingAccount) 34 | 35 | const actor = context.getAgent(Principal.anonymous()).getActor(canister.ledger) 36 | 37 | const result = await actor.account_balance({ account: invokingAccount.toUint8Array() }) as any 38 | 39 | console.log(result) 40 | 41 | assert.equal(result.e8s, 100_000_000_000n) 42 | }) 43 | 44 | it('transfer ICP', async function () { 45 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 46 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 47 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 48 | 49 | const mintingAccount = getAccount(mintingPrincipal, 0) 50 | const invokingAccount = getAccount(invokingPrincipal, 0) 51 | const targetAccount = getAccount(targetPrincipal, 0) 52 | 53 | const canister = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 54 | 55 | const actor = context.getAgent(invokingPrincipal).getActor(canister.ledger) 56 | const args = { 57 | amount: { e8s: 100_000 }, 58 | memo: 0, 59 | fee: { e8s: 10_000 }, 60 | from_subaccount: [], 61 | to: targetAccount.toUint8Array(), 62 | created_at_time: [] 63 | } 64 | const result = await actor.transfer(args) as any 65 | 66 | console.log(result) 67 | 68 | assert.equal(result.Ok, 1n) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/management.spec.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { TestContext } from '../src' 3 | import { parse_candid_to_js } from '../src/wasm_tools/pkg/wasm_tools' 4 | import fs from 'fs' 5 | 6 | import { idlFactory } from '../src/mgmt.did' 7 | import { IDL } from "@dfinity/candid" 8 | 9 | const context = new TestContext() 10 | 11 | describe('Management Canister', function () { 12 | it('raw_rand', async function () { 13 | const did = fs.readFileSync('./src/management_canister.did').toString() 14 | const ts = parse_candid_to_js(did) 15 | fs.writeFileSync('mgmt.did.js', ts) 16 | 17 | const result = await context.getAgent(Principal.anonymous()).getActor('aaaaa-aa').raw_rand() 18 | console.log(result) 19 | }) 20 | 21 | it('provisional_create_canister_with_cycles', async function () { 22 | const idl = idlFactory({ IDL }) 23 | 24 | const args = new Uint8Array([68, 73, 68, 76, 5, 108, 2, 227, 249, 245, 217, 8, 1, 216, 163, 140, 168, 13, 2, 108, 4, 192, 207, 242, 113, 2, 215, 224, 155, 144, 2, 3, 222, 235, 181, 169, 14, 2, 168, 130, 172, 198, 15, 2, 110, 125, 110, 4, 109, 104, 1, 0, 0, 0, 0, 0, 0]) 25 | 26 | for (const field of idl._fields) { 27 | if (field[0] === 'provisional_create_canister_with_cycles') { 28 | const argTypes = field[1].argTypes 29 | // const retTypes = field[1].retTypes 30 | 31 | IDL.decode(argTypes, args) 32 | } 33 | } 34 | }) 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /test/mockAgent.spec.ts: -------------------------------------------------------------------------------- 1 | import { Actor } from '@dfinity/agent' 2 | import { Principal } from '@dfinity/principal' 3 | import { assert } from 'chai' 4 | import { TestContext, getAccount, LedgerHelper } from '../src' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const agent = require('@dfinity/agent/lib/cjs/utils/bls') 8 | agent.blsVerify = async (pk: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise => { 9 | return true 10 | } 11 | 12 | const context = new TestContext() 13 | 14 | describe('Mock Agent', function () { 15 | it('get_id with @dfinity actor', async function () { 16 | console.profile() 17 | const caller = Principal.anonymous() 18 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 19 | const actor = Actor.createActor(canister.getIdlBuilder(), { 20 | agent: context.getAgent(caller), 21 | canisterId: canister.get_id() 22 | }) 23 | 24 | const result = await actor.test_caller() as any[] 25 | 26 | assert.equal(caller.toString(), result.toString()) 27 | console.profileEnd() 28 | }) 29 | 30 | it('test_inter_canister with @dfinity actor', async function () { 31 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 32 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 33 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 34 | 35 | const targetAccount = getAccount(targetPrincipal, 0) 36 | 37 | // Download and deploy ledger canister 38 | const ledgerHelper = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 39 | const ledgerActor = context.getAgent(invokingPrincipal).getActor(ledgerHelper.ledger) 40 | // const ledgerActor = Actor.createActor(ledgerHelper.ledger.getIdlBuilder(), { 41 | // agent: context.getAgent(invokingPrincipal), 42 | // canisterId: ledgerHelper.ledger.get_id() 43 | // }) 44 | // Deploy our test canister 45 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 46 | const actor = Actor.createActor(canister.getIdlBuilder(), { 47 | agent: context.getAgent(invokingPrincipal), 48 | canisterId: canister.get_id() 49 | }) 50 | 51 | // Supply our canister with 1_000_000 of ICP 52 | const args = LedgerHelper.getSendArgs(canister.get_id(), 1_000_000) 53 | await ledgerActor.transfer(args) 54 | 55 | // Invoke inter canister call 56 | const result = await actor.test_inter_canister( 57 | ledgerHelper.ledger.get_id(), 58 | targetAccount.toHex(), 59 | 100_000) 60 | 61 | console.log('returns!!!') 62 | 63 | console.log(result) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/motoko.did: -------------------------------------------------------------------------------- 1 | type WhitelistSlot = 2 | record { 3 | end: Time__2; 4 | start: Time__2; 5 | }; 6 | type User = 7 | variant { 8 | address: AccountIdentifier; 9 | "principal": principal; 10 | }; 11 | type UpdateInformationRequest = record { 12 | metrics: opt CollectMetricsRequestType;}; 13 | type UpdateCallsAggregatedData = vec nat64; 14 | type TransferResponse = 15 | variant { 16 | err: 17 | variant { 18 | CannotNotify: AccountIdentifier; 19 | InsufficientBalance; 20 | InvalidToken: TokenIdentifier; 21 | Other: text; 22 | Rejected; 23 | Unauthorized: AccountIdentifier; 24 | }; 25 | ok: Balance; 26 | }; 27 | type TransferRequest = 28 | record { 29 | amount: Balance; 30 | from: User; 31 | memo: Memo; 32 | notify: bool; 33 | subaccount: opt SubAccount; 34 | to: User; 35 | token: TokenIdentifier; 36 | }; 37 | type Transaction = 38 | record { 39 | buyer: AccountIdentifier__1; 40 | price: nat64; 41 | seller: principal; 42 | time: Time; 43 | token: TokenIdentifier__1; 44 | }; 45 | type TokenIndex__4 = nat32; 46 | type TokenIndex__3 = nat32; 47 | type TokenIndex__2 = nat32; 48 | type TokenIndex__1 = nat32; 49 | type TokenIndex = nat32; 50 | type TokenIdentifier__3 = text; 51 | type TokenIdentifier__2 = text; 52 | type TokenIdentifier__1 = text; 53 | type TokenIdentifier = text; 54 | type Time__2 = int; 55 | type Time__1 = int; 56 | type Time = int; 57 | type SubAccount__3 = vec nat8; 58 | type SubAccount__2 = vec nat8; 59 | type SubAccount__1 = vec nat8; 60 | type SubAccount = vec nat8; 61 | type StatusResponse = 62 | record { 63 | cycles: opt nat64; 64 | heap_memory_size: opt nat64; 65 | memory_size: opt nat64; 66 | }; 67 | type StatusRequest = 68 | record { 69 | cycles: bool; 70 | heap_memory_size: bool; 71 | memory_size: bool; 72 | }; 73 | type StableState__5 = 74 | record { 75 | _nextTokenIdState: TokenIndex__4; 76 | _ownersState: vec record { 77 | AccountIdentifier__6; 78 | vec TokenIndex__4; 79 | }; 80 | _registryState: vec record { 81 | TokenIndex__4; 82 | AccountIdentifier__6; 83 | }; 84 | _supplyState: Balance__2; 85 | _tokenMetadataState: vec record { 86 | TokenIndex__4; 87 | Metadata; 88 | }; 89 | }; 90 | type StableState__4 = record {_isShuffledState: bool;}; 91 | type StableState__3 = 92 | record { 93 | _failedSalesState: vec record { 94 | AccountIdentifier__4; 95 | SubAccount__1; 96 | }; 97 | _nextSubAccountState: nat; 98 | _saleTransactionsState: vec SaleTransaction; 99 | _salesSettlementsState: vec record { 100 | AccountIdentifier__4; 101 | Sale; 102 | }; 103 | _soldIcpState: nat64; 104 | _soldState: nat; 105 | _tokensForSaleState: vec TokenIndex__2; 106 | _totalToSellState: nat; 107 | _whitelistStable: vec record { 108 | nat64; 109 | AccountIdentifier__4; 110 | WhitelistSlot; 111 | }; 112 | }; 113 | type StableState__2 = 114 | record { 115 | _frontendsState: vec record { 116 | text; 117 | Frontend; 118 | }; 119 | _tokenListingState: vec record { 120 | TokenIndex__1; 121 | Listing; 122 | }; 123 | _tokenSettlementState: vec record { 124 | TokenIndex__1; 125 | Settlement; 126 | }; 127 | _transactionsState: vec Transaction; 128 | }; 129 | type StableState__1 = record {_disbursementsState: vec Disbursement;}; 130 | type StableState = record {_assetsState: vec Asset;}; 131 | type StableChunk__6 = 132 | opt 133 | variant { 134 | legacy: StableState__5; 135 | v1: 136 | record { 137 | nextTokenId: TokenIndex__4; 138 | owners: vec record { 139 | AccountIdentifier__6; 140 | vec TokenIndex__4; 141 | }; 142 | registry: vec record { 143 | TokenIndex__4; 144 | AccountIdentifier__6; 145 | }; 146 | supply: Balance__2; 147 | tokenMetadata: vec record { 148 | TokenIndex__4; 149 | Metadata; 150 | }; 151 | }; 152 | }; 153 | type StableChunk__5 = 154 | opt variant { 155 | legacy: StableState__4; 156 | v1: record {isShuffled: bool;}; 157 | }; 158 | type StableChunk__4 = 159 | opt 160 | variant { 161 | legacy: StableState__3; 162 | v1: 163 | record { 164 | failedSales: vec record { 165 | AccountIdentifier__4; 166 | SubAccount__1; 167 | }; 168 | nextSubAccount: nat; 169 | saleTransactionChunk: vec SaleTransaction; 170 | saleTransactionCount: nat; 171 | salesSettlements: vec record { 172 | AccountIdentifier__4; 173 | Sale; 174 | }; 175 | sold: nat; 176 | soldIcp: nat64; 177 | tokensForSale: vec TokenIndex__2; 178 | totalToSell: nat; 179 | whitelist: vec record { 180 | nat64; 181 | AccountIdentifier__4; 182 | WhitelistSlot; 183 | }; 184 | }; 185 | v1_chunk: record {saleTransactionChunk: vec SaleTransaction;}; 186 | }; 187 | type StableChunk__3 = 188 | opt 189 | variant { 190 | legacy: StableState__2; 191 | v1: 192 | record { 193 | frontends: vec record { 194 | text; 195 | Frontend; 196 | }; 197 | tokenListing: vec record { 198 | TokenIndex__1; 199 | Listing; 200 | }; 201 | tokenSettlement: vec record { 202 | TokenIndex__1; 203 | Settlement; 204 | }; 205 | transactionChunk: vec Transaction; 206 | transactionCount: nat; 207 | }; 208 | v1_chunk: record {transactionChunk: vec Transaction;}; 209 | }; 210 | type StableChunk__2 = 211 | opt 212 | variant { 213 | legacy: StableState__1; 214 | v1: record {disbursements: vec Disbursement;}; 215 | }; 216 | type StableChunk__1 = 217 | opt variant { 218 | legacy: StableState; 219 | v1: record {assets: vec Asset;}; 220 | }; 221 | type StableChunk = variant { 222 | v1: 223 | record { 224 | assets: StableChunk__1; 225 | disburser: StableChunk__2; 226 | marketplace: StableChunk__3; 227 | sale: StableChunk__4; 228 | shuffle: StableChunk__5; 229 | tokens: StableChunk__6; 230 | };}; 231 | type Settlement = 232 | record { 233 | buyer: AccountIdentifier__1; 234 | buyerFrontend: opt text; 235 | price: nat64; 236 | seller: principal; 237 | sellerFrontend: opt text; 238 | subaccount: SubAccount__3; 239 | }; 240 | type SaleTransaction = 241 | record { 242 | buyer: AccountIdentifier__4; 243 | price: nat64; 244 | seller: principal; 245 | time: Time__1; 246 | tokens: vec TokenIndex__2; 247 | }; 248 | type SaleSettings = 249 | record { 250 | bulkPricing: vec record { 251 | nat64; 252 | nat64; 253 | }; 254 | endTime: Time__1; 255 | openEdition: bool; 256 | price: nat64; 257 | remaining: nat; 258 | salePrice: nat64; 259 | sold: nat; 260 | startTime: Time__1; 261 | totalToSell: nat; 262 | whitelist: bool; 263 | whitelistTime: Time__1; 264 | }; 265 | type Sale = 266 | record { 267 | buyer: AccountIdentifier__4; 268 | expires: Time__1; 269 | price: nat64; 270 | slot: opt WhitelistSlot; 271 | subaccount: SubAccount__1; 272 | tokens: vec TokenIndex__2; 273 | }; 274 | type Result_9 = 275 | variant { 276 | err: CommonError__2; 277 | ok: AccountIdentifier__6; 278 | }; 279 | type Result_8 = 280 | variant { 281 | err: CommonError__1; 282 | ok: record { 283 | AccountIdentifier__1; 284 | opt Listing; 285 | }; 286 | }; 287 | type Result_7 = 288 | variant { 289 | err: CommonError__1; 290 | ok: AccountIdentifier__1; 291 | }; 292 | type Result_6 = 293 | variant { 294 | err: CommonError; 295 | ok: Metadata__1; 296 | }; 297 | type Result_5 = 298 | variant { 299 | err: text; 300 | ok: record { 301 | AccountIdentifier__4; 302 | nat64; 303 | }; 304 | }; 305 | type Result_4 = 306 | variant { 307 | err: text; 308 | ok; 309 | }; 310 | type Result_3 = 311 | variant { 312 | err: CommonError__1; 313 | ok; 314 | }; 315 | type Result_2 = 316 | variant { 317 | err: CommonError; 318 | ok: Balance__1; 319 | }; 320 | type Result_1 = 321 | variant { 322 | err: CommonError; 323 | ok: vec TokenIndex; 324 | }; 325 | type Result = 326 | variant { 327 | err: CommonError; 328 | ok: vec record { 329 | TokenIndex; 330 | opt Listing; 331 | opt blob; 332 | }; 333 | }; 334 | type NumericEntity = 335 | record { 336 | avg: nat64; 337 | first: nat64; 338 | last: nat64; 339 | max: nat64; 340 | min: nat64; 341 | }; 342 | type Nanos = nat64; 343 | type MetricsResponse = record {metrics: opt CanisterMetrics;}; 344 | type MetricsRequest = record {parameters: GetMetricsParameters;}; 345 | type MetricsGranularity = 346 | variant { 347 | daily; 348 | hourly; 349 | }; 350 | type Metadata__2 = 351 | variant { 352 | fungible: 353 | record { 354 | decimals: nat8; 355 | metadata: opt blob; 356 | name: text; 357 | symbol: text; 358 | }; 359 | nonfungible: record {metadata: opt blob;}; 360 | }; 361 | type Metadata__1 = 362 | variant { 363 | fungible: 364 | record { 365 | decimals: nat8; 366 | metadata: opt blob; 367 | name: text; 368 | symbol: text; 369 | }; 370 | nonfungible: record {metadata: opt blob;}; 371 | }; 372 | type Metadata = 373 | variant { 374 | fungible: 375 | record { 376 | decimals: nat8; 377 | metadata: opt blob; 378 | name: text; 379 | symbol: text; 380 | }; 381 | nonfungible: record {metadata: opt blob;}; 382 | }; 383 | type Memo = blob; 384 | type LogMessagesData = 385 | record { 386 | message: text; 387 | timeNanos: Nanos; 388 | }; 389 | type Listing = 390 | record { 391 | buyerFrontend: opt text; 392 | locked: opt Time; 393 | price: nat64; 394 | seller: principal; 395 | sellerFrontend: opt text; 396 | }; 397 | type ListRequest = 398 | record { 399 | from_subaccount: opt SubAccount__3; 400 | frontendIdentifier: opt text; 401 | price: opt nat64; 402 | token: TokenIdentifier__1; 403 | }; 404 | type HttpStreamingStrategy = variant { 405 | Callback: 406 | record { 407 | callback: func () -> (); 408 | token: HttpStreamingCallbackToken; 409 | };}; 410 | type HttpStreamingCallbackToken = 411 | record { 412 | content_encoding: text; 413 | index: nat; 414 | key: text; 415 | sha256: opt blob; 416 | }; 417 | type HttpStreamingCallbackResponse = 418 | record { 419 | body: blob; 420 | token: opt HttpStreamingCallbackToken; 421 | }; 422 | type HttpResponse = 423 | record { 424 | body: blob; 425 | headers: vec HeaderField; 426 | status_code: nat16; 427 | streaming_strategy: opt HttpStreamingStrategy; 428 | }; 429 | type HttpRequest = 430 | record { 431 | body: blob; 432 | headers: vec HeaderField; 433 | method: text; 434 | url: text; 435 | }; 436 | type HourlyMetricsData = 437 | record { 438 | canisterCycles: CanisterCyclesAggregatedData; 439 | canisterHeapMemorySize: CanisterHeapMemoryAggregatedData; 440 | canisterMemorySize: CanisterMemoryAggregatedData; 441 | timeMillis: int; 442 | updateCalls: UpdateCallsAggregatedData; 443 | }; 444 | type HeaderField = 445 | record { 446 | text; 447 | text; 448 | }; 449 | type GetMetricsParameters = 450 | record { 451 | dateFromMillis: nat; 452 | dateToMillis: nat; 453 | granularity: MetricsGranularity; 454 | }; 455 | type GetLogMessagesParameters = 456 | record { 457 | count: nat32; 458 | filter: opt GetLogMessagesFilter; 459 | fromTimeNanos: opt Nanos; 460 | }; 461 | type GetLogMessagesFilter = 462 | record { 463 | analyzeCount: nat32; 464 | messageContains: opt text; 465 | messageRegex: opt text; 466 | }; 467 | type GetLatestLogMessagesParameters = 468 | record { 469 | count: nat32; 470 | filter: opt GetLogMessagesFilter; 471 | upToTimeNanos: opt Nanos; 472 | }; 473 | type GetInformationResponse = 474 | record { 475 | logs: opt CanisterLogResponse; 476 | metrics: opt MetricsResponse; 477 | status: opt StatusResponse; 478 | version: opt nat; 479 | }; 480 | type GetInformationRequest = 481 | record { 482 | logs: opt CanisterLogRequest; 483 | metrics: opt MetricsRequest; 484 | status: opt StatusRequest; 485 | version: bool; 486 | }; 487 | type Frontend = 488 | record { 489 | accountIdentifier: AccountIdentifier__1; 490 | fee: nat64; 491 | }; 492 | type File = 493 | record { 494 | ctype: text; 495 | data: vec blob; 496 | }; 497 | type Extension = text; 498 | type Disbursement = 499 | record { 500 | amount: nat64; 501 | fromSubaccount: SubAccount__2; 502 | to: AccountIdentifier__5; 503 | tokenIndex: TokenIndex__3; 504 | }; 505 | type DailyMetricsData = 506 | record { 507 | canisterCycles: NumericEntity; 508 | canisterHeapMemorySize: NumericEntity; 509 | canisterMemorySize: NumericEntity; 510 | timeMillis: int; 511 | updateCalls: nat64; 512 | }; 513 | type CommonError__3 = 514 | variant { 515 | InvalidToken: TokenIdentifier; 516 | Other: text; 517 | }; 518 | type CommonError__2 = 519 | variant { 520 | InvalidToken: TokenIdentifier; 521 | Other: text; 522 | }; 523 | type CommonError__1 = 524 | variant { 525 | InvalidToken: TokenIdentifier; 526 | Other: text; 527 | }; 528 | type CommonError = 529 | variant { 530 | InvalidToken: TokenIdentifier; 531 | Other: text; 532 | }; 533 | type CollectMetricsRequestType = 534 | variant { 535 | force; 536 | normal; 537 | }; 538 | type CanisterMetricsData = 539 | variant { 540 | daily: vec DailyMetricsData; 541 | hourly: vec HourlyMetricsData; 542 | }; 543 | type CanisterMetrics = record {data: CanisterMetricsData;}; 544 | type CanisterMemoryAggregatedData = vec nat64; 545 | type CanisterLogResponse = 546 | variant { 547 | messages: CanisterLogMessages; 548 | messagesInfo: CanisterLogMessagesInfo; 549 | }; 550 | type CanisterLogRequest = 551 | variant { 552 | getLatestMessages: GetLatestLogMessagesParameters; 553 | getMessages: GetLogMessagesParameters; 554 | getMessagesInfo; 555 | }; 556 | type CanisterLogMessagesInfo = 557 | record { 558 | count: nat32; 559 | features: vec opt CanisterLogFeature; 560 | firstTimeNanos: opt Nanos; 561 | lastTimeNanos: opt Nanos; 562 | }; 563 | type CanisterLogMessages = 564 | record { 565 | data: vec LogMessagesData; 566 | lastAnalyzedMessageTimeNanos: opt Nanos; 567 | }; 568 | type CanisterLogFeature = 569 | variant { 570 | filterMessageByContains; 571 | filterMessageByRegex; 572 | }; 573 | type CanisterHeapMemoryAggregatedData = vec nat64; 574 | type CanisterCyclesAggregatedData = vec nat64; 575 | type Canister = 576 | service { 577 | acceptCycles: () -> (); 578 | addAsset: (Asset) -> (nat); 579 | airdropTokens: (nat) -> (); 580 | allSettlements: () -> (vec record { 581 | TokenIndex__1; 582 | Settlement; 583 | }) query; 584 | availableCycles: () -> (nat) query; 585 | backupChunk: (nat, nat) -> (StableChunk) query; 586 | balance: (BalanceRequest) -> (BalanceResponse) query; 587 | bearer: (TokenIdentifier__3) -> (Result_9) query; 588 | cronDisbursements: () -> (); 589 | cronFailedSales: () -> (); 590 | cronSalesSettlements: () -> (); 591 | cronSettlements: () -> (); 592 | deleteFrontend: (text) -> (); 593 | details: (TokenIdentifier__1) -> (Result_8) query; 594 | enableSale: () -> (nat); 595 | extensions: () -> (vec Extension) query; 596 | failedSales: () -> 597 | (vec record { 598 | AccountIdentifier__4; 599 | SubAccount__1; 600 | }) query; 601 | frontends: () -> (vec record { 602 | text; 603 | Frontend; 604 | }); 605 | getCanistergeekInformation: (GetInformationRequest) -> 606 | (GetInformationResponse) query; 607 | getChunkCount: (nat) -> (nat) query; 608 | getDisbursements: () -> (vec Disbursement) query; 609 | getMinter: () -> (principal) query; 610 | getRegistry: () -> (vec record { 611 | TokenIndex; 612 | AccountIdentifier__2; 613 | }) query; 614 | getTokenToAssetMapping: () -> (vec record { 615 | TokenIndex; 616 | text; 617 | }) query; 618 | getTokens: () -> (vec record { 619 | TokenIndex; 620 | Metadata__1; 621 | }) query; 622 | grow: (nat) -> (nat); 623 | http_request: (HttpRequest) -> (HttpResponse) query; 624 | http_request_streaming_callback: (HttpStreamingCallbackToken) -> 625 | (HttpStreamingCallbackResponse) query; 626 | initCap: () -> (Result_4); 627 | initMint: () -> (Result_4); 628 | list: (ListRequest) -> (Result_3); 629 | listings: () -> (vec record { 630 | TokenIndex__1; 631 | Listing; 632 | Metadata__2; 633 | }) query; 634 | lock: (TokenIdentifier__1, nat64, AccountIdentifier__1, SubAccount__3, 635 | opt text) -> (Result_7); 636 | metadata: (TokenIdentifier__2) -> (Result_6) query; 637 | pendingCronJobs: () -> 638 | (record { 639 | disbursements: nat; 640 | failedSettlements: nat; 641 | }) query; 642 | putFrontend: (text, Frontend) -> (); 643 | reserve: (nat64, nat64, AccountIdentifier__4, SubAccount__1) -> (Result_5); 644 | restoreChunk: (StableChunk) -> (); 645 | retrieve: (AccountIdentifier__4) -> (Result_4); 646 | saleTransactions: () -> (vec SaleTransaction) query; 647 | salesSettings: (AccountIdentifier__3) -> (SaleSettings) query; 648 | salesSettlements: () -> (vec record { 649 | AccountIdentifier__4; 650 | Sale; 651 | }) query; 652 | settle: (TokenIdentifier__1) -> (Result_3); 653 | settlements: () -> 654 | (vec record { 655 | TokenIndex__1; 656 | AccountIdentifier__1; 657 | nat64; 658 | }) query; 659 | shuffleTokensForSale: () -> (); 660 | stats: () -> (nat64, nat64, nat64, nat64, nat, nat, nat) query; 661 | streamAsset: (nat, bool, blob) -> (); 662 | supply: () -> (Result_2) query; 663 | toAccountIdentifier: (text, nat) -> (AccountIdentifier__3) query; 664 | tokens: (AccountIdentifier__2) -> (Result_1) query; 665 | tokens_ext: (AccountIdentifier__2) -> (Result) query; 666 | transactions: () -> (vec Transaction) query; 667 | transfer: (TransferRequest) -> (TransferResponse); 668 | updateCanistergeekInformation: (UpdateInformationRequest) -> (); 669 | updateThumb: (text, File) -> (opt nat); 670 | }; 671 | type Balance__2 = nat; 672 | type Balance__1 = nat; 673 | type BalanceResponse = 674 | variant { 675 | err: CommonError__3; 676 | ok: Balance; 677 | }; 678 | type BalanceRequest = 679 | record { 680 | token: TokenIdentifier; 681 | user: User; 682 | }; 683 | type Balance = nat; 684 | type Asset = 685 | record { 686 | metadata: opt File; 687 | name: text; 688 | payload: File; 689 | thumbnail: opt File; 690 | }; 691 | type AccountIdentifier__6 = text; 692 | type AccountIdentifier__5 = text; 693 | type AccountIdentifier__4 = text; 694 | type AccountIdentifier__3 = text; 695 | type AccountIdentifier__2 = text; 696 | type AccountIdentifier__1 = text; 697 | type AccountIdentifier = text; 698 | service : (principal) -> Canister 699 | -------------------------------------------------------------------------------- /test/server.spec.ts: -------------------------------------------------------------------------------- 1 | import { fromHexString } from '@dfinity/candid' 2 | import { assert } from 'chai' 3 | import { Tree, makeHashTreeOld, mergeTrees } from '../src/hash_tree' 4 | import { Bls } from '../src/bls' 5 | import { getReadResponse } from '../src/server' 6 | 7 | describe('Server Tests', function () { 8 | it('read_state', async function () { 9 | console.profile() 10 | const message = new Uint8Array(fromHexString('0ed555d9bfa07404c59b17116793348fdea037856fe57d835ba81b5ad16211fd')) 11 | const msgId = '12342342423432424234234234234234234323423342343242343243' 12 | 13 | 14 | const tree = new Tree(); 15 | tree.insertValue(['request_status', msgId, 'reply'], Buffer.from(message)) 16 | tree.insertValue(['request_status', msgId, 'status'], 'replied') 17 | const treeHash = tree.getHashTree() 18 | 19 | const tree1 = makeHashTreeOld(['request_status', msgId, 'reply'], Buffer.from(message)) 20 | const tree2 = makeHashTreeOld(['request_status', msgId, 'status'], 'replied') 21 | const treeHash2 = mergeTrees(tree1, tree2) 22 | 23 | console.profileEnd() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/test.spec.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal' 2 | import { assert } from 'chai' 3 | import { TestContext, getAccount, LedgerHelper } from '../src' 4 | import { latestRelease } from '../src/helpers/ledger_helper' 5 | 6 | const context = new TestContext() 7 | 8 | describe('LightIc', function () { 9 | it('list installed canisters', async function () { 10 | await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 11 | await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 12 | await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 13 | await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 14 | await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 15 | 16 | const canisters = context.replica.get_canisters() 17 | 18 | //there is management canister installed by default 19 | assert.equal(canisters.length, 6) 20 | }) 21 | 22 | afterEach(function () { 23 | // Clean LightIc 24 | context.clean() 25 | }) 26 | }) 27 | 28 | describe('Ledger Helper', function () { 29 | it('get wasm', async function () { 30 | await LedgerHelper.checkAndDownload(latestRelease) 31 | }) 32 | }) 33 | 34 | describe('ic0', function () { 35 | afterEach(function () { 36 | // Clean LightIc 37 | context.clean() 38 | }) 39 | 40 | describe('caller', function () { 41 | it('should match', async function () { 42 | const caller = Principal.anonymous() 43 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 44 | const actor = context.getAgent(caller).getActor(canister) 45 | 46 | const result = await actor.test_caller() as any[] 47 | 48 | assert.equal(caller.toString(), result.toString()) 49 | }) 50 | }) 51 | describe('id', function () { 52 | it('should match', async function () { 53 | const caller = Principal.anonymous() 54 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 55 | const actor = context.getAgent(caller).getActor(canister) 56 | const result = await actor.test_id() as any[] 57 | 58 | assert.equal(canister.get_id().toString(), result.toString()) 59 | }) 60 | }) 61 | 62 | describe('trap', function () { 63 | it('should trap', async function () { 64 | const caller = Principal.anonymous() 65 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 66 | const actor = context.getAgent(caller).getActor(canister) 67 | try { 68 | await actor.test_trap() 69 | } catch (e) { 70 | console.log(e) 71 | } 72 | 73 | await actor.test_id() 74 | }) 75 | }) 76 | 77 | describe('stable memory', function () { 78 | it('size', async function () { 79 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 80 | 81 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 82 | const actor = context.getAgent(invokingPrincipal).getActor(canister) 83 | 84 | const size = await actor.test_stable_size() 85 | assert.equal(size, 0) 86 | }) 87 | it('grow', async function () { 88 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 89 | 90 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 91 | const actor = context.getAgent(invokingPrincipal).getActor(canister) 92 | 93 | await actor.test_stable_grow() 94 | const size = await actor.test_stable_size() 95 | assert.equal(size, 1) 96 | }) 97 | }) 98 | 99 | describe('inter canister call', function () { 100 | it('should pass', async function () { 101 | const mintingPrincipal = Principal.fromText('3zjeh-xtbtx-mwebn-37a43-7nbck-qgquk-xtrny-42ujn-gzaxw-ncbzw-kqe') 102 | const invokingPrincipal = Principal.fromText('7gaq2-4kttl-vtbt4-oo47w-igteo-cpk2k-57h3p-yioqe-wkawi-wz45g-jae') 103 | const targetPrincipal = Principal.fromText('o2ivq-5dsz3-nba5d-pwbk2-hdd3i-vybeq-qfz35-rqg27-lyesf-xghzc-3ae') 104 | 105 | const targetAccount = getAccount(targetPrincipal, 0) 106 | 107 | // Download and deploy ledger canister 108 | const ledgerHelper = await LedgerHelper.defaults(context, mintingPrincipal, invokingPrincipal) 109 | const ledgerActor = context.getAgent(invokingPrincipal).getActor(ledgerHelper.ledger) 110 | 111 | // Deploy our test canister 112 | const canister = await context.deploy('./spec_test/target/wasm32-unknown-unknown/release/spec_test.wasm') 113 | const actor = context.getAgent(invokingPrincipal).getActor(canister) 114 | 115 | console.profile() 116 | 117 | // Supply our canister with 1_000_000 of ICP 118 | const args = LedgerHelper.getSendArgs(canister.get_id(), 1_000_000) 119 | await ledgerActor.transfer(args) 120 | 121 | // Invoke inter canister call 122 | const result = await actor.test_inter_canister( 123 | ledgerHelper.ledger.get_id(), 124 | targetAccount.toHex(), 125 | 100_000) 126 | 127 | console.log(result) 128 | console.profileEnd() 129 | }) 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { u64IntoCanisterId, u64IntoPrincipalId } from "../src/utils"; 3 | 4 | describe('utils', function () { 5 | it('u64IntoPrincipalId' , async function () { 6 | const prin = u64IntoPrincipalId(1n); 7 | const printText = prin.toText(); 8 | 9 | console.log(printText); 10 | 11 | assert.equal(printText, "qzox2-lqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aae"); 12 | assert.equal(u64IntoPrincipalId(0n).toText(), "4vnki-cqaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aae"); 13 | }); 14 | 15 | it('u64IntoCanisterId' , async function () { 16 | const prin = u64IntoCanisterId(0n); 17 | const printText = prin.toText(); 18 | 19 | assert.equal(printText, "rwlgt-iiaaa-aaaaa-aaaaa-cai"); 20 | }); 21 | }); -------------------------------------------------------------------------------- /tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "ES2020", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "../lib/esm", 9 | "strictNullChecks": true, 10 | }, 11 | "lib": ["es2015"], 12 | } 13 | 14 | --------------------------------------------------------------------------------