diff --git a/.github/workflows/build-ironfish-rust-nodejs.yml b/.github/workflows/build-ironfish-rust-nodejs.yml index a9708ea729..28b46b6206 100644 --- a/.github/workflows/build-ironfish-rust-nodejs.yml +++ b/.github/workflows/build-ironfish-rust-nodejs.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: settings: - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-apple-darwin - host: windows-latest @@ -24,16 +24,16 @@ jobs: - host: macos-latest target: x86_64-unknown-linux-gnu - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-musl - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-apple-darwin - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-gnu - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-musl name: Build ${{ matrix.settings.target }} @@ -94,19 +94,19 @@ jobs: - host: windows-latest target: x86_64-pc-windows-msvc - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-gnu docker: node:18-slim - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-musl docker: node:18-alpine - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs:aarch64-16 - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-musl docker: arm64v8/node:18-alpine platform: linux/arm64/v8 diff --git a/.github/workflows/check-pr-branch.yml b/.github/workflows/check-pr-branch.yml index 5ca2196c3d..d8769dbfff 100644 --- a/.github/workflows/check-pr-branch.yml +++ b/.github/workflows/check-pr-branch.yml @@ -8,7 +8,7 @@ on: jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - if: ${{ contains(github.event.pull_request.base.ref, 'master') && !contains(github.event.pull_request.title, 'master') }} run: | diff --git a/.github/workflows/ci-regenerate-fixtures.yml b/.github/workflows/ci-regenerate-fixtures.yml index dc766b127f..bafd232b5b 100644 --- a/.github/workflows/ci-regenerate-fixtures.yml +++ b/.github/workflows/ci-regenerate-fixtures.yml @@ -7,7 +7,7 @@ on: jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd24ed63fc..940458b6eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository @@ -51,7 +51,7 @@ jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/3, 2/3, 3/3] @@ -96,7 +96,7 @@ jobs: testslow: name: Slow Tests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/2, 2/2] diff --git a/.github/workflows/deploy-node-docker-image.yml b/.github/workflows/deploy-node-docker-image.yml index aa82644504..82ba5a79c6 100644 --- a/.github/workflows/deploy-node-docker-image.yml +++ b/.github/workflows/deploy-node-docker-image.yml @@ -37,7 +37,7 @@ permissions: jobs: Deploy: name: Deploy - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository diff --git a/.github/workflows/deploy-npm-ironfish-cli.yml b/.github/workflows/deploy-npm-ironfish-cli.yml index 9de31cb06c..f663af98aa 100644 --- a/.github/workflows/deploy-npm-ironfish-cli.yml +++ b/.github/workflows/deploy-npm-ironfish-cli.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository uses: actions/checkout@v4 diff --git a/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml b/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml index 881c78d97d..f3fd64021b 100644 --- a/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml +++ b/.github/workflows/deploy-npm-ironfish-rust-nodejs.yml @@ -13,7 +13,7 @@ jobs: publish: name: Publish - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: - build-and-test defaults: diff --git a/.github/workflows/deploy-npm-ironfish.yml b/.github/workflows/deploy-npm-ironfish.yml index d3bfa98fd4..4651cdfb7a 100644 --- a/.github/workflows/deploy-npm-ironfish.yml +++ b/.github/workflows/deploy-npm-ironfish.yml @@ -5,7 +5,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository uses: actions/checkout@v4 diff --git a/.github/workflows/push-version-to-api.yml b/.github/workflows/push-version-to-api.yml index 1c01249c35..957f7b1951 100644 --- a/.github/workflows/push-version-to-api.yml +++ b/.github/workflows/push-version-to-api.yml @@ -14,7 +14,7 @@ on: jobs: Push: name: Push Version to API - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check out Git repository diff --git a/.github/workflows/rust_ci.yml b/.github/workflows/rust_ci.yml index 8e3c12006c..a04a612bf8 100644 --- a/.github/workflows/rust_ci.yml +++ b/.github/workflows/rust_ci.yml @@ -6,6 +6,7 @@ on: - "ironfish-phase2/**" - "ironfish-rust/**" - "ironfish-rust-nodejs/**" + - "ironfish-rust-wasm/**" - "ironfish-zkp/**" - "rust-toolchain" - ".github/workflows/rust*" @@ -20,6 +21,7 @@ on: - "ironfish-phase2/**" - "ironfish-rust/**" - "ironfish-rust-nodejs/**" + - "ironfish-rust-wasm/**" - "ironfish-zkp/**" - "rust-toolchain" - ".github/workflows/rust*" @@ -31,7 +33,7 @@ name: Rust CI jobs: rust_lint: name: Lint Rust - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -48,17 +50,20 @@ jobs: - name: Check for license headers for ironfish-rust-nodejs run: ./ci/lintHeaders.sh ./ironfish-rust-nodejs/src *.rs - - name: "`cargo fmt` check on ironfish-rust" + - name: Check for license headers for ironfish-rust-wasm + run: ./ci/lintHeaders.sh ./ironfish-rust-wasm/src *.rs + + - name: cargo fmt run: | cargo fmt --all -- --check - - name: "Clippy check on ironfish-rust" + - name: cargo clippy run: | cargo clippy --all-targets --all-features -- -D warnings cargo_check: name: Check Rust - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -78,7 +83,7 @@ jobs: cargo_vet: name: Vet Dependencies - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -98,7 +103,7 @@ jobs: ironfish_rust: name: Test ironfish-rust - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/2, 2/2] @@ -137,7 +142,7 @@ jobs: ironfish_rust_no_default_features: name: Test ironfish-rust (no default features) - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/2, 2/2] @@ -163,7 +168,7 @@ jobs: ironfish_rust_all_features: name: Test ironfish-rust (all features) - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/2, 2/2] @@ -189,7 +194,7 @@ jobs: ironfish_zkp: name: Test ironfish-zkp - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -220,3 +225,29 @@ jobs: with: token: ${{secrets.CODECOV_TOKEN}} flags: ironfish-zkp + + ironfish_wasm: + name: Test ironfish-rust-wasm + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + shared-key: wasm + + - name: Install wasm-pack + # use the installation method reccommended on + # https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/continuous-integration.html#github-actions + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Run tests in Firefox + run: | + cd ironfish-rust-wasm + wasm-pack test --headless --firefox + + - name: Run tests in Chrome + run: | + cd ironfish-rust-wasm + wasm-pack test --headless --chrome diff --git a/.github/workflows/rust_ci_cache.yml b/.github/workflows/rust_ci_cache.yml index 792183f62a..16d0edccb1 100644 --- a/.github/workflows/rust_ci_cache.yml +++ b/.github/workflows/rust_ci_cache.yml @@ -19,7 +19,7 @@ name: Cache Rust build jobs: build-rust-cache: name: Build and cache rust code - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index e80aef1b43..ee028eb588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,6 +492,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-crc32-nostd" version = "1.3.1" @@ -1150,8 +1160,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1628,6 +1640,23 @@ dependencies = [ "signal-hook", ] +[[package]] +name = "ironfish-wasm" +version = "0.1.0" +dependencies = [ + "blstrs", + "getrandom", + "group 0.12.1", + "hex-literal", + "ironfish", + "ironfish-jubjub", + "ironfish_zkp", + "rand", + "rayon", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "ironfish_zkp" version = "0.2.0" @@ -1671,9 +1700,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1804,6 +1833,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "mio" version = "0.8.11" @@ -2273,24 +2312,24 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", + "wasm_sync", ] [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", + "wasm_sync", ] [[package]] @@ -2476,6 +2515,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -3053,34 +3098,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -3090,9 +3136,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3100,22 +3146,59 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "wasm_sync" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff360cade7fec41ff0e9d2cda57fe58258c5f16def0e21302394659e6bbb0ea" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] [[package]] name = "web-sys" diff --git a/Cargo.toml b/Cargo.toml index 955ccc7d88..68ba04e0c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "benchmarks", "ironfish-rust", "ironfish-rust-nodejs", + "ironfish-rust-wasm", "ironfish-zkp", ] diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 5c2198ac39..f399ab3a0e 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.9.0", + "version": "2.10.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -62,16 +62,16 @@ }, "dependencies": { "@ironfish/multisig-broker": "0.3.0", - "@ironfish/rust-nodejs": "2.8.0", - "@ironfish/sdk": "2.9.0", + "@ironfish/rust-nodejs": "2.9.0", + "@ironfish/sdk": "2.10.0", "@ledgerhq/errors": "6.19.1", "@ledgerhq/hw-transport-node-hid": "6.29.5", "@oclif/core": "4.0.11", "@oclif/plugin-help": "6.2.5", "@oclif/plugin-not-found": "3.2.10", "@oclif/plugin-warn-if-update-available": "3.1.8", - "@zondax/ledger-ironfish": "1.0.0", - "@zondax/ledger-js": "1.0.1", + "@zondax/ledger-ironfish": "1.1.0", + "@zondax/ledger-js": "1.2.0", "axios": "1.7.7", "bech32": "2.0.0", "blessed": "0.1.81", diff --git a/ironfish-cli/src/commands/migrations/revert.ts b/ironfish-cli/src/commands/migrations/revert.ts index 39bf7b3780..49b2e2ba42 100644 --- a/ironfish-cli/src/commands/migrations/revert.ts +++ b/ironfish-cli/src/commands/migrations/revert.ts @@ -1,17 +1,50 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { AccountDecryptionFailedError, EncryptedWalletMigrationError } from '@ironfish/sdk' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' +import { inputPrompt } from '../../ui' export class RevertCommand extends IronfishCommand { static description = `revert the last run migration` + static flags = { + passphrase: Flags.string({ + description: 'Passphrase to unlock the wallet database with', + }), + } + static hidden = true async start(): Promise { - await this.parse(RevertCommand) + const { flags } = await this.parse(RevertCommand) const node = await this.sdk.node() - await node.migrator.revert() + + let walletPassphrase = flags.passphrase + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await node.migrator.revert({ walletPassphrase }) + break + } catch (e) { + if ( + e instanceof EncryptedWalletMigrationError || + e instanceof AccountDecryptionFailedError + ) { + this.logger.info(e.message) + walletPassphrase = await inputPrompt( + 'Enter your passphrase to unlock the wallet', + true, + { + password: true, + }, + ) + } else { + throw e + } + } + } } } diff --git a/ironfish-cli/src/commands/migrations/start.ts b/ironfish-cli/src/commands/migrations/start.ts index 125d7715e3..12d5db5455 100644 --- a/ironfish-cli/src/commands/migrations/start.ts +++ b/ironfish-cli/src/commands/migrations/start.ts @@ -1,8 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { AccountDecryptionFailedError, EncryptedWalletMigrationError } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' +import { inputPrompt } from '../../ui' export class StartCommand extends IronfishCommand { static description = `run migrations` @@ -17,12 +19,39 @@ export class StartCommand extends IronfishCommand { char: 'q', default: false, }), + passphrase: Flags.string({ + description: 'Passphrase to unlock the wallet database with', + }), } async start(): Promise { const { flags } = await this.parse(StartCommand) const node = await this.sdk.node() - await node.migrator.migrate({ quiet: flags.quiet, dryRun: flags.dry }) + + let walletPassphrase = flags.passphrase + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await node.migrator.migrate({ quiet: flags.quiet, dryRun: flags.dry, walletPassphrase }) + break + } catch (e) { + if ( + e instanceof EncryptedWalletMigrationError || + e instanceof AccountDecryptionFailedError + ) { + this.logger.info(e.message) + walletPassphrase = await inputPrompt( + 'Enter your passphrase to unlock the wallet', + true, + { + password: true, + }, + ) + } else { + throw e + } + } + } } } diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 847951f645..16f9d20f26 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -1,7 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, FullNode, NodeUtils, PromiseUtils } from '@ironfish/sdk' +import { + Assert, + EncryptedWalletMigrationError, + FullNode, + NodeUtils, + PromiseUtils, +} from '@ironfish/sdk' import { Flags } from '@oclif/core' import inspector from 'node:inspector' import { v4 as uuid } from 'uuid' @@ -223,7 +229,19 @@ export default class Start extends IronfishCommand { } this.log(` `) - await NodeUtils.waitForOpen(node, () => this.closing) + try { + await NodeUtils.waitForOpen(node, () => this.closing) + } catch (e) { + if (e instanceof EncryptedWalletMigrationError) { + this.logger.error(e.message) + this.logger.error( + 'Run `ironfish migrations:start` to enter wallet passphrase and migrate wallet databases', + ) + this.exit(1) + } else { + throw e + } + } if (this.closing) { return startDoneResolve() diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 7ebc9d8f7b..d6288e60f7 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -87,6 +87,10 @@ This will destroy tokens and decrease supply for a given asset.` default: false, description: 'Wait for the transaction to be confirmed', }), + ledger: Flags.boolean({ + default: false, + description: 'Burn a transaction using a Ledger device', + }), } async start(): Promise { @@ -221,6 +225,18 @@ This will destroy tokens and decrease supply for a given asset.` await this.confirm(assetData, amount, raw.fee, account, flags.confirm) + if (flags.ledger) { + await ui.sendTransactionWithLedger( + client, + raw, + account, + flags.watch, + flags.confirm, + this.logger, + ) + this.exit(0) + } + ux.action.start('Sending the transaction') const response = await client.wallet.postTransaction({ diff --git a/ironfish-cli/src/commands/wallet/ledger/address.ts b/ironfish-cli/src/commands/wallet/ledger/address.ts new file mode 100644 index 0000000000..57f5086966 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/ledger/address.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IronfishCommand } from '../../../command' +import { JsonFlags, RemoteFlags } from '../../../flags' +import { LedgerSingleSigner } from '../../../ledger' +import * as ui from '../../../ui' + +export class AddressCommand extends IronfishCommand { + static description = `verify the ledger device's public address` + + static flags = { + ...RemoteFlags, + ...JsonFlags, + } + + async start(): Promise { + const ledger = new LedgerSingleSigner() + + const address = await ui.ledger({ + ledger, + message: 'Retrieve Wallet Address', + approval: true, + action: () => ledger.getPublicAddress(true), + }) + + this.log(ui.card({ Address: address })) + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index f2e9103da2..f35a05cf03 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -125,20 +125,16 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { signers: string[], ): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity - const rawCommitments = await ledger.dkgGetCommitments(unsignedTransaction) + const rawCommitments = await ui.ledger({ + ledger, + message: 'Get Commitments', + approval: true, + action: () => ledger.dkgGetCommitments(unsignedTransaction), + }) const signingCommitment = multisig.SigningCommitment.fromRaw( identity, diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 488f786e1c..3edae07d70 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -101,15 +101,6 @@ export class DkgRound1Command extends IronfishCommand { minSigners: number, ): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity @@ -118,8 +109,12 @@ export class DkgRound1Command extends IronfishCommand { identities.push(identity) } - // TODO(hughy): determine how to handle multiple identities using index - const { publicPackage, secretPackage } = await ledger.dkgRound1(0, identities, minSigners) + const { publicPackage, secretPackage } = await ui.ledger({ + ledger, + message: 'Round1 on Ledger', + approval: true, + action: () => ledger.dkgRound1(0, identities, minSigners), + }) this.log('\nRound 1 Encrypted Secret Package:\n') this.log(secretPackage.toString('hex')) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index a09afdb7aa..d51b9582dc 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -98,22 +98,13 @@ export class DkgRound2Command extends IronfishCommand { round1SecretPackage: string, ): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - // TODO(hughy): determine how to handle multiple identities using index - const { publicPackage, secretPackage } = await ledger.dkgRound2( - 0, - round1PublicPackages, - round1SecretPackage, - ) + const { publicPackage, secretPackage } = await ui.ledger({ + ledger, + message: 'Round2 on Ledger', + approval: true, + action: () => ledger.dkgRound2(0, round1PublicPackages, round1SecretPackage), + }) this.log('\nRound 2 Encrypted Secret Package:\n') this.log(secretPackage.toString('hex')) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 02541ef461..15794f71c6 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -163,15 +163,6 @@ export class DkgRound3Command extends IronfishCommand { accountCreatedAt?: number, ): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity @@ -192,9 +183,9 @@ export class DkgRound3Command extends IronfishCommand { .sort((a, b) => a.senderIdentity.localeCompare(b.senderIdentity)) // Extract raw parts from round1 and round2 public packages - const participants = [] - const round1FrostPackages = [] - const gskBytes = [] + const participants: string[] = [] + const round1FrostPackages: string[] = [] + const gskBytes: string[] = [] for (const pkg of round1PublicPackages) { // Exclude participant's own identity and round1 public package if (pkg.identity !== identity) { @@ -208,19 +199,33 @@ export class DkgRound3Command extends IronfishCommand { const round2FrostPackages = round2PublicPackages.map((pkg) => pkg.frostPackage) // Perform round3 with Ledger - await ledger.dkgRound3( - 0, - participants, - round1FrostPackages, - round2FrostPackages, - round2SecretPackage, - gskBytes, - ) + await ui.ledger({ + ledger, + message: 'Round3 on Ledger', + approval: true, + action: () => + ledger.dkgRound3( + 0, + participants, + round1FrostPackages, + round2FrostPackages, + round2SecretPackage, + gskBytes, + ), + }) // Retrieve all multisig account keys and publicKeyPackage - const dkgKeys = await ledger.dkgRetrieveKeys() + const dkgKeys = await ui.ledger({ + ledger, + message: 'Getting Ledger DKG keys', + action: () => ledger.dkgRetrieveKeys(), + }) - const publicKeyPackage = await ledger.dkgGetPublicPackage() + const publicKeyPackage = await ui.ledger({ + ledger, + message: 'Getting Ledger Public Package', + action: () => ledger.dkgGetPublicPackage(), + }) const accountImport = { ...dkgKeys, @@ -250,7 +255,12 @@ export class DkgRound3Command extends IronfishCommand { this.log('Creating an encrypted backup of multisig keys from your Ledger device...') this.log() - const encryptedKeys = await ledger.dkgBackupKeys() + const encryptedKeys = await ui.ledger({ + ledger, + message: 'Backup DKG Keys', + approval: true, + action: () => ledger.dkgBackupKeys(), + }) this.log() this.log('Encrypted Ledger Multisig Backup:') diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index d9e7df0f66..06c1e43055 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -72,17 +72,11 @@ export class MultisigIdentityCreate extends IronfishCommand { async getIdentityFromLedger(): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - // TODO(hughy): support multiple identities using index - return ledger.dkgGetIdentity(0) + return ui.ledger({ + ledger, + message: 'Getting Ledger Identity', + action: () => ledger.dkgGetIdentity(0), + }) } } diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 7bc41efa17..9eb930ad49 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -121,20 +121,16 @@ export class CreateSignatureShareCommand extends IronfishCommand { frostSigningPackage: string, ): Promise { const ledger = new LedgerMultiSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity - const frostSignatureShare = await ledger.dkgSign(unsignedTransaction, frostSigningPackage) + const frostSignatureShare = await ui.ledger({ + ledger, + message: 'Sign Transaction', + approval: true, + action: () => ledger.dkgSign(unsignedTransaction, frostSigningPackage), + }) const signatureShare = multisig.SignatureShare.fromFrost( frostSignatureShare, diff --git a/ironfish-cli/src/commands/wallet/transactions/index.ts b/ironfish-cli/src/commands/wallet/transactions/index.ts index 9b4576c32e..31a34615c3 100644 --- a/ironfish-cli/src/commands/wallet/transactions/index.ts +++ b/ironfish-cli/src/commands/wallet/transactions/index.ts @@ -8,26 +8,48 @@ import { GetAccountTransactionsResponse, PartialRecursive, RpcAsset, + RpcClient, + RpcWalletTransaction, TransactionType, } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' -import { RemoteFlags } from '../../../flags' +import { DateFlag, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' import { getAssetsByIDs, useAccount } from '../../../utils' import { extractChainportDataFromTransaction } from '../../../utils/chainport' -import { Format, TableCols } from '../../../utils/table' +import { TableCols, TableOutput } from '../../../utils/table' const { sort: _, ...tableFlags } = ui.TableFlags + export class TransactionsCommand extends IronfishCommand { static description = `list the account's transactions` + static examples = [ + { + description: 'List all transactions in the current wallet:', + command: '$ <%= config.bin %> <%= command.id %>', + }, + { + description: + 'Export transactions in all wallets for the month of october in an accounting friendly format:', + command: + '$ <%= config.bin %> <%= command.id %> --no-account --filter.start 2024-10-01 --filter.end 2024-11-01 --output csv --format transfers', + }, + ] + static flags = { ...RemoteFlags, ...tableFlags, account: Flags.string({ char: 'a', - description: 'Name of the account to get transactions for', + multiple: true, + description: 'show transactions from this account', + }), + 'no-account': Flags.boolean({ + description: 'show transactions for all accounts', + exclusive: ['account'], + aliases: ['no-a'], }), transaction: Flags.string({ char: 't', @@ -45,49 +67,95 @@ export class TransactionsCommand extends IronfishCommand { description: 'Number of block confirmations needed to confirm a transaction', }), notes: Flags.boolean({ - default: false, description: 'Include data from transaction output notes', }), + format: Flags.string({ + description: 'show the data in a specified view', + exclusive: ['notes'], + options: ['notes', 'transactions', 'transfers'], + helpGroup: 'OUTPUT', + }), + 'filter.start': DateFlag({ + description: 'include transactions after this date (inclusive). Example: 2023-04-01', + }), + 'filter.end': DateFlag({ + description: 'include transactions before this date (exclusive). Example: 2023-05-01', + }), } async start(): Promise { const { flags } = await this.parse(TransactionsCommand) - const format: Format = + const output: TableOutput = flags.csv || flags.output === 'csv' - ? Format.csv + ? TableOutput.csv : flags.output === 'json' - ? Format.json - : Format.cli + ? TableOutput.json + : TableOutput.cli + + const format = + flags.notes || flags.format === 'notes' + ? 'notes' + : flags.format === 'transactions' + ? 'transactions' + : flags.format === 'transfers' + ? 'transfers' + : 'transfers' const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - const account = await useAccount(client, flags.account) + const allAccounts = (await client.wallet.getAccounts()).content.accounts - const networkId = (await client.chain.getNetworkInfo()).content.networkId + let accounts = flags.account + if (flags['no-account']) { + accounts = allAccounts + } else if (!accounts) { + const account = await useAccount(client, undefined) + accounts = [account] + } - const response = client.wallet.getAccountTransactionsStream({ - account, - hash: flags.transaction, - sequence: flags.sequence, - limit: flags.limit, - offset: flags.offset, - confirmations: flags.confirmations, - notes: flags.notes, - }) + const accountsByAddress = new Map( + await Promise.all( + allAccounts.map>(async (account) => { + const response = await client.wallet.getAccountPublicKey({ account }) + return [response.content.publicKey, response.content.account] + }), + ), + ) - const columns = this.getColumns(flags.extended, flags.notes, format) + const networkId = (await client.chain.getNetworkInfo()).content.networkId - let hasTransactions = false + const transactions = this.getTransactions( + client, + accounts, + flags.transaction, + flags.sequence, + flags.limit, + flags.offset, + flags.confirmations, + format === 'notes' || format === 'transfers', + ) + let hasTransactions = false let transactionRows: PartialRecursive[] = [] - for await (const transaction of response.contentStream()) { + + for await (const { account, transaction } of transactions) { if (transactionRows.length >= flags.limit) { break } - if (flags.notes) { + + if (flags['filter.start'] && transaction.timestamp < flags['filter.start'].valueOf()) { + continue + } + + if (flags['filter.end'] && transaction.timestamp >= flags['filter.end'].valueOf()) { + continue + } + + if (format === 'notes' || format === 'transfers') { Assert.isNotUndefined(transaction.notes) + const assetLookup = await getAssetsByIDs( client, transaction.notes.map((n) => n.assetId) || [], @@ -102,7 +170,15 @@ export class TransactionsCommand extends IronfishCommand { : ('Bridge (incoming)' as TransactionType) } - transactionRows = this.getTransactionRowsByNote(assetLookup, transaction, format) + transactionRows = transactionRows.concat( + this.getTransactionRowsByNote( + assetLookup, + accountsByAddress, + transaction, + output, + format, + ), + ) } else { const assetLookup = await getAssetsByIDs( client, @@ -110,11 +186,15 @@ export class TransactionsCommand extends IronfishCommand { account, flags.confirmations, ) - transactionRows = this.getTransactionRows(assetLookup, transaction, format) + transactionRows = transactionRows.concat( + this.getTransactionRows(assetLookup, transaction, output), + ) } hasTransactions = true } + const columns = this.getColumns(flags.extended, format, output) + ui.table(transactionRows, columns, { printLine: this.log.bind(this), ...flags, @@ -125,10 +205,37 @@ export class TransactionsCommand extends IronfishCommand { } } + async *getTransactions( + client: RpcClient, + accounts: string[], + hash?: string, + sequence?: number, + limit?: number, + offset?: number, + confirmations?: number, + notes?: boolean, + ): AsyncGenerator<{ account: string; transaction: RpcWalletTransaction }, void> { + for (const account of accounts) { + const response = client.wallet.getAccountTransactionsStream({ + account, + hash: hash, + sequence: sequence, + limit: limit, + offset: offset, + confirmations: confirmations, + notes: notes, + }) + + for await (const transaction of response.contentStream()) { + yield { account, transaction } + } + } + } + getTransactionRows( assetLookup: { [key: string]: RpcAsset }, transaction: GetAccountTransactionsResponse, - format: Format, + output: TableOutput, ): PartialRecursive[] { const nativeAssetId = Asset.nativeId().toString('hex') @@ -153,7 +260,7 @@ export class TransactionsCommand extends IronfishCommand { // exclude the native asset in cli output if no amount was sent/received // and it was not the only asset exchanged - if (format === Format.cli && amount === 0n && assetCount > 1) { + if (output === TableOutput.cli && amount === 0n && assetCount > 1) { assetCount -= 1 continue } @@ -171,7 +278,7 @@ export class TransactionsCommand extends IronfishCommand { } // include full transaction details in first row or non-cli-formatted output - if (transactionRows.length === 0 || format !== Format.cli) { + if (transactionRows.length === 0 || output !== TableOutput.cli) { transactionRows.push({ ...transaction, ...transactionRow, @@ -187,8 +294,10 @@ export class TransactionsCommand extends IronfishCommand { getTransactionRowsByNote( assetLookup: { [key: string]: RpcAsset }, + accountLookup: Map, transaction: GetAccountTransactionsResponse, - format: Format, + output: TableOutput, + format: 'notes' | 'transactions' | 'transfers', ): PartialRecursive[] { Assert.isNotUndefined(transaction.notes) const transactionRows = [] @@ -210,11 +319,21 @@ export class TransactionsCommand extends IronfishCommand { const sender = note.sender const recipient = note.owner const memo = note.memo + const senderName = accountLookup.get(note.sender) + const recipientName = accountLookup.get(note.owner) - const group = this.getRowGroup(index, noteCount, transactionRows.length) + let group = this.getRowGroup(index, noteCount, transactionRows.length) + + if (format === 'transfers') { + if (note.sender === note.owner && !transaction.mints.length) { + continue + } else { + group = '' + } + } // include full transaction details in first row or non-cli-formatted output - if (transactionRows.length === 0 || format !== Format.cli) { + if (transactionRows.length === 0 || output !== TableOutput.cli) { transactionRows.push({ ...transaction, group, @@ -225,7 +344,9 @@ export class TransactionsCommand extends IronfishCommand { amount, feePaid, sender, + senderName, recipient, + recipientName, memo, }) } else { @@ -237,7 +358,9 @@ export class TransactionsCommand extends IronfishCommand { assetSymbol, amount, sender, + senderName, recipient, + recipientName, memo, }) } @@ -248,8 +371,8 @@ export class TransactionsCommand extends IronfishCommand { getColumns( extended: boolean, - notes: boolean, - format: Format, + output: 'notes' | 'transactions' | 'transfers', + format: TableOutput, ): ui.TableColumns> { let columns: ui.TableColumns> = { timestamp: TableCols.timestamp({ @@ -262,7 +385,7 @@ export class TransactionsCommand extends IronfishCommand { }, type: { header: 'Type', - minWidth: notes ? 18 : 8, + minWidth: output === 'notes' || output === 'transfers' ? 18 : 8, get: (row) => row.type ?? '', }, hash: { @@ -321,13 +444,21 @@ export class TransactionsCommand extends IronfishCommand { }, } - if (notes) { + if (output === 'notes' || output === 'transfers') { columns = { ...columns, + senderName: { + header: 'Sender', + get: (row) => row.senderName ?? '', + }, sender: { header: 'Sender Address', get: (row) => row.sender ?? '', }, + recipientName: { + header: 'Recipient', + get: (row) => row.recipientName ?? '', + }, recipient: { header: 'Recipient Address', get: (row) => row.recipient ?? '', @@ -339,7 +470,7 @@ export class TransactionsCommand extends IronfishCommand { } } - if (format === Format.cli) { + if (format === TableOutput.cli) { columns = { group: { header: '', @@ -387,6 +518,8 @@ type TransactionRow = { expiration: number submittedSequence: number sender: string + senderName?: string recipient: string + recipientName?: string memo?: string } diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index ca5c7ebc40..3e50976213 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -2,13 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { CurrencyUtils, RpcClient, Transaction } from '@ironfish/sdk' +import { CurrencyUtils, RpcClient, Transaction, UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import { LedgerSingleSigner } from '../../../ledger' import * as ui from '../../../ui' -import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' +import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' export class TransactionsSignCommand extends IronfishCommand { static description = `sign an unsigned transaction` @@ -45,9 +45,9 @@ export class TransactionsSignCommand extends IronfishCommand { this.error('Cannot use --watch without --broadcast') } - let unsignedTransaction = flags.unsignedTransaction - if (!unsignedTransaction) { - unsignedTransaction = await ui.longPrompt('Enter the unsigned transaction', { + let unsignedTransactionHex = flags.unsignedTransaction + if (!unsignedTransactionHex) { + unsignedTransactionHex = await ui.longPrompt('Enter the unsigned transaction', { required: true, }) } @@ -55,12 +55,17 @@ export class TransactionsSignCommand extends IronfishCommand { let signedTransaction: string let account: string + const unsignedTransaction = new UnsignedTransaction( + Buffer.from(unsignedTransactionHex, 'hex'), + ) + await renderUnsignedTransactionDetails(client, unsignedTransaction, undefined, this.logger) + if (flags.ledger) { - const response = await this.signWithLedger(client, unsignedTransaction) + const response = await this.signWithLedger(client, unsignedTransactionHex) signedTransaction = response.transaction account = response.account } else { - const response = await this.signWithAccount(client, unsignedTransaction) + const response = await this.signWithAccount(client, unsignedTransactionHex) signedTransaction = response.transaction account = response.account } @@ -77,8 +82,6 @@ export class TransactionsSignCommand extends IronfishCommand { this.log(`\nHash: ${transaction.hash().toString('hex')}`) this.log(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) - await renderTransactionDetails(client, transaction, account, this.logger) - if (flags.broadcast && response.content.accepted === false) { this.error( `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, @@ -110,17 +113,15 @@ export class TransactionsSignCommand extends IronfishCommand { private async signWithLedger(client: RpcClient, unsignedTransaction: string) { const ledger = new LedgerSingleSigner() - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - const signature = (await ledger.sign(unsignedTransaction)).toString('hex') + const signature = ( + await ui.ledger({ + ledger, + message: 'Sign Transaction', + approval: true, + action: () => ledger.sign(unsignedTransaction), + }) + ).toString('hex') this.log(`\nSignature: ${signature}`) diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index c1b0eaeab2..6f6c4b8c5e 100644 --- a/ironfish-cli/src/flags.ts +++ b/ironfish-cli/src/flags.ts @@ -212,3 +212,17 @@ export const EnumLanguageKeyFlag = Flags.custom({ + parse: async (input, _ctx, opts) => { + const parsed = new Date(input) + + if (Number.isNaN(parsed.valueOf())) { + throw new Error( + `The value provided for ${opts.name} is an invalid format. It must be a valid date.`, + ) + } + + return Promise.resolve(parsed) + }, +}) diff --git a/ironfish-cli/src/ledger/ledger.ts b/ironfish-cli/src/ledger/ledger.ts index ef52631a62..d1d11c3ee5 100644 --- a/ironfish-cli/src/ledger/ledger.ts +++ b/ironfish-cli/src/ledger/ledger.ts @@ -24,6 +24,9 @@ export const IronfishLedgerStatusCodes = { UNKNOWN_TRANSPORT_ERROR: 0xffff, INVALID_TX_HASH: 0xb025, PANIC: 0xe000, + EXPERT_MODE_REQUIRED: 0x6984, + DKG_EXPERT_MODE_REQUIRED: 0xb027, + INVALID_DKG_STATUS: 0xb022, } export class Ledger { @@ -71,6 +74,13 @@ export class Ledger { throw new LedgerPanicError() } else if (error.returnCode === IronfishLedgerStatusCodes.GP_AUTH_FAILED) { throw new LedgerGPAuthFailed() + } else if (error.returnCode === IronfishLedgerStatusCodes.INVALID_DKG_STATUS) { + throw new LedgerInvalidDkgStatusError() + } else if ( + error.returnCode === IronfishLedgerStatusCodes.EXPERT_MODE_REQUIRED || + error.returnCode === IronfishLedgerStatusCodes.DKG_EXPERT_MODE_REQUIRED + ) { + throw new LedgerExpertModeError() } else if ( [ IronfishLedgerStatusCodes.COMMAND_NOT_ALLOWED, @@ -183,3 +193,5 @@ export class LedgerAppNotOpen extends LedgerError {} export class LedgerActionRejected extends LedgerError {} export class LedgerInvalidTxHash extends LedgerError {} export class LedgerPanicError extends LedgerError {} +export class LedgerExpertModeError extends LedgerError {} +export class LedgerInvalidDkgStatusError extends LedgerError {} diff --git a/ironfish-cli/src/ledger/ledgerSingleSigner.ts b/ironfish-cli/src/ledger/ledgerSingleSigner.ts index 8dc313a3a7..12c4178752 100644 --- a/ironfish-cli/src/ledger/ledgerSingleSigner.ts +++ b/ironfish-cli/src/ledger/ledgerSingleSigner.ts @@ -10,9 +10,9 @@ export class LedgerSingleSigner extends Ledger { super(false) } - getPublicAddress = async () => { + getPublicAddress = async (showInDevice: boolean = false) => { const response: KeyResponse = await this.tryInstruction((app) => - app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), + app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, showInDevice), ) if (!isResponseAddress(response)) { diff --git a/ironfish-cli/src/ui/ledger.ts b/ironfish-cli/src/ui/ledger.ts index de6f79365d..1fa4ea769b 100644 --- a/ironfish-cli/src/ui/ledger.ts +++ b/ironfish-cli/src/ui/ledger.ts @@ -19,7 +19,9 @@ import { LedgerClaNotSupportedError, LedgerConnectError, LedgerDeviceLockedError, + LedgerExpertModeError, LedgerGPAuthFailed, + LedgerInvalidDkgStatusError, LedgerPanicError, LedgerPortIsBusyError, LedgerSingleSigner, @@ -70,23 +72,24 @@ export async function ledger({ // is trying to enter their pin. When we run into this error, we // cannot send any commands to the Ledger in the app's CLA. ux.action.stop('Ledger Locked') - - const confirmed = await ui.confirmList( + await confirmRetryAction( 'Ledger Locked. Unlock and press enter to retry:', - 'Retry', + wasRunning, ) - - if (!confirmed) { - ux.stdout('Operation aborted.') - ux.exit(0) - } - - if (!wasRunning) { - ux.action.start(message) - } + } else if (e instanceof LedgerExpertModeError) { + // Polling the device may prevent the user from navigating to the + // expert mode screen in the app and enabling expert mode. + ux.action.stop('Expert mode required to send custom assets') + await confirmRetryAction('Enable expert mode and press enter to retry:', wasRunning) } else if (e instanceof LedgerActionRejected) { - ux.action.status = 'User Rejected Ledger Request!' - ux.stdout('User Rejected Ledger Request!') + ux.action.stop('User Rejected Ledger Request!') + await confirmRetryAction('Request rejected. Retry?', wasRunning) + } else if (e instanceof LedgerInvalidDkgStatusError) { + ux.action.stop('Ironfish DKG Ledger App does not have any multisig keys!') + ux.stdout( + 'Use `wallet:multisig:ledger:restore` to restore an encrypted backup to your Ledger', + ) + ux.exit(1) } else if (e instanceof LedgerConnectError) { ux.action.status = 'Connect and unlock your Ledger' } else if (e instanceof LedgerAppNotOpen) { @@ -119,6 +122,19 @@ export async function ledger({ } } +async function confirmRetryAction(message: string, actionRunning: boolean): Promise { + const confirmed = await ui.confirmList(message, 'Retry') + + if (!confirmed) { + ux.stdout('Operation aborted.') + ux.exit(0) + } + + if (!actionRunning) { + ux.action.start(message) + } +} + export async function sendTransactionWithLedger( client: RpcClient, raw: RawTransaction, diff --git a/ironfish-cli/src/ui/retry.ts b/ironfish-cli/src/ui/retry.ts index addc2038eb..ae948603a1 100644 --- a/ironfish-cli/src/ui/retry.ts +++ b/ironfish-cli/src/ui/retry.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { ErrorUtils, Logger } from '@ironfish/sdk' import { ux } from '@oclif/core' +import { ExitError } from '@oclif/core/errors' import { confirmList } from './prompt' export async function retryStep( @@ -18,6 +19,10 @@ export async function retryStep( const result = await stepFunction() return result } catch (error) { + if (error instanceof ExitError) { + throw error + } + logger.log(`An Error Occurred: ${ErrorUtils.renderError(error)}`) if (askToRetry) { diff --git a/ironfish-cli/src/ui/table.ts b/ironfish-cli/src/ui/table.ts index 2112e70b97..fd24116a9b 100644 --- a/ironfish-cli/src/ui/table.ts +++ b/ironfish-cli/src/ui/table.ts @@ -36,18 +36,22 @@ export interface TableOptions { export const TableFlags = { csv: Flags.boolean({ description: 'output is csv format [alias: --output=csv]', + helpGroup: 'OUTPUT', }), extended: Flags.boolean({ description: 'show extra columns', + helpGroup: 'OUTPUT', }), 'no-header': Flags.boolean({ description: 'hide table header from output', exclusive: ['csv'], + helpGroup: 'OUTPUT', }), output: Flags.string({ - description: 'output in a more machine friendly format', + description: 'output in different file types', exclusive: ['csv'], options: ['csv', 'json'], + helpGroup: 'OUTPUT', }), sort: Flags.string({ description: "property to sort by (prepend '-' for descending)", diff --git a/ironfish-cli/src/utils/chainport/requests.ts b/ironfish-cli/src/utils/chainport/requests.ts index 8a43f087e8..b750940550 100644 --- a/ironfish-cli/src/utils/chainport/requests.ts +++ b/ironfish-cli/src/utils/chainport/requests.ts @@ -76,11 +76,11 @@ const makeChainportRequest = async (url: string): Promise = }) .catch((error) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const chainportError = error.response?.data?.error?.description as string - if (chainportError) { - throw new Error(chainportError) + const apiError = error.response?.data?.message as string + if (apiError) { + throw new Error('Chainport error: ' + apiError) } else { - throw new Error('Chainport error - ' + error) + throw new Error('Chainport error: ' + error) } }) diff --git a/ironfish-cli/src/utils/table.ts b/ironfish-cli/src/utils/table.ts index 60220eb957..550b890beb 100644 --- a/ironfish-cli/src/utils/table.ts +++ b/ironfish-cli/src/utils/table.ts @@ -61,9 +61,9 @@ const timestamp = >(options?: { const asset = >(options?: { extended?: boolean - format?: Format + format?: TableOutput }): Partial>> => { - if (options?.extended || options?.format !== Format.cli) { + if (options?.extended || options?.format !== TableOutput.cli) { return { assetId: { header: 'Asset ID', @@ -127,7 +127,7 @@ function truncateCol(value: string, maxWidth: number | null): string { return value.slice(0, maxWidth - 1) + '…' } -export enum Format { +export enum TableOutput { cli = 'cli', csv = 'csv', json = 'json', diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index 5d99096c7b..8152de239a 100644 --- a/ironfish-rust-nodejs/npm/darwin-arm64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-arm64", - "version": "2.8.0", + "version": "2.9.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index 24ff1dd74d..27cbe53b82 100644 --- a/ironfish-rust-nodejs/npm/darwin-x64/package.json +++ b/ironfish-rust-nodejs/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-darwin-x64", - "version": "2.8.0", + "version": "2.9.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json index ec73406c28..8876823604 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-gnu", - "version": "2.8.0", + "version": "2.9.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json index b9f8fabc34..47ef3dbeea 100644 --- a/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-arm64-musl", - "version": "2.8.0", + "version": "2.9.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json index e20a64a84e..ea87f2e4bc 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-gnu", - "version": "2.8.0", + "version": "2.9.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json index 229c711a77..0a85ffb2fd 100644 --- a/ironfish-rust-nodejs/npm/linux-x64-musl/package.json +++ b/ironfish-rust-nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-linux-x64-musl", - "version": "2.8.0", + "version": "2.9.0", "os": [ "linux" ], diff --git a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json index 258a070237..d27a740c19 100644 --- a/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json +++ b/ironfish-rust-nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs-win32-x64-msvc", - "version": "2.8.0", + "version": "2.9.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index 9ab2d913c5..41d1ed5c13 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "2.8.0", + "version": "2.9.0", "description": "Node.js bindings for Rust code required by the Iron Fish SDK", "main": "index.js", "types": "index.d.ts", diff --git a/ironfish-rust-wasm/Cargo.toml b/ironfish-rust-wasm/Cargo.toml new file mode 100644 index 0000000000..fbc0276266 --- /dev/null +++ b/ironfish-rust-wasm/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "ironfish-wasm" +version = "0.1.0" +license = "MPL-2.0" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +publish = false + +[lib] +crate-type = ["cdylib"] + +[features] +default = ["transaction-proofs"] + +download-params = ["ironfish/download-params"] +note-encryption-stats = ["ironfish/note-encryption-stats"] +transaction-proofs = ["ironfish/transaction-proofs"] + +[dependencies] +blstrs = "0.6.0" +getrandom = { version = "0.2.8", features = ["js"] } # need to explicitly enable the `js` feature in order to run in a browser +group = "0.12.0" +ironfish = { version = "0.3.0", path = "../ironfish-rust", default-features = false } +ironfish-jubjub = "0.1.0" +ironfish_zkp = { version = "0.2.0", path = "../ironfish-zkp" } +rand = "0.8.5" +rayon = { version = "1.8.1", features = ["web_spin_lock"] } # need to explicitly enable the `web_spin_lock` in order to run in a browser +wasm-bindgen = "0.2.95" + +[dev-dependencies] +hex-literal = "0.4.1" +wasm-bindgen-test = "0.3.45" diff --git a/ironfish-rust-wasm/src/assets.rs b/ironfish-rust-wasm/src/assets.rs new file mode 100644 index 0000000000..d74761d51b --- /dev/null +++ b/ironfish-rust-wasm/src/assets.rs @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + errors::IronfishError, + keys::PublicAddress, + primitives::{ExtendedPoint, SubgroupPoint}, + wasm_bindgen_wrapper, +}; +use ironfish::errors::IronfishErrorKind; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct Asset(ironfish::assets::asset::Asset); +} + +#[wasm_bindgen] +impl Asset { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::assets::asset::Asset::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + self.0.write(&mut buf).expect("failed to serialize asset"); + buf + } + + #[wasm_bindgen(js_name = fromParts)] + pub fn from_parts( + creator: PublicAddress, + name: &str, + metadata: &str, + ) -> Result { + Ok(Self(ironfish::assets::asset::Asset::new( + creator.as_ref().to_owned(), + name, + metadata, + )?)) + } + + #[wasm_bindgen(js_name = fromPartsWithNonce)] + pub fn from_parts_with_nonce( + creator: PublicAddress, + name: &[u8], + metadata: &[u8], + nonce: u8, + ) -> Result { + let name = name + .try_into() + .map_err(|_| IronfishErrorKind::InvalidData)?; + let metadata = metadata + .try_into() + .map_err(|_| IronfishErrorKind::InvalidData)?; + Ok(Self(ironfish::assets::asset::Asset::new_with_nonce( + creator.as_ref().to_owned(), + name, + metadata, + nonce, + )?)) + } + + #[wasm_bindgen(getter)] + pub fn metadata(&self) -> Vec { + self.0.metadata().to_vec() + } + + #[wasm_bindgen(getter)] + pub fn name(&self) -> Vec { + self.0.name().to_vec() + } + + #[wasm_bindgen(getter)] + pub fn nonce(&self) -> u8 { + self.0.nonce() + } + + #[wasm_bindgen(getter)] + pub fn creator(&self) -> PublicAddress { + PublicAddress::deserialize(self.0.creator().as_slice()) + .expect("failed to deserialize public address") + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> AssetIdentifier { + self.0.id().to_owned().into() + } + + #[wasm_bindgen(getter, js_name = assetGenerator)] + pub fn asset_generator(&self) -> ExtendedPoint { + self.0.asset_generator().into() + } + + #[wasm_bindgen(getter, js_name = valueCommitmentGenerator)] + pub fn value_commitment_generator(&self) -> SubgroupPoint { + self.0.value_commitment_generator().into() + } +} + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct AssetIdentifier(ironfish::assets::asset_identifier::AssetIdentifier); +} + +#[wasm_bindgen] +impl AssetIdentifier { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self( + ironfish::assets::asset_identifier::AssetIdentifier::read(bytes)?, + )) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.as_bytes().to_vec() + } + + #[wasm_bindgen(getter)] + pub fn native() -> Self { + Self(ironfish::assets::asset_identifier::NATIVE_ASSET) + } + + #[wasm_bindgen(getter, js_name = assetGenerator)] + pub fn asset_generator(&self) -> ExtendedPoint { + self.0.asset_generator().into() + } + + #[wasm_bindgen(getter, js_name = valueCommitmentGenerator)] + pub fn value_commitment_generator(&self) -> SubgroupPoint { + self.0.value_commitment_generator().into() + } +} + +#[cfg(test)] +mod tests { + mod asset { + use crate::{assets::Asset, keys::PublicAddress}; + use hex_literal::hex; + use wasm_bindgen_test::wasm_bindgen_test; + + fn test_address() -> PublicAddress { + PublicAddress::deserialize( + hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0").as_slice(), + ) + .unwrap() + } + + fn test_asset() -> Asset { + let asset = Asset::from_parts(test_address(), "name", "meta").unwrap(); + + assert_eq!(asset.creator(), test_address()); + assert_eq!( + asset.name(), + b"name\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" + ); + assert_eq!( + asset.metadata(), + b"meta\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\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\0\0\0\0\0\0\0\0\0\0\0\ + \0\0\0\0\0\0\0\0\0\0\0\0\0" + ); + assert_eq!( + asset.id().serialize(), + hex!("2b845f8f97b90d2279bf502eb3ebdf71bf47460b083ca926421b0c7ee68ec816") + ); + + asset + } + + #[test] + #[wasm_bindgen_test] + fn serialize_deserialize_roundtrip() { + let asset = test_asset(); + + let serialization = asset.serialize(); + let deserialized = Asset::deserialize(&serialization[..]).unwrap(); + + assert_eq!(asset, deserialized); + assert_eq!(serialization, deserialized.serialize()); + } + + #[test] + #[wasm_bindgen_test] + fn from_parts_with_nonce() { + let asset = Asset::from_parts_with_nonce( + test_address(), + b"name\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", + b"meta\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\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\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, + ) + .unwrap(); + assert_eq!(asset, test_asset()); + } + } + + mod asset_identifier { + use crate::assets::AssetIdentifier; + use hex_literal::hex; + use wasm_bindgen_test::wasm_bindgen_test; + + #[test] + #[wasm_bindgen_test] + fn serialize_deserialize_roundtrip() { + let serialization = + hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1"); + let id = AssetIdentifier::deserialize(&serialization[..]).unwrap(); + assert_eq!(id.serialize(), serialization); + } + + #[test] + #[wasm_bindgen_test] + fn native() { + let id = AssetIdentifier::native(); + assert_eq!( + id.serialize(), + hex!("51f33a2f14f92735e562dc658a5639279ddca3d5079a6d1242b2a588a9cbf44c") + ); + } + } +} diff --git a/ironfish-rust-wasm/src/errors.rs b/ironfish-rust-wasm/src/errors.rs new file mode 100644 index 0000000000..10e0ee9bff --- /dev/null +++ b/ironfish-rust-wasm/src/errors.rs @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Debug)] +pub struct IronfishError(ironfish::errors::IronfishError); + +impl From for IronfishError +where + ironfish::errors::IronfishError: From, +{ + fn from(e: T) -> Self { + Self(ironfish::errors::IronfishError::from(e)) + } +} + +impl AsRef for IronfishError { + fn as_ref(&self) -> &ironfish::errors::IronfishError { + &self.0 + } +} + +impl AsRef for IronfishError { + fn as_ref(&self) -> &ironfish::errors::IronfishErrorKind { + &self.0.kind + } +} diff --git a/ironfish-rust-wasm/src/keys/mod.rs b/ironfish-rust-wasm/src/keys/mod.rs new file mode 100644 index 0000000000..5d67846bdb --- /dev/null +++ b/ironfish-rust-wasm/src/keys/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +mod proof_generation_key; +mod public_address; +mod sapling_key; +mod view_keys; + +pub use proof_generation_key::ProofGenerationKey; +pub use public_address::PublicAddress; +pub use sapling_key::SaplingKey; +pub use view_keys::{IncomingViewKey, OutgoingViewKey, ViewKey}; diff --git a/ironfish-rust-wasm/src/keys/proof_generation_key.rs b/ironfish-rust-wasm/src/keys/proof_generation_key.rs new file mode 100644 index 0000000000..cdde330c97 --- /dev/null +++ b/ironfish-rust-wasm/src/keys/proof_generation_key.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + errors::IronfishError, + primitives::{Fr, SubgroupPoint}, + wasm_bindgen_wrapper, +}; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct ProofGenerationKey(ironfish::keys::ProofGenerationKey); +} + +#[wasm_bindgen] +impl ProofGenerationKey { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::keys::ProofGenerationKey::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + #[wasm_bindgen(js_name = fromParts)] + pub fn from_parts(ak: SubgroupPoint, nsk: Fr) -> Self { + Self(ironfish::keys::ProofGenerationKey::new( + ak.into(), + nsk.into(), + )) + } +} diff --git a/ironfish-rust-wasm/src/keys/public_address.rs b/ironfish-rust-wasm/src/keys/public_address.rs new file mode 100644 index 0000000000..8ceda76464 --- /dev/null +++ b/ironfish-rust-wasm/src/keys/public_address.rs @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{errors::IronfishError, wasm_bindgen_wrapper}; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct PublicAddress(ironfish::PublicAddress); +} + +#[wasm_bindgen] +impl PublicAddress { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::PublicAddress::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.public_address().to_vec() + } + + #[wasm_bindgen(getter)] + pub fn bytes(&self) -> Vec { + self.0.public_address().to_vec() + } + + #[wasm_bindgen(getter)] + pub fn hex(&self) -> String { + self.0.hex_public_address() + } +} + +#[cfg(test)] +mod tests { + use crate::keys::public_address::PublicAddress; + use hex_literal::hex; + use wasm_bindgen_test::wasm_bindgen_test; + + #[test] + #[wasm_bindgen_test] + fn valid_address() { + let bytes = hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0"); + let addr = PublicAddress::deserialize(&bytes[..]) + .expect("valid address deserialization should have succeeded"); + assert_eq!(addr.serialize(), bytes); + assert_eq!(addr.bytes(), bytes); + assert_eq!( + addr.hex(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0" + ); + } + + #[test] + #[wasm_bindgen_test] + fn invalid_address() { + let bytes = hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"); + PublicAddress::deserialize(&bytes[..]) + .expect_err("invalid address deserialization should have failed"); + } +} diff --git a/ironfish-rust-wasm/src/keys/sapling_key.rs b/ironfish-rust-wasm/src/keys/sapling_key.rs new file mode 100644 index 0000000000..ce357af063 --- /dev/null +++ b/ironfish-rust-wasm/src/keys/sapling_key.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + errors::IronfishError, + keys::{IncomingViewKey, OutgoingViewKey, ProofGenerationKey, PublicAddress, ViewKey}, + wasm_bindgen_wrapper, +}; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct SaplingKey(ironfish::keys::SaplingKey); +} + +#[wasm_bindgen] +impl SaplingKey { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::keys::SaplingKey::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + self.0 + .write(&mut buf) + .expect("failed to serialize sapling key"); + buf + } + + #[wasm_bindgen] + pub fn random() -> Self { + Self(ironfish::keys::SaplingKey::generate_key()) + } + + #[wasm_bindgen(js_name = fromHex)] + pub fn from_hex(hex: &str) -> Result { + Ok(Self(ironfish::keys::SaplingKey::from_hex(hex)?)) + } + + #[wasm_bindgen(js_name = toHex)] + pub fn to_hex(&self) -> String { + self.0.hex_spending_key() + } + + // TODO: to/fromWords + + #[wasm_bindgen(getter, js_name = publicAddress)] + pub fn public_address(&self) -> PublicAddress { + self.0.public_address().into() + } + + #[wasm_bindgen(getter, js_name = spendingKey)] + pub fn spending_key(&self) -> Vec { + self.0.spending_key().to_vec() + } + + #[wasm_bindgen(getter, js_name = incomingViewKey)] + pub fn incoming_view_key(&self) -> IncomingViewKey { + self.0.incoming_view_key().to_owned().into() + } + + #[wasm_bindgen(getter, js_name = outgoingViewKey)] + pub fn outgoing_view_key(&self) -> OutgoingViewKey { + self.0.outgoing_view_key().to_owned().into() + } + + #[wasm_bindgen(getter, js_name = viewKey)] + pub fn view_key(&self) -> ViewKey { + self.0.view_key().to_owned().into() + } + + #[wasm_bindgen(getter, js_name = proofGenerationKey)] + pub fn proof_generation_key(&self) -> ProofGenerationKey { + self.0.sapling_proof_generation_key().into() + } +} + +#[cfg(test)] +mod tests { + use crate::keys::{IncomingViewKey, OutgoingViewKey, ProofGenerationKey, SaplingKey, ViewKey}; + use wasm_bindgen_test::wasm_bindgen_test; + + macro_rules! assert_serde_ok { + ( $type:ty, $key:expr ) => { + assert_eq!( + $key, + <$type>::deserialize($key.serialize().as_slice()).expect("deserialization failed") + ) + }; + } + + #[test] + #[wasm_bindgen_test] + fn serialization_roundtrip() { + let key = SaplingKey::random(); + assert_serde_ok!(SaplingKey, key); + assert_serde_ok!(IncomingViewKey, key.incoming_view_key()); + assert_serde_ok!(OutgoingViewKey, key.outgoing_view_key()); + assert_serde_ok!(ViewKey, key.view_key()); + assert_serde_ok!(ProofGenerationKey, key.proof_generation_key()); + } +} diff --git a/ironfish-rust-wasm/src/keys/view_keys.rs b/ironfish-rust-wasm/src/keys/view_keys.rs new file mode 100644 index 0000000000..6166bfd85f --- /dev/null +++ b/ironfish-rust-wasm/src/keys/view_keys.rs @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + errors::IronfishError, keys::PublicAddress, primitives::PublicKey, wasm_bindgen_wrapper, +}; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct IncomingViewKey(ironfish::keys::IncomingViewKey); +} + +#[wasm_bindgen] +impl IncomingViewKey { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::keys::IncomingViewKey::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + #[wasm_bindgen(js_name = fromHex)] + pub fn from_hex(hex: &str) -> Result { + Ok(Self(ironfish::keys::IncomingViewKey::from_hex(hex)?)) + } + + #[wasm_bindgen(js_name = toHex)] + pub fn to_hex(&self) -> String { + self.0.hex_key() + } + + // TODO: to/fromWords + + #[wasm_bindgen(getter, js_name = publicAddress)] + pub fn public_address(&self) -> PublicAddress { + self.0.public_address().into() + } +} + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct OutgoingViewKey(ironfish::keys::OutgoingViewKey); +} + +#[wasm_bindgen] +impl OutgoingViewKey { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::keys::OutgoingViewKey::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + #[wasm_bindgen(js_name = fromHex)] + pub fn from_hex(hex: &str) -> Result { + Ok(Self(ironfish::keys::OutgoingViewKey::from_hex(hex)?)) + } + + #[wasm_bindgen(js_name = toHex)] + pub fn to_hex(&self) -> String { + self.0.hex_key() + } + + // TODO: to/fromWords +} + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct ViewKey(ironfish::keys::ViewKey); +} + +#[wasm_bindgen] +impl ViewKey { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::keys::ViewKey::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + #[wasm_bindgen(js_name = fromHex)] + pub fn from_hex(hex: &str) -> Result { + Ok(Self(ironfish::keys::ViewKey::from_hex(hex)?)) + } + + #[wasm_bindgen(js_name = toHex)] + pub fn to_hex(&self) -> String { + self.0.hex_key() + } + + #[wasm_bindgen(getter, js_name = publicAddress)] + pub fn public_address(&self) -> Result { + self.0 + .public_address() + .map(|a| a.into()) + .map_err(|e| e.into()) + } + + #[wasm_bindgen(getter, js_name = authorizingKey)] + pub fn authorizing_key(&self) -> PublicKey { + self.0.authorizing_key.into() + } + + #[wasm_bindgen(getter, js_name = nullifierDerivingKey)] + pub fn nullifier_deriving_key(&self) -> PublicKey { + self.0.nullifier_deriving_key.into() + } +} diff --git a/ironfish-rust-wasm/src/lib.rs b/ironfish-rust-wasm/src/lib.rs new file mode 100644 index 0000000000..95d627b928 --- /dev/null +++ b/ironfish-rust-wasm/src/lib.rs @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![warn(clippy::dbg_macro)] +#![warn(clippy::print_stderr)] +#![warn(clippy::print_stdout)] +#![warn(unreachable_pub)] +#![warn(unused_crate_dependencies)] +#![warn(unused_macro_rules)] +#![warn(unused_qualifications)] + +// These dependencies exist only to ensure that some browser-specific features are enabled, and are +// not actually used in our code +use getrandom as _; +use rayon as _; + +pub mod assets; +pub mod errors; +pub mod keys; +pub mod merkle_note; +pub mod note; +pub mod primitives; +pub mod transaction; + +/// Creates a [`wasm_bindgen`] wrapper for an existing type. +/// +/// This macro can be invoked as follows: +/// +/// ``` +/// wasm_bindgen_wrapper! { +/// #[derive(Clone, Debug)] +/// pub struct FooBinding(Foo); +/// } +/// ``` +/// +/// and expands to the following: +/// +/// ``` +/// #[wasm_bindgen] +/// #[derive(Clone, Debug)] +/// pub struct FooBinding(Foo); +/// +/// impl From for FooBinding { ... } +/// impl From for Foo { ... } +/// impl AsRef for FooBinding { ... } +/// impl Borrow for FooBinding { ... } +/// ``` +macro_rules! wasm_bindgen_wrapper { + ($( + $( #[ $meta:meta ] )* + $vis:vis struct $name:ident ( $inner:ty ) ; + )*) => {$( + $(#[$meta])* + #[::wasm_bindgen::prelude::wasm_bindgen] + $vis struct $name($inner); + + impl ::std::convert::From<$inner> for $name { + fn from(x: $inner) -> Self { + Self(x) + } + } + + impl ::std::convert::From<$name> for $inner { + fn from(x: $name) -> Self { + x.0 + } + } + + impl ::std::convert::AsRef<$inner> for $name { + fn as_ref(&self) -> &$inner { + &self.0 + } + } + + impl ::std::borrow::Borrow<$inner> for $name { + fn borrow(&self) -> &$inner { + &self.0 + } + } + )*} +} + +use wasm_bindgen_wrapper; + +#[cfg(test)] +mod tests { + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +} diff --git a/ironfish-rust-wasm/src/merkle_note.rs b/ironfish-rust-wasm/src/merkle_note.rs new file mode 100644 index 0000000000..60185d9e50 --- /dev/null +++ b/ironfish-rust-wasm/src/merkle_note.rs @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::{ + errors::IronfishError, + keys::{IncomingViewKey, OutgoingViewKey}, + note::Note, + primitives::Scalar, + wasm_bindgen_wrapper, +}; +use wasm_bindgen::prelude::*; + +wasm_bindgen_wrapper! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct MerkleNote(ironfish::MerkleNote); +} + +#[wasm_bindgen] +impl MerkleNote { + #[wasm_bindgen(constructor)] + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(Self(ironfish::MerkleNote::read(bytes)?)) + } + + #[wasm_bindgen] + pub fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + self.0 + .write(&mut buf) + .expect("failed to serialize merkle note"); + buf + } + + #[wasm_bindgen(getter, js_name = merkleHash)] + pub fn merkle_hash(&self) -> MerkleNoteHash { + self.0.merkle_hash().into() + } + + #[wasm_bindgen(js_name = decryptNoteForOwner)] + pub fn decrypt_note_for_owner( + &self, + owner_view_key: &IncomingViewKey, + ) -> Result { + self.0 + .decrypt_note_for_owner(owner_view_key.as_ref()) + .map(|n| n.into()) + .map_err(|e| e.into()) + } + + #[wasm_bindgen(js_name = decryptNoteForOwners)] + pub fn decrypt_note_for_owners(&self, owner_view_keys: Vec) -> Vec { + // The original `decrypt_note_for_owners` returns a `Vec>`. Here instead we + // are filtering out all errors. This likely makes this method hard to use in practice, + // because the information for mapping between the original owner and the resulting note is + // lost. However, returing a `Vec` or a `Vec