diff --git a/.dockerignore b/.dockerignore index 9743482e93..9dca91015d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -45,6 +45,7 @@ test/config.ini # wasm wasm-wrappers/pkg/ +wasm-wrappers/js-bindings-test/dist/ # 'mintlayer-data' will be mapped to home directories of docker containers, so everything # inside it will be generated by the containers. diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 43b8733718..4923324f68 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -35,14 +35,22 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - name: Install TypeScript & Knip + run: npm install typescript knip - name: Install wasm-pack run: cargo install wasm-pack - name: Build the wasm module working-directory: ./wasm-wrappers run: wasm-pack build --target nodejs + - name: Compile the tests + working-directory: ./wasm-wrappers + run: tsc --project js-bindings-test/tsconfig.json - name: Run the tests working-directory: ./wasm-wrappers - run: node js-bindings/node-entry.js + run: node --enable-source-maps js-bindings-test/node-entry.js + - name: Run Knip + working-directory: ./wasm-wrappers/js-bindings-test + run: npx knip wasm_artifacts: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 8083c11e6a..dbf53d624f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ test/config.ini # wasm wasm-wrappers/pkg/ +wasm-wrappers/js-bindings-test/dist/ # 'mintlayer-data' will be mapped to home directories of docker containers, so everything # inside it will be generated by the containers. diff --git a/build-tools/codecheck/codecheck.py b/build-tools/codecheck/codecheck.py index 679779b1e4..2450277984 100755 --- a/build-tools/codecheck/codecheck.py +++ b/build-tools/codecheck/codecheck.py @@ -34,7 +34,8 @@ '.git', 'build-tools/docker/example-mainnet/mintlayer-data', 'build-tools/docker/example-mainnet-dns-server/mintlayer-data', - 'wasm-wrappers/pkg' + 'wasm-wrappers/pkg', + 'wasm-wrappers/js-bindings-test/dist', ] diff --git a/do_checks.sh b/do_checks.sh index 59eff04f7d..776775ce32 100755 --- a/do_checks.sh +++ b/do_checks.sh @@ -5,6 +5,8 @@ set -e SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) PYTHON=$(which python || which python3) +cd "$SCRIPT_DIR" + cargo fmt --check -- --config newline_style=Unix # Install cargo deny first with: cargo install cargo-deny. @@ -36,7 +38,7 @@ cargo clippy --all-features --workspace --lib --bins --examples -- \ -D clippy::string_slice # Install requirements with: pip install -r ./build-tools/codecheck/requirements.txt -"$PYTHON" "$SCRIPT_DIR/build-tools/codecheck/codecheck.py" +"$PYTHON" "build-tools/codecheck/codecheck.py" # Ensure that wasm documentation is up-to-date cargo run -p wasm-doc-gen -- -o wasm-wrappers/WASM-API.md --check diff --git a/wasm-wrappers/README.md b/wasm-wrappers/README.md index 242d38a9b9..f79afffefb 100644 --- a/wasm-wrappers/README.md +++ b/wasm-wrappers/README.md @@ -1,11 +1,12 @@ -## Basic wasm bindings for mintlayer +# Basic wasm bindings for mintlayer This module has different basic functionalities of mintlayer compiled into wasm for various purposes, primarily interfacing with other systems and languages without having to rewrite code. ##### Note: This was tested on x86_64 Linux, and may not work on other platforms. It didn't work on M1 Mac directly (particularly the build. A pre-built wasm binary works fine on a browser, see below for more information). +## Running the tests -### To run in a web browser +### Preparation Make sure you have wasm-pack and the wasm32-unknown-unknown target installed: @@ -16,48 +17,53 @@ cargo install wasm-pack Also make sure you have `clang` installed. It's required. -To build the wasm package from the crate, run (in the wasm Cargo.toml directory): - -``` -wasm-pack build --target web -``` - **Note for mac users**: `llvm` installed by Xcode doesn't support wasm targets, but the homebrew version does, these commands may make it possible to compile to wasm targets. Note that using these commands could have other side effects on your toolchain. Please consider researching the clang toolchain and how it works before using them. We do not recommend copying and pasting commands without fully understanding the side-effects. ``` brew install llvm AR=/opt/homebrew/opt/llvm/bin/llvm-ar CC=/opt/homebrew/opt/llvm/bin/clang wasm-pack build --target web ``` -To test the wasm binary. First, install `http-server` web server (feel free to use any other web-server of your choosing): +Also, install TypeScript: +``` +npm install -g typescript +``` + +### Compile the tests via `tsc` +In the wasm Cargo.toml directory, run: ``` -cargo install http-server +tsc --project js-bindings-test/tsconfig.json ``` -Then run the http server, and then choose the file `js-bindings/index.html`: +### Run the tests in a web browser + +To build the wasm package from the crate, run (in the wasm Cargo.toml directory): ``` -http-server --port 8080 --verbose +wasm-pack build --target web ``` -If you're using a remote server, either tunnel to port 8080, or expose that port and run this (assuming you understand the security risks): +To test the wasm binary, first install `http-server` web server (feel free to use any other web-server of your choosing): ``` -http-server --port 8080 --host 0.0.0.0 --verbose +cargo install http-server ``` -The ported wasm functions are exported to the file `js-bindings/index.js` and used in the file `js-bindings/index.html` with a basic test/example in them using JavaScript. Use your browser's console to see the output. +Then run the http server: -### To run in Node.js +``` +http-server --port 8080 +``` -Make sure you have wasm-pack and the wasm32-unknown-unknown target installed: +If you're using a remote server, either tunnel to port 8080, or expose that port and run this (assuming you understand the security risks): ``` -rustup target add wasm32-unknown-unknown -cargo install wasm-pack +http-server --port 8080 --host 0.0.0.0 ``` -Also make sure you have `clang` installed. It's required. +To run test tests, choose the file `js-bindings-test/index.html` in the browser. Use browser's console to see the output. + +### Run the tests in Node.js To build the wasm package from the crate, run (in the wasm Cargo.toml directory): @@ -65,12 +71,31 @@ To build the wasm package from the crate, run (in the wasm Cargo.toml directory) wasm-pack build --target nodejs ``` -Finally, to run the example, run: +Finally, to run the tests, run: + +``` +node --enable-source-maps js-bindings-test/node-entry.js +``` + +### Run `knip` + +We use `knip` to make sure that there are no unused exports in `js-bindings-test/tests` (which could +mean that some of the tests are never run). + +**Note: unused local definitions are caught by the TypeScript compiler itself, via the `noUnusedLocals` setting.** +To run `knip` locally, first install it: ``` -node js-bindings/node-entry.js +npm install -g knip ``` +And then run (in the wasm Cargo.toml directory): +``` +(cd js-bindings-test && npx knip) +``` + +**Note: to explicitly exclude an export from `knip`'s report, annotate it with `/** @public */`.** + ### Further documentation on wasm - https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm diff --git a/wasm-wrappers/WASM-API.md b/wasm-wrappers/WASM-API.md index f3c479b1d9..e144ddf779 100644 --- a/wasm-wrappers/WASM-API.md +++ b/wasm-wrappers/WASM-API.md @@ -108,16 +108,6 @@ produced by `make_transaction_intent_message_to_sign`. one instead of the other). `network` - the network being used (needed to decode the addresses). -### Function: `encode_output_transfer` - -Given a destination address, an amount and a network type (mainnet, testnet, etc), this function -creates an output of type Transfer, and returns it as bytes. - -### Function: `encode_output_token_transfer` - -Given a destination address, an amount, token ID (in address form) and a network type (mainnet, testnet, etc), this function -creates an output of type Transfer for tokens, and returns it as bytes. - ### Function: `staking_pool_spend_maturity_block_count` Given the current block height and a network type (mainnet, testnet, etc), @@ -148,50 +138,11 @@ until the given timestamp Given a block height, this function returns the output timelock which is used in locked outputs to lock an output until that block height is reached. -### Function: `encode_output_lock_then_transfer` - -Given a valid receiving address, and a locking rule as bytes (available in this file), -and a network type (mainnet, testnet, etc), this function creates an output of type -LockThenTransfer with the parameters provided. - -### Function: `encode_output_token_lock_then_transfer` - -Given a valid receiving address, token ID (in address form), a locking rule as bytes (available in this file), -and a network type (mainnet, testnet, etc), this function creates an output of type -LockThenTransfer with the parameters provided. - -### Function: `encode_output_coin_burn` - -Given an amount, this function creates an output (as bytes) to burn a given amount of coins - -### Function: `encode_output_token_burn` - -Given an amount, token ID (in address form) and network type (mainnet, testnet, etc), -this function creates an output (as bytes) to burn a given amount of tokens - -### Function: `encode_output_create_delegation` - -Given a pool id as string, an owner address and a network type (mainnet, testnet, etc), -this function returns an output (as bytes) to create a delegation to the given pool. -The owner address is the address that is authorized to withdraw from that delegation. - -### Function: `encode_output_delegate_staking` - -Given a delegation id (as string, in address form), an amount and a network type (mainnet, testnet, etc), -this function returns an output (as bytes) that would delegate coins to be staked in the specified delegation id. - ### Function: `encode_stake_pool_data` This function returns the staking pool data needed to create a staking pool in an output as bytes, given its parameters and the network type (testnet, mainnet, etc). -### Function: `encode_output_create_stake_pool` - -Given a pool id, staking data as bytes and the network type (mainnet, testnet, etc), -this function returns an output that creates that staking pool. -Note that the pool id is mandated to be taken from the hash of the first input. -It is not arbitrary. - ### Function: `fungible_token_issuance_fee` Returns the fee that needs to be paid by a transaction for issuing a new fungible token @@ -221,49 +172,19 @@ Given the current block height and a network type (mainnet, testnet, etc), this will return the fee that needs to be paid by a transaction for changing the authority of a token The current block height information is used in case a network upgrade changed the value. -### Function: `encode_output_issue_fungible_token` - -Given the parameters needed to issue a fungible token, and a network type (mainnet, testnet, etc), -this function creates an output that issues that token. - ### Function: `get_token_id` Returns the Fungible/NFT Token ID for the given inputs of a transaction -### Function: `encode_output_issue_nft` - -Given the parameters needed to issue an NFT, and a network type (mainnet, testnet, etc), -this function creates an output that issues that NFT. - -### Function: `encode_output_data_deposit` - -Given data to be deposited in the blockchain, this function provides the output that deposits this data - ### Function: `data_deposit_fee` Returns the fee that needs to be paid by a transaction for issuing a data deposit -### Function: `encode_output_htlc` - -Given the parameters needed to create hash timelock contract, and a network type (mainnet, testnet, etc), -this function creates an output. - ### Function: `extract_htlc_secret` Given a signed transaction and input outpoint that spends an htlc utxo, extract a secret that is encoded in the corresponding input signature -### Function: `encode_input_for_utxo` - -Given an output source id as bytes, and an output index, together representing a utxo, -this function returns the input that puts them together, as bytes. - -### Function: `encode_input_for_withdraw_from_delegation` - -Given a delegation id, an amount and a network type (mainnet, testnet, etc), this function -creates an input that withdraws from a delegation. -A nonce is needed because this spends from an account. The nonce must be in sequence for everything in that account. - ### Function: `estimate_transaction_size` Given the inputs, along each input's destination that can spend that input @@ -326,6 +247,17 @@ It is recommended to use a strict `Transaction` size and set the second paramete Calculate the "effective balance" of a pool, given the total pool balance and pledge by the pool owner/staker. The effective balance is how the influence of a pool is calculated due to its balance. +### Function: `encode_input_for_utxo` + +Given an output source id as bytes, and an output index, together representing a utxo, +this function returns the input that puts them together, as bytes. + +### Function: `encode_input_for_withdraw_from_delegation` + +Given a delegation id, an amount and a network type (mainnet, testnet, etc), this function +creates an input that withdraws from a delegation. +A nonce is needed because this spends from an account. The nonce must be in sequence for everything in that account. + ### Function: `encode_input_for_mint_tokens` Given a token_id, an amount of tokens to mint and nonce return an encoded mint tokens input @@ -354,13 +286,6 @@ Given a token_id, new authority destination and nonce return an encoded change t Given a token_id, new metadata uri and nonce return an encoded change token metadata uri input -### Function: `encode_create_order_output` - -Given ask and give amounts and a conclude key create output that creates an order. - -'ask_token_id': the parameter represents a Token if it's Some and coins otherwise. -'give_token_id': the parameter represents a Token if it's Some and coins otherwise. - ### Function: `encode_input_for_fill_order` Given an amount to fill an order (which is described in terms of ask currency) and a destination @@ -382,6 +307,81 @@ Given an order id create an input that concludes the order. Note: the nonce is only needed before the orders V1 fork activation. After the fork the nonce is ignored and any value can be passed for the parameter. +### Function: `encode_output_transfer` + +Given a destination address, an amount and a network type (mainnet, testnet, etc), this function +creates an output of type Transfer, and returns it as bytes. + +### Function: `encode_output_token_transfer` + +Given a destination address, an amount, token ID (in address form) and a network type (mainnet, testnet, etc), this function +creates an output of type Transfer for tokens, and returns it as bytes. + +### Function: `encode_output_lock_then_transfer` + +Given a valid receiving address, and a locking rule as bytes (available in this file), +and a network type (mainnet, testnet, etc), this function creates an output of type +LockThenTransfer with the parameters provided. + +### Function: `encode_output_token_lock_then_transfer` + +Given a valid receiving address, token ID (in address form), a locking rule as bytes (available in this file), +and a network type (mainnet, testnet, etc), this function creates an output of type +LockThenTransfer with the parameters provided. + +### Function: `encode_output_coin_burn` + +Given an amount, this function creates an output (as bytes) to burn a given amount of coins + +### Function: `encode_output_token_burn` + +Given an amount, token ID (in address form) and network type (mainnet, testnet, etc), +this function creates an output (as bytes) to burn a given amount of tokens + +### Function: `encode_output_create_delegation` + +Given a pool id as string, an owner address and a network type (mainnet, testnet, etc), +this function returns an output (as bytes) to create a delegation to the given pool. +The owner address is the address that is authorized to withdraw from that delegation. + +### Function: `encode_output_delegate_staking` + +Given a delegation id (as string, in address form), an amount and a network type (mainnet, testnet, etc), +this function returns an output (as bytes) that would delegate coins to be staked in the specified delegation id. + +### Function: `encode_output_create_stake_pool` + +Given a pool id, staking data as bytes and the network type (mainnet, testnet, etc), +this function returns an output that creates that staking pool. +Note that the pool id is mandated to be taken from the hash of the first input. +It is not arbitrary. + +### Function: `encode_output_issue_fungible_token` + +Given the parameters needed to issue a fungible token, and a network type (mainnet, testnet, etc), +this function creates an output that issues that token. + +### Function: `encode_output_issue_nft` + +Given the parameters needed to issue an NFT, and a network type (mainnet, testnet, etc), +this function creates an output that issues that NFT. + +### Function: `encode_output_data_deposit` + +Given data to be deposited in the blockchain, this function provides the output that deposits this data + +### Function: `encode_output_htlc` + +Given the parameters needed to create hash timelock contract, and a network type (mainnet, testnet, etc), +this function creates an output. + +### Function: `encode_create_order_output` + +Given ask and give amounts and a conclude key create output that creates an order. + +'ask_token_id': the parameter represents a Token if it's Some and coins otherwise. +'give_token_id': the parameter represents a Token if it's Some and coins otherwise. + ### Enum: `Network` The network, for which an operation to be done. Mainnet, testnet, etc. diff --git a/wasm-wrappers/js-bindings/index.html b/wasm-wrappers/js-bindings-test/index.html similarity index 100% rename from wasm-wrappers/js-bindings/index.html rename to wasm-wrappers/js-bindings-test/index.html diff --git a/wasm-wrappers/js-bindings-test/knip.json b/wasm-wrappers/js-bindings-test/knip.json new file mode 100644 index 0000000000..2d0e7f2ce6 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": ["tests/main.ts"], + "project": ["tests/**/*.ts"], + "includeEntryExports": true, + "ignoreExportsUsedInFile": true +} diff --git a/wasm-wrappers/js-bindings-test/node-entry.js b/wasm-wrappers/js-bindings-test/node-entry.js new file mode 100644 index 0000000000..b80f387c80 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/node-entry.js @@ -0,0 +1,7 @@ +import { run_all_tests } from "./dist/main.js"; + +async function node_run() { + await run_all_tests(); +} + +node_run(); diff --git a/wasm-wrappers/js-bindings-test/package.json b/wasm-wrappers/js-bindings-test/package.json new file mode 100644 index 0000000000..c688c16b72 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/package.json @@ -0,0 +1,7 @@ +{ + "name": "js-bindings-test", + "version": "0.1.0", + "license": "MIT", + "main": "index.js", + "type": "module" +} diff --git a/wasm-wrappers/js-bindings-test/tests/defs.ts b/wasm-wrappers/js-bindings-test/tests/defs.ts new file mode 100644 index 0000000000..8e531c48d3 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/defs.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Here we have some general definitions that are used by other tests. + +import { + make_private_key, + public_key_from_private_key, +} from "../../pkg/wasm_wrappers.js"; + +// Taken from TESTNET_FORK_HEIGHT_5_ORDERS_V1 in common/src/chain/config/builder.rs. +// This will be updated to the actual height after we choose one. +export const ORDERS_V1_TESTNET_FORK_HEIGHT = 999_999_999; + +export const MNEMONIC = + "walk exile faculty near leg neutral license matrix maple invite cupboard hat opinion excess coffee leopard latin regret document core limb crew dizzy movie"; + +// A random private key that is generated only once and printed to the console. +// Note: simply putting `const PRIVATE_KEY = make_private_key()` to the global scope won't +// work if the tests are run in the browser. +export const get_predefined_prv_key = (function () { + let PRIVATE_KEY: Uint8Array | null = null; + return function () { + if (!PRIVATE_KEY) { + PRIVATE_KEY = make_private_key(); + console.log(`PRIVATE_KEY = ${PRIVATE_KEY}`); + } + return PRIVATE_KEY; + } +})(); + +// The public key corresponding to get_predefined_prv_key(). +export const get_predefined_pub_key = (function () { + let PUBLIC_KEY: Uint8Array | null = null; + return function () { + if (!PUBLIC_KEY) { + PUBLIC_KEY = public_key_from_private_key(get_predefined_prv_key()); + console.log(`PUBLIC_KEY = ${PUBLIC_KEY}`); + } + return PUBLIC_KEY; + } +})(); + +// Some token id. +export const TOKEN_ID = "tmltk15tgfrs49rv88v8utcllqh0nvpaqtgvn26vdxhuner5m6ewg9c3msn9fxns"; + +// Some pool id +export const POOL_ID = "tpool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqza035u"; + +// Some order id +export const ORDER_ID = "tordr1xxt0avjtt4flkq0tnlyphmdm4aaj9vmkx5r2m4g863nw3lgf7nzs7mlkqc"; diff --git a/wasm-wrappers/js-bindings-test/tests/main.ts b/wasm-wrappers/js-bindings-test/tests/main.ts new file mode 100644 index 0000000000..bc8d234c10 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/main.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + run_one_test, +} from "./utils.js"; + +import { test_address_generation } from "./test_address_generation.js"; +import { test_encode_other_inputs } from "./test_encode_other_inputs.js"; +import { test_encode_other_outputs } from "./test_encode_other_outputs.js"; +import { test_htlc } from "./test_htlc.js"; +import { test_misc } from "./test_misc.js"; +import { test_orders } from "./test_orders.js"; +import { test_signed_transaction_intent } from "./test_signed_transaction_intent.js"; +import { test_transaction_and_witness_encoding } from "./test_transaction_and_witness_encoding.js"; + +/** @public */ +export function run_all_tests() { + run_one_test(test_address_generation); + run_one_test(test_encode_other_inputs); + run_one_test(test_encode_other_outputs); + run_one_test(test_htlc); + run_one_test(test_misc); + run_one_test(test_orders); + run_one_test(test_signed_transaction_intent); + run_one_test(test_transaction_and_witness_encoding); +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_address_generation.ts b/wasm-wrappers/js-bindings-test/tests/test_address_generation.ts new file mode 100644 index 0000000000..b127748063 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_address_generation.ts @@ -0,0 +1,156 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + extended_public_key_from_extended_private_key, + make_change_address, + make_change_address_public_key, + make_default_account_privkey, + make_receiving_address, + make_receiving_address_public_key, + Network, + public_key_from_private_key, + pubkey_to_pubkeyhash_address, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays, + get_err_msg, + run_one_test, + TEXT_ENCODER, +} from "./utils.js"; + +import { + MNEMONIC, +} from "./defs.js"; + +// Some address. +// It corresponds to `make_receiving_address(make_default_account_privkey(MNEMONIC,Network.Testnet), 0)`, +// but most tests don't care. +export const ADDRESS = "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n"; + +export function test_address_generation() { + run_one_test(predefined_address_test); + run_one_test(general_test); +} + +export function predefined_address_test() { + const account_private_key = make_default_account_privkey( + MNEMONIC, + Network.Testnet + ); + console.log(`acc private key = ${account_private_key}`); + + const receiving_privkey = make_receiving_address(account_private_key, 0); + console.log(`receiving privkey = ${receiving_privkey}`); + + const receiving_pubkey = public_key_from_private_key(receiving_privkey); + const address = pubkey_to_pubkeyhash_address( + receiving_pubkey, + Network.Testnet + ); + console.log(`address = ${address}`); + if (address != ADDRESS) { + throw new Error("Incorrect address generated"); + } +} + +export function general_test() { + const bad_priv_key = TEXT_ENCODER.encode("bad"); + + try { + make_receiving_address(bad_priv_key, 0); + throw new Error("Invalid private key worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid private key encoding")) { + throw e; + } + console.log("Tested decoding bad account private key successfully"); + } + + try { + make_change_address(bad_priv_key, 0); + throw new Error("Invalid private key worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid private key encoding")) { + throw e; + } + console.log("Tested decoding bad account private key successfully"); + } + + { + const account_private_key = make_default_account_privkey( + MNEMONIC, + Network.Mainnet + ); + console.log(`acc private key = ${account_private_key}`); + + const extended_public_key = extended_public_key_from_extended_private_key(account_private_key); + + const receiving_privkey = make_receiving_address(account_private_key, 0); + console.log(`receiving privkey = ${receiving_privkey}`); + + // test bad key index + try { + make_receiving_address(account_private_key, 1 << 31); + throw new Error("Invalid key index worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid key index, MSB bit set")) { + throw e; + } + console.log("Tested invalid key index with set MSB bit successfully"); + } + + const receiving_pubkey = public_key_from_private_key(receiving_privkey); + const receiving_pubkey2 = make_receiving_address_public_key(extended_public_key, 0); + assert_eq_arrays(receiving_pubkey, receiving_pubkey2); + + const address = pubkey_to_pubkeyhash_address( + receiving_pubkey, + Network.Mainnet + ); + console.log(`address = ${address}`); + if (address != "mtc1qyqmdpxk2w42w37qsdj0e8g54ysvnlvpny3svzqx") { + throw new Error("Incorrect address generated"); + } + + const change_privkey = make_change_address(account_private_key, 0); + console.log(`change privkey = ${change_privkey}`); + + // test bad key index + try { + make_change_address(account_private_key, 1 << 31); + throw new Error("Invalid key index worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid key index, MSB bit set")) { + throw e; + } + console.log("Tested invalid key index with set MSB bit successfully"); + } + + const change_pubkey = public_key_from_private_key(change_privkey); + const change_pubkey2 = make_change_address_public_key(extended_public_key, 0); + assert_eq_arrays(change_pubkey, change_pubkey2); + + const caddress = pubkey_to_pubkeyhash_address( + change_pubkey, + Network.Mainnet + ); + console.log(`address = ${caddress}`); + if (caddress != "mtc1qxyhrpytqrvjalg2dzw4tdvzt2zz8ps6nyav2n56") { + throw new Error("Incorrect address generated"); + } + } +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_encode_other_inputs.ts b/wasm-wrappers/js-bindings-test/tests/test_encode_other_inputs.ts new file mode 100644 index 0000000000..377d1117d5 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_encode_other_inputs.ts @@ -0,0 +1,220 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Here we test inputs encoding, except for orders and htlcs, which have their separate test files. + +import { + Amount, + encode_input_for_change_token_authority, + encode_input_for_change_token_metadata_uri, + encode_input_for_freeze_token, + encode_input_for_lock_token_supply, + encode_input_for_mint_tokens, + encode_input_for_unfreeze_token, + encode_input_for_unmint_tokens, + encode_input_for_utxo, + encode_input_for_withdraw_from_delegation, + Network, + TokenUnfreezable, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays, + get_err_msg, + run_one_test, + TEXT_ENCODER, +} from "./utils.js"; + +import { + TOKEN_ID, +} from "./defs.js"; +import { + ADDRESS +} from "./test_address_generation.js"; + +// Some test inputs - a UTXO and a delegation withdrawal +export const INPUTS = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, +]; + +// OutpointSourceId used in INPUTS. +export const TX_OUTPOINT = new Uint8Array(33).fill(0) + +export function test_encode_other_inputs() { + run_one_test(predefined_inputs_test); + run_one_test(general_test); +} + +function predefined_inputs_test() { + const tx_input = encode_input_for_utxo(TX_OUTPOINT, 1); + const deleg_id = + "mdelg1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqut3aj8"; + const tx_input2 = encode_input_for_withdraw_from_delegation( + deleg_id, + Amount.from_atoms("1"), + BigInt(1), + Network.Mainnet + ); + const inputs = [...tx_input, ...tx_input2]; + assert_eq_arrays(inputs, INPUTS); +} + +export function general_test() { + try { + encode_input_for_utxo(TEXT_ENCODER.encode("asd"), 1); + throw new Error("Invalid outpoint encoding worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid outpoint ID encoding")) { + throw e; + } + console.log("Tested invalid outpoint ID successfully"); + } + + try { + encode_input_for_withdraw_from_delegation( + "invalid delegation id", + Amount.from_atoms("1"), + BigInt(1), + Network.Mainnet + ); + throw new Error("Invalid delegation id encoding worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log("Tested invalid delegation id in account successfully"); + } + + const mint_tokens_input = encode_input_for_mint_tokens( + TOKEN_ID, + Amount.from_atoms("100"), + BigInt(1), + Network.Testnet + ); + const expected_mint_tokens_input = [ + 2, 4, 0, 162, 208, 145, 194, 165, 27, + 14, 118, 31, 139, 199, 254, 11, 190, 108, + 15, 64, 180, 50, 106, 211, 26, 107, 242, + 121, 29, 55, 172, 185, 5, 196, 119, 145, + 1 + ]; + + assert_eq_arrays(mint_tokens_input, expected_mint_tokens_input); + console.log("mint tokens encoding ok"); + + const unmint_tokens_input = encode_input_for_unmint_tokens( + TOKEN_ID, + BigInt(2), + Network.Testnet + ); + const expected_unmint_tokens_input = [ + 2, 8, 1, 162, 208, 145, 194, 165, + 27, 14, 118, 31, 139, 199, 254, 11, + 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, + 5, 196, 119 + ]; + + assert_eq_arrays(unmint_tokens_input, expected_unmint_tokens_input); + console.log("unmint tokens encoding ok"); + + const lock_token_supply_input = encode_input_for_lock_token_supply( + TOKEN_ID, + BigInt(2), + Network.Testnet + ); + const expected_lock_token_supply_input = [ + 2, 8, 2, 162, 208, 145, 194, 165, + 27, 14, 118, 31, 139, 199, 254, 11, + 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, + 5, 196, 119 + ]; + + assert_eq_arrays(lock_token_supply_input, expected_lock_token_supply_input); + console.log("lock token supply encoding ok"); + + const freeze_token_input = encode_input_for_freeze_token( + TOKEN_ID, + TokenUnfreezable.Yes, + BigInt(2), + Network.Testnet + ); + const expected_freeze_token_input = [ + 2, 8, 3, 162, 208, 145, 194, 165, + 27, 14, 118, 31, 139, 199, 254, 11, + 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, + 5, 196, 119, 1 + ]; + + assert_eq_arrays(freeze_token_input, expected_freeze_token_input); + console.log("freeze token encoding ok"); + + const unfreeze_token_input = encode_input_for_unfreeze_token( + TOKEN_ID, + BigInt(2), + Network.Testnet + ); + const expected_unfreeze_token_input = [ + 2, 8, 4, 162, 208, 145, 194, 165, + 27, 14, 118, 31, 139, 199, 254, 11, + 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, + 5, 196, 119 + ]; + + assert_eq_arrays(unfreeze_token_input, expected_unfreeze_token_input); + console.log("unfreeze token encoding ok"); + + const change_token_authority_input = encode_input_for_change_token_authority( + TOKEN_ID, + ADDRESS, + BigInt(2), + Network.Testnet + ); + const expected_change_token_authority_input = [ + 2, 8, 5, 162, 208, 145, 194, 165, 27, 14, 118, + 31, 139, 199, 254, 11, 190, 108, 15, 64, 180, 50, + 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, + 196, 119, 1, 91, 58, 110, 176, 100, 207, 6, 194, + 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, + 178 + ]; + + assert_eq_arrays(change_token_authority_input, expected_change_token_authority_input); + console.log("change token authority encoding ok"); + + const change_token_metadata_uri = encode_input_for_change_token_metadata_uri( + TOKEN_ID, + ADDRESS, + BigInt(2), + Network.Testnet + ); + const expected_change_token_metadata_uri = [ + 2, 8, 8, 162, 208, 145, 194, 165, 27, 14, 118, 31, + 139, 199, 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, 5, 196, 119, 176, + 116, 109, 116, 49, 113, 57, 100, 110, 53, 109, 52, 115, + 118, 110, 56, 115, 100, 115, 51, 102, 99, 121, 48, 57, + 107, 112, 120, 114, 101, 102, 110, 117, 55, 53, 120, 101, + 107, 103, 114, 53, 119, 97, 51, 110 + ]; + + assert_eq_arrays(change_token_metadata_uri, expected_change_token_metadata_uri); + console.log("change token metadata uri encoding ok"); +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts b/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts new file mode 100644 index 0000000000..291b2959b5 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_encode_other_outputs.ts @@ -0,0 +1,523 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Here we test outputs encoding, except for orders and htlcs, which have their separate test files. + +import { + Amount, + FreezableToken, + encode_lock_until_height, + encode_output_coin_burn, + encode_output_create_stake_pool, + encode_output_issue_fungible_token, + encode_output_issue_nft, + encode_output_lock_then_transfer, + encode_output_token_burn, + encode_output_token_lock_then_transfer, + encode_output_token_transfer, + encode_stake_pool_data, + make_default_account_privkey, + make_receiving_address, + Network, + public_key_from_private_key, + TotalSupply, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays, + get_err_msg, + run_one_test, + TEXT_ENCODER, +} from "./utils.js"; + +import { + MNEMONIC, + POOL_ID, + TOKEN_ID, +} from "./defs.js"; +import { ADDRESS } from "./test_address_generation.js"; + +export const OUTPUT_LOCK_THEN_TRANSFER = [ + 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, + 195, 202, 103, 207, 80, 217, 178, 0, 145, 1 +]; + +export const OUTPUT_CREATE_STAKE_POOL = [ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, 195, + 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, 158, 169, + 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, 217, 2, + 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, 194, + 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, +]; + +// Some tx outputs - LockThenTransfer and CreateStakePool +export const OUTPUTS = [...OUTPUT_LOCK_THEN_TRANSFER, ...OUTPUT_CREATE_STAKE_POOL]; + +export function test_encode_other_outputs() { + run_one_test(create_stake_pool_test); + run_one_test(stake_pool_data_test); + run_one_test(coin_burn_test); + run_one_test(token_burn_test); + run_one_test(lock_then_transfer_test); + run_one_test(token_lock_then_transfer_test); + run_one_test(token_transfer_test); + run_one_test(issue_fungible_token_test); + run_one_test(issue_nft_test); +} + +function create_stake_pool_test() { + const vrf_public_key = + "tvrfpk1qpk0t6np4gyl084fv328h6ahjvwcsaktrzfrs0xeqtrzpp0l7p28knrnn57"; + + const pool_data = encode_stake_pool_data( + Amount.from_atoms("40000"), + ADDRESS, + vrf_public_key, + ADDRESS, + 100, + Amount.from_atoms("0"), + Network.Testnet + ); + const expected_pool_data = [ + 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, + 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, 158, + 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, 217, + 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, 194, + 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, + ]; + + assert_eq_arrays(pool_data, expected_pool_data); + + const lock = encode_lock_until_height(BigInt(100)); + const output = encode_output_lock_then_transfer( + Amount.from_atoms("100"), + ADDRESS, + lock, + Network.Testnet + ); + + try { + const invalid_pool_data = TEXT_ENCODER.encode("invalid pool data"); + encode_output_create_stake_pool( + POOL_ID, + invalid_pool_data, + Network.Testnet + ); + throw new Error("Invalid pool data worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid stake pool data encoding")) { + throw e; + } + console.log("Tested invalid pool data successfully"); + } + const stake_pool_output = encode_output_create_stake_pool( + POOL_ID, + pool_data, + Network.Testnet + ); + const outputs = [...output, ...stake_pool_output]; + + assert_eq_arrays(outputs, OUTPUTS); +} + +function stake_pool_data_test() { + const vrf_public_key = + "tvrfpk1qpk0t6np4gyl084fv328h6ahjvwcsaktrzfrs0xeqtrzpp0l7p28knrnn57"; + + try { + const invalid_margin_ratio_per_thousand = 2000; + encode_stake_pool_data( + Amount.from_atoms("40000"), + ADDRESS, + vrf_public_key, + ADDRESS, + invalid_margin_ratio_per_thousand, + Amount.from_atoms("0"), + Network.Testnet + ); + throw new Error("Invalid margin_ratio_per_thousand worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid per thousand 2000, valid range is [0, 1000]")) { + throw e; + } + console.log("Tested invalid margin_ratio_per_thousand successfully"); + } +} + +function coin_burn_test() { + try { + encode_output_coin_burn(Amount.from_atoms("invalid amount")); + throw new Error("Invalid value for amount worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid atoms amount")) { + throw e; + } + console.log("Tested invalid amount successfully"); + } +} + +function token_burn_test() { + try { + encode_output_token_burn( + Amount.from_atoms("invalid amount"), + TOKEN_ID, + Network.Testnet + ); + throw new Error("Invalid value for amount worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid atoms amount")) { + throw e; + } + console.log("Tested invalid amount successfully"); + } + try { + const invalid_token_id = "asd"; + encode_output_token_burn( + Amount.from_atoms("100"), + invalid_token_id, + Network.Testnet + ); + throw new Error("Invalid token id worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log("Tested invalid token id successfully for token burn"); + } + + const token_burn = encode_output_token_burn( + Amount.from_atoms("100"), + TOKEN_ID, + Network.Testnet + ); + const expected_token_burn = [ + 2, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, + 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, + 196, 119, 145, 1, + ]; + assert_eq_arrays(token_burn, expected_token_burn); +} + +function lock_then_transfer_test() { + try { + const invalid_lock = TEXT_ENCODER.encode("invalid lock"); + encode_output_lock_then_transfer( + Amount.from_atoms("100"), + ADDRESS, + invalid_lock, + Network.Testnet + ); + throw new Error("Invalid lock worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid time lock encoding")) { + throw e; + } + console.log("Tested invalid lock successfully"); + } +} + +function token_lock_then_transfer_test() { + try { + const invalid_lock = TEXT_ENCODER.encode("invalid lock"); + encode_output_token_lock_then_transfer( + Amount.from_atoms("100"), + ADDRESS, + TOKEN_ID, + invalid_lock, + Network.Testnet + ); + throw new Error("Invalid lock worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid time lock encoding")) { + throw e; + } + console.log("Tested invalid token lock successfully"); + } + + try { + const invalid_token_id = "asd"; + const lock = encode_lock_until_height(BigInt(100)); + encode_output_token_lock_then_transfer( + Amount.from_atoms("100"), + ADDRESS, + invalid_token_id, + lock, + Network.Testnet + ); + throw new Error("Invalid token id worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log("Tested invalid token id successfully"); + } + + const lock = encode_lock_until_height(BigInt(100)); + + const token_lock_transfer_out = encode_output_token_lock_then_transfer( + Amount.from_atoms("100"), + ADDRESS, + TOKEN_ID, + lock, + Network.Testnet + ); + const expected_token_lock_transfer_out = [ + 1, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, + 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, + 196, 119, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, + 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, + ]; + assert_eq_arrays(token_lock_transfer_out, expected_token_lock_transfer_out); +} + +function token_transfer_test() { + try { + const invalid_address = "invalid address"; + encode_output_token_transfer( + Amount.from_atoms("100"), + invalid_address, + TOKEN_ID, + Network.Testnet + ); + throw new Error("Invalid address worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log( + "Tested invalid address in encode output token transfer successfully" + ); + } + + try { + const invalid_token_id = "invalid token"; + encode_output_token_transfer( + Amount.from_atoms("100"), + ADDRESS, + invalid_token_id, + Network.Testnet + ); + throw new Error("Invalid token id worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log( + "Tested invalid token id successfully in output token transfer" + ); + } + + const token_transfer_out = encode_output_token_transfer( + Amount.from_atoms("100"), + ADDRESS, + TOKEN_ID, + Network.Testnet + ); + const expected_token_transfer_out = [ + 0, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, + 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, + 196, 119, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, + 4, 195, 202, 103, 207, 80, 217, 178, + ]; + + assert_eq_arrays(token_transfer_out, expected_token_transfer_out); +} + +function issue_fungible_token_test() { + let encoded_fungible_token = encode_output_issue_fungible_token( + ADDRESS, + "XXX", + "http://uri.com", + 2, + TotalSupply.Unlimited, + null, + FreezableToken.Yes, + BigInt(1), + Network.Testnet + ); + + const expected_fungible_token = [ + 7, 1, 12, 88, 88, 88, 2, 56, 104, 116, + 116, 112, 58, 47, 47, 117, 114, 105, 46, 99, + 111, 109, 2, 1, 91, 58, 110, 176, 100, 207, + 6, 194, 41, 193, 30, 91, 4, 195, 202, 103, + 207, 80, 217, 178, 1 + ]; + + assert_eq_arrays(encoded_fungible_token, expected_fungible_token); +} + +function issue_nft_test() { + const account_pubkey = make_default_account_privkey( + MNEMONIC, + Network.Testnet + ); + const receiving_privkey = make_receiving_address(account_pubkey, 0); + const receiving_pubkey = public_key_from_private_key(receiving_privkey); + + let encoded_nft = encode_output_issue_nft( + TOKEN_ID, + ADDRESS, + "nft", + "XXX", + "desc", + Uint8Array.from([1, 2, 3, 4]), + receiving_pubkey, + "http://uri", + "http://icon", + "http://foo", + BigInt(1), + Network.Testnet + ); + + const expected_nft_encoding = [ + 8, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, + 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, 26, 107, + 242, 121, 29, 55, 172, 185, 5, 196, 119, 0, 1, 0, + 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, + 180, 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, + 130, 186, 209, 98, 181, 118, 233, 133, 89, 12, 110, 102, + 116, 16, 100, 101, 115, 99, 12, 88, 88, 88, 44, 104, + 116, 116, 112, 58, 47, 47, 105, 99, 111, 110, 40, 104, + 116, 116, 112, 58, 47, 47, 102, 111, 111, 40, 104, 116, + 116, 112, 58, 47, 47, 117, 114, 105, 16, 1, 2, 3, + 4, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, + 30, 91, 4, 195, 202, 103, 207, 80, 217, 178 + ]; + + assert_eq_arrays(encoded_nft, expected_nft_encoding); + + try { + const invalid_token_id = "asd"; + encode_output_issue_nft( + invalid_token_id, + ADDRESS, + "nft", + "XXX", + "desc", + Uint8Array.from([1, 2, 3, 4, 5]), + undefined, + undefined, + undefined, + undefined, + BigInt(1), + Network.Testnet + ); + throw new Error("Invalid token id worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log("Tested invalid token id successfully"); + } + + try { + const creator_public_key_hash = Uint8Array.from([1, 2, 3, 4, 5]); + encode_output_issue_nft( + TOKEN_ID, + ADDRESS, + "nft", + "XXX", + "desc", + Uint8Array.from([1, 2, 3]), + creator_public_key_hash, + undefined, + undefined, + undefined, + BigInt(1), + Network.Testnet + ); + throw new Error("Invalid creator worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Cannot decode NFT creator as a public key")) { + throw e; + } + console.log("Tested invalid creator successfully"); + } + + try { + const empty_ticker = ""; + encode_output_issue_nft( + TOKEN_ID, + ADDRESS, + "nft", + empty_ticker, + "desc", + Uint8Array.from([1, 2, 3]), + undefined, + undefined, + undefined, + undefined, + BigInt(1), + Network.Testnet + ); + throw new Error("Invalid ticker worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid ticker length")) { + throw e; + } + console.log("Tested invalid ticker successfully"); + } + + try { + const empty_name = ""; + encode_output_issue_nft( + TOKEN_ID, + ADDRESS, + empty_name, + "xxx", + "desc", + Uint8Array.from([1, 2, 3]), + undefined, + undefined, + undefined, + undefined, + BigInt(1), + Network.Testnet + ); + throw new Error("Invalid name worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid name length")) { + throw e; + } + console.log("Tested invalid name successfully"); + } + + try { + const empty_description = ""; + encode_output_issue_nft( + TOKEN_ID, + ADDRESS, + "name", + "XXX", + empty_description, + Uint8Array.from([1, 2, 3]), + undefined, + undefined, + undefined, + undefined, + BigInt(1), + Network.Testnet + ); + throw new Error("Invalid description worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid description length")) { + throw e; + } + console.log("Tested invalid description successfully"); + } +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_htlc.ts b/wasm-wrappers/js-bindings-test/tests/test_htlc.ts new file mode 100644 index 0000000000..6d2cc24d65 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_htlc.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Amount, + encode_lock_until_height, + encode_multisig_challenge, + encode_output_htlc, + encode_signed_transaction, + encode_transaction, + encode_witness_htlc_multisig, + encode_witness_htlc_secret, + extract_htlc_secret, + make_default_account_privkey, + make_private_key, + make_receiving_address, + public_key_from_private_key, + Network, + SignatureHashType, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays +} from "./utils.js"; + +import { + MNEMONIC, + TOKEN_ID, +} from "./defs.js"; +import { + ADDRESS +} from "./test_address_generation.js"; +import { + INPUTS, + TX_OUTPOINT, +} from "./test_encode_other_inputs.js"; +import { + OUTPUTS, +} from "./test_encode_other_outputs.js"; + +export function test_htlc() { + const account_pubkey = make_default_account_privkey( + MNEMONIC, + Network.Testnet + ); + const receiving_privkey = make_receiving_address(account_pubkey, 0); + + const secret = [0, 229, 233, 72, 110, 22, 64, 36, 69, 188, 238, 51, 130, 168, 185, 241, 73, 48, 120, 151, 140, 45, 46, 39, 50, 207, 18, 50, 243, 30, 115, 93] + const secret_hash = "b5a48c7780e597de8012346fb30761965248e3f2" + + const htlc_coins_output = encode_output_htlc( + Amount.from_atoms("40000"), + undefined, + secret_hash, + ADDRESS, + ADDRESS, + encode_lock_until_height(BigInt(100)), + Network.Testnet + ); + console.log("htlc with coins encoding ok"); + + const htlc_tokens_output = encode_output_htlc( + Amount.from_atoms("40000"), + TOKEN_ID, + secret_hash, + ADDRESS, + ADDRESS, + encode_lock_until_height(BigInt(100)), + Network.Testnet + ); + console.log("htlc with tokens encoding ok"); + + const opt_htlc_utxos = [1, ...htlc_coins_output, 1, ...htlc_tokens_output]; + const tx = encode_transaction(Uint8Array.from(INPUTS), Uint8Array.from(OUTPUTS), BigInt(0)); + // encode witness with secret + const witness_with_htlc_secret = encode_witness_htlc_secret( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + tx, + Uint8Array.from(opt_htlc_utxos), + 0, + Uint8Array.from(secret), + Network.Testnet + ); + console.log("Tested encode witness with htlc secret successfully"); + + // encode multisig challenge + const alice_sk = make_private_key(); + const alice_pk = public_key_from_private_key(alice_sk); + const bob_sk = make_private_key(); + const bob_pk = public_key_from_private_key(bob_sk); + let challenge = encode_multisig_challenge(Uint8Array.from([...alice_pk, ...bob_pk]), 2, Network.Testnet); + console.log("Tested multisig challenge successfully"); + + // encode mutlisig witness + const witness_with_htlc_multisig_1 = encode_witness_htlc_multisig( + SignatureHashType.ALL, + alice_sk, + 0, + new Uint8Array([]), + challenge, + tx, + Uint8Array.from(opt_htlc_utxos), + 1, + Network.Testnet + ); + console.log("Tested encode multisig witness 0 successfully"); + + const witness_with_htlc_multisig = encode_witness_htlc_multisig( + SignatureHashType.ALL, + bob_sk, + 1, + witness_with_htlc_multisig_1, + challenge, + tx, + Uint8Array.from(opt_htlc_utxos), + 1, + Network.Testnet + ); + console.log("Tested encode multisig witness 1 successfully"); + + // encode signed tx with secret and multi + const htlc_signed_tx = encode_signed_transaction(tx, Uint8Array.from([...witness_with_htlc_secret, ...witness_with_htlc_multisig])); + // extract secret from signed tx + const secret_extracted = extract_htlc_secret(htlc_signed_tx, true, TX_OUTPOINT, 1); + assert_eq_arrays(secret, secret_extracted); +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_misc.ts b/wasm-wrappers/js-bindings-test/tests/test_misc.ts new file mode 100644 index 0000000000..29b20862b6 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_misc.ts @@ -0,0 +1,302 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Amount, + get_token_id, + get_transaction_id, + effective_pool_balance, + make_default_account_privkey, + make_private_key, + Network, + pubkey_to_pubkeyhash_address, + public_key_from_private_key, + sign_challenge, + sign_message_for_spending, + staking_pool_spend_maturity_block_count, + verify_challenge, + verify_signature_for_spending, +} from "../../pkg/wasm_wrappers.js"; + +import { + gen_random_int, + get_err_msg, + run_one_test, + TEXT_ENCODER, +} from "./utils.js"; + +import { + get_predefined_prv_key, + get_predefined_pub_key, +} from "./defs.js"; +import { + INPUTS, +} from "./test_encode_other_inputs.js"; + +export function test_misc() { + run_one_test(test_verify_signature_for_spending); + run_one_test(test_public_key_from_bad_private_key); + run_one_test(test_make_default_account_privkey_from_bad_mnemonic); + run_one_test(test_sign_challenge); + run_one_test(test_staking_pool_spend_maturity_block_count); + run_one_test(test_get_token_id); + run_one_test(test_effective_pool_balance); + run_one_test(test_get_transaction_id); +} + +function test_verify_signature_for_spending() { + const prv_key = get_predefined_prv_key(); + const pub_key = get_predefined_pub_key(); + const message = TEXT_ENCODER.encode("Hello, world!"); + + const signature = sign_message_for_spending(prv_key, message); + + const verified = verify_signature_for_spending(pub_key, signature, message); + + if (!verified) { + throw new Error("Signature verification failed!"); + } + const verified_bad = verify_signature_for_spending( + pub_key, + signature, + TEXT_ENCODER.encode("bro!") + ); + if (verified_bad) { + throw new Error("Invalid message signature verification passed!"); + } +} + +function test_public_key_from_bad_private_key() { + // Attempt to use a bad private key to get a public key (test returned Result<> object, which will become a string error) + const bad_priv_key = TEXT_ENCODER.encode("bad"); + try { + public_key_from_private_key(bad_priv_key); + throw new Error("Invalid private key worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid private key encoding")) { + throw new Error( + "Invalid private key resulted in an unexpected error message!" + ); + } + console.log("Tested decoding bad private key successfully"); + } +} + +function test_make_default_account_privkey_from_bad_mnemonic() { + try { + const invalid_mnemonic = "asd asd"; + make_default_account_privkey(invalid_mnemonic, Network.Mainnet); + throw new Error("Invalid mnemonic worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid mnemonic string")) { + throw e; + } + console.log("Tested invalid mnemonic successfully"); + } +} + +function test_sign_challenge() { + const prv_key = get_predefined_prv_key(); + const pub_key = get_predefined_pub_key(); + const message = TEXT_ENCODER.encode("Hello, world!"); + + let challenge = sign_challenge(prv_key, message); + let address = pubkey_to_pubkeyhash_address(pub_key, Network.Testnet); + let result = verify_challenge(address, Network.Testnet, challenge, message); + if (!result) { + throw new Error("Invalid sing and verify challenge"); + } + + const different_priv_key = make_private_key(); + const different_pub_key = public_key_from_private_key(different_priv_key); + let different_address = pubkey_to_pubkeyhash_address(different_pub_key, Network.Testnet); + try { + verify_challenge(different_address, Network.Testnet, challenge, message); + } catch (e) { + if (!get_err_msg(e).includes("Public key to public key hash mismatch")) { + throw e; + } + console.log("Tested verify with different address successfully"); + } +} + +function test_staking_pool_spend_maturity_block_count() { + const lock_for_blocks = staking_pool_spend_maturity_block_count( + BigInt(1000), + Network.Mainnet + ); + console.log(`lock for blocks ${lock_for_blocks}`); + if (lock_for_blocks != BigInt(7200)) { + throw new Error("Incorrect lock for blocks"); + } +} + +function test_get_token_id() { + try { + get_token_id(new Uint8Array(), BigInt(1), Network.Testnet); + throw "Token Id generated without a UTXO input somehow!"; + } catch (e) { + const msg = get_err_msg(e); + if (!(msg.includes("No UTXO inputs for token id creation") || + msg.includes("No inputs for token id creation"))) { + throw e; + } + console.log("Tested no UTXO inputs for token ID successfully"); + } + + { + const expected_token_id = + "tmltk13cncdptay55g9ajhrkaw0fp46r0tspq9kptul8vj2q7yvd69n4zsl24gea"; + const token_id = get_token_id(Uint8Array.from(INPUTS), BigInt(1), Network.Testnet); + console.log(token_id); + + if (token_id != expected_token_id) { + throw new Error("Different token id"); + } + } +} + +function test_effective_pool_balance() { + { + const eff_bal = effective_pool_balance( + Network.Mainnet, + Amount.from_atoms("0"), + Amount.from_atoms("0") + ); + if (eff_bal.atoms() != "0") { + throw new Error(`Effective balance test failed ${eff_bal}`); + } + } + + { + const eff_bal = effective_pool_balance( + Network.Mainnet, + Amount.from_atoms("4000000000000000"), + Amount.from_atoms("20000000000000000") + ); + if (eff_bal.atoms() != "18679147907594054") { + throw new Error(`Effective balance test failed ${eff_bal}`); + } + } + + { + // capped + const eff_bal = effective_pool_balance( + Network.Mainnet, + Amount.from_atoms("59999080000000000"), + Amount.from_atoms("59999080000000000") + ); + if (eff_bal.atoms() != "59999080000000000") { + throw new Error(`Effective balance test failed ${eff_bal}`); + } + } + + { + // over capped + const over_capped = gen_random_int(0, 4, "over_capped"); + const capped = 6 + over_capped; + const eff_bal = effective_pool_balance( + Network.Mainnet, + Amount.from_atoms(`${capped}0000000000000000`), + Amount.from_atoms(`${capped}0000000000000000`) + ); + if (eff_bal.atoms() != "59999080000000000") { + throw new Error(`Effective balance test failed ${eff_bal}`); + } + } +} + +function test_get_transaction_id() { + const tx_bin = [ + 1, 0, 4, 0, 0, 255, 93, 154, 148, 57, 14, 233, 114, 8, 211, 26, 165, 195, + 181, 221, 189, 141, 249, 211, 8, 6, 157, 242, 235, 245, 40, 63, 124, 227, + 228, 38, 20, 1, 0, 0, 0, 8, 3, 64, 249, 146, 78, 77, 160, 175, 125, 200, + 197, 190, 113, 169, 201, 224, 89, 98, 199, 191, 78, 249, 97, 39, 253, 231, + 167, 180, 225, 70, 158, 72, 98, 15, 0, 128, 224, 55, 121, 195, 17, 2, 0, 3, + 101, 128, 126, 59, 65, 71, 203, 151, 139, 120, 113, 94, 96, 96, 96, 146, + 248, 157, 199, 105, 88, 110, 152, 69, 104, 80, 189, 59, 68, 156, 135, 180, + 0, 32, 48, 21, 233, 239, 159, 193, 66, 86, 158, 15, 150, 107, 192, 24, 132, + 100, 250, 113, 42, 132, 30, 20, 0, 46, 15, 233, 82, 160, 118, 162, 108, 1, + 229, 57, 197, 240, 206, 186, 146, 122, 184, 248, 245, 95, 39, 74, 247, 57, + 206, 78, 239, 55, 0, 0, 11, 0, 32, 74, 169, 209, 1, 0, 0, 11, 64, 158, 76, + 53, 93, 1, 1, 153, 228, 236, 58, 91, 23, 97, 64, 239, 156, 213, 140, 125, + 53, 121, 253, 176, 236, 178, 26, + ]; + + const tx_signed_bin = [ + 1, 0, 4, 0, 0, 255, 93, 154, 148, 57, 14, 233, 114, 8, 211, 26, 165, 195, + 181, 221, 189, 141, 249, 211, 8, 6, 157, 242, 235, 245, 40, 63, 124, 227, + 228, 38, 20, 1, 0, 0, 0, 8, 3, 64, 249, 146, 78, 77, 160, 175, 125, 200, + 197, 190, 113, 169, 201, 224, 89, 98, 199, 191, 78, 249, 97, 39, 253, 231, + 167, 180, 225, 70, 158, 72, 98, 15, 0, 128, 224, 55, 121, 195, 17, 2, 0, 3, + 101, 128, 126, 59, 65, 71, 203, 151, 139, 120, 113, 94, 96, 96, 96, 146, + 248, 157, 199, 105, 88, 110, 152, 69, 104, 80, 189, 59, 68, 156, 135, 180, + 0, 32, 48, 21, 233, 239, 159, 193, 66, 86, 158, 15, 150, 107, 192, 24, 132, + 100, 250, 113, 42, 132, 30, 20, 0, 46, 15, 233, 82, 160, 118, 162, 108, 1, + 229, 57, 197, 240, 206, 186, 146, 122, 184, 248, 245, 95, 39, 74, 247, 57, + 206, 78, 239, 55, 0, 0, 11, 0, 32, 74, 169, 209, 1, 0, 0, 11, 64, 158, 76, + 53, 93, 1, 1, 153, 228, 236, 58, 91, 23, 97, 64, 239, 156, 213, 140, 125, + 53, 121, 253, 176, 236, 178, 26, 4, 1, 1, 141, 1, 0, 2, 237, 221, 0, 59, + 251, 99, 51, 18, 62, 104, 42, 190, 105, 35, 218, 29, 56, 250, 164, 240, 224, + 217, 226, 238, 66, 213, 170, 70, 193, 82, 163, 72, 0, 167, 73, 163, 12, 140, + 156, 51, 105, 108, 228, 7, 252, 20, 94, 188, 152, 36, 225, 123, 119, 141, + 13, 156, 204, 129, 41, 190, 82, 243, 123, 116, 22, 14, 96, 246, 104, 154, + 194, 244, 129, 7, 30, 26, 99, 217, 207, 15, 110, 171, 132, 194, 112, 59, 94, + 159, 34, 156, 216, 24, 140, 224, 146, 237, 212, + ]; + + const expected_tx_id = + "35a7938c2a2aad5ae324e7d0536de245bf9e439169aa3c16f1492be117e5d0e0"; + + { + const tx_id = get_transaction_id(Uint8Array.from(tx_bin), true); + if (tx_id != expected_tx_id) { + throw new Error( + `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` + ); + } + } + + { + const tx_id = get_transaction_id(Uint8Array.from(tx_bin), false); + if (tx_id != expected_tx_id) { + throw new Error( + `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` + ); + } + } + + { + const tx_id = get_transaction_id(Uint8Array.from(tx_signed_bin), false); + if (tx_id != expected_tx_id) { + throw new Error( + `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` + ); + } + } + + { + try { + get_transaction_id(Uint8Array.from(tx_signed_bin), true); + throw new Error("Invalid witnesses worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction encoding")) { + throw new Error( + "Invalid transaction encoding resulted in an unexpected error message!" + ); + } + } + } +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_orders.ts b/wasm-wrappers/js-bindings-test/tests/test_orders.ts new file mode 100644 index 0000000000..d3c8699dc9 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_orders.ts @@ -0,0 +1,191 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Amount, + encode_create_order_output, + encode_input_for_conclude_order, + encode_input_for_fill_order, + encode_input_for_freeze_order, + Network, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays, + gen_random_int, + get_err_msg, +} from "./utils.js"; + +import { + ORDER_ID, + ORDERS_V1_TESTNET_FORK_HEIGHT, + TOKEN_ID, +} from "./defs.js"; +import { + ADDRESS +} from "./test_address_generation.js"; + +export function test_orders() { + const order_output = encode_create_order_output( + Amount.from_atoms("40000"), + undefined, + Amount.from_atoms("10000"), + TOKEN_ID, + ADDRESS, + Network.Testnet + ); + const expected_order_output = [ + 11, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, + 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, + 0, 2, 113, 2, 0, 2, 162, 208, 145, 194, 165, + 27, 14, 118, 31, 139, 199, 254, 11, 190, 108, 15, + 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, + 172, 185, 5, 196, 119, 65, 156 + ]; + + assert_eq_arrays(order_output, expected_order_output); + console.log("create order coins for tokens encoding ok"); + + const create_order_output_2 = encode_create_order_output( + Amount.from_atoms("10000"), + TOKEN_ID, + Amount.from_atoms("40000"), + undefined, + ADDRESS, + Network.Testnet + ); + const expected_create_order_output_2 = [ + 11, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, + 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, + 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, + 199, 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, + 26, 107, 242, 121, 29, 55, 172, 185, 5, 196, 119, + 65, 156, 0, 2, 113, 2, 0 + ]; + + assert_eq_arrays(create_order_output_2, expected_create_order_output_2); + console.log("create order tokens for coins encoding ok"); + + // Note: the exact heights don't matter as long as they are at the "correct side" of the fork. + const order_v0_height = gen_random_int(0, ORDERS_V1_TESTNET_FORK_HEIGHT - 1, "order_v0_height"); + const order_v1_height = order_v0_height + ORDERS_V1_TESTNET_FORK_HEIGHT; + // Note: the nonce is ignored since orders v1. + const order_v1_nonce = gen_random_int(0, 1000000, "order_v1_nonce"); + const fill_order_v0_input = encode_input_for_fill_order( + ORDER_ID, + Amount.from_atoms("40000"), + ADDRESS, + BigInt(1), + BigInt(order_v0_height), + Network.Testnet + ); + const expected_fill_order_v0_input = [ + 2, 4, 7, 49, 150, 254, 178, 75, 93, 83, 251, + 1, 235, 159, 200, 27, 237, 187, 175, 123, 34, 179, + 118, 53, 6, 173, 213, 7, 212, 102, 232, 253, 9, + 244, 197, 2, 113, 2, 0, 1, 91, 58, 110, 176, + 100, 207, 6, 194, 41, 193, 30, 91, 4, 195, 202, + 103, 207, 80, 217, 178 + ]; + + assert_eq_arrays(fill_order_v0_input, expected_fill_order_v0_input); + console.log("fill order v0 encoding ok"); + + const fill_order_v1_input = encode_input_for_fill_order( + ORDER_ID, + Amount.from_atoms("40000"), + ADDRESS, + BigInt(order_v1_nonce), + BigInt(order_v1_height), + Network.Testnet + ); + const expected_fill_order_v1_input = [ + 3, 0, 49, 150, 254, 178, 75, 93, + 83, 251, 1, 235, 159, 200, 27, 237, + 187, 175, 123, 34, 179, 118, 53, 6, + 173, 213, 7, 212, 102, 232, 253, 9, + 244, 197, 2, 113, 2, 0, 1, 91, + 58, 110, 176, 100, 207, 6, 194, 41, + 193, 30, 91, 4, 195, 202, 103, 207, + 80, 217, 178 + ]; + + assert_eq_arrays(fill_order_v1_input, expected_fill_order_v1_input); + console.log("fill order v1 encoding ok"); + + const conclude_order_v0_input = encode_input_for_conclude_order( + ORDER_ID, + BigInt(1), + BigInt(order_v0_height), + Network.Testnet + ); + const expected_conclude_order_v0_input = [ + 2, 4, 6, 49, 150, 254, 178, 75, + 93, 83, 251, 1, 235, 159, 200, 27, + 237, 187, 175, 123, 34, 179, 118, 53, + 6, 173, 213, 7, 212, 102, 232, 253, + 9, 244, 197 + ]; + + assert_eq_arrays(conclude_order_v0_input, expected_conclude_order_v0_input); + console.log("conclude order v0 encoding ok"); + + const conclude_order_v1_input = encode_input_for_conclude_order( + ORDER_ID, + BigInt(order_v1_nonce), + BigInt(order_v1_height), + Network.Testnet + ); + const expected_conclude_order_v1_input = [ + 3, 2, 49, 150, 254, 178, 75, 93, + 83, 251, 1, 235, 159, 200, 27, 237, + 187, 175, 123, 34, 179, 118, 53, 6, + 173, 213, 7, 212, 102, 232, 253, 9, + 244, 197 + ]; + + assert_eq_arrays(conclude_order_v1_input, expected_conclude_order_v1_input); + console.log("conclude order v1 encoding ok"); + + const freeze_order_input = encode_input_for_freeze_order( + ORDER_ID, + BigInt(order_v1_height), + Network.Testnet + ); + const expected_freeze_order_input = [ + 3, 1, 49, 150, 254, 178, 75, 93, + 83, 251, 1, 235, 159, 200, 27, 237, + 187, 175, 123, 34, 179, 118, 53, 6, + 173, 213, 7, 212, 102, 232, 253, 9, + 244, 197 + ]; + + assert_eq_arrays(freeze_order_input, expected_freeze_order_input); + console.log("freeze order encoding ok"); + + try { + encode_input_for_freeze_order( + ORDER_ID, + BigInt(order_v0_height), + Network.Testnet + ); + throw new Error("Freezing an order before v1 worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Orders V1 not activated")) { + throw e; + } + console.log("Tested order freezing before v1 successfully"); + } +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_signed_transaction_intent.ts b/wasm-wrappers/js-bindings-test/tests/test_signed_transaction_intent.ts new file mode 100644 index 0000000000..2c259433c6 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_signed_transaction_intent.ts @@ -0,0 +1,134 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + encode_signed_transaction_intent, + make_transaction_intent_message_to_sign, + Network, + sign_challenge, + verify_transaction_intent, +} from "../../pkg/wasm_wrappers.js"; + +import { + get_err_msg, + assert_eq_arrays, + TEXT_ENCODER, +} from "./utils.js"; + +export function test_signed_transaction_intent() { + try { + const invalid_tx_id = "invalid tx id"; + make_transaction_intent_message_to_sign("intent", invalid_tx_id); + throw new Error("Invalid tx id worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Error parsing transaction id")) { + throw e; + } + } + + const tx_id = "DFC2BB0CC4C7F3ED3FE682A48EE9F78BCD4962E55E7BC239BD340EC22AFF8657"; + const message = make_transaction_intent_message_to_sign("the intent", tx_id); + const expected_message = TEXT_ENCODER.encode( + ""); + assert_eq_arrays(message, expected_message); + + try { + const invalid_signatures = "invalid signatures"; + encode_signed_transaction_intent(message, invalid_signatures); + throw new Error("Invalid signatures worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Error decoding a JsValue as an array of arrays of bytes")) { + throw e; + } + } + + { + const prv_key1 = [ + 0, 142, 11, 183, 83, 79, 207, 79, 18, 172, 116, 88, 251, 128, 146, 254, 82, + 156, 229, 110, 160, 187, 104, 237, 182, 59, 95, 108, 203, 22, 138, 173, 147 + ]; + const pubkey_addr1 = "rpmt1qgqqxtunp0gdsysq9g3fke9pesl4w8xg3t7ynssfrvqetae0d9nqn3prq3mdt7"; + const pubkeyhash_addr1 = "rmt1qxtlh84a7fflmeem9g4wtmyp2px42gnxwqprnjlw"; + const prv_key2 = [ + 0, 52, 13, 17, 187, 88, 27, 23, 211, 24, 13, 103, 68, 60, 205, 11, 221, + 141, 15, 97, 7, 234, 184, 222, 38, 85, 151, 118, 0, 154, 109, 134, 42 + ]; + const pubkey_addr2 = "rpmt1qgqqylj755w0rlejn3cjadtrhskkzyxqs9nq7mura3z467fkaam7ppxkjr77n7"; + const pubkeyhash_addr2 = "rmt1qx0y7ktusde6d4hf9474z28dwcsys3uk5qxphddl"; + + const signature1 = sign_challenge(Uint8Array.from(prv_key1), message); + const signature2 = sign_challenge(Uint8Array.from(prv_key2), message); + + const signed_intent = encode_signed_transaction_intent(message, [Array.from(signature1), Array.from(signature2)]); + + verify_transaction_intent(message, signed_intent, [pubkey_addr1, pubkeyhash_addr2], Network.Regtest); + verify_transaction_intent(message, signed_intent, [pubkeyhash_addr1, pubkey_addr2], Network.Regtest); + + try { + verify_transaction_intent(message, signed_intent, [pubkeyhash_addr2, pubkey_addr1], Network.Regtest); + throw new Error("Mismatched addresses worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Public key to public key hash mismatch")) { + throw e; + } + } + + const bad_signature1 = sign_challenge(Uint8Array.from(prv_key1), Uint8Array.from([...message, 123])); + const bad_signed_intent = encode_signed_transaction_intent(message, [Array.from(bad_signature1), Array.from(signature2)]); + + try { + verify_transaction_intent(message, bad_signed_intent, [pubkey_addr1, pubkey_addr2], Network.Regtest); + throw new Error("Bad signature worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Signature verification failed")) { + throw e; + } + } + } + + { + // Encode some predefined signatures to ensure stability of the encoding. + const signature1 = [ + 0, 3, 47, 147, 11, 208, 216, 18, 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, + 247, 47, 105, 102, 9, 196, 35, 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, + 14, 233, 201, 16, 104, 217, 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, + 124, 53, 201, 124, 194, 7, 45, 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163 + ]; + const signature2 = [ + 0, 2, 126, 94, 165, 28, 241, 255, 50, 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, + 69, 93, 121, 54, 239, 119, 224, 132, 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, + 9, 21, 200, 244, 222, 219, 172, 85, 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, + 98, 248, 161, 3, 119, 149, 13, 159, 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 + ]; + const expected_encoded_signed_intent = [ + 105, 1, 60, 116, 120, 95, 105, 100, 58, 100, 102, 99, 50, 98, 98, 48, 99, 99, 52, 99, 55, 102, 51, 101, 100, 51, + 102, 101, 54, 56, 50, 97, 52, 56, 101, 101, 57, 102, 55, 56, 98, 99, 100, 52, 57, 54, 50, 101, 53, 53, 101, 55, + 98, 99, 50, 51, 57, 98, 100, 51, 52, 48, 101, 99, 50, 50, 97, 102, 102, 56, 54, 53, 55, 59, 105, 110, 116, 101, + 110, 116, 58, 116, 104, 101, 32, 105, 110, 116, 101, 110, 116, 62, 8, 141, 1, 0, 3, 47, 147, 11, 208, 216, 18, + 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, 247, 47, 105, 102, 9, 196, 35, + 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, 14, 233, 201, 16, 104, 217, + 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, 124, 53, 201, 124, 194, 7, 45, + 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163, 141, 1, 0, 2, 126, 94, 165, 28, 241, 255, 50, + 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, 69, 93, 121, 54, 239, 119, 224, 132, + 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, 9, 21, 200, 244, 222, 219, 172, 85, + 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, 98, 248, 161, 3, 119, 149, 13, 159, + 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 + ]; + + const encoded_signed_intent = + encode_signed_transaction_intent(message, [signature1, signature2]); + assert_eq_arrays(encoded_signed_intent, expected_encoded_signed_intent); + } +} diff --git a/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts b/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts new file mode 100644 index 0000000000..d9d63b7e9d --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/test_transaction_and_witness_encoding.ts @@ -0,0 +1,310 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + encode_signed_transaction, + encode_transaction, + encode_witness, + encode_witness_no_signature, + estimate_transaction_size, + make_default_account_privkey, + make_receiving_address, + Network, + SignatureHashType, +} from "../../pkg/wasm_wrappers.js"; + +import { + assert_eq_arrays, + get_err_msg, + TEXT_ENCODER, +} from "./utils.js"; + +import { + MNEMONIC, +} from "./defs.js"; +import { + ADDRESS +} from "./test_address_generation.js"; +import { + INPUTS, +} from "./test_encode_other_inputs.js"; +import { + OUTPUTS, + OUTPUT_CREATE_STAKE_POOL, + OUTPUT_LOCK_THEN_TRANSFER, +} from "./test_encode_other_outputs.js"; + +export function test_transaction_and_witness_encoding() { + const account_pubkey = make_default_account_privkey( + MNEMONIC, + Network.Testnet + ); + const receiving_privkey = make_receiving_address(account_pubkey, 0); + + try { + const invalid_inputs = TEXT_ENCODER.encode("invalid inputs"); + encode_transaction(invalid_inputs, Uint8Array.from(OUTPUTS), BigInt(0)); + throw new Error("Invalid inputs worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction input encoding")) { + throw e; + } + console.log("Tested invalid inputs successfully"); + } + + try { + const invalid_outputs = TEXT_ENCODER.encode("invalid outputs"); + encode_transaction(Uint8Array.from(INPUTS), invalid_outputs, BigInt(0)); + throw new Error("Invalid outputs worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction output encoding")) { + throw e; + } + console.log("Tested invalid outputs successfully"); + } + + const tx = encode_transaction(Uint8Array.from(INPUTS), Uint8Array.from(OUTPUTS), BigInt(0)); + const expected_tx = [ + 1, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 8, 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, + 91, 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, + 4, 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, + 158, 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, + 217, 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, + 194, 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, + ]; + assert_eq_arrays(tx, expected_tx); + console.log("tx encoding ok"); + + const witness = encode_witness_no_signature(); + const expected_no_signature_witness = [0, 0]; + assert_eq_arrays(witness, expected_no_signature_witness); + console.log("empty witness encoding ok"); + + const opt_utxos = [1, ...OUTPUT_LOCK_THEN_TRANSFER, 1, ...OUTPUT_CREATE_STAKE_POOL]; + + try { + const invalid_private_key = TEXT_ENCODER.encode("invalid private key"); + encode_witness( + SignatureHashType.ALL, + invalid_private_key, + ADDRESS, + tx, + Uint8Array.from(opt_utxos), + 0, + Network.Testnet + ); + throw new Error("Invalid private key worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid private key encoding")) { + throw e; + } + console.log("Tested invalid private key in encode witness successfully"); + } + try { + const invalid_address = "invalid address"; + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + invalid_address, + tx, + Uint8Array.from(opt_utxos), + 0, + Network.Testnet + ); + throw new Error("Invalid address worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid addressable")) { + throw e; + } + console.log("Tested invalid address in encode witness successfully"); + } + try { + const invalid_tx = TEXT_ENCODER.encode("invalid tx"); + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + invalid_tx, + Uint8Array.from(opt_utxos), + 0, + Network.Testnet + ); + throw new Error("Invalid transaction worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction encoding")) { + throw e; + } + console.log("Tested invalid transaction in encode witness successfully"); + } + try { + const invalid_utxos = TEXT_ENCODER.encode("invalid utxos"); + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + tx, + invalid_utxos, + 0, + Network.Testnet + ); + throw new Error("Invalid utxo worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction input utxo encoding")) { + throw e; + } + console.log("Tested invalid utxo in encode witness successfully"); + } + try { + const invalid_utxos_count = Uint8Array.from([0]); + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + tx, + invalid_utxos_count, + 0, + Network.Testnet + ); + throw new Error("Invalid utxo worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Utxos count does not match inputs count")) { + throw e; + } + console.log("Tested invalid utxo count in encode witness successfully"); + } + try { + const invalid_input_idx = 999; + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + tx, + Uint8Array.from(opt_utxos), + invalid_input_idx, + Network.Testnet + ); + throw new Error("Invalid address worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid input index")) { + throw e; + } + console.log("Tested invalid input index in encode witness successfully"); + } + // all ok + encode_witness( + SignatureHashType.ALL, + receiving_privkey, + ADDRESS, + tx, + Uint8Array.from(opt_utxos), + 0, + Network.Testnet + ); + + // as signatures are random, hardcode one so we can test the encodings for the signed transaction + const random_witness2 = [ + 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, 180, + 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, 130, 186, 209, 98, + 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, 15, 21, 164, 83, 151, 14, + 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, 203, 31, 216, 54, 141, 223, + 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, 114, 99, 6, 19, 220, 156, + 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, 2, 254, 122, 169, 127, 235, + 158, 62, 127, 177, 12, 228, + ]; + + try { + const invalid_witnesses = TEXT_ENCODER.encode("invalid witnesses"); + encode_signed_transaction(tx, invalid_witnesses); + throw new Error("Invalid witnesses worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction witness encoding")) { + throw e; + } + console.log("Tested invalid witnesses successfully"); + } + + try { + encode_signed_transaction(tx, witness); + throw new Error("Invalid number of witnesses worked somehow!"); + } catch (e) { + if ( + !get_err_msg(e).includes( + "The number of signatures does not match the number of inputs" + ) + ) { + throw e; + } + console.log("Tested invalid number of witnesses successfully"); + } + + try { + const invalid_tx = TEXT_ENCODER.encode("invalid tx"); + encode_signed_transaction(invalid_tx, witness); + throw new Error("Invalid transaction worked somehow!"); + } catch (e) { + if (!get_err_msg(e).includes("Invalid transaction encoding")) { + throw e; + } + console.log("Tested invalid transaction successfully"); + } + + let witnesses = [...random_witness2, ...random_witness2]; + const signed_tx = encode_signed_transaction(tx, Uint8Array.from(witnesses)); + const expected_signed_tx = [ + 1, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4, 8, 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, + 91, 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, + 4, 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, + 158, 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, + 217, 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, + 194, 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, 8, + 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, 180, + 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, 130, 186, 209, 98, + 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, 15, 21, 164, 83, 151, 14, + 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, 203, 31, 216, 54, 141, 223, + 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, 114, 99, 6, 19, 220, 156, + 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, 2, 254, 122, 169, 127, 235, + 158, 62, 127, 177, 12, 228, 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, + 44, 38, 35, 73, 145, 212, 180, 49, 115, 4, 150, 204, 250, 205, 123, 131, + 201, 114, 130, 186, 209, 98, 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, + 15, 21, 164, 83, 151, 14, 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, + 203, 31, 216, 54, 141, 223, 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, + 114, 99, 6, 19, 220, 156, 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, + 2, 254, 122, 169, 127, 235, 158, 62, 127, 177, 12, 228, + ]; + assert_eq_arrays(signed_tx, expected_signed_tx); + + const estimated_size = estimate_transaction_size( + Uint8Array.from(INPUTS), + [ADDRESS, ADDRESS], + Uint8Array.from(OUTPUTS), + Network.Testnet + ); + if (estimated_size != expected_signed_tx.length) { + throw new Error("wrong estimated size"); + } + console.log( + `estimated size ${estimated_size} vs real ${expected_signed_tx.length}` + ); +} diff --git a/wasm-wrappers/js-bindings-test/tests/utils.ts b/wasm-wrappers/js-bindings-test/tests/utils.ts new file mode 100644 index 0000000000..21678f78e0 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tests/utils.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const TEXT_ENCODER = new TextEncoder(); + +type AnyArray = any[] | Uint8Array; + +export function assert_eq_arrays(arr1: AnyArray, arr2: AnyArray) { + const equal = arr1.length == arr2.length && arr1.every((value, index) => value == arr2[index]); + + assert(equal, `array1 [${arr1}] differs from array2 [${arr2}]`); +} + +export function assert(condition: any, message: any) { + if (!condition) { + throw Error('Assertion failed: ' + (message || '')); + } +} + +export function run_one_test(test_func: () => void) { + console.group(`Running ${test_func.name}`); + test_func(); + console.groupEnd(); +} + +export function get_err_msg(error: unknown) { + if (error instanceof Error) return error.message + return String(error) +} + +// Generate a random integer in the [min, max] interval and print in to the console. +export function gen_random_int(min: number, max: number, description: string) { + const min_int = Math.ceil(min); + const max_int = Math.floor(max); + const result = Math.floor(Math.random() * (max_int - min_int + 1) + min_int); + console.log(`Generated ${description}: ${result}`); + + return result; +} diff --git a/wasm-wrappers/js-bindings-test/tsconfig.json b/wasm-wrappers/js-bindings-test/tsconfig.json new file mode 100644 index 0000000000..40a9824481 --- /dev/null +++ b/wasm-wrappers/js-bindings-test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "nodenext", + "sourceMap": true, + "outDir": "./dist", + "noUnusedLocals": true, + "strict": true + } +} diff --git a/wasm-wrappers/js-bindings/web-entry.js b/wasm-wrappers/js-bindings-test/web-entry.js similarity index 64% rename from wasm-wrappers/js-bindings/web-entry.js rename to wasm-wrappers/js-bindings-test/web-entry.js index c0651c2593..3befff875c 100644 --- a/wasm-wrappers/js-bindings/web-entry.js +++ b/wasm-wrappers/js-bindings-test/web-entry.js @@ -1,11 +1,11 @@ import init from "../pkg/wasm_wrappers.js"; -import { run_test } from "./wasm_test.js"; +import { run_all_tests } from "./dist/main.js"; async function web_run() { // Initialize the wasm module await init(); - await run_test(); + await run_all_tests(); } web_run(); diff --git a/wasm-wrappers/js-bindings/node-entry.js b/wasm-wrappers/js-bindings/node-entry.js deleted file mode 100644 index fad7ecd405..0000000000 --- a/wasm-wrappers/js-bindings/node-entry.js +++ /dev/null @@ -1,7 +0,0 @@ -import { run_test } from "./wasm_test.js"; - -async function node_run() { - await run_test(); -} - -node_run(); diff --git a/wasm-wrappers/js-bindings/package.json b/wasm-wrappers/js-bindings/package.json deleted file mode 100644 index 629d88c891..0000000000 --- a/wasm-wrappers/js-bindings/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "wasm-wrappers-example", - "version": "0.1.0", - "license": "MIT", - "main": "index.js", - "type": "module" - } diff --git a/wasm-wrappers/js-bindings/wasm_test.js b/wasm-wrappers/js-bindings/wasm_test.js deleted file mode 100644 index 9670d76605..0000000000 --- a/wasm-wrappers/js-bindings/wasm_test.js +++ /dev/null @@ -1,1634 +0,0 @@ -// Use ES module import syntax to import functionality from the module -// that we have compiled. -// -// Note that the `default` import is an initialization function which -// will "boot" the module and make it ready to use. Currently browsers -// don't support natively imported WebAssembly as an ES module, but -// eventually the manual initialization won't be required! -import { - make_private_key, - public_key_from_private_key, - sign_message_for_spending, - verify_signature_for_spending, - make_default_account_privkey, - make_receiving_address, - make_change_address, - pubkey_to_pubkeyhash_address, - make_receiving_address_public_key, - make_change_address_public_key, - extended_public_key_from_extended_private_key, - Network, - encode_input_for_utxo, - encode_output_coin_burn, - encode_output_token_burn, - encode_transaction, - encode_witness_no_signature, - encode_signed_transaction, - encode_lock_until_height, - encode_output_create_stake_pool, - encode_output_token_transfer, - encode_output_lock_then_transfer, - encode_output_token_lock_then_transfer, - encode_stake_pool_data, - encode_output_htlc, - encode_witness, - encode_witness_htlc_secret, - encode_multisig_challenge, - encode_witness_htlc_multisig, - extract_htlc_secret, - encode_create_order_output, - encode_input_for_fill_order, - encode_input_for_freeze_order, - encode_input_for_conclude_order, - SignatureHashType, - encode_input_for_withdraw_from_delegation, - estimate_transaction_size, - staking_pool_spend_maturity_block_count, - get_transaction_id, - effective_pool_balance, - Amount, - TotalSupply, - FreezableToken, - encode_output_issue_nft, - encode_output_issue_fungible_token, - sign_challenge, - verify_challenge, - get_token_id, - make_transaction_intent_message_to_sign, - encode_signed_transaction_intent, - verify_transaction_intent, - encode_input_for_mint_tokens, - encode_input_for_unmint_tokens, - encode_input_for_lock_token_supply, - encode_input_for_freeze_token, - TokenUnfreezable, - encode_input_for_unfreeze_token, - encode_input_for_change_token_authority, - encode_input_for_change_token_metadata_uri, -} from "../pkg/wasm_wrappers.js"; - -// Taken from TESTNET_FORK_HEIGHT_5_ORDERS_V1 in common/src/chain/config/builder.rs. -// This will be updated to the actual height after we choose one. -const ORDERS_V1_TESTNET_FORK_HEIGHT = 999999999; - -function assert_eq_arrays(arr1, arr2) { - const equal = arr1.length == arr2.length && arr1.every((value, index) => value == arr2[index]); - - assert(equal, `array1 [${arr1}] differs from array2 [${arr2}]`); -} - -function assert(condition, message) { - if (!condition) { - throw Error('Assertion failed: ' + (message || '')); - } -} - -function run_one_test(test_func) { - console.log(`>> Running ${test_func.name}`); - test_func(); - console.log(`<< Done running ${test_func.name}`); -} - -export async function run_test() { - // Try signature verification - const priv_key = make_private_key(); - console.log(`priv key = ${priv_key}`); - const pub_key = public_key_from_private_key(priv_key); - console.log(`pub key = ${pub_key}`); - const message = "Hello, world!"; - const signature = sign_message_for_spending(priv_key, message); - console.log(`signature = ${signature}`); - const verified = verify_signature_for_spending(pub_key, signature, message); - console.log(`verified valid message with correct key = ${verified}`); - if (!verified) { - throw new Error("Signature verification failed!"); - } - const verified_bad = verify_signature_for_spending( - pub_key, - signature, - "bro!" - ); - if (verified_bad) { - throw new Error("Invalid message signature verification passed!"); - } - - // Attempt to use a bad private key to get a public key (test returned Result<> object, which will become a string error) - const bad_priv_key = "bad"; - try { - public_key_from_private_key(bad_priv_key); - throw new Error("Invalid private key worked somehow!"); - } catch (e) { - if (!e.includes("Invalid private key encoding")) { - throw new Error( - "Invalid private key resulted in an unexpected error message!" - ); - } - console.log("Tested decoding bad private key successfully"); - } - - try { - const invalid_mnemonic = "asd asd"; - make_default_account_privkey(invalid_mnemonic, Network.Mainnet); - throw new Error("Invalid mnemonic worked somehow!"); - } catch (e) { - if (!e.includes("Invalid mnemonic string")) { - throw e; - } - console.log("Tested invalid mnemonic successfully"); - } - - { - let challenge = sign_challenge(priv_key, message); - let address = pubkey_to_pubkeyhash_address(pub_key, Network.Testnet); - let result = verify_challenge(address, Network.Testnet, challenge, message); - if (!result) { - throw new Error("Invalid sing and verify challenge"); - } - - const different_priv_key = make_private_key(); - const different_pub_key = public_key_from_private_key(different_priv_key); - let different_address = pubkey_to_pubkeyhash_address(different_pub_key, Network.Testnet); - try { - verify_challenge(different_address, Network.Testnet, challenge, message); - } catch (e) { - if (!e.includes("Public key to public key hash mismatch")) { - throw e; - } - console.log("Tested verify with different address successfully"); - } - } - - try { - make_receiving_address(bad_priv_key, 0); - throw new Error("Invalid private key worked somehow!"); - } catch (e) { - if (!e.includes("Invalid private key encoding")) { - throw e; - } - console.log("Tested decoding bad account private key successfully"); - } - - try { - make_change_address(bad_priv_key, 0); - throw new Error("Invalid private key worked somehow!"); - } catch (e) { - if (!e.includes("Invalid private key encoding")) { - throw e; - } - console.log("Tested decoding bad account private key successfully"); - } - - const mnemonic = - "walk exile faculty near leg neutral license matrix maple invite cupboard hat opinion excess coffee leopard latin regret document core limb crew dizzy movie"; - { - const account_private_key = make_default_account_privkey( - mnemonic, - Network.Mainnet - ); - console.log(`acc private key = ${account_private_key}`); - - const extended_public_key = extended_public_key_from_extended_private_key(account_private_key); - - const receiving_privkey = make_receiving_address(account_private_key, 0); - console.log(`receiving privkey = ${receiving_privkey}`); - - // test bad key index - try { - make_receiving_address(account_private_key, 1 << 31); - throw new Error("Invalid key index worked somehow!"); - } catch (e) { - if (!e.includes("Invalid key index, MSB bit set")) { - throw e; - } - console.log("Tested invalid key index with set MSB bit successfully"); - } - - const receiving_pubkey = public_key_from_private_key(receiving_privkey); - const receiving_pubkey2 = make_receiving_address_public_key(extended_public_key, 0); - assert_eq_arrays(receiving_pubkey, receiving_pubkey2); - - const address = pubkey_to_pubkeyhash_address( - receiving_pubkey, - Network.Mainnet - ); - console.log(`address = ${address}`); - if (address != "mtc1qyqmdpxk2w42w37qsdj0e8g54ysvnlvpny3svzqx") { - throw new Error("Incorrect address generated"); - } - - const change_privkey = make_change_address(account_private_key, 0); - console.log(`change privkey = ${change_privkey}`); - - // test bad key index - try { - make_change_address(account_private_key, 1 << 31); - throw new Error("Invalid key index worked somehow!"); - } catch (e) { - if (!e.includes("Invalid key index, MSB bit set")) { - throw e; - } - console.log("Tested invalid key index with set MSB bit successfully"); - } - - const change_pubkey = public_key_from_private_key(change_privkey); - const change_pubkey2 = make_change_address_public_key(extended_public_key, 0); - assert_eq_arrays(change_pubkey, change_pubkey2); - - const caddress = pubkey_to_pubkeyhash_address( - change_pubkey, - Network.Mainnet - ); - console.log(`address = ${caddress}`); - if (caddress != "mtc1qxyhrpytqrvjalg2dzw4tdvzt2zz8ps6nyav2n56") { - throw new Error("Incorrect address generated"); - } - } - - { - // Test generating an address for Testnet - const account_private_key = make_default_account_privkey( - mnemonic, - Network.Testnet - ); - console.log(`acc private key = ${account_private_key}`); - - const receiving_privkey = make_receiving_address(account_private_key, 0); - console.log(`receiving privkey = ${receiving_privkey}`); - - const receiving_pubkey = public_key_from_private_key(receiving_privkey); - const address = pubkey_to_pubkeyhash_address( - receiving_pubkey, - Network.Testnet - ); - console.log(`address = ${address}`); - if (address != "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n") { - throw new Error("Incorrect address generated"); - } - } - - { - const lock_for_blocks = staking_pool_spend_maturity_block_count( - BigInt(1000) - ); - console.log(`lock for blocks ${lock_for_blocks}`); - if (lock_for_blocks != 7200) { - throw new Error("Incorrect lock for blocks"); - } - } - - { - try { - encode_input_for_utxo("asd", 1); - throw new Error("Invalid outpoint encoding worked somehow!"); - } catch (e) { - if (!e.includes("Invalid outpoint ID encoding")) { - throw e; - } - console.log("Tested invalid outpoint ID successfully"); - } - try { - encode_input_for_withdraw_from_delegation( - "invalid delegation id", - Amount.from_atoms("1"), - BigInt(1), - Network.Mainnet - ); - throw new Error("Invalid delegation id encoding worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log("Tested invalid delegation id in account successfully"); - } - - // Test encoding full transaction - const tx_outpoint = new Uint8Array(33).fill(0); - const tx_input = encode_input_for_utxo(tx_outpoint, 1); - const deleg_id = - "mdelg1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqut3aj8"; - const tx_input2 = encode_input_for_withdraw_from_delegation( - deleg_id, - Amount.from_atoms("1"), - BigInt(1), - Network.Mainnet - ); - const inputs = [...tx_input, ...tx_input2]; - const expected_inputs = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, - ]; - - assert_eq_arrays(inputs, expected_inputs); - - try { - get_token_id([], BigInt(1), Network.Testnet); - throw "Token Id generated without a UTXO input somehow!"; - } catch (e) { - if (!(e.includes("No UTXO inputs for token id creation") || - e.includes("No inputs for token id creation"))) { - throw e; - } - console.log("Tested no UTXO inputs for token ID successfully"); - } - - { - const expected_token_id = - "tmltk13cncdptay55g9ajhrkaw0fp46r0tspq9kptul8vj2q7yvd69n4zsl24gea"; - const token_id = get_token_id(inputs, BigInt(1), Network.Testnet); - console.log(token_id); - - if (token_id != expected_token_id) { - throw new Error("Different token id"); - } - - } - - const token_id = - "tmltk15tgfrs49rv88v8utcllqh0nvpaqtgvn26vdxhuner5m6ewg9c3msn9fxns"; - try { - encode_output_coin_burn(Amount.from_atoms("invalid amount")); - throw new Error("Invalid value for amount worked somehow!"); - } catch (e) { - if (!e.includes("Invalid atoms amount")) { - throw e; - } - console.log("Tested invalid amount successfully"); - } - try { - encode_output_token_burn( - Amount.from_atoms("invalid amount"), - token_id, - Network.Testnet - ); - throw new Error("Invalid value for amount worked somehow!"); - } catch (e) { - if (!e.includes("Invalid atoms amount")) { - throw e; - } - console.log("Tested invalid amount successfully"); - } - try { - const invalid_token_id = "asd"; - encode_output_token_burn( - Amount.from_atoms("100"), - invalid_token_id, - Network.Testnet - ); - throw new Error("Invalid token id worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log("Tested invalid token id successfully for token burn"); - } - - const token_burn = encode_output_token_burn( - Amount.from_atoms("100"), - token_id, - Network.Testnet - ); - const expected_token_burn = [ - 2, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, - 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, - 196, 119, 145, 1, - ]; - assert_eq_arrays(token_burn, expected_token_burn); - - const address = "tmt1q9dn5m4svn8sds3fcy09kpxrefnu75xekgr5wa3n"; - - try { - const invalid_lock = "invalid lock"; - encode_output_lock_then_transfer( - Amount.from_atoms("100"), - address, - invalid_lock, - Network.Testnet - ); - throw new Error("Invalid lock worked somehow!"); - } catch (e) { - if (!e.includes("Invalid time lock encoding")) { - throw e; - } - console.log("Tested invalid lock successfully"); - } - - try { - const invalid_lock = "invalid lock"; - encode_output_token_lock_then_transfer( - Amount.from_atoms("100"), - address, - token_id, - invalid_lock, - Network.Testnet - ); - throw new Error("Invalid lock worked somehow!"); - } catch (e) { - if (!e.includes("Invalid time lock encoding")) { - throw e; - } - console.log("Tested invalid token lock successfully"); - } - - try { - const invalid_token_id = "asd"; - const lock = encode_lock_until_height(BigInt(100)); - encode_output_token_lock_then_transfer( - Amount.from_atoms("100"), - address, - invalid_token_id, - lock, - Network.Testnet - ); - throw new Error("Invalid token id worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log("Tested invalid token id successfully"); - } - - const lock = encode_lock_until_height(BigInt(100)); - const output = encode_output_lock_then_transfer( - Amount.from_atoms("100"), - address, - lock, - Network.Testnet - ); - - const token_lock_transfer_out = encode_output_token_lock_then_transfer( - Amount.from_atoms("100"), - address, - token_id, - lock, - Network.Testnet - ); - const expected_token_lock_transfer_out = [ - 1, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, - 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, - 196, 119, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, - 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, - ]; - assert_eq_arrays(token_lock_transfer_out, expected_token_lock_transfer_out); - - try { - const invalid_address = "invalid address"; - encode_output_token_transfer( - Amount.from_atoms("100"), - invalid_address, - token_id, - Network.Testnet - ); - throw new Error("Invalid address worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log( - "Tested invalid address in encode output token transfer successfully" - ); - } - - try { - const invalid_token_id = "invalid token"; - encode_output_token_transfer( - Amount.from_atoms("100"), - address, - invalid_token_id, - Network.Testnet - ); - throw new Error("Invalid token id worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log( - "Tested invalid token id successfully in output token transfer" - ); - } - - const token_transfer_out = encode_output_token_transfer( - Amount.from_atoms("100"), - address, - token_id, - Network.Testnet - ); - const expected_token_transfer_out = [ - 0, 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, 254, 11, 190, - 108, 15, 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, - 196, 119, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, - 4, 195, 202, 103, 207, 80, 217, 178, - ]; - - assert_eq_arrays(token_transfer_out, expected_token_transfer_out); - - const vrf_public_key = - "tvrfpk1qpk0t6np4gyl084fv328h6ahjvwcsaktrzfrs0xeqtrzpp0l7p28knrnn57"; - - try { - const invalid_margin_ratio_per_thousand = 2000; - encode_stake_pool_data( - Amount.from_atoms("40000"), - address, - vrf_public_key, - address, - invalid_margin_ratio_per_thousand, - Amount.from_atoms("0"), - Network.Testnet - ); - throw new Error("Invalid margin_ratio_per_thousand worked somehow!"); - } catch (e) { - if (!e.includes("Invalid per thousand 2000, valid range is [0, 1000]")) { - throw e; - } - console.log("Tested invalid margin_ratio_per_thousand successfully"); - } - - const pool_data = encode_stake_pool_data( - Amount.from_atoms("40000"), - address, - vrf_public_key, - address, - 100, - Amount.from_atoms("0"), - Network.Testnet - ); - const expected_pool_data = [ - 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, - 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, 158, - 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, 217, - 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, 194, - 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, - ]; - - assert_eq_arrays(pool_data, expected_pool_data); - - let encoded_fungible_token = encode_output_issue_fungible_token( - address, - "XXX", - "http://uri.com", - 2, - TotalSupply.Unlimited, - null, - FreezableToken.Yes, - BigInt(1), - Network.Testnet - ); - - const expected_fungible_token = [ - 7, 1, 12, 88, 88, 88, 2, 56, 104, 116, - 116, 112, 58, 47, 47, 117, 114, 105, 46, 99, - 111, 109, 2, 1, 91, 58, 110, 176, 100, 207, - 6, 194, 41, 193, 30, 91, 4, 195, 202, 103, - 207, 80, 217, 178, 1 - ]; - - assert_eq_arrays(encoded_fungible_token, expected_fungible_token); - - const account_pubkey = make_default_account_privkey( - mnemonic, - Network.Testnet - ); - const receiving_privkey = make_receiving_address(account_pubkey, 0); - const receiving_pubkey = public_key_from_private_key(receiving_privkey); - - let encoded_nft = encode_output_issue_nft( - token_id, - address, - "nft", - "XXX", - "desc", - "1234", - receiving_pubkey, - "http://uri", - "http://icon", - "http://foo", - BigInt(1), - Network.Testnet - ); - - const expected_nft_encoding = [ - 8, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, 199, - 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, 26, 107, - 242, 121, 29, 55, 172, 185, 5, 196, 119, 0, 1, 0, - 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, - 180, 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, - 130, 186, 209, 98, 181, 118, 233, 133, 89, 12, 110, 102, - 116, 16, 100, 101, 115, 99, 12, 88, 88, 88, 44, 104, - 116, 116, 112, 58, 47, 47, 105, 99, 111, 110, 40, 104, - 116, 116, 112, 58, 47, 47, 102, 111, 111, 40, 104, 116, - 116, 112, 58, 47, 47, 117, 114, 105, 16, 1, 2, 3, - 4, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, - 30, 91, 4, 195, 202, 103, 207, 80, 217, 178 - ]; - - assert_eq_arrays(encoded_nft, expected_nft_encoding); - - try { - const invalid_token_id = "asd"; - encode_output_issue_nft( - invalid_token_id, - address, - "nft", - "XXX", - "desc", - "12345", - undefined, - undefined, - undefined, - undefined, - BigInt(1), - Network.Testnet - ); - throw new Error("Invalid token id worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log("Tested invalid token id successfully"); - } - - try { - const creator_public_key_hash = address; - encode_output_issue_nft( - token_id, - address, - "nft", - "XXX", - "desc", - "123", - creator_public_key_hash, - undefined, - undefined, - undefined, - BigInt(1), - Network.Testnet - ); - throw new Error("Invalid creator worked somehow!"); - } catch (e) { - if (!e.includes("Cannot decode NFT creator as a public key")) { - throw e; - } - console.log("Tested invalid creator successfully"); - } - - try { - const empty_ticker = ""; - encode_output_issue_nft( - token_id, - address, - "nft", - empty_ticker, - "desc", - "123", - undefined, - undefined, - undefined, - undefined, - BigInt(1), - Network.Testnet - ); - throw new Error("Invalid ticker worked somehow!"); - } catch (e) { - if (!e.includes("Invalid ticker length")) { - throw e; - } - console.log("Tested invalid ticker successfully"); - } - - try { - const empty_name = ""; - encode_output_issue_nft( - token_id, - address, - empty_name, - "xxx", - "desc", - "123", - undefined, - undefined, - undefined, - undefined, - BigInt(1), - Network.Testnet - ); - throw new Error("Invalid name worked somehow!"); - } catch (e) { - if (!e.includes("Invalid name length")) { - throw e; - } - console.log("Tested invalid name successfully"); - } - - try { - const empty_description = ""; - encode_output_issue_nft( - token_id, - address, - "name", - "XXX", - empty_description, - "123", - undefined, - undefined, - undefined, - undefined, - BigInt(1), - Network.Testnet - ); - throw new Error("Invalid description worked somehow!"); - } catch (e) { - if (!e.includes("Invalid description length")) { - throw e; - } - console.log("Tested invalid description successfully"); - } - - const pool_id = - "tpool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqza035u"; - try { - const invalid_pool_data = "invalid pool data"; - encode_output_create_stake_pool( - pool_id, - invalid_pool_data, - Network.Testnet - ); - throw new Error("Invalid pool data worked somehow!"); - } catch (e) { - if (!e.includes("Invalid stake pool data encoding")) { - throw e; - } - console.log("Tested invalid pool data successfully"); - } - const stake_pool_output = encode_output_create_stake_pool( - pool_id, - pool_data, - Network.Testnet - ); - const outputs = [...output, ...stake_pool_output]; - - const expected_outputs = [ - 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, - 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, - 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, 4, 195, - 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, 158, 169, - 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, 217, 2, - 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, 194, - 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, - ]; - - assert_eq_arrays(outputs, expected_outputs); - - const secret = [0, 229, 233, 72, 110, 22, 64, 36, 69, 188, 238, 51, 130, 168, 185, 241, 73, 48, 120, 151, 140, 45, 46, 39, 50, 207, 18, 50, 243, 30, 115, 93] - const secret_hash = "b5a48c7780e597de8012346fb30761965248e3f2" - - const htlc_coins_output = encode_output_htlc( - Amount.from_atoms("40000"), - undefined, - secret_hash, - address, - address, - encode_lock_until_height(BigInt(100)), - Network.Testnet - ); - console.log("htlc with coins encoding ok"); - - const htlc_tokens_output = encode_output_htlc( - Amount.from_atoms("40000"), - token_id, - secret_hash, - address, - address, - encode_lock_until_height(BigInt(100)), - Network.Testnet - ); - console.log("htlc with tokens encoding ok"); - - const mint_tokens_input = encode_input_for_mint_tokens( - token_id, - Amount.from_atoms("100"), - BigInt(1), - Network.Testnet - ); - const expected_mint_tokens_input = [ - 2, 4, 0, 162, 208, 145, 194, 165, 27, - 14, 118, 31, 139, 199, 254, 11, 190, 108, - 15, 64, 180, 50, 106, 211, 26, 107, 242, - 121, 29, 55, 172, 185, 5, 196, 119, 145, - 1 - ]; - - assert_eq_arrays(mint_tokens_input, expected_mint_tokens_input); - console.log("mint tokens encoding ok"); - - const unmint_tokens_input = encode_input_for_unmint_tokens( - token_id, - BigInt(2), - Network.Testnet - ); - const expected_unmint_tokens_input = [ - 2, 8, 1, 162, 208, 145, 194, 165, - 27, 14, 118, 31, 139, 199, 254, 11, - 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, - 5, 196, 119 - ]; - - assert_eq_arrays(unmint_tokens_input, expected_unmint_tokens_input); - console.log("unmint tokens encoding ok"); - - const lock_token_supply_input = encode_input_for_lock_token_supply( - token_id, - BigInt(2), - Network.Testnet - ); - const expected_lock_token_supply_input = [ - 2, 8, 2, 162, 208, 145, 194, 165, - 27, 14, 118, 31, 139, 199, 254, 11, - 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, - 5, 196, 119 - ]; - - assert_eq_arrays(lock_token_supply_input, expected_lock_token_supply_input); - console.log("lock token supply encoding ok"); - - const freeze_token_input = encode_input_for_freeze_token( - token_id, - TokenUnfreezable.Yes, - BigInt(2), - Network.Testnet - ); - const expected_freeze_token_input = [ - 2, 8, 3, 162, 208, 145, 194, 165, - 27, 14, 118, 31, 139, 199, 254, 11, - 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, - 5, 196, 119, 1 - ]; - - assert_eq_arrays(freeze_token_input, expected_freeze_token_input); - console.log("freeze token encoding ok"); - - const unfreeze_token_input = encode_input_for_unfreeze_token( - token_id, - BigInt(2), - Network.Testnet - ); - const expected_unfreeze_token_input = [ - 2, 8, 4, 162, 208, 145, 194, 165, - 27, 14, 118, 31, 139, 199, 254, 11, - 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, - 5, 196, 119 - ]; - - assert_eq_arrays(unfreeze_token_input, expected_unfreeze_token_input); - console.log("unfreeze token encoding ok"); - - const change_token_authority_input = encode_input_for_change_token_authority( - token_id, - address, - BigInt(2), - Network.Testnet - ); - const expected_change_token_authority_input = [ - 2, 8, 5, 162, 208, 145, 194, 165, 27, 14, 118, - 31, 139, 199, 254, 11, 190, 108, 15, 64, 180, 50, - 106, 211, 26, 107, 242, 121, 29, 55, 172, 185, 5, - 196, 119, 1, 91, 58, 110, 176, 100, 207, 6, 194, - 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, - 178 - ]; - - assert_eq_arrays(change_token_authority_input, expected_change_token_authority_input); - console.log("change token authority encoding ok"); - - const change_token_metadata_uri = encode_input_for_change_token_metadata_uri( - token_id, - address, - BigInt(2), - Network.Testnet - ); - const expected_change_token_metadata_uri = [ - 2, 8, 8, 162, 208, 145, 194, 165, 27, 14, 118, 31, - 139, 199, 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, 5, 196, 119, 176, - 116, 109, 116, 49, 113, 57, 100, 110, 53, 109, 52, 115, - 118, 110, 56, 115, 100, 115, 51, 102, 99, 121, 48, 57, - 107, 112, 120, 114, 101, 102, 110, 117, 55, 53, 120, 101, - 107, 103, 114, 53, 119, 97, 51, 110 - ]; - - assert_eq_arrays(change_token_metadata_uri, expected_change_token_metadata_uri); - console.log("change token metadata uri encoding ok"); - - const order_output = encode_create_order_output( - Amount.from_atoms("40000"), - undefined, - Amount.from_atoms("10000"), - token_id, - address, - Network.Testnet - ); - const expected_order_output = [ - 11, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, - 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, - 0, 2, 113, 2, 0, 2, 162, 208, 145, 194, 165, - 27, 14, 118, 31, 139, 199, 254, 11, 190, 108, 15, - 64, 180, 50, 106, 211, 26, 107, 242, 121, 29, 55, - 172, 185, 5, 196, 119, 65, 156 - ]; - - assert_eq_arrays(order_output, expected_order_output); - console.log("create order coins for tokens encoding ok"); - - const create_order_output_2 = encode_create_order_output( - Amount.from_atoms("10000"), - token_id, - Amount.from_atoms("40000"), - undefined, - address, - Network.Testnet - ); - const expected_create_order_output_2 = [ - 11, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, - 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, - 2, 162, 208, 145, 194, 165, 27, 14, 118, 31, 139, - 199, 254, 11, 190, 108, 15, 64, 180, 50, 106, 211, - 26, 107, 242, 121, 29, 55, 172, 185, 5, 196, 119, - 65, 156, 0, 2, 113, 2, 0 - ]; - - assert_eq_arrays(create_order_output_2, expected_create_order_output_2); - console.log("create order tokens for coins encoding ok"); - - const order_id = "tordr1xxt0avjtt4flkq0tnlyphmdm4aaj9vmkx5r2m4g863nw3lgf7nzs7mlkqc"; - // Note: the exact heights don't matter as long as they are at the "correct side" of the fork. - const order_v0_height = Math.floor(Math.random() * ORDERS_V1_TESTNET_FORK_HEIGHT); - const order_v1_height = order_v0_height + ORDERS_V1_TESTNET_FORK_HEIGHT; - // Note: the nonce is ignored since orders v1. - const order_v1_nonce = Math.floor(Math.random() * 1000000); - const fill_order_v0_input = encode_input_for_fill_order( - order_id, - Amount.from_atoms("40000"), - address, - BigInt(1), - BigInt(order_v0_height), - Network.Testnet - ); - const expected_fill_order_v0_input = [ - 2, 4, 7, 49, 150, 254, 178, 75, 93, 83, 251, - 1, 235, 159, 200, 27, 237, 187, 175, 123, 34, 179, - 118, 53, 6, 173, 213, 7, 212, 102, 232, 253, 9, - 244, 197, 2, 113, 2, 0, 1, 91, 58, 110, 176, - 100, 207, 6, 194, 41, 193, 30, 91, 4, 195, 202, - 103, 207, 80, 217, 178 - ]; - - assert_eq_arrays(fill_order_v0_input, expected_fill_order_v0_input); - console.log("fill order v0 encoding ok"); - - const fill_order_v1_input = encode_input_for_fill_order( - order_id, - Amount.from_atoms("40000"), - address, - BigInt(order_v1_nonce), - BigInt(order_v1_height), - Network.Testnet - ); - const expected_fill_order_v1_input = [ - 3, 0, 49, 150, 254, 178, 75, 93, - 83, 251, 1, 235, 159, 200, 27, 237, - 187, 175, 123, 34, 179, 118, 53, 6, - 173, 213, 7, 212, 102, 232, 253, 9, - 244, 197, 2, 113, 2, 0, 1, 91, - 58, 110, 176, 100, 207, 6, 194, 41, - 193, 30, 91, 4, 195, 202, 103, 207, - 80, 217, 178 - ]; - - assert_eq_arrays(fill_order_v1_input, expected_fill_order_v1_input); - console.log("fill order v1 encoding ok"); - - const conclude_order_v0_input = encode_input_for_conclude_order( - order_id, - BigInt(1), - BigInt(order_v0_height), - Network.Testnet - ); - const expected_conclude_order_v0_input = [ - 2, 4, 6, 49, 150, 254, 178, 75, - 93, 83, 251, 1, 235, 159, 200, 27, - 237, 187, 175, 123, 34, 179, 118, 53, - 6, 173, 213, 7, 212, 102, 232, 253, - 9, 244, 197 - ]; - - assert_eq_arrays(conclude_order_v0_input, expected_conclude_order_v0_input); - console.log("conclude order v0 encoding ok"); - - const conclude_order_v1_input = encode_input_for_conclude_order( - order_id, - BigInt(order_v1_nonce), - BigInt(order_v1_height), - Network.Testnet - ); - const expected_conclude_order_v1_input = [ - 3, 2, 49, 150, 254, 178, 75, 93, - 83, 251, 1, 235, 159, 200, 27, 237, - 187, 175, 123, 34, 179, 118, 53, 6, - 173, 213, 7, 212, 102, 232, 253, 9, - 244, 197 - ]; - - assert_eq_arrays(conclude_order_v1_input, expected_conclude_order_v1_input); - console.log("conclude order v1 encoding ok"); - - const freeze_order_input = encode_input_for_freeze_order( - order_id, - BigInt(order_v1_height), - Network.Testnet - ); - const expected_freeze_order_input = [ - 3, 1, 49, 150, 254, 178, 75, 93, - 83, 251, 1, 235, 159, 200, 27, 237, - 187, 175, 123, 34, 179, 118, 53, 6, - 173, 213, 7, 212, 102, 232, 253, 9, - 244, 197 - ]; - - assert_eq_arrays(freeze_order_input, expected_freeze_order_input); - console.log("freeze order encoding ok"); - - try { - encode_input_for_freeze_order( - order_id, - BigInt(order_v0_height), - Network.Testnet - ); - throw new Error("Freezing an order before v1 worked somehow!"); - } catch (e) { - if (!e.includes("Orders V1 not activated")) { - throw e; - } - console.log("Tested order freezing before v1 successfully"); - } - - try { - const invalid_inputs = "invalid inputs"; - encode_transaction(invalid_inputs, outputs, BigInt(0)); - throw new Error("Invalid inputs worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction input encoding")) { - throw e; - } - console.log("Tested invalid inputs successfully"); - } - - try { - const invalid_outputs = "invalid outputs"; - encode_transaction(inputs, invalid_outputs, BigInt(0)); - throw new Error("Invalid outputs worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction output encoding")) { - throw e; - } - console.log("Tested invalid outputs successfully"); - } - - const tx = encode_transaction(inputs, outputs, BigInt(0)); - const expected_tx = [ - 1, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 4, 8, 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, - 91, 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, - 4, 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, - 158, 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, - 217, 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, - 194, 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, - ]; - assert_eq_arrays(tx, expected_tx); - console.log("tx encoding ok"); - - const witness = encode_witness_no_signature(); - const expected_no_signature_witness = [0, 0]; - assert_eq_arrays(witness, expected_no_signature_witness); - console.log("empty witness encoding ok"); - - const opt_utxos = [1, ...output, 1, ...stake_pool_output]; - - try { - const invalid_private_key = "invalid private key"; - encode_witness( - SignatureHashType.ALL, - invalid_private_key, - address, - tx, - opt_utxos, - 0, - Network.Testnet - ); - throw new Error("Invalid private key worked somehow!"); - } catch (e) { - if (!e.includes("Invalid private key encoding")) { - throw e; - } - console.log("Tested invalid private key in encode witness successfully"); - } - try { - const invalid_address = "invalid address"; - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - invalid_address, - tx, - opt_utxos, - 0, - Network.Testnet - ); - throw new Error("Invalid address worked somehow!"); - } catch (e) { - if (!e.includes("Invalid addressable")) { - throw e; - } - console.log("Tested invalid address in encode witness successfully"); - } - try { - const invalid_tx = "invalid tx"; - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - address, - invalid_tx, - opt_utxos, - 0, - Network.Testnet - ); - throw new Error("Invalid transaction worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction encoding")) { - throw e; - } - console.log("Tested invalid transaction in encode witness successfully"); - } - try { - // Note: if "invalid utxos" were passed to `encode_witness` directly (i.e. as a string instead - // of an array), the `inputs: &[u8]` parameter of `encode_witness` would contain 13 zeroes, - // which would be parsed as 13 `Option::None` and the error would be about an incorrect witness - // count. - const invalid_utxos = [...Buffer.from("invalid utxos")] - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - address, - tx, - invalid_utxos, - 0, - Network.Testnet - ); - throw new Error("Invalid utxo worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction input encoding")) { - throw e; - } - console.log("Tested invalid utxo in encode witness successfully"); - } - try { - const invalid_utxos_count = [0]; - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - address, - tx, - invalid_utxos_count, - 0, - Network.Testnet - ); - throw new Error("Invalid utxo worked somehow!"); - } catch (e) { - if (!e.includes("Utxos count does not match inputs count")) { - throw e; - } - console.log("Tested invalid utxo count in encode witness successfully"); - } - try { - const invalid_input_idx = 999; - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - address, - tx, - opt_utxos, - invalid_input_idx, - Network.Testnet - ); - throw new Error("Invalid address worked somehow!"); - } catch (e) { - if (!e.includes("Invalid input index")) { - throw e; - } - console.log("Tested invalid input index in encode witness successfully"); - } - // all ok - encode_witness( - SignatureHashType.ALL, - receiving_privkey, - address, - tx, - opt_utxos, - 0, - Network.Testnet - ); - - // as signatures are random, hardcode one so we can test the encodings for the signed transaction - const random_witness2 = [ - 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, 180, - 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, 130, 186, 209, 98, - 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, 15, 21, 164, 83, 151, 14, - 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, 203, 31, 216, 54, 141, 223, - 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, 114, 99, 6, 19, 220, 156, - 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, 2, 254, 122, 169, 127, 235, - 158, 62, 127, 177, 12, 228, - ]; - - try { - const invalid_witnesses = "invalid witnesses"; - encode_signed_transaction(tx, invalid_witnesses); - throw new Error("Invalid witnesses worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction witness encoding")) { - throw e; - } - console.log("Tested invalid witnesses successfully"); - } - - try { - encode_signed_transaction(tx, witness); - throw new Error("Invalid number of witnesses worked somehow!"); - } catch (e) { - if ( - !e.includes( - "The number of signatures does not match the number of inputs" - ) - ) { - throw e; - } - console.log("Tested invalid number of witnesses successfully"); - } - - try { - const invalid_tx = "invalid tx"; - encode_signed_transaction(invalid_tx, witness); - throw new Error("Invalid transaction worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction encoding")) { - throw e; - } - console.log("Tested invalid transaction successfully"); - } - - let witnesses = [...random_witness2, ...random_witness2]; - const signed_tx = encode_signed_transaction(tx, witnesses); - const expected_signed_tx = [ - 1, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 4, 8, 1, 0, 145, 1, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, - 91, 4, 195, 202, 103, 207, 80, 217, 178, 0, 145, 1, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 113, 2, 0, 1, 91, 58, 110, 176, 100, 207, 6, 194, 41, 193, 30, 91, - 4, 195, 202, 103, 207, 80, 217, 178, 0, 108, 245, 234, 97, 170, 9, 247, - 158, 169, 100, 84, 123, 235, 183, 147, 29, 136, 118, 203, 24, 146, 56, 60, - 217, 2, 198, 32, 133, 255, 240, 84, 123, 1, 91, 58, 110, 176, 100, 207, 6, - 194, 41, 193, 30, 91, 4, 195, 202, 103, 207, 80, 217, 178, 100, 0, 0, 8, - 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, 44, 38, 35, 73, 145, 212, 180, - 49, 115, 4, 150, 204, 250, 205, 123, 131, 201, 114, 130, 186, 209, 98, - 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, 15, 21, 164, 83, 151, 14, - 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, 203, 31, 216, 54, 141, 223, - 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, 114, 99, 6, 19, 220, 156, - 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, 2, 254, 122, 169, 127, 235, - 158, 62, 127, 177, 12, 228, 1, 1, 141, 1, 0, 2, 227, 252, 33, 195, 223, - 44, 38, 35, 73, 145, 212, 180, 49, 115, 4, 150, 204, 250, 205, 123, 131, - 201, 114, 130, 186, 209, 98, 181, 118, 233, 133, 89, 0, 99, 87, 109, 227, - 15, 21, 164, 83, 151, 14, 235, 106, 83, 230, 40, 64, 146, 112, 52, 103, - 203, 31, 216, 54, 141, 223, 27, 175, 133, 164, 172, 239, 122, 121, 17, 88, - 114, 99, 6, 19, 220, 156, 167, 40, 17, 211, 196, 45, 209, 111, 170, 161, - 2, 254, 122, 169, 127, 235, 158, 62, 127, 177, 12, 228, - ]; - assert_eq_arrays(signed_tx, expected_signed_tx); - - const opt_htlc_utxos = [1, ...htlc_coins_output, 1, ...htlc_tokens_output]; - const htlc_tx = encode_transaction(inputs, outputs, BigInt(0)); - // encode witness with secret - const witness_with_htlc_secret = encode_witness_htlc_secret( - SignatureHashType.ALL, - receiving_privkey, - address, - htlc_tx, - opt_htlc_utxos, - 0, - secret, - Network.Testnet - ); - console.log("Tested encode witness with htlc secret successfully"); - - // encode multisig challenge - const alice_sk = make_private_key(); - const alice_pk = public_key_from_private_key(alice_sk); - const bob_sk = make_private_key(); - const bob_pk = public_key_from_private_key(bob_sk); - let challenge = encode_multisig_challenge([...alice_pk, ...bob_pk], 2, Network.Testnet); - console.log("Tested multisig challenge successfully"); - - // encode mutlisig witness - const witness_with_htlc_multisig_1 = encode_witness_htlc_multisig( - SignatureHashType.ALL, - alice_sk, - 0, - new Uint8Array([]), - challenge, - tx, - opt_htlc_utxos, - 1, - Network.Testnet - ); - console.log("Tested encode multisig witness 0 successfully"); - - const witness_with_htlc_multisig = encode_witness_htlc_multisig( - SignatureHashType.ALL, - bob_sk, - 1, - witness_with_htlc_multisig_1, - challenge, - tx, - opt_htlc_utxos, - 1, - Network.Testnet - ); - console.log("Tested encode multisig witness 1 successfully"); - - // encode signed tx with secret and multi - const htlc_signed_tx = encode_signed_transaction(htlc_tx, [...witness_with_htlc_secret, ...witness_with_htlc_multisig]); - // extract secret from signed tx - const secret_extracted = extract_htlc_secret(htlc_signed_tx, true, tx_outpoint, 1); - assert_eq_arrays(secret, secret_extracted); - - const estimated_size = estimate_transaction_size( - inputs, - [address, address], - outputs, - Network.Testnet - ); - if (estimated_size != expected_signed_tx.length) { - throw new Error("wrong estimated size"); - } - console.log( - `estimated size ${estimated_size} vs real ${expected_signed_tx.length}` - ); - } - - { - const eff_bal = effective_pool_balance( - Network.Mainnet, - Amount.from_atoms("0"), - Amount.from_atoms("0") - ); - if (eff_bal.atoms() != "0") { - throw new Error(`Effective balance test failed ${eff_bal}`); - } - } - - { - const eff_bal = effective_pool_balance( - Network.Mainnet, - Amount.from_atoms("4000000000000000"), - Amount.from_atoms("20000000000000000") - ); - if (eff_bal.atoms() != "18679147907594054") { - throw new Error(`Effective balance test failed ${eff_bal}`); - } - } - - { - // capped - const eff_bal = effective_pool_balance( - Network.Mainnet, - Amount.from_atoms("59999080000000000"), - Amount.from_atoms("59999080000000000") - ); - if (eff_bal.atoms() != "59999080000000000") { - throw new Error(`Effective balance test failed ${eff_bal}`); - } - } - - { - // over capped - const over_capped = Math.floor(Math.random() * 4); - const capped = 6 + over_capped; - const eff_bal = effective_pool_balance( - Network.Mainnet, - Amount.from_atoms(`${capped}0000000000000000`), - Amount.from_atoms(`${capped}0000000000000000`) - ); - if (eff_bal.atoms() != "59999080000000000") { - throw new Error(`Effective balance test failed ${eff_bal}`); - } - } - - run_one_test(test_get_transaction_id); - run_one_test(test_signed_transaction_intent); -} - -function test_get_transaction_id() { - const tx_bin = [ - 1, 0, 4, 0, 0, 255, 93, 154, 148, 57, 14, 233, 114, 8, 211, 26, 165, 195, - 181, 221, 189, 141, 249, 211, 8, 6, 157, 242, 235, 245, 40, 63, 124, 227, - 228, 38, 20, 1, 0, 0, 0, 8, 3, 64, 249, 146, 78, 77, 160, 175, 125, 200, - 197, 190, 113, 169, 201, 224, 89, 98, 199, 191, 78, 249, 97, 39, 253, 231, - 167, 180, 225, 70, 158, 72, 98, 15, 0, 128, 224, 55, 121, 195, 17, 2, 0, 3, - 101, 128, 126, 59, 65, 71, 203, 151, 139, 120, 113, 94, 96, 96, 96, 146, - 248, 157, 199, 105, 88, 110, 152, 69, 104, 80, 189, 59, 68, 156, 135, 180, - 0, 32, 48, 21, 233, 239, 159, 193, 66, 86, 158, 15, 150, 107, 192, 24, 132, - 100, 250, 113, 42, 132, 30, 20, 0, 46, 15, 233, 82, 160, 118, 162, 108, 1, - 229, 57, 197, 240, 206, 186, 146, 122, 184, 248, 245, 95, 39, 74, 247, 57, - 206, 78, 239, 55, 0, 0, 11, 0, 32, 74, 169, 209, 1, 0, 0, 11, 64, 158, 76, - 53, 93, 1, 1, 153, 228, 236, 58, 91, 23, 97, 64, 239, 156, 213, 140, 125, - 53, 121, 253, 176, 236, 178, 26, - ]; - - const tx_signed_bin = [ - 1, 0, 4, 0, 0, 255, 93, 154, 148, 57, 14, 233, 114, 8, 211, 26, 165, 195, - 181, 221, 189, 141, 249, 211, 8, 6, 157, 242, 235, 245, 40, 63, 124, 227, - 228, 38, 20, 1, 0, 0, 0, 8, 3, 64, 249, 146, 78, 77, 160, 175, 125, 200, - 197, 190, 113, 169, 201, 224, 89, 98, 199, 191, 78, 249, 97, 39, 253, 231, - 167, 180, 225, 70, 158, 72, 98, 15, 0, 128, 224, 55, 121, 195, 17, 2, 0, 3, - 101, 128, 126, 59, 65, 71, 203, 151, 139, 120, 113, 94, 96, 96, 96, 146, - 248, 157, 199, 105, 88, 110, 152, 69, 104, 80, 189, 59, 68, 156, 135, 180, - 0, 32, 48, 21, 233, 239, 159, 193, 66, 86, 158, 15, 150, 107, 192, 24, 132, - 100, 250, 113, 42, 132, 30, 20, 0, 46, 15, 233, 82, 160, 118, 162, 108, 1, - 229, 57, 197, 240, 206, 186, 146, 122, 184, 248, 245, 95, 39, 74, 247, 57, - 206, 78, 239, 55, 0, 0, 11, 0, 32, 74, 169, 209, 1, 0, 0, 11, 64, 158, 76, - 53, 93, 1, 1, 153, 228, 236, 58, 91, 23, 97, 64, 239, 156, 213, 140, 125, - 53, 121, 253, 176, 236, 178, 26, 4, 1, 1, 141, 1, 0, 2, 237, 221, 0, 59, - 251, 99, 51, 18, 62, 104, 42, 190, 105, 35, 218, 29, 56, 250, 164, 240, 224, - 217, 226, 238, 66, 213, 170, 70, 193, 82, 163, 72, 0, 167, 73, 163, 12, 140, - 156, 51, 105, 108, 228, 7, 252, 20, 94, 188, 152, 36, 225, 123, 119, 141, - 13, 156, 204, 129, 41, 190, 82, 243, 123, 116, 22, 14, 96, 246, 104, 154, - 194, 244, 129, 7, 30, 26, 99, 217, 207, 15, 110, 171, 132, 194, 112, 59, 94, - 159, 34, 156, 216, 24, 140, 224, 146, 237, 212, - ]; - - const expected_tx_id = - "35a7938c2a2aad5ae324e7d0536de245bf9e439169aa3c16f1492be117e5d0e0"; - - { - const tx_id = get_transaction_id(tx_bin, true); - if (tx_id != expected_tx_id) { - throw new Error( - `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` - ); - } - } - - { - const tx_id = get_transaction_id(tx_bin, false); - if (tx_id != expected_tx_id) { - throw new Error( - `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` - ); - } - } - - { - const tx_id = get_transaction_id(tx_signed_bin, false); - if (tx_id != expected_tx_id) { - throw new Error( - `Decoded transaction id mismatch: ${tx_id} != ${expected_tx_id}` - ); - } - } - - { - try { - get_transaction_id(tx_signed_bin, true); - throw new Error("Invalid witnesses worked somehow!"); - } catch (e) { - if (!e.includes("Invalid transaction encoding")) { - throw new Error( - "Invalid transaction encoding resulted in an unexpected error message!" - ); - } - } - } -} - -function test_signed_transaction_intent() { - try { - const invalid_tx_id = "invalid tx id"; - make_transaction_intent_message_to_sign("intent", invalid_tx_id); - throw new Error("Invalid tx id worked somehow!"); - } catch (e) { - if (!e.includes("Error parsing transaction id")) { - throw e; - } - } - - const tx_id = "DFC2BB0CC4C7F3ED3FE682A48EE9F78BCD4962E55E7BC239BD340EC22AFF8657"; - const message = make_transaction_intent_message_to_sign("the intent", tx_id); - const expected_message = new TextEncoder().encode( - ""); - assert_eq_arrays(message, expected_message); - - try { - const invalid_signatures = "invalid signatures"; - encode_signed_transaction_intent(message, invalid_signatures); - throw new Error("Invalid signatures worked somehow!"); - } catch (e) { - if (!e.includes("Error decoding a JsValue as an array of arrays of bytes")) { - throw e; - } - } - - { - const prv_key1 = [ - 0, 142, 11, 183, 83, 79, 207, 79, 18, 172, 116, 88, 251, 128, 146, 254, 82, - 156, 229, 110, 160, 187, 104, 237, 182, 59, 95, 108, 203, 22, 138, 173, 147 - ]; - const pubkey_addr1 = "rpmt1qgqqxtunp0gdsysq9g3fke9pesl4w8xg3t7ynssfrvqetae0d9nqn3prq3mdt7"; - const pubkeyhash_addr1 = "rmt1qxtlh84a7fflmeem9g4wtmyp2px42gnxwqprnjlw"; - const prv_key2 = [ - 0, 52, 13, 17, 187, 88, 27, 23, 211, 24, 13, 103, 68, 60, 205, 11, 221, - 141, 15, 97, 7, 234, 184, 222, 38, 85, 151, 118, 0, 154, 109, 134, 42 - ]; - const pubkey_addr2 = "rpmt1qgqqylj755w0rlejn3cjadtrhskkzyxqs9nq7mura3z467fkaam7ppxkjr77n7"; - const pubkeyhash_addr2 = "rmt1qx0y7ktusde6d4hf9474z28dwcsys3uk5qxphddl"; - - const signature1 = sign_challenge(prv_key1, message); - const signature2 = sign_challenge(prv_key2, message); - - const signed_intent = encode_signed_transaction_intent(message, [Array.from(signature1), Array.from(signature2)]); - - verify_transaction_intent(message, signed_intent, [pubkey_addr1, pubkeyhash_addr2], Network.Regtest); - verify_transaction_intent(message, signed_intent, [pubkeyhash_addr1, pubkey_addr2], Network.Regtest); - - try { - verify_transaction_intent(message, signed_intent, [pubkeyhash_addr2, pubkey_addr1], Network.Regtest); - throw new Error("Mismatched addresses worked somehow!"); - } catch (e) { - if (!e.includes("Public key to public key hash mismatch")) { - throw e; - } - } - - const bad_signature1 = sign_challenge(prv_key1, [...message, 123]); - const bad_signed_intent = encode_signed_transaction_intent(message, [Array.from(bad_signature1), Array.from(signature2)]); - - try { - verify_transaction_intent(message, bad_signed_intent, [pubkey_addr1, pubkey_addr2], Network.Regtest); - throw new Error("Bad signature worked somehow!"); - } catch (e) { - if (!e.includes("Signature verification failed")) { - throw e; - } - } - } - - { - // Encode some predefined signatures to ensure stability of the encoding. - const signature1 = [ - 0, 3, 47, 147, 11, 208, 216, 18, 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, - 247, 47, 105, 102, 9, 196, 35, 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, - 14, 233, 201, 16, 104, 217, 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, - 124, 53, 201, 124, 194, 7, 45, 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163 - ]; - const signature2 = [ - 0, 2, 126, 94, 165, 28, 241, 255, 50, 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, - 69, 93, 121, 54, 239, 119, 224, 132, 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, - 9, 21, 200, 244, 222, 219, 172, 85, 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, - 98, 248, 161, 3, 119, 149, 13, 159, 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 - ]; - const expected_encoded_signed_intent = [ - 105, 1, 60, 116, 120, 95, 105, 100, 58, 100, 102, 99, 50, 98, 98, 48, 99, 99, 52, 99, 55, 102, 51, 101, 100, 51, - 102, 101, 54, 56, 50, 97, 52, 56, 101, 101, 57, 102, 55, 56, 98, 99, 100, 52, 57, 54, 50, 101, 53, 53, 101, 55, - 98, 99, 50, 51, 57, 98, 100, 51, 52, 48, 101, 99, 50, 50, 97, 102, 102, 56, 54, 53, 55, 59, 105, 110, 116, 101, - 110, 116, 58, 116, 104, 101, 32, 105, 110, 116, 101, 110, 116, 62, 8, 141, 1, 0, 3, 47, 147, 11, 208, 216, 18, - 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, 247, 47, 105, 102, 9, 196, 35, - 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, 14, 233, 201, 16, 104, 217, - 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, 124, 53, 201, 124, 194, 7, 45, - 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163, 141, 1, 0, 2, 126, 94, 165, 28, 241, 255, 50, - 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, 69, 93, 121, 54, 239, 119, 224, 132, - 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, 9, 21, 200, 244, 222, 219, 172, 85, - 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, 98, 248, 161, 3, 119, 149, 13, 159, - 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 - ]; - - const encoded_signed_intent = - encode_signed_transaction_intent(message, [signature1, signature2]); - assert_eq_arrays(encoded_signed_intent, expected_encoded_signed_intent); - } -} diff --git a/wasm-wrappers/src/encode_input.rs b/wasm-wrappers/src/encode_input.rs new file mode 100644 index 0000000000..6985b5b862 --- /dev/null +++ b/wasm-wrappers/src/encode_input.rs @@ -0,0 +1,281 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use wasm_bindgen::prelude::*; + +use common::{ + chain::{ + config::Builder, AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, + OrderAccountCommand, OrdersVersion, OutPointSourceId, TxInput, UtxoOutPoint, + }, + primitives::BlockHeight, +}; +use serialization::{DecodeAll, Encode}; + +use crate::{ + error::Error, + types::{Amount, Network, TokenUnfreezable}, + utils::parse_addressable, +}; + +/// Given an output source id as bytes, and an output index, together representing a utxo, +/// this function returns the input that puts them together, as bytes. +#[wasm_bindgen] +pub fn encode_input_for_utxo( + outpoint_source_id: &[u8], + output_index: u32, +) -> Result, Error> { + let outpoint_source_id = OutPointSourceId::decode_all(&mut &outpoint_source_id[..]) + .map_err(Error::InvalidOutpointIdEncoding)?; + let input = TxInput::Utxo(UtxoOutPoint::new(outpoint_source_id, output_index)); + Ok(input.encode()) +} + +/// Given a delegation id, an amount and a network type (mainnet, testnet, etc), this function +/// creates an input that withdraws from a delegation. +/// A nonce is needed because this spends from an account. The nonce must be in sequence for everything in that account. +#[wasm_bindgen] +pub fn encode_input_for_withdraw_from_delegation( + delegation_id: &str, + amount: Amount, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let delegation_id = parse_addressable(&chain_config, delegation_id)?; + let input = TxInput::Account(AccountOutPoint::new( + AccountNonce::new(nonce), + AccountSpending::DelegationBalance(delegation_id, amount), + )); + Ok(input.encode()) +} + +/// Given a token_id, an amount of tokens to mint and nonce return an encoded mint tokens input +#[wasm_bindgen] +pub fn encode_input_for_mint_tokens( + token_id: &str, + amount: Amount, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let amount = amount.as_internal_amount()?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::MintTokens(token_id, amount), + ); + Ok(input.encode()) +} + +/// Given a token_id and nonce return an encoded unmint tokens input +#[wasm_bindgen] +pub fn encode_input_for_unmint_tokens( + token_id: &str, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::UnmintTokens(token_id), + ); + Ok(input.encode()) +} + +/// Given a token_id and nonce return an encoded lock_token_supply input +#[wasm_bindgen] +pub fn encode_input_for_lock_token_supply( + token_id: &str, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::LockTokenSupply(token_id), + ); + Ok(input.encode()) +} + +/// Given a token_id, is token unfreezable and nonce return an encoded freeze token input +#[wasm_bindgen] +pub fn encode_input_for_freeze_token( + token_id: &str, + is_token_unfreezable: TokenUnfreezable, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::FreezeToken(token_id, is_token_unfreezable.into()), + ); + Ok(input.encode()) +} + +/// Given a token_id and nonce return an encoded unfreeze token input +#[wasm_bindgen] +pub fn encode_input_for_unfreeze_token( + token_id: &str, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::UnfreezeToken(token_id), + ); + Ok(input.encode()) +} + +/// Given a token_id, new authority destination and nonce return an encoded change token authority input +#[wasm_bindgen] +pub fn encode_input_for_change_token_authority( + token_id: &str, + new_authority: &str, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let new_authority = parse_addressable(&chain_config, new_authority)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::ChangeTokenAuthority(token_id, new_authority), + ); + Ok(input.encode()) +} + +/// Given a token_id, new metadata uri and nonce return an encoded change token metadata uri input +#[wasm_bindgen] +pub fn encode_input_for_change_token_metadata_uri( + token_id: &str, + new_metadata_uri: &str, + nonce: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let input = TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::ChangeTokenMetadataUri(token_id, new_metadata_uri.into()), + ); + Ok(input.encode()) +} + +/// Given an amount to fill an order (which is described in terms of ask currency) and a destination +/// for result outputs create an input that fills the order. +/// +/// Note: the nonce is only needed before the orders V1 fork activation. After the fork the nonce is +/// ignored and any value can be passed for the parameter. +#[wasm_bindgen] +pub fn encode_input_for_fill_order( + order_id: &str, + fill_amount: Amount, + destination: &str, + nonce: u64, + current_block_height: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let order_id = parse_addressable(&chain_config, order_id)?; + let fill_amount = fill_amount.as_internal_amount()?; + let destination = parse_addressable(&chain_config, destination)?; + let orders_version = chain_config + .chainstate_upgrades() + .version_at_height(BlockHeight::new(current_block_height)) + .1 + .orders_version(); + + let input = match orders_version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::FillOrder(order_id, fill_amount, destination), + ), + OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + order_id, + fill_amount, + destination, + )), + }; + Ok(input.encode()) +} + +/// Given an order id create an input that freezes the order. +/// +/// Note: order freezing is available only after the orders V1 fork activation. +#[wasm_bindgen] +pub fn encode_input_for_freeze_order( + order_id: &str, + current_block_height: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let order_id = parse_addressable(&chain_config, order_id)?; + let orders_version = chain_config + .chainstate_upgrades() + .version_at_height(BlockHeight::new(current_block_height)) + .1 + .orders_version(); + + let input = match orders_version { + OrdersVersion::V0 => { + return Err(Error::OrdersV1NotActivatedAtSpecifiedHeight); + } + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)) + } + }; + + Ok(input.encode()) +} + +/// Given an order id create an input that concludes the order. +/// +/// Note: the nonce is only needed before the orders V1 fork activation. After the fork the nonce is +/// ignored and any value can be passed for the parameter. +#[wasm_bindgen] +pub fn encode_input_for_conclude_order( + order_id: &str, + nonce: u64, + current_block_height: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let order_id = parse_addressable(&chain_config, order_id)?; + let orders_version = chain_config + .chainstate_upgrades() + .version_at_height(BlockHeight::new(current_block_height)) + .1 + .orders_version(); + + let input = match orders_version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(nonce), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + + Ok(input.encode()) +} diff --git a/wasm-wrappers/src/encode_output.rs b/wasm-wrappers/src/encode_output.rs new file mode 100644 index 0000000000..40096b6281 --- /dev/null +++ b/wasm-wrappers/src/encode_output.rs @@ -0,0 +1,375 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::str::FromStr as _; + +use wasm_bindgen::prelude::*; + +use common::chain::{ + config::Builder, + htlc::{HashedTimelockContract, HtlcSecretHash}, + output_value::OutputValue::{self, Coin, TokenV1}, + stakelock::StakePoolData, + timelock::OutputTimeLock, + tokens::{ + Metadata, NftIssuance, NftIssuanceV0, TokenCreator, TokenIssuance, TokenIssuanceV1, + TokenTotalSupply, + }, + ChainConfig, OrderData, TxOutput, +}; +use crypto::key::PublicKey; +use serialization::{DecodeAll, Encode}; + +use crate::{ + error::Error, + types::{Amount, FreezableToken, Network, TotalSupply}, + utils::parse_addressable, +}; + +/// Given a destination address, an amount and a network type (mainnet, testnet, etc), this function +/// creates an output of type Transfer, and returns it as bytes. +#[wasm_bindgen] +pub fn encode_output_transfer( + amount: Amount, + address: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let destination = parse_addressable(&chain_config, address)?; + + let output = TxOutput::Transfer(Coin(amount), destination); + Ok(output.encode()) +} + +/// Given a destination address, an amount, token ID (in address form) and a network type (mainnet, testnet, etc), this function +/// creates an output of type Transfer for tokens, and returns it as bytes. +#[wasm_bindgen] +pub fn encode_output_token_transfer( + amount: Amount, + address: &str, + token_id: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let destination = parse_addressable(&chain_config, address)?; + let token = parse_addressable(&chain_config, token_id)?; + + let output = TxOutput::Transfer(TokenV1(token, amount), destination); + Ok(output.encode()) +} + +/// Given a valid receiving address, and a locking rule as bytes (available in this file), +/// and a network type (mainnet, testnet, etc), this function creates an output of type +/// LockThenTransfer with the parameters provided. +#[wasm_bindgen] +pub fn encode_output_lock_then_transfer( + amount: Amount, + address: &str, + lock: &[u8], + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let destination = parse_addressable(&chain_config, address)?; + let lock = + OutputTimeLock::decode_all(&mut &lock[..]).map_err(Error::InvalidTimeLockEncoding)?; + + let output = TxOutput::LockThenTransfer(Coin(amount), destination, lock); + Ok(output.encode()) +} + +/// Given a valid receiving address, token ID (in address form), a locking rule as bytes (available in this file), +/// and a network type (mainnet, testnet, etc), this function creates an output of type +/// LockThenTransfer with the parameters provided. +#[wasm_bindgen] +pub fn encode_output_token_lock_then_transfer( + amount: Amount, + address: &str, + token_id: &str, + lock: &[u8], + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let destination = parse_addressable(&chain_config, address)?; + let lock = + OutputTimeLock::decode_all(&mut &lock[..]).map_err(Error::InvalidTimeLockEncoding)?; + let token = parse_addressable(&chain_config, token_id)?; + + let output = TxOutput::LockThenTransfer(TokenV1(token, amount), destination, lock); + Ok(output.encode()) +} + +/// Given an amount, this function creates an output (as bytes) to burn a given amount of coins +#[wasm_bindgen] +pub fn encode_output_coin_burn(amount: Amount) -> Result, Error> { + let amount = amount.as_internal_amount()?; + + let output = TxOutput::Burn(Coin(amount)); + Ok(output.encode()) +} + +/// Given an amount, token ID (in address form) and network type (mainnet, testnet, etc), +/// this function creates an output (as bytes) to burn a given amount of tokens +#[wasm_bindgen] +pub fn encode_output_token_burn( + amount: Amount, + token_id: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let token = parse_addressable(&chain_config, token_id)?; + + let output = TxOutput::Burn(TokenV1(token, amount)); + Ok(output.encode()) +} + +/// Given a pool id as string, an owner address and a network type (mainnet, testnet, etc), +/// this function returns an output (as bytes) to create a delegation to the given pool. +/// The owner address is the address that is authorized to withdraw from that delegation. +#[wasm_bindgen] +pub fn encode_output_create_delegation( + pool_id: &str, + owner_address: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let destination = parse_addressable(&chain_config, owner_address)?; + let pool_id = parse_addressable(&chain_config, pool_id)?; + + let output = TxOutput::CreateDelegationId(destination, pool_id); + Ok(output.encode()) +} + +/// Given a delegation id (as string, in address form), an amount and a network type (mainnet, testnet, etc), +/// this function returns an output (as bytes) that would delegate coins to be staked in the specified delegation id. +#[wasm_bindgen] +pub fn encode_output_delegate_staking( + amount: Amount, + delegation_id: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let amount = amount.as_internal_amount()?; + let delegation_id = parse_addressable(&chain_config, delegation_id)?; + + let output = TxOutput::DelegateStaking(amount, delegation_id); + Ok(output.encode()) +} + +/// Given a pool id, staking data as bytes and the network type (mainnet, testnet, etc), +/// this function returns an output that creates that staking pool. +/// Note that the pool id is mandated to be taken from the hash of the first input. +/// It is not arbitrary. +#[wasm_bindgen] +pub fn encode_output_create_stake_pool( + pool_id: &str, + pool_data: &[u8], + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let pool_id = parse_addressable(&chain_config, pool_id)?; + let pool_data = StakePoolData::decode_all(&mut &pool_data[..]) + .map_err(Error::InvalidStakePoolDataEncoding)?; + + let output = TxOutput::CreateStakePool(pool_id, Box::new(pool_data)); + Ok(output.encode()) +} + +fn parse_token_total_supply( + value: TotalSupply, + amount: Option, +) -> Result { + let supply = match value { + TotalSupply::Lockable => TokenTotalSupply::Lockable, + TotalSupply::Unlimited => TokenTotalSupply::Unlimited, + TotalSupply::Fixed => TokenTotalSupply::Fixed( + amount.ok_or(Error::FixedTotalSupplyButNoAmount)?.as_internal_amount()?, + ), + }; + + Ok(supply) +} + +/// Given the parameters needed to issue a fungible token, and a network type (mainnet, testnet, etc), +/// this function creates an output that issues that token. +#[allow(clippy::too_many_arguments)] +#[wasm_bindgen] +pub fn encode_output_issue_fungible_token( + authority: &str, + token_ticker: &str, + metadata_uri: &str, + number_of_decimals: u8, + total_supply: TotalSupply, + supply_amount: Option, + is_token_freezable: FreezableToken, + _current_block_height: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let authority = parse_addressable(&chain_config, authority)?; + let token_ticker = token_ticker.into(); + let metadata_uri = metadata_uri.into(); + let total_supply = parse_token_total_supply(total_supply, supply_amount)?; + let is_freezable = is_token_freezable.into(); + + let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { + authority, + token_ticker, + metadata_uri, + number_of_decimals, + total_supply, + is_freezable, + }); + + tx_verifier::check_tokens_issuance(&chain_config, &token_issuance) + .map_err(Error::InvalidTokenParameters)?; + + let output = TxOutput::IssueFungibleToken(Box::new(token_issuance)); + Ok(output.encode()) +} + +/// Given the parameters needed to issue an NFT, and a network type (mainnet, testnet, etc), +/// this function creates an output that issues that NFT. +#[allow(clippy::too_many_arguments)] +#[wasm_bindgen] +pub fn encode_output_issue_nft( + token_id: &str, + authority: &str, + name: &str, + ticker: &str, + description: &str, + media_hash: &[u8], + creator: Option>, + media_uri: Option, + icon_uri: Option, + additional_metadata_uri: Option, + _current_block_height: u64, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let token_id = parse_addressable(&chain_config, token_id)?; + let authority = parse_addressable(&chain_config, authority)?; + let name = name.into(); + let ticker = ticker.into(); + let media_uri = media_uri.map(Into::into).into(); + let icon_uri = icon_uri.map(Into::into).into(); + let media_hash = media_hash.into(); + let additional_metadata_uri = additional_metadata_uri.map(Into::into).into(); + let creator = creator + .map(|pk| PublicKey::decode_all(&mut pk.as_slice())) + .transpose() + .map_err(Error::InvalidNftCreatorPublicKey)? + .map(|public_key| TokenCreator { public_key }); + + let nft_issuance = NftIssuanceV0 { + metadata: Metadata { + media_hash, + media_uri, + ticker, + additional_metadata_uri, + description: description.into(), + name, + icon_uri, + creator, + }, + }; + + tx_verifier::check_nft_issuance_data(&chain_config, &nft_issuance) + .map_err(Error::InvalidTokenParameters)?; + + let output = TxOutput::IssueNft(token_id, Box::new(NftIssuance::V0(nft_issuance)), authority); + Ok(output.encode()) +} + +/// Given data to be deposited in the blockchain, this function provides the output that deposits this data +#[wasm_bindgen] +pub fn encode_output_data_deposit(data: &[u8]) -> Result, Error> { + let output = TxOutput::DataDeposit(data.into()); + Ok(output.encode()) +} + +/// Given the parameters needed to create hash timelock contract, and a network type (mainnet, testnet, etc), +/// this function creates an output. +#[wasm_bindgen] +pub fn encode_output_htlc( + amount: Amount, + token_id: Option, + secret_hash: &str, + spend_address: &str, + refund_address: &str, + refund_timelock: &[u8], + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let output_value = parse_output_value(&chain_config, &amount, token_id)?; + let refund_timelock = OutputTimeLock::decode_all(&mut &refund_timelock[..]) + .map_err(Error::InvalidTimeLockEncoding)?; + let secret_hash = + HtlcSecretHash::from_str(secret_hash).map_err(Error::HtlcSecretHashParseError)?; + + let spend_key = parse_addressable(&chain_config, spend_address)?; + let refund_key = parse_addressable(&chain_config, refund_address)?; + + let htlc = HashedTimelockContract { + secret_hash, + spend_key, + refund_timelock, + refund_key, + }; + let output = TxOutput::Htlc(output_value, Box::new(htlc)); + Ok(output.encode()) +} + +/// Given ask and give amounts and a conclude key create output that creates an order. +/// +/// 'ask_token_id': the parameter represents a Token if it's Some and coins otherwise. +/// 'give_token_id': the parameter represents a Token if it's Some and coins otherwise. +#[wasm_bindgen] +pub fn encode_create_order_output( + ask_amount: Amount, + ask_token_id: Option, + give_amount: Amount, + give_token_id: Option, + conclude_address: &str, + network: Network, +) -> Result, Error> { + let chain_config = Builder::new(network.into()).build(); + let ask = parse_output_value(&chain_config, &ask_amount, ask_token_id)?; + let give = parse_output_value(&chain_config, &give_amount, give_token_id)?; + let conclude_key = parse_addressable(&chain_config, conclude_address)?; + + let order = OrderData::new(conclude_key, ask, give); + let output = TxOutput::CreateOrder(Box::new(order)); + Ok(output.encode()) +} + +fn parse_output_value( + chain_config: &ChainConfig, + amount: &Amount, + token_id: Option, +) -> Result { + let amount = amount.as_internal_amount()?; + match token_id { + Some(token_id) => { + let token_id = parse_addressable(chain_config, &token_id)?; + Ok(OutputValue::TokenV1(token_id, amount)) + } + None => Ok(OutputValue::Coin(amount)), + } +} diff --git a/wasm-wrappers/src/error.rs b/wasm-wrappers/src/error.rs index 7da7e95557..0a9991d66f 100644 --- a/wasm-wrappers/src/error.rs +++ b/wasm-wrappers/src/error.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 RBB S.r.l +// Copyright (c) 2021-2025 RBB S.r.l // opensource@mintlayer.org // SPDX-License-Identifier: MIT // Licensed under the MIT License; @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use wasm_bindgen::JsValue; + use common::{ address::AddressError, chain::{ @@ -31,8 +33,8 @@ use common::{ size_estimation::SizeEstimationError, }; use consensus::EffectivePoolBalanceError; -use wasm_bindgen::JsValue; +#[allow(clippy::enum_variant_names)] #[derive(thiserror::Error, Debug, Clone)] pub enum Error { #[error("Invalid private key encoding: {0}")] @@ -59,6 +61,9 @@ pub enum Error { #[error("Invalid transaction input encoding: {0}")] InvalidInputEncoding(serialization::Error), + #[error("Invalid transaction input utxo encoding: {0}")] + InvalidInputUtxoEncoding(serialization::Error), + #[error("Invalid transaction witness encoding: {0}")] InvalidWitnessEncoding(serialization::Error), diff --git a/wasm-wrappers/src/lib.rs b/wasm-wrappers/src/lib.rs index 45ffeb38ff..7477eb464b 100644 --- a/wasm-wrappers/src/lib.rs +++ b/wasm-wrappers/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 RBB S.r.l +// Copyright (c) 2021-2025 RBB S.r.l // opensource@mintlayer.org // SPDX-License-Identifier: MIT // Licensed under the MIT License; @@ -36,14 +36,13 @@ use gloo_utils::format::JsValueSerdeExt as _; use wasm_bindgen::prelude::*; use common::{ - address::{pubkeyhash::PublicKeyHash, traits::Addressable, Address}, + address::{pubkeyhash::PublicKeyHash, Address}, chain::{ block::timestamp::BlockTimestamp, classic_multisig::ClassicMultisigChallenge, - config::{Builder, ChainType, BIP44_PATH}, - htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, + config::{Builder, BIP44_PATH}, + htlc::HtlcSecret, make_token_id, - output_value::OutputValue::{self, Coin, TokenV1}, signature::{ inputsig::{ arbitrary_message::{produce_message_challenge, ArbitraryMessageSignature}, @@ -57,21 +56,14 @@ use common::{ standard_signature::StandardInputSignature, InputWitness, InputWitnessTag, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::signature_hash, }, stakelock::StakePoolData, timelock::OutputTimeLock, - tokens::{ - IsTokenFreezable, IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, - TokenCreator, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, - }, - AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, ChainConfig, Destination, - OrderAccountCommand, OrderData, OrdersVersion, OutPointSourceId, SignedTransaction, - SignedTransactionIntent, Transaction, TxInput, TxOutput, UtxoOutPoint, - }, - primitives::{ - self, amount::UnsignedIntType, per_thousand::PerThousand, BlockHeight, Id, Idable, H256, + Destination, OutPointSourceId, SignedTransaction, SignedTransactionIntent, Transaction, + TxInput, TxOutput, UtxoOutPoint, }, + primitives::{per_thousand::PerThousand, BlockHeight, Id, Idable, H256}, size_estimation::{ input_signature_size_from_destination, outputs_encoded_size, tx_size_with_num_inputs_and_outputs, @@ -82,137 +74,25 @@ use crypto::key::{ hdkd::{child_number::ChildNumber, derivable::Derivable, u31::U31}, KeyKind, PrivateKey, PublicKey, Signature, }; -use error::Error; use serialization::{Decode, DecodeAll, Encode}; -pub mod error; +use crate::{ + error::Error, + types::{Amount, Network, SignatureHashType, SourceId}, + utils::{decode_raw_array, parse_addressable}, +}; + +mod encode_input; +mod encode_output; +mod error; +#[cfg(test)] +mod tests; +mod types; +mod utils; const RECEIVE_FUNDS_INDEX: ChildNumber = ChildNumber::from_normal(U31::from_u32_with_msb(0).0); const CHANGE_FUNDS_INDEX: ChildNumber = ChildNumber::from_normal(U31::from_u32_with_msb(1).0); -#[wasm_bindgen] -/// Amount type abstraction. The amount type is stored in a string -/// since JavaScript number type cannot fit 128-bit integers. -/// The amount is given as an integer in units of "atoms". -/// Atoms are the smallest, indivisible amount of a coin or token. -pub struct Amount { - atoms: String, -} - -#[wasm_bindgen] -impl Amount { - #[wasm_bindgen] - pub fn from_atoms(atoms: String) -> Self { - Self { atoms } - } - - #[wasm_bindgen] - pub fn atoms(self) -> String { - self.atoms - } - - fn as_internal_amount(&self) -> Result { - UnsignedIntType::from_str(&self.atoms) - .ok() - .map(primitives::Amount::from_atoms) - .ok_or_else(|| Error::AtomsAmountParseError { - atoms: self.atoms.clone(), - }) - } - - fn from_internal_amount(amount: primitives::Amount) -> Self { - Self { - atoms: amount.into_atoms().to_string(), - } - } -} - -#[wasm_bindgen] -#[derive(Debug, Copy, Clone)] -/// The network, for which an operation to be done. Mainnet, testnet, etc. -pub enum Network { - Mainnet, - Testnet, - Regtest, - Signet, -} - -impl From for ChainType { - fn from(value: Network) -> Self { - match value { - Network::Mainnet => ChainType::Mainnet, - Network::Testnet => ChainType::Testnet, - Network::Regtest => ChainType::Regtest, - Network::Signet => ChainType::Signet, - } - } -} - -/// Indicates whether a token can be frozen -#[wasm_bindgen] -pub enum FreezableToken { - No, - Yes, -} - -impl From for IsTokenFreezable { - fn from(value: FreezableToken) -> Self { - match value { - FreezableToken::No => IsTokenFreezable::No, - FreezableToken::Yes => IsTokenFreezable::Yes, - } - } -} - -/// Indicates whether a token can be unfrozen once frozen -#[wasm_bindgen] -pub enum TokenUnfreezable { - No, - Yes, -} - -impl From for IsTokenUnfreezable { - fn from(value: TokenUnfreezable) -> Self { - match value { - TokenUnfreezable::No => IsTokenUnfreezable::No, - TokenUnfreezable::Yes => IsTokenUnfreezable::Yes, - } - } -} - -/// The token supply of a specific token, set on issuance -#[wasm_bindgen] -pub enum TotalSupply { - /// Can be issued with no limit, but then can be locked to have a fixed supply. - Lockable, - /// Unlimited supply, no limits except for numeric limits due to u128 - Unlimited, - /// On issuance, the total number of coins is fixed - Fixed, -} - -fn parse_token_total_supply( - value: TotalSupply, - amount: Option, -) -> Result { - let supply = match value { - TotalSupply::Lockable => TokenTotalSupply::Lockable, - TotalSupply::Unlimited => TokenTotalSupply::Unlimited, - TotalSupply::Fixed => TokenTotalSupply::Fixed( - amount.ok_or(Error::FixedTotalSupplyButNoAmount)?.as_internal_amount()?, - ), - }; - - Ok(supply) -} - -/// A utxo can either come from a transaction or a block reward. This enum signifies that. -#[wasm_bindgen] -pub enum SourceId { - Transaction, - BlockReward, -} - /// A utxo can either come from a transaction or a block reward. /// Given a source id, whether from a block reward or transaction, this function /// takes a generic id with it, and returns serialized binary data of the id @@ -226,28 +106,6 @@ pub fn encode_outpoint_source_id(id: &[u8], source: SourceId) -> Vec { .encode() } -/// The part of the transaction that will be committed in the signature. Similar to bitcoin's sighash. -#[wasm_bindgen] -pub enum SignatureHashType { - ALL, - NONE, - SINGLE, - ANYONECANPAY, -} - -impl From for SigHashType { - fn from(value: SignatureHashType) -> Self { - let value = match value { - SignatureHashType::ALL => SigHashType::ALL, - SignatureHashType::SINGLE => SigHashType::SINGLE, - SignatureHashType::ANYONECANPAY => SigHashType::ANYONECANPAY, - SignatureHashType::NONE => SigHashType::NONE, - }; - - SigHashType::try_from(value).expect("should not fail") - } -} - /// Generates a new, random private key from entropy #[wasm_bindgen] pub fn make_private_key() -> Vec { @@ -285,8 +143,8 @@ pub fn make_default_account_privkey(mnemonic: &str, network: Network) -> Result< /// From an extended private key create a receiving private key for a given key index /// derivation path: current_derivation_path/0/key_index #[wasm_bindgen] -pub fn make_receiving_address(private_key_bytes: &[u8], key_index: u32) -> Result, Error> { - let account_privkey = ExtendedPrivateKey::decode_all(&mut &private_key_bytes[..]) +pub fn make_receiving_address(private_key: &[u8], key_index: u32) -> Result, Error> { + let account_privkey = ExtendedPrivateKey::decode_all(&mut &private_key[..]) .map_err(Error::InvalidPrivateKeyEncoding)?; let private_key = derive(account_privkey, RECEIVE_FUNDS_INDEX, key_index)?.private_key(); Ok(private_key.encode()) @@ -295,8 +153,8 @@ pub fn make_receiving_address(private_key_bytes: &[u8], key_index: u32) -> Resul /// From an extended private key create a change private key for a given key index /// derivation path: current_derivation_path/1/key_index #[wasm_bindgen] -pub fn make_change_address(private_key_bytes: &[u8], key_index: u32) -> Result, Error> { - let account_privkey = ExtendedPrivateKey::decode_all(&mut &private_key_bytes[..]) +pub fn make_change_address(private_key: &[u8], key_index: u32) -> Result, Error> { + let account_privkey = ExtendedPrivateKey::decode_all(&mut &private_key[..]) .map_err(Error::InvalidPrivateKeyEncoding)?; let private_key = derive(account_privkey, CHANGE_FUNDS_INDEX, key_index)?.private_key(); Ok(private_key.encode()) @@ -305,12 +163,9 @@ pub fn make_change_address(private_key_bytes: &[u8], key_index: u32) -> Result Result { - let public_key = PublicKey::decode_all(&mut &public_key_bytes[..]) - .map_err(Error::InvalidPublicKeyEncoding)?; +pub fn pubkey_to_pubkeyhash_address(public_key: &[u8], network: Network) -> Result { + let public_key = + PublicKey::decode_all(&mut &public_key[..]).map_err(Error::InvalidPublicKeyEncoding)?; let chain_config = Builder::new(network.into()).build(); let public_key_hash = PublicKeyHash::from(&public_key); @@ -333,10 +188,8 @@ pub fn public_key_from_private_key(private_key: &[u8]) -> Result, Error> /// Return the extended public key from an extended private key #[wasm_bindgen] -pub fn extended_public_key_from_extended_private_key( - private_key_bytes: &[u8], -) -> Result, Error> { - let extended_private_key = ExtendedPrivateKey::decode_all(&mut &private_key_bytes[..]) +pub fn extended_public_key_from_extended_private_key(private_key: &[u8]) -> Result, Error> { + let extended_private_key = ExtendedPrivateKey::decode_all(&mut &private_key[..]) .map_err(Error::InvalidPrivateKeyEncoding)?; let extended_public_key = extended_private_key.to_public_key(); Ok(extended_public_key.encode()) @@ -346,10 +199,10 @@ pub fn extended_public_key_from_extended_private_key( /// derivation path: current_derivation_path/0/key_index #[wasm_bindgen] pub fn make_receiving_address_public_key( - extended_public_key_bytes: &[u8], + extended_public_key: &[u8], key_index: u32, ) -> Result, Error> { - let account_publickey = ExtendedPublicKey::decode_all(&mut &extended_public_key_bytes[..]) + let account_publickey = ExtendedPublicKey::decode_all(&mut &extended_public_key[..]) .map_err(Error::InvalidPrivateKeyEncoding)?; let public_key = derive(account_publickey, RECEIVE_FUNDS_INDEX, key_index)?.into_public_key(); Ok(public_key.encode()) @@ -359,10 +212,10 @@ pub fn make_receiving_address_public_key( /// derivation path: current_derivation_path/1/key_index #[wasm_bindgen] pub fn make_change_address_public_key( - extended_public_key_bytes: &[u8], + extended_public_key: &[u8], key_index: u32, ) -> Result, Error> { - let account_publickey = ExtendedPublicKey::decode_all(&mut &extended_public_key_bytes[..]) + let account_publickey = ExtendedPublicKey::decode_all(&mut &extended_public_key[..]) .map_err(Error::InvalidPrivateKeyEncoding)?; let public_key = derive(account_publickey, CHANGE_FUNDS_INDEX, key_index)?.into_public_key(); Ok(public_key.encode()) @@ -459,19 +312,6 @@ pub fn verify_challenge( Ok(true) } -fn parse_addressable( - chain_config: &ChainConfig, - address: &str, -) -> Result { - let addressable = Address::from_string(chain_config, address) - .map_err(|error| Error::AddressableParseError { - addressable: address.to_owned(), - error, - })? - .into_object(); - Ok(addressable) -} - /// Return the message that has to be signed to produce a signed transaction intent. #[wasm_bindgen] pub fn make_transaction_intent_message_to_sign( @@ -555,40 +395,6 @@ pub fn verify_transaction_intent( Ok(()) } -/// Given a destination address, an amount and a network type (mainnet, testnet, etc), this function -/// creates an output of type Transfer, and returns it as bytes. -#[wasm_bindgen] -pub fn encode_output_transfer( - amount: Amount, - address: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let destination = parse_addressable(&chain_config, address)?; - - let output = TxOutput::Transfer(Coin(amount), destination); - Ok(output.encode()) -} - -/// Given a destination address, an amount, token ID (in address form) and a network type (mainnet, testnet, etc), this function -/// creates an output of type Transfer for tokens, and returns it as bytes. -#[wasm_bindgen] -pub fn encode_output_token_transfer( - amount: Amount, - address: &str, - token_id: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let destination = parse_addressable(&chain_config, address)?; - let token = parse_addressable(&chain_config, token_id)?; - - let output = TxOutput::Transfer(TokenV1(token, amount), destination); - Ok(output.encode()) -} - /// Given the current block height and a network type (mainnet, testnet, etc), /// this function returns the number of blocks, after which a pool that decommissioned, /// will have its funds unlocked and available for spending. @@ -638,106 +444,6 @@ pub fn encode_lock_until_height(block_height: u64) -> Vec { output.encode() } -/// Given a valid receiving address, and a locking rule as bytes (available in this file), -/// and a network type (mainnet, testnet, etc), this function creates an output of type -/// LockThenTransfer with the parameters provided. -#[wasm_bindgen] -pub fn encode_output_lock_then_transfer( - amount: Amount, - address: &str, - lock: &[u8], - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let destination = parse_addressable(&chain_config, address)?; - let lock = - OutputTimeLock::decode_all(&mut &lock[..]).map_err(Error::InvalidTimeLockEncoding)?; - - let output = TxOutput::LockThenTransfer(Coin(amount), destination, lock); - Ok(output.encode()) -} - -/// Given a valid receiving address, token ID (in address form), a locking rule as bytes (available in this file), -/// and a network type (mainnet, testnet, etc), this function creates an output of type -/// LockThenTransfer with the parameters provided. -#[wasm_bindgen] -pub fn encode_output_token_lock_then_transfer( - amount: Amount, - address: &str, - token_id: &str, - lock: &[u8], - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let destination = parse_addressable(&chain_config, address)?; - let lock = - OutputTimeLock::decode_all(&mut &lock[..]).map_err(Error::InvalidTimeLockEncoding)?; - let token = parse_addressable(&chain_config, token_id)?; - - let output = TxOutput::LockThenTransfer(TokenV1(token, amount), destination, lock); - Ok(output.encode()) -} - -/// Given an amount, this function creates an output (as bytes) to burn a given amount of coins -#[wasm_bindgen] -pub fn encode_output_coin_burn(amount: Amount) -> Result, Error> { - let amount = amount.as_internal_amount()?; - - let output = TxOutput::Burn(Coin(amount)); - Ok(output.encode()) -} - -/// Given an amount, token ID (in address form) and network type (mainnet, testnet, etc), -/// this function creates an output (as bytes) to burn a given amount of tokens -#[wasm_bindgen] -pub fn encode_output_token_burn( - amount: Amount, - token_id: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let token = parse_addressable(&chain_config, token_id)?; - - let output = TxOutput::Burn(TokenV1(token, amount)); - Ok(output.encode()) -} - -/// Given a pool id as string, an owner address and a network type (mainnet, testnet, etc), -/// this function returns an output (as bytes) to create a delegation to the given pool. -/// The owner address is the address that is authorized to withdraw from that delegation. -#[wasm_bindgen] -pub fn encode_output_create_delegation( - pool_id: &str, - owner_address: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let destination = parse_addressable(&chain_config, owner_address)?; - let pool_id = parse_addressable(&chain_config, pool_id)?; - - let output = TxOutput::CreateDelegationId(destination, pool_id); - Ok(output.encode()) -} - -/// Given a delegation id (as string, in address form), an amount and a network type (mainnet, testnet, etc), -/// this function returns an output (as bytes) that would delegate coins to be staked in the specified delegation id. -#[wasm_bindgen] -pub fn encode_output_delegate_staking( - amount: Amount, - delegation_id: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let delegation_id = parse_addressable(&chain_config, delegation_id)?; - - let output = TxOutput::DelegateStaking(amount, delegation_id); - Ok(output.encode()) -} - /// This function returns the staking pool data needed to create a staking pool in an output as bytes, /// given its parameters and the network type (testnet, mainnet, etc). #[wasm_bindgen] @@ -770,25 +476,6 @@ pub fn encode_stake_pool_data( Ok(pool_data.encode()) } -/// Given a pool id, staking data as bytes and the network type (mainnet, testnet, etc), -/// this function returns an output that creates that staking pool. -/// Note that the pool id is mandated to be taken from the hash of the first input. -/// It is not arbitrary. -#[wasm_bindgen] -pub fn encode_output_create_stake_pool( - pool_id: &str, - pool_data: &[u8], - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let pool_id = parse_addressable(&chain_config, pool_id)?; - let pool_data = StakePoolData::decode_all(&mut &pool_data[..]) - .map_err(Error::InvalidStakePoolDataEncoding)?; - - let output = TxOutput::CreateStakePool(pool_id, Box::new(pool_data)); - Ok(output.encode()) -} - /// Returns the fee that needs to be paid by a transaction for issuing a new fungible token #[wasm_bindgen] pub fn fungible_token_issuance_fee(_current_block_height: u64, network: Network) -> Amount { @@ -841,63 +528,21 @@ pub fn token_change_authority_fee(current_block_height: u64, network: Network) - ) } -/// Given the parameters needed to issue a fungible token, and a network type (mainnet, testnet, etc), -/// this function creates an output that issues that token. -#[allow(clippy::too_many_arguments)] -#[wasm_bindgen] -pub fn encode_output_issue_fungible_token( - authority: &str, - token_ticker: &str, - metadata_uri: &str, - number_of_decimals: u8, - total_supply: TotalSupply, - supply_amount: Option, - is_token_freezable: FreezableToken, - _current_block_height: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let authority = parse_addressable(&chain_config, authority)?; - let token_ticker = token_ticker.into(); - let metadata_uri = metadata_uri.into(); - let total_supply = parse_token_total_supply(total_supply, supply_amount)?; - let is_freezable = is_token_freezable.into(); - - let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { - authority, - token_ticker, - metadata_uri, - number_of_decimals, - total_supply, - is_freezable, - }); - - tx_verifier::check_tokens_issuance(&chain_config, &token_issuance) - .map_err(Error::InvalidTokenParameters)?; - - let output = TxOutput::IssueFungibleToken(Box::new(token_issuance)); - Ok(output.encode()) -} - /// Returns the Fungible/NFT Token ID for the given inputs of a transaction #[wasm_bindgen] pub fn get_token_id( - mut inputs: &[u8], + inputs: &[u8], current_block_height: u64, network: Network, ) -> Result { let chain_config = Builder::new(network.into()).build(); - let mut tx_inputs = vec![]; - while !inputs.is_empty() { - let input = TxInput::decode(&mut inputs).map_err(Error::InvalidInputEncoding)?; - tx_inputs.push(input); - } + let inputs = decode_raw_array::(inputs).map_err(Error::InvalidInputEncoding)?; let token_id = make_token_id( &chain_config, BlockHeight::new(current_block_height), - &tx_inputs, + &inputs, )?; Ok(Address::new(&chain_config, token_id) @@ -905,66 +550,6 @@ pub fn get_token_id( .to_string()) } -/// Given the parameters needed to issue an NFT, and a network type (mainnet, testnet, etc), -/// this function creates an output that issues that NFT. -#[allow(clippy::too_many_arguments)] -#[wasm_bindgen] -pub fn encode_output_issue_nft( - token_id: &str, - authority: &str, - name: &str, - ticker: &str, - description: &str, - media_hash: &[u8], - creator: Option>, - media_uri: Option, - icon_uri: Option, - additional_metadata_uri: Option, - _current_block_height: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let authority = parse_addressable(&chain_config, authority)?; - let name = name.into(); - let ticker = ticker.into(); - let media_uri = media_uri.map(Into::into).into(); - let icon_uri = icon_uri.map(Into::into).into(); - let media_hash = media_hash.into(); - let additional_metadata_uri = additional_metadata_uri.map(Into::into).into(); - let creator = creator - .map(|pk| PublicKey::decode_all(&mut pk.as_slice())) - .transpose() - .map_err(Error::InvalidNftCreatorPublicKey)? - .map(|public_key| TokenCreator { public_key }); - - let nft_issuance = NftIssuanceV0 { - metadata: Metadata { - media_hash, - media_uri, - ticker, - additional_metadata_uri, - description: description.into(), - name, - icon_uri, - creator, - }, - }; - - tx_verifier::check_nft_issuance_data(&chain_config, &nft_issuance) - .map_err(Error::InvalidTokenParameters)?; - - let output = TxOutput::IssueNft(token_id, Box::new(NftIssuance::V0(nft_issuance)), authority); - Ok(output.encode()) -} - -/// Given data to be deposited in the blockchain, this function provides the output that deposits this data -#[wasm_bindgen] -pub fn encode_output_data_deposit(data: &[u8]) -> Result, Error> { - let output = TxOutput::DataDeposit(data.into()); - Ok(output.encode()) -} - /// Returns the fee that needs to be paid by a transaction for issuing a data deposit #[wasm_bindgen] pub fn data_deposit_fee(current_block_height: u64, network: Network) -> Amount { @@ -974,58 +559,11 @@ pub fn data_deposit_fee(current_block_height: u64, network: Network) -> Amount { ) } -fn parse_output_value( - chain_config: &ChainConfig, - amount: &Amount, - token_id: Option, -) -> Result { - let amount = amount.as_internal_amount()?; - match token_id { - Some(token_id) => { - let token_id = parse_addressable(chain_config, &token_id)?; - Ok(OutputValue::TokenV1(token_id, amount)) - } - None => Ok(OutputValue::Coin(amount)), - } -} - -/// Given the parameters needed to create hash timelock contract, and a network type (mainnet, testnet, etc), -/// this function creates an output. -#[wasm_bindgen] -pub fn encode_output_htlc( - amount: Amount, - token_id: Option, - secret_hash: &str, - spend_address: &str, - refund_address: &str, - refund_timelock: &[u8], - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let output_value = parse_output_value(&chain_config, &amount, token_id)?; - let refund_timelock = OutputTimeLock::decode_all(&mut &refund_timelock[..]) - .map_err(Error::InvalidTimeLockEncoding)?; - let secret_hash = - HtlcSecretHash::from_str(secret_hash).map_err(Error::HtlcSecretHashParseError)?; - - let spend_key = parse_addressable(&chain_config, spend_address)?; - let refund_key = parse_addressable(&chain_config, refund_address)?; - - let htlc = HashedTimelockContract { - secret_hash, - spend_key, - refund_timelock, - refund_key, - }; - let output = TxOutput::Htlc(output_value, Box::new(htlc)); - Ok(output.encode()) -} - /// Given a signed transaction and input outpoint that spends an htlc utxo, extract a secret that is /// encoded in the corresponding input signature #[wasm_bindgen] pub fn extract_htlc_secret( - signed_tx_bytes: &[u8], + signed_tx: &[u8], strict_byte_size: bool, htlc_outpoint_source_id: &[u8], htlc_output_index: u32, @@ -1035,11 +573,10 @@ pub fn extract_htlc_secret( let htlc_utxo_outpoint = UtxoOutPoint::new(outpoint_source_id, htlc_output_index); let tx = if strict_byte_size { - SignedTransaction::decode_all(&mut &signed_tx_bytes[..]) + SignedTransaction::decode_all(&mut &signed_tx[..]) .map_err(Error::InvalidTransactionEncoding)? } else { - SignedTransaction::decode(&mut &signed_tx_bytes[..]) - .map_err(Error::InvalidTransactionEncoding)? + SignedTransaction::decode(&mut &signed_tx[..]).map_err(Error::InvalidTransactionEncoding)? }; let htlc_position = tx @@ -1073,39 +610,6 @@ pub fn extract_htlc_secret( } } -/// Given an output source id as bytes, and an output index, together representing a utxo, -/// this function returns the input that puts them together, as bytes. -#[wasm_bindgen] -pub fn encode_input_for_utxo( - outpoint_source_id: &[u8], - output_index: u32, -) -> Result, Error> { - let outpoint_source_id = OutPointSourceId::decode_all(&mut &outpoint_source_id[..]) - .map_err(Error::InvalidOutpointIdEncoding)?; - let input = TxInput::Utxo(UtxoOutPoint::new(outpoint_source_id, output_index)); - Ok(input.encode()) -} - -/// Given a delegation id, an amount and a network type (mainnet, testnet, etc), this function -/// creates an input that withdraws from a delegation. -/// A nonce is needed because this spends from an account. The nonce must be in sequence for everything in that account. -#[wasm_bindgen] -pub fn encode_input_for_withdraw_from_delegation( - delegation_id: &str, - amount: Amount, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let amount = amount.as_internal_amount()?; - let delegation_id = parse_addressable(&chain_config, delegation_id)?; - let input = TxInput::Account(AccountOutPoint::new( - AccountNonce::new(nonce), - AccountSpending::DelegationBalance(delegation_id, amount), - )); - Ok(input.encode()) -} - /// Given the inputs, along each input's destination that can spend that input /// (e.g. If we are spending a UTXO in input number 1 and it is owned by address mtc1xxxx, then it is mtc1xxxx in element number 2 in the vector/list. /// for Account inputs that spend from a delegation it is the owning address of that delegation, @@ -1116,20 +620,15 @@ pub fn encode_input_for_withdraw_from_delegation( pub fn estimate_transaction_size( inputs: &[u8], input_utxos_destinations: Vec, - mut outputs: &[u8], + outputs: &[u8], network: Network, ) -> Result { let chain_config = Builder::new(network.into()).build(); - let mut tx_outputs = vec![]; - while !outputs.is_empty() { - let output = TxOutput::decode(&mut outputs).map_err(Error::InvalidOutputEncoding)?; - tx_outputs.push(output); - } + let outputs = decode_raw_array::(outputs).map_err(Error::InvalidOutputEncoding)?; - let size = - tx_size_with_num_inputs_and_outputs(tx_outputs.len(), input_utxos_destinations.len()) - .map_err(Error::TransactionSizeEstimationError)? - + outputs_encoded_size(tx_outputs.as_slice()); + let size = tx_size_with_num_inputs_and_outputs(outputs.len(), input_utxos_destinations.len()) + .map_err(Error::TransactionSizeEstimationError)? + + outputs_encoded_size(outputs.as_slice()); let inputs_size = inputs.len(); let mut total_size = size + inputs_size; @@ -1149,24 +648,11 @@ pub fn estimate_transaction_size( /// Given inputs as bytes, outputs as bytes, and flags settings, this function returns /// the transaction that contains them all, as bytes. #[wasm_bindgen] -pub fn encode_transaction( - mut inputs: &[u8], - mut outputs: &[u8], - flags: u64, -) -> Result, Error> { - let mut tx_outputs = vec![]; - while !outputs.is_empty() { - let output = TxOutput::decode(&mut outputs).map_err(Error::InvalidOutputEncoding)?; - tx_outputs.push(output); - } - - let mut tx_inputs = vec![]; - while !inputs.is_empty() { - let input = TxInput::decode(&mut inputs).map_err(Error::InvalidInputEncoding)?; - tx_inputs.push(input); - } +pub fn encode_transaction(inputs: &[u8], outputs: &[u8], flags: u64) -> Result, Error> { + let inputs = decode_raw_array::(inputs).map_err(Error::InvalidInputEncoding)?; + let outputs = decode_raw_array::(outputs).map_err(Error::InvalidOutputEncoding)?; - let tx = Transaction::new(flags as u128, tx_inputs, tx_outputs).expect("no error"); + let tx = Transaction::new(flags as u128, inputs, outputs).expect("no error"); Ok(tx.encode()) } @@ -1181,28 +667,25 @@ pub fn encode_witness_no_signature() -> Vec { #[wasm_bindgen] pub fn encode_witness( sighashtype: SignatureHashType, - private_key_bytes: &[u8], + private_key: &[u8], input_owner_destination: &str, - transaction_bytes: &[u8], - mut inputs: &[u8], - input_num: u32, + transaction: &[u8], + input_utxos: &[u8], + input_index: u32, network: Network, ) -> Result, Error> { let chain_config = Builder::new(network.into()).build(); - let private_key = PrivateKey::decode_all(&mut &private_key_bytes[..]) - .map_err(Error::InvalidPrivateKeyEncoding)?; + let private_key = + PrivateKey::decode_all(&mut &private_key[..]).map_err(Error::InvalidPrivateKeyEncoding)?; let destination = parse_addressable(&chain_config, input_owner_destination)?; - let tx = Transaction::decode_all(&mut &transaction_bytes[..]) + let tx = Transaction::decode_all(&mut &transaction[..]) .map_err(Error::InvalidTransactionEncoding)?; - let mut input_utxos = vec![]; - while !inputs.is_empty() { - let utxo = Option::::decode(&mut inputs).map_err(Error::InvalidInputEncoding)?; - input_utxos.push(utxo); - } + let input_utxos = decode_raw_array::>(input_utxos) + .map_err(Error::InvalidInputUtxoEncoding)?; let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); @@ -1212,7 +695,7 @@ pub fn encode_witness( destination, &tx, &utxos, - input_num as usize, + input_index as usize, randomness::make_true_rng(), ) .map(InputWitness::Standard) @@ -1227,29 +710,26 @@ pub fn encode_witness( #[wasm_bindgen] pub fn encode_witness_htlc_secret( sighashtype: SignatureHashType, - private_key_bytes: &[u8], + private_key: &[u8], input_owner_destination: &str, - transaction_bytes: &[u8], - mut inputs: &[u8], - input_num: u32, + transaction: &[u8], + input_utxos: &[u8], + input_index: u32, mut secret: &[u8], network: Network, ) -> Result, Error> { let chain_config = Builder::new(network.into()).build(); - let private_key = PrivateKey::decode_all(&mut &private_key_bytes[..]) - .map_err(Error::InvalidPrivateKeyEncoding)?; + let private_key = + PrivateKey::decode_all(&mut &private_key[..]).map_err(Error::InvalidPrivateKeyEncoding)?; let destination = parse_addressable(&chain_config, input_owner_destination)?; - let tx = Transaction::decode_all(&mut &transaction_bytes[..]) + let tx = Transaction::decode_all(&mut &transaction[..]) .map_err(Error::InvalidTransactionEncoding)?; - let mut input_utxos = vec![]; - while !inputs.is_empty() { - let utxo = Option::::decode(&mut inputs).map_err(Error::InvalidInputEncoding)?; - input_utxos.push(utxo); - } + let input_utxos = decode_raw_array::>(input_utxos) + .map_err(Error::InvalidInputUtxoEncoding)?; let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); @@ -1261,7 +741,7 @@ pub fn encode_witness_htlc_secret( destination, &tx, &utxos, - input_num as usize, + input_index as usize, secret, randomness::make_true_rng(), ) @@ -1275,7 +755,7 @@ pub fn encode_witness_htlc_secret( /// the multisig challenge, as bytes. #[wasm_bindgen] pub fn encode_multisig_challenge( - mut public_keys_bytes: &[u8], + public_keys: &[u8], min_required_signatures: u8, network: Network, ) -> Result, Error> { @@ -1284,12 +764,8 @@ pub fn encode_multisig_challenge( let min_sigs = NonZeroU8::new(min_required_signatures).ok_or(Error::ZeroMultisigRequiredSigs)?; - let mut public_keys = vec![]; - while !public_keys_bytes.is_empty() { - let public_key = - PublicKey::decode(&mut public_keys_bytes).map_err(Error::InvalidPublicKeyEncoding)?; - public_keys.push(public_key); - } + let public_keys = + decode_raw_array::(public_keys).map_err(Error::InvalidPublicKeyEncoding)?; let challenge = ClassicMultisigChallenge::new(&chain_config, min_sigs, public_keys) .map_err(Error::MultisigChallengeCreationError)?; @@ -1306,32 +782,29 @@ pub fn encode_multisig_challenge( #[wasm_bindgen] pub fn encode_witness_htlc_multisig( sighashtype: SignatureHashType, - private_key_bytes: &[u8], + private_key: &[u8], key_index: u8, mut input_witness: &[u8], mut multisig_challenge: &[u8], - transaction_bytes: &[u8], - mut utxos: &[u8], - input_num: u32, + transaction: &[u8], + input_utxos: &[u8], + input_index: u32, network: Network, ) -> Result, Error> { let chain_config = Builder::new(network.into()).build(); - let private_key = PrivateKey::decode_all(&mut &private_key_bytes[..]) - .map_err(Error::InvalidPrivateKeyEncoding)?; + let private_key = + PrivateKey::decode_all(&mut &private_key[..]).map_err(Error::InvalidPrivateKeyEncoding)?; - let tx = Transaction::decode_all(&mut &transaction_bytes[..]) + let tx = Transaction::decode_all(&mut &transaction[..]) .map_err(Error::InvalidTransactionEncoding)?; - let mut input_utxos = vec![]; - while !utxos.is_empty() { - let utxo = Option::::decode(&mut utxos).map_err(Error::InvalidInputEncoding)?; - input_utxos.push(utxo); - } + let input_utxos = decode_raw_array::>(input_utxos) + .map_err(Error::InvalidInputUtxoEncoding)?; let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); let sighashtype = sighashtype.into(); - let sighash = signature_hash(sighashtype, &tx, &utxos, input_num as usize) + let sighash = signature_hash(sighashtype, &tx, &utxos, input_index as usize) .map_err(Error::SighashCalculationError)?; let mut rng = randomness::make_true_rng(); @@ -1387,21 +860,14 @@ pub fn encode_witness_htlc_multisig( /// Given an unsigned transaction and signatures, this function returns a SignedTransaction object as bytes. #[wasm_bindgen] -pub fn encode_signed_transaction( - transaction_bytes: &[u8], - mut signatures: &[u8], -) -> Result, Error> { - let mut tx_signatures = vec![]; - while !signatures.is_empty() { - let signature = - InputWitness::decode(&mut signatures).map_err(Error::InvalidWitnessEncoding)?; - tx_signatures.push(signature); - } +pub fn encode_signed_transaction(transaction: &[u8], signatures: &[u8]) -> Result, Error> { + let signatures = + decode_raw_array::(signatures).map_err(Error::InvalidWitnessEncoding)?; - let tx = Transaction::decode_all(&mut &transaction_bytes[..]) + let tx = Transaction::decode_all(&mut &transaction[..]) .map_err(Error::InvalidTransactionEncoding)?; - let tx = SignedTransaction::new(tx, tx_signatures).map_err(Error::TransactionCreationError)?; + let tx = SignedTransaction::new(tx, signatures).map_err(Error::TransactionCreationError)?; Ok(tx.encode()) } @@ -1415,16 +881,11 @@ pub fn encode_signed_transaction( /// since the signatures are appended at the end of the `Transaction` object as a vector to create a `SignedTransaction`. /// It is recommended to use a strict `Transaction` size and set the second parameter to `true`. #[wasm_bindgen] -pub fn get_transaction_id( - transaction_bytes: &[u8], - strict_byte_size: bool, -) -> Result { +pub fn get_transaction_id(transaction: &[u8], strict_byte_size: bool) -> Result { let tx = if strict_byte_size { - Transaction::decode_all(&mut &transaction_bytes[..]) - .map_err(Error::InvalidTransactionEncoding)? + Transaction::decode_all(&mut &transaction[..]).map_err(Error::InvalidTransactionEncoding)? } else { - Transaction::decode(&mut &transaction_bytes[..]) - .map_err(Error::InvalidTransactionEncoding)? + Transaction::decode(&mut &transaction[..]).map_err(Error::InvalidTransactionEncoding)? }; let tx_id = tx.get_id(); @@ -1452,323 +913,3 @@ pub fn effective_pool_balance( Ok(Amount::from_internal_amount(effective_balance)) } - -/// Given a token_id, an amount of tokens to mint and nonce return an encoded mint tokens input -#[wasm_bindgen] -pub fn encode_input_for_mint_tokens( - token_id: &str, - amount: Amount, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let amount = amount.as_internal_amount()?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::MintTokens(token_id, amount), - ); - Ok(input.encode()) -} - -/// Given a token_id and nonce return an encoded unmint tokens input -#[wasm_bindgen] -pub fn encode_input_for_unmint_tokens( - token_id: &str, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::UnmintTokens(token_id), - ); - Ok(input.encode()) -} - -/// Given a token_id and nonce return an encoded lock_token_supply input -#[wasm_bindgen] -pub fn encode_input_for_lock_token_supply( - token_id: &str, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::LockTokenSupply(token_id), - ); - Ok(input.encode()) -} - -/// Given a token_id, is token unfreezable and nonce return an encoded freeze token input -#[wasm_bindgen] -pub fn encode_input_for_freeze_token( - token_id: &str, - is_token_unfreezable: TokenUnfreezable, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::FreezeToken(token_id, is_token_unfreezable.into()), - ); - Ok(input.encode()) -} - -/// Given a token_id and nonce return an encoded unfreeze token input -#[wasm_bindgen] -pub fn encode_input_for_unfreeze_token( - token_id: &str, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::UnfreezeToken(token_id), - ); - Ok(input.encode()) -} - -/// Given a token_id, new authority destination and nonce return an encoded change token authority input -#[wasm_bindgen] -pub fn encode_input_for_change_token_authority( - token_id: &str, - new_authority: &str, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let new_authority = parse_addressable(&chain_config, new_authority)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::ChangeTokenAuthority(token_id, new_authority), - ); - Ok(input.encode()) -} - -/// Given a token_id, new metadata uri and nonce return an encoded change token metadata uri input -#[wasm_bindgen] -pub fn encode_input_for_change_token_metadata_uri( - token_id: &str, - new_metadata_uri: &str, - nonce: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let token_id = parse_addressable(&chain_config, token_id)?; - let input = TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::ChangeTokenMetadataUri(token_id, new_metadata_uri.into()), - ); - Ok(input.encode()) -} - -/// Given ask and give amounts and a conclude key create output that creates an order. -/// -/// 'ask_token_id': the parameter represents a Token if it's Some and coins otherwise. -/// 'give_token_id': the parameter represents a Token if it's Some and coins otherwise. -#[wasm_bindgen] -pub fn encode_create_order_output( - ask_amount: Amount, - ask_token_id: Option, - give_amount: Amount, - give_token_id: Option, - conclude_address: &str, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let ask = parse_output_value(&chain_config, &ask_amount, ask_token_id)?; - let give = parse_output_value(&chain_config, &give_amount, give_token_id)?; - let conclude_key = parse_addressable(&chain_config, conclude_address)?; - - let order = OrderData::new(conclude_key, ask, give); - let output = TxOutput::CreateOrder(Box::new(order)); - Ok(output.encode()) -} - -/// Given an amount to fill an order (which is described in terms of ask currency) and a destination -/// for result outputs create an input that fills the order. -/// -/// Note: the nonce is only needed before the orders V1 fork activation. After the fork the nonce is -/// ignored and any value can be passed for the parameter. -#[wasm_bindgen] -pub fn encode_input_for_fill_order( - order_id: &str, - fill_amount: Amount, - destination: &str, - nonce: u64, - current_block_height: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let order_id = parse_addressable(&chain_config, order_id)?; - let fill_amount = fill_amount.as_internal_amount()?; - let destination = parse_addressable(&chain_config, destination)?; - let orders_version = chain_config - .chainstate_upgrades() - .version_at_height(BlockHeight::new(current_block_height)) - .1 - .orders_version(); - - let input = match orders_version { - OrdersVersion::V0 => TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::FillOrder(order_id, fill_amount, destination), - ), - OrdersVersion::V1 => TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( - order_id, - fill_amount, - destination, - )), - }; - Ok(input.encode()) -} - -/// Given an order id create an input that freezes the order. -/// -/// Note: order freezing is available only after the orders V1 fork activation. -#[wasm_bindgen] -pub fn encode_input_for_freeze_order( - order_id: &str, - current_block_height: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let order_id = parse_addressable(&chain_config, order_id)?; - let orders_version = chain_config - .chainstate_upgrades() - .version_at_height(BlockHeight::new(current_block_height)) - .1 - .orders_version(); - - let input = match orders_version { - OrdersVersion::V0 => { - return Err(Error::OrdersV1NotActivatedAtSpecifiedHeight); - } - OrdersVersion::V1 => { - TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)) - } - }; - - Ok(input.encode()) -} - -/// Given an order id create an input that concludes the order. -/// -/// Note: the nonce is only needed before the orders V1 fork activation. After the fork the nonce is -/// ignored and any value can be passed for the parameter. -#[wasm_bindgen] -pub fn encode_input_for_conclude_order( - order_id: &str, - nonce: u64, - current_block_height: u64, - network: Network, -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let order_id = parse_addressable(&chain_config, order_id)?; - let orders_version = chain_config - .chainstate_upgrades() - .version_at_height(BlockHeight::new(current_block_height)) - .1 - .orders_version(); - - let input = match orders_version { - OrdersVersion::V0 => TxInput::AccountCommand( - AccountNonce::new(nonce), - AccountCommand::ConcludeOrder(order_id), - ), - OrdersVersion::V1 => { - TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) - } - }; - - Ok(input.encode()) -} - -#[cfg(test)] -mod tests { - use randomness::Rng; - use rstest::rstest; - use test_utils::random::{make_seedable_rng, Seed}; - - use super::*; - - #[rstest] - #[trace] - #[case(Seed::from_entropy())] - fn sign_and_verify(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - - let key = make_private_key(); - assert_eq!(key.len(), 33); - - let public_key = public_key_from_private_key(&key).unwrap(); - - let message_size = 1 + rng.gen::() % 10000; - let message: Vec = (0..message_size).map(|_| rng.gen::()).collect(); - - let signature = sign_message_for_spending(&key, &message).unwrap(); - - { - // Valid reference signature - let verification_result = - verify_signature_for_spending(&public_key, &signature, &message).unwrap(); - assert!(verification_result); - } - { - // Tamper with the message - let mut tampered_message = message.clone(); - let tamper_bit_index = rng.gen::() % message_size; - tampered_message[tamper_bit_index] = tampered_message[tamper_bit_index].wrapping_add(1); - let verification_result = - verify_signature_for_spending(&public_key, &signature, &tampered_message).unwrap(); - assert!(!verification_result); - } - { - // Tamper with the signature - let mut tampered_signature = signature.clone(); - // Ignore the first byte because the it is the key kind - let tamper_bit_index = 1 + rng.gen::() % (signature.len() - 1); - tampered_signature[tamper_bit_index] = - tampered_signature[tamper_bit_index].wrapping_add(1); - let verification_result = - verify_signature_for_spending(&public_key, &tampered_signature, &message).unwrap(); - assert!(!verification_result); - } - { - // Wrong keys - let private_key = make_private_key(); - let public_key = public_key_from_private_key(&private_key).unwrap(); - let verification_result = - verify_signature_for_spending(&public_key, &signature, &message).unwrap(); - assert!(!verification_result); - } - } - - #[test] - fn transaction_get_id() { - let expected_tx_id = "35a7938c2a2aad5ae324e7d0536de245bf9e439169aa3c16f1492be117e5d0e0"; - let tx_hex = "0100040000ff5d9a94390ee97208d31aa5c3b5ddbd8df9d308069df2ebf5283f7ce3e4261401000000080340f9924e4da0af7dc8c5be71a9c9e05962c7bf4ef96127fde7a7b4e1469e48620f0080e03779c31102000365807e3b4147cb978b78715e60606092f89dc769586e98456850bd3b449c87b400203015e9ef9fc142569e0f966bc0188464fa712a841e14002e0fe952a076a26c01e539c5f0ceba927ab8f8f55f274af739ce4eef3700000b00204aa9d10100000b409e4c355d010199e4ec3a5b176140ef9cd58c7d3579fdb0ecb21a"; - let tx_signed_hex = "0100040000ff5d9a94390ee97208d31aa5c3b5ddbd8df9d308069df2ebf5283f7ce3e4261401000000080340f9924e4da0af7dc8c5be71a9c9e05962c7bf4ef96127fde7a7b4e1469e48620f0080e03779c31102000365807e3b4147cb978b78715e60606092f89dc769586e98456850bd3b449c87b400203015e9ef9fc142569e0f966bc0188464fa712a841e14002e0fe952a076a26c01e539c5f0ceba927ab8f8f55f274af739ce4eef3700000b00204aa9d10100000b409e4c355d010199e4ec3a5b176140ef9cd58c7d3579fdb0ecb21a0401018d010002eddd003bfb6333123e682abe6923da1d38faa4f0e0d9e2ee42d5aa46c152a34800a749a30c8c9c33696ce407fc145ebc9824e17b778d0d9ccc8129be52f37b74160e60f6689ac2f481071e1a63d9cf0f6eab84c2703b5e9f229cd8188ce092edd4"; - - let tx_bin = hex::decode(tx_hex).unwrap(); - let tx_signed_bin = hex::decode(tx_signed_hex).unwrap(); - - assert_eq!(get_transaction_id(&tx_bin, true).unwrap(), expected_tx_id); - assert_eq!(get_transaction_id(&tx_bin, false).unwrap(), expected_tx_id); - - get_transaction_id(&tx_signed_bin, true).unwrap_err(); - assert_eq!( - get_transaction_id(&tx_signed_bin, false).unwrap(), - expected_tx_id - ); - } -} diff --git a/wasm-wrappers/src/tests.rs b/wasm-wrappers/src/tests.rs new file mode 100644 index 0000000000..468117a377 --- /dev/null +++ b/wasm-wrappers/src/tests.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use rstest::rstest; + +use randomness::Rng; +use test_utils::random::{make_seedable_rng, Seed}; + +use super::*; + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn sign_and_verify(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let key = make_private_key(); + assert_eq!(key.len(), 33); + + let public_key = public_key_from_private_key(&key).unwrap(); + + let message_size = 1 + rng.gen::() % 10000; + let message: Vec = (0..message_size).map(|_| rng.gen::()).collect(); + + let signature = sign_message_for_spending(&key, &message).unwrap(); + + { + // Valid reference signature + let verification_result = + verify_signature_for_spending(&public_key, &signature, &message).unwrap(); + assert!(verification_result); + } + { + // Tamper with the message + let mut tampered_message = message.clone(); + let tamper_bit_index = rng.gen::() % message_size; + tampered_message[tamper_bit_index] = tampered_message[tamper_bit_index].wrapping_add(1); + let verification_result = + verify_signature_for_spending(&public_key, &signature, &tampered_message).unwrap(); + assert!(!verification_result); + } + { + // Tamper with the signature + let mut tampered_signature = signature.clone(); + // Ignore the first byte because the it is the key kind + let tamper_bit_index = 1 + rng.gen::() % (signature.len() - 1); + tampered_signature[tamper_bit_index] = tampered_signature[tamper_bit_index].wrapping_add(1); + let verification_result = + verify_signature_for_spending(&public_key, &tampered_signature, &message).unwrap(); + assert!(!verification_result); + } + { + // Wrong keys + let private_key = make_private_key(); + let public_key = public_key_from_private_key(&private_key).unwrap(); + let verification_result = + verify_signature_for_spending(&public_key, &signature, &message).unwrap(); + assert!(!verification_result); + } +} + +#[test] +fn transaction_get_id() { + let expected_tx_id = "35a7938c2a2aad5ae324e7d0536de245bf9e439169aa3c16f1492be117e5d0e0"; + let tx_hex = "0100040000ff5d9a94390ee97208d31aa5c3b5ddbd8df9d308069df2ebf5283f7ce3e4261401000000080340f9924e4da0af7dc8c5be71a9c9e05962c7bf4ef96127fde7a7b4e1469e48620f0080e03779c31102000365807e3b4147cb978b78715e60606092f89dc769586e98456850bd3b449c87b400203015e9ef9fc142569e0f966bc0188464fa712a841e14002e0fe952a076a26c01e539c5f0ceba927ab8f8f55f274af739ce4eef3700000b00204aa9d10100000b409e4c355d010199e4ec3a5b176140ef9cd58c7d3579fdb0ecb21a"; + let tx_signed_hex = "0100040000ff5d9a94390ee97208d31aa5c3b5ddbd8df9d308069df2ebf5283f7ce3e4261401000000080340f9924e4da0af7dc8c5be71a9c9e05962c7bf4ef96127fde7a7b4e1469e48620f0080e03779c31102000365807e3b4147cb978b78715e60606092f89dc769586e98456850bd3b449c87b400203015e9ef9fc142569e0f966bc0188464fa712a841e14002e0fe952a076a26c01e539c5f0ceba927ab8f8f55f274af739ce4eef3700000b00204aa9d10100000b409e4c355d010199e4ec3a5b176140ef9cd58c7d3579fdb0ecb21a0401018d010002eddd003bfb6333123e682abe6923da1d38faa4f0e0d9e2ee42d5aa46c152a34800a749a30c8c9c33696ce407fc145ebc9824e17b778d0d9ccc8129be52f37b74160e60f6689ac2f481071e1a63d9cf0f6eab84c2703b5e9f229cd8188ce092edd4"; + + let tx_bin = hex::decode(tx_hex).unwrap(); + let tx_signed_bin = hex::decode(tx_signed_hex).unwrap(); + + assert_eq!(get_transaction_id(&tx_bin, true).unwrap(), expected_tx_id); + assert_eq!(get_transaction_id(&tx_bin, false).unwrap(), expected_tx_id); + + get_transaction_id(&tx_signed_bin, true).unwrap_err(); + assert_eq!( + get_transaction_id(&tx_signed_bin, false).unwrap(), + expected_tx_id + ); +} diff --git a/wasm-wrappers/src/types.rs b/wasm-wrappers/src/types.rs new file mode 100644 index 0000000000..5c1ea15830 --- /dev/null +++ b/wasm-wrappers/src/types.rs @@ -0,0 +1,155 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use wasm_bindgen::prelude::*; + +use common::{ + chain::{ + config::ChainType, + signature::sighash::sighashtype::SigHashType, + tokens::{IsTokenFreezable, IsTokenUnfreezable}, + }, + primitives::{self}, +}; + +use crate::{error::Error, utils::internal_amount_from_atoms_str}; + +#[wasm_bindgen] +/// Amount type abstraction. The amount type is stored in a string +/// since JavaScript number type cannot fit 128-bit integers. +/// The amount is given as an integer in units of "atoms". +/// Atoms are the smallest, indivisible amount of a coin or token. +pub struct Amount { + atoms: String, +} + +#[wasm_bindgen] +impl Amount { + #[wasm_bindgen] + pub fn from_atoms(atoms: String) -> Self { + Self { atoms } + } + + #[wasm_bindgen] + pub fn atoms(self) -> String { + self.atoms + } +} + +impl Amount { + pub fn as_internal_amount(&self) -> Result { + internal_amount_from_atoms_str(&self.atoms) + } + + pub fn from_internal_amount(amount: primitives::Amount) -> Self { + Self { + atoms: amount.into_atoms().to_string(), + } + } +} + +#[wasm_bindgen] +#[derive(Debug, Copy, Clone)] +/// The network, for which an operation to be done. Mainnet, testnet, etc. +pub enum Network { + Mainnet, + Testnet, + Regtest, + Signet, +} + +impl From for ChainType { + fn from(value: Network) -> Self { + match value { + Network::Mainnet => ChainType::Mainnet, + Network::Testnet => ChainType::Testnet, + Network::Regtest => ChainType::Regtest, + Network::Signet => ChainType::Signet, + } + } +} + +/// Indicates whether a token can be frozen +#[wasm_bindgen] +pub enum FreezableToken { + No, + Yes, +} + +impl From for IsTokenFreezable { + fn from(value: FreezableToken) -> Self { + match value { + FreezableToken::No => IsTokenFreezable::No, + FreezableToken::Yes => IsTokenFreezable::Yes, + } + } +} + +/// Indicates whether a token can be unfrozen once frozen +#[wasm_bindgen] +pub enum TokenUnfreezable { + No, + Yes, +} + +impl From for IsTokenUnfreezable { + fn from(value: TokenUnfreezable) -> Self { + match value { + TokenUnfreezable::No => IsTokenUnfreezable::No, + TokenUnfreezable::Yes => IsTokenUnfreezable::Yes, + } + } +} + +/// The token supply of a specific token, set on issuance +#[wasm_bindgen] +pub enum TotalSupply { + /// Can be issued with no limit, but then can be locked to have a fixed supply. + Lockable, + /// Unlimited supply, no limits except for numeric limits due to u128 + Unlimited, + /// On issuance, the total number of coins is fixed + Fixed, +} + +/// A utxo can either come from a transaction or a block reward. This enum signifies that. +#[wasm_bindgen] +pub enum SourceId { + Transaction, + BlockReward, +} + +/// The part of the transaction that will be committed in the signature. Similar to bitcoin's sighash. +#[allow(clippy::upper_case_acronyms)] +#[wasm_bindgen] +pub enum SignatureHashType { + ALL, + NONE, + SINGLE, + ANYONECANPAY, +} + +impl From for SigHashType { + fn from(value: SignatureHashType) -> Self { + let value = match value { + SignatureHashType::ALL => SigHashType::ALL, + SignatureHashType::SINGLE => SigHashType::SINGLE, + SignatureHashType::ANYONECANPAY => SigHashType::ANYONECANPAY, + SignatureHashType::NONE => SigHashType::NONE, + }; + + SigHashType::try_from(value).expect("should not fail") + } +} diff --git a/wasm-wrappers/src/utils.rs b/wasm-wrappers/src/utils.rs new file mode 100644 index 0000000000..49a646d308 --- /dev/null +++ b/wasm-wrappers/src/utils.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::str::FromStr as _; + +use common::{ + address::{traits::Addressable, Address}, + chain::ChainConfig, + primitives, +}; +use serialization::Decode; + +use crate::error::Error; + +pub fn decode_raw_array(mut array: &[u8]) -> Result, serialization::Error> { + let mut result = Vec::new(); + + while !array.is_empty() { + let item = T::decode(&mut array)?; + result.push(item); + } + + Ok(result) +} + +pub fn parse_addressable( + chain_config: &ChainConfig, + address: &str, +) -> Result { + let addressable = Address::from_string(chain_config, address) + .map_err(|error| Error::AddressableParseError { + addressable: address.to_owned(), + error, + })? + .into_object(); + Ok(addressable) +} + +pub fn internal_amount_from_atoms_str(atoms: &str) -> Result { + primitives::amount::UnsignedIntType::from_str(atoms) + .ok() + .map(primitives::Amount::from_atoms) + .ok_or_else(|| Error::AtomsAmountParseError { + atoms: atoms.to_owned(), + }) +} diff --git a/wasm-wrappers/wasm-doc-gen/src/main.rs b/wasm-wrappers/wasm-doc-gen/src/main.rs index 7f0fe0c04e..0fc07f0ad0 100644 --- a/wasm-wrappers/wasm-doc-gen/src/main.rs +++ b/wasm-wrappers/wasm-doc-gen/src/main.rs @@ -214,24 +214,27 @@ fn write_to_stream<'a, D: Documentable>( fn write_docs_to_data_vec( docs_title: Option>, - library_file_path: impl AsRef, + library_file_paths: impl IntoIterator>, ) -> anyhow::Result> { - let file_to_doc = std::fs::read_to_string(library_file_path).expect("Source file not found"); - let file_to_doc_contents = syn::parse_file(&file_to_doc).expect("Unable to parse file"); + let mut expected_doc_file_data = Vec::new(); - let fn_docs = FunctionData::pull_from_items(&file_to_doc_contents.items); - let enum_docs = EnumData::pull_from_items(&file_to_doc_contents.items); - let struct_docs = StructData::pull_from_items(&file_to_doc_contents.items); + for library_file_path in library_file_paths { + let file_to_doc = + std::fs::read_to_string(library_file_path).expect("Source file not found"); + let file_to_doc_contents = syn::parse_file(&file_to_doc).expect("Unable to parse file"); - let mut expected_doc_file_data = Vec::new(); + let fn_docs = FunctionData::pull_from_items(&file_to_doc_contents.items); + let enum_docs = EnumData::pull_from_items(&file_to_doc_contents.items); + let struct_docs = StructData::pull_from_items(&file_to_doc_contents.items); - { - let mut stream: std::io::BufWriter> = - std::io::BufWriter::new(Box::new(&mut expected_doc_file_data)); + { + let mut stream: std::io::BufWriter> = + std::io::BufWriter::new(Box::new(&mut expected_doc_file_data)); - write_to_stream(docs_title.as_ref(), fn_docs, &mut stream)?; - write_to_stream(docs_title.as_ref(), enum_docs, &mut stream)?; - write_to_stream(docs_title.as_ref(), struct_docs, &mut stream)?; + write_to_stream(docs_title.as_ref(), fn_docs, &mut stream)?; + write_to_stream(docs_title.as_ref(), enum_docs, &mut stream)?; + write_to_stream(docs_title.as_ref(), struct_docs, &mut stream)?; + } } Ok(expected_doc_file_data) @@ -254,7 +257,15 @@ fn open_output_file(file_path: impl AsRef) -> anyhow::Result anyhow::Result<()> { let args = DocGenRunOptions::parse(); - let generated_doc_data = write_docs_to_data_vec(args.doc_title, "wasm-wrappers/src/lib.rs")?; + let generated_doc_data = write_docs_to_data_vec( + args.doc_title, + [ + "wasm-wrappers/src/lib.rs", + "wasm-wrappers/src/encode_input.rs", + "wasm-wrappers/src/encode_output.rs", + "wasm-wrappers/src/types.rs", + ], + )?; if args.check { if let Some(ref file_path) = args.output_file {