diff --git a/.github/workflows/deploy-node-docker-image.yml b/.github/workflows/deploy-node-docker-image.yml index 08e9922184..aa82644504 100644 --- a/.github/workflows/deploy-node-docker-image.yml +++ b/.github/workflows/deploy-node-docker-image.yml @@ -25,6 +25,10 @@ on: description: 'AWS:{GIT_SHA}' type: boolean default: false + aws_tag_git_branch: + description: 'AWS:{BRANCH}' + type: boolean + default: false permissions: contents: read @@ -116,6 +120,13 @@ jobs: docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} + # Used to deploy images for specific branches + - name: Deploy Node Image to AWS:${{ github.ref_name }} + if: ${{ inputs.aws_tag_git_branch && github.ref_type == 'branch' }} + run: | + docker tag ironfish ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} + docker push ${{ secrets.AWS_NODE_REGISTRY_URL }}/ironfish:${{ github.ref_name }} + - name: Deploy Node Image to AWS:testnet if: ${{ inputs.aws_tag_testnet }} run: | diff --git a/Cargo.lock b/Cargo.lock index daf2fdc4a1..cfb4a048eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,18 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash 0.5.0", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -204,6 +216,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "blake2b_simd" version = "1.0.0" @@ -499,10 +520,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] -name = "const-crc32" -version = "1.2.0" +name = "const-crc32-nostd" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23eeda20729a10d016ff87bc5b3547d771e7aa9e356ec2e65abd899c02c2d66e" +checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" [[package]] name = "const-oid" @@ -549,9 +570,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -704,7 +725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901" dependencies = [ "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -732,7 +753,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -753,13 +774,13 @@ dependencies = [ [[package]] name = "derive-getters" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" +checksum = "0a6433aac097572ea8ccc60b3f2e756c661c9aeed9225cdd4d0cb119cb7ff6ba" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.77", ] [[package]] @@ -943,7 +964,7 @@ checksum = "55a9a55d1dab3b07854648d48e366f684aefe2ac78ae28cec3bf65e3cd53d9a3" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -1058,35 +1079,37 @@ dependencies = [ [[package]] name = "frost-core" -version = "1.0.0" +version = "2.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d6280625f1603d160df24b23e4984a6a7286f41455ae606823d0104c32e834" +checksum = "ed1383227a6606aacf5df9a17ff57824c6971a0ab225b69b911bec0ba7bbb869" dependencies = [ "byteorder", - "const-crc32", + "const-crc32-nostd", "debugless-unwrap", "derive-getters", "document-features", "hex", - "itertools 0.12.0", + "itertools 0.13.0", "postcard", "rand_core", "serde", "serdect", "thiserror", + "thiserror-nostd-notrait", "visibility", "zeroize", ] [[package]] name = "frost-rerandomized" -version = "1.0.0" +version = "2.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c58f58ea009000db490efd9a3936d0035647a2b00c7ba8f3868c2ed0306b0b" +checksum = "bdb14a6054f9ce5aa4912c60c11392d42c43acec8295ee1df1f67a9d0b7a73ee" dependencies = [ "derive-getters", "document-features", "frost-core", + "hex", "rand_core", ] @@ -1314,6 +1337,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.11.0" @@ -1462,19 +1494,21 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" name = "ironfish" version = "0.3.0" dependencies = [ + "argon2", "bellperson", "blake2b_simd", "blake2s_simd", "blake3", "blstrs", "byteorder", - "chacha20poly1305 0.9.1", + "chacha20poly1305 0.10.1", "crypto_box", "ff 0.12.1", "fish_hash", "group 0.12.1", "hex", "hex-literal", + "hkdf", "ironfish-frost", "ironfish_zkp", "jubjub 0.9.0 (git+https://github.com/iron-fish/jubjub.git?branch=blstrs)", @@ -1490,7 +1524,7 @@ dependencies = [ [[package]] name = "ironfish-frost" version = "0.1.0" -source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#d2b082e3cd25d12073c1b113da941960c08fcb32" +source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#d4681df8a5a613fd9e716212aae3be8602acd227" dependencies = [ "blake3", "chacha20 0.9.1", @@ -1548,9 +1582,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1627,12 +1661,12 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "winapi", + "windows-targets", ] [[package]] @@ -1724,9 +1758,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.13.2" +version = "2.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e" +checksum = "1277600d452e570cc83cf5f4e8efb389cc21e5cbefadcfba7239f4551e2e3e99" dependencies = [ "bitflags 2.3.3", "ctor", @@ -1743,23 +1777,23 @@ checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" [[package]] name = "napi-derive" -version = "2.13.0" +version = "2.16.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367" +checksum = "150d87c4440b9f4815cb454918db498b5aae9a57aa743d20783fe75381181d01" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.77", ] [[package]] name = "napi-derive-backend" -version = "1.0.52" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" +checksum = "0cd81b794fc1d6051acf8c4f3cb4f82833b0621272a232b4ff0cf3df1dbddb61" dependencies = [ "convert_case", "once_cell", @@ -1767,14 +1801,14 @@ dependencies = [ "quote", "regex", "semver", - "syn 1.0.107", + "syn 2.0.77", ] [[package]] name = "napi-sys" -version = "2.2.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] @@ -1845,9 +1879,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" @@ -1884,7 +1918,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -1959,6 +1993,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pasta_curves" version = "0.4.1" @@ -1994,7 +2039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" dependencies = [ "crypto-mac", - "password-hash", + "password-hash 0.3.2", ] [[package]] @@ -2115,18 +2160,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2218,7 +2263,7 @@ dependencies = [ [[package]] name = "reddsa" version = "0.5.1" -source = "git+https://github.com/ZcashFoundation/reddsa.git?rev=311baf8865f6e21527d1f20750d8f2cf5c9e531a#311baf8865f6e21527d1f20750d8f2cf5c9e531a" +source = "git+https://github.com/ZcashFoundation/reddsa.git?rev=ed49e9ca0699a6450f6d4a9fe62ff168f5ea1ead#ed49e9ca0699a6450f6d4a9fe62ff168f5ea1ead" dependencies = [ "blake2b_simd", "byteorder", @@ -2451,7 +2496,7 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -2611,9 +2656,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2677,6 +2722,26 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "thiserror-nostd-notrait" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8444e638022c44d2a9337031dee8acb732bcc7fbf52ac654edc236b26408b61" +dependencies = [ + "thiserror-nostd-notrait-impl", +] + +[[package]] +name = "thiserror-nostd-notrait-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585e5ef40a784ce60b49c67d762110688d211d395d39e096be204535cf64590e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -2925,7 +2990,7 @@ checksum = "b3fd98999db9227cf28e59d83e1f120f42bc233d4b152e8fab9bc87d5bb1e0f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -3221,8 +3286,6 @@ checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core", - "serde", - "zeroize", ] [[package]] diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index a33ae829c6..0e143eec4c 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.5.0", + "version": "2.6.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -59,11 +59,10 @@ "oclif:version": "oclif readme && git add README.md" }, "dependencies": { - "@ironfish/rust-nodejs": "2.5.0", - "@ironfish/sdk": "2.5.0", + "@ironfish/rust-nodejs": "2.6.0", + "@ironfish/sdk": "2.6.0", "@ledgerhq/hw-transport-node-hid": "6.29.1", "@oclif/core": "4.0.11", - "@oclif/plugin-autocomplete": "3.1.6", "@oclif/plugin-help": "6.2.5", "@oclif/plugin-not-found": "3.2.10", "@oclif/plugin-warn-if-update-available": "3.1.8", @@ -98,18 +97,47 @@ "plugins": [ "@oclif/plugin-help", "@oclif/plugin-not-found", - "@oclif/plugin-warn-if-update-available", - "@oclif/plugin-autocomplete" + "@oclif/plugin-warn-if-update-available" ], "topics": { - "wallet:scanning": { - "description": "Turn on or off scanning for accounts" + "chain": { + "description": "commands for the blockchain" }, "chain:blocks": { "description": "commands to look at blocks" }, "chain:assets": { "description": "commands to look at assets" + }, + "chain:transactions": { + "description": "commands to look at transactions" + }, + "rpc": { + "description": "commands for the RPC server" + }, + "miners": { + "description": "commands for mining" + }, + "mempool": { + "description": "commands for the mempool" + }, + "wallet": { + "description": "commands for the wallet" + }, + "wallet:multisig": { + "description": "commands for multisig accounts" + }, + "wallet:notes": { + "description": "commands for account notes" + }, + "wallet:scanning": { + "description": "commands for managing scanning" + }, + "wallet:transactions": { + "description": "commands for account transactions" + }, + "workers": { + "description": "commands for the worker pool" } } }, diff --git a/ironfish-cli/src/commands/browse.ts b/ironfish-cli/src/commands/browse.ts index 5faa6bcbad..4bc986f143 100644 --- a/ironfish-cli/src/commands/browse.ts +++ b/ironfish-cli/src/commands/browse.ts @@ -7,7 +7,7 @@ import { IronfishCommand } from '../command' import { PlatformUtils } from '../utils' export class BrowseCommand extends IronfishCommand { - static description = `Browse to your data directory` + static description = 'open the data folder' static flags = { cd: Flags.boolean({ diff --git a/ironfish-cli/src/commands/chain/assets/info.ts b/ironfish-cli/src/commands/chain/assets/info.ts index 354e9e40a7..e9eaa1c4e2 100644 --- a/ironfish-cli/src/commands/chain/assets/info.ts +++ b/ironfish-cli/src/commands/chain/assets/info.ts @@ -4,13 +4,21 @@ import { BufferUtils } from '@ironfish/sdk' import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' -import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../../flags' +import { JsonFlags, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' export default class AssetInfo extends IronfishCommand { static description = 'show asset information' static enableJsonFlag = true + static examples = [ + { + description: 'show the native $IRON asset info', + command: + 'ironfish chain:assets:info 51f33a2f14f92735e562dc658a5639279ddca3d5079a6d1242b2a588a9cbf44c', + }, + ] + static args = { id: Args.string({ required: true, @@ -20,7 +28,7 @@ export default class AssetInfo extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, } async start(): Promise { diff --git a/ironfish-cli/src/commands/chain/blocks/info.ts b/ironfish-cli/src/commands/chain/blocks/info.ts index b3bdfecc7e..2ff668e343 100644 --- a/ironfish-cli/src/commands/chain/blocks/info.ts +++ b/ironfish-cli/src/commands/chain/blocks/info.ts @@ -4,7 +4,7 @@ import { BufferUtils, CurrencyUtils, TimeUtils } from '@ironfish/sdk' import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' -import { ColorFlag, ColorFlagKey } from '../../../flags' +import { JsonFlags, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' export default class BlockInfo extends IronfishCommand { @@ -19,7 +19,8 @@ export default class BlockInfo extends IronfishCommand { } static flags = { - [ColorFlagKey]: ColorFlag, + ...RemoteFlags, + ...JsonFlags, } async start(): Promise { diff --git a/ironfish-cli/src/commands/chain/broadcast.ts b/ironfish-cli/src/commands/chain/broadcast.ts index 9d2bb42032..e10d32e18c 100644 --- a/ironfish-cli/src/commands/chain/broadcast.ts +++ b/ironfish-cli/src/commands/chain/broadcast.ts @@ -8,10 +8,6 @@ import { RemoteFlags } from '../../flags' export class BroadcastCommand extends IronfishCommand { static description = 'broadcast a transaction to the network' - static flags = { - ...RemoteFlags, - } - static args = { transaction: Args.string({ required: true, @@ -19,6 +15,10 @@ export class BroadcastCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(BroadcastCommand) const { transaction } = args @@ -26,8 +26,20 @@ export class BroadcastCommand extends IronfishCommand { ux.action.start(`Broadcasting transaction`) const client = await this.connectRpc() const response = await client.chain.broadcastTransaction({ transaction }) - if (response.content) { + + if (response.content.accepted && response.content.broadcasted) { ux.action.stop(`Transaction broadcasted: ${response.content.hash}`) + } else { + ux.action.stop() + this.error( + `Transaction broadcast may have failed.${ + !response.content.accepted ? ' Transaction was not accepted by the node.' : '' + }${ + !response.content.broadcasted + ? ' Transaction was not broadcasted to the network.' + : '' + }`, + ) } } } diff --git a/ironfish-cli/src/commands/chain/download.ts b/ironfish-cli/src/commands/chain/download.ts index 1ae08d936d..ab0c155c3f 100644 --- a/ironfish-cli/src/commands/chain/download.ts +++ b/ironfish-cli/src/commands/chain/download.ts @@ -9,7 +9,7 @@ import { DownloadedSnapshot, getDefaultManifestUrl, SnapshotDownloader } from '. import { confirmOrQuit, ProgressBar, ProgressBarPresets } from '../../ui' export default class Download extends IronfishCommand { - static description = 'download the chain' + static description = 'download the blockchain quickly' static flags = { manifestUrl: Flags.string({ diff --git a/ironfish-cli/src/commands/chain/export.ts b/ironfish-cli/src/commands/chain/export.ts index 129d25aaed..bfdeef018f 100644 --- a/ironfish-cli/src/commands/chain/export.ts +++ b/ironfish-cli/src/commands/chain/export.ts @@ -9,16 +9,7 @@ import { RemoteFlags } from '../../flags' import { ProgressBar } from '../../ui' export default class Export extends IronfishCommand { - static description = 'export the chain to a file' - - static flags = { - ...RemoteFlags, - path: Flags.string({ - char: 'p', - required: false, - description: 'The path to export the chain to', - }), - } + static description = 'export the blockchain to a file' static args = { start: Args.integer({ @@ -32,6 +23,15 @@ export default class Export extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + path: Flags.string({ + char: 'p', + required: false, + description: 'The path to export the chain to', + }), + } + async start(): Promise { const { flags, args } = await this.parse(Export) diff --git a/ironfish-cli/src/commands/chain/forks.ts b/ironfish-cli/src/commands/chain/forks.ts index 1b1b0fa375..4c6709d005 100644 --- a/ironfish-cli/src/commands/chain/forks.ts +++ b/ironfish-cli/src/commands/chain/forks.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { GossipForkCounter } from '../../utils/gossipForkCounter' export default class ForksCommand extends IronfishCommand { - static description = 'detect forks that are being mined' + static description = 'show forks that are being mined' static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/chain/power.ts b/ironfish-cli/src/commands/chain/power.ts index 632448e77d..4c33380897 100644 --- a/ironfish-cli/src/commands/chain/power.ts +++ b/ironfish-cli/src/commands/chain/power.ts @@ -4,25 +4,26 @@ import { FileUtils } from '@ironfish/sdk' import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' export default class Power extends IronfishCommand { static description = "show the network's mining power" static enableJsonFlag = true - static flags = { - [ColorFlagKey]: ColorFlag, - history: Flags.integer({ + static args = { + block: Args.integer({ required: false, - description: - 'The number of blocks to look back to calculate the network hashes per second', + description: 'The sequence of the block to estimate network speed for', }), } - static args = { - block: Args.integer({ + static flags = { + ...RemoteFlags, + ...JsonFlags, + history: Flags.integer({ required: false, - description: 'The sequence of the block to estimate network speed for', + description: + 'The number of blocks to look back to calculate the network hashes per second', }), } diff --git a/ironfish-cli/src/commands/chain/prune.ts b/ironfish-cli/src/commands/chain/prune.ts index 19527e45d6..130daf9418 100644 --- a/ironfish-cli/src/commands/chain/prune.ts +++ b/ironfish-cli/src/commands/chain/prune.ts @@ -6,7 +6,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' export default class Prune extends IronfishCommand { - static description = 'remove unused blocks from the chain' + static description = 'delete unused blocks from the blockchain' static flags = { dry: Flags.boolean({ diff --git a/ironfish-cli/src/commands/chain/status.ts b/ironfish-cli/src/commands/chain/status.ts index b856b1f565..989850d8a9 100644 --- a/ironfish-cli/src/commands/chain/status.ts +++ b/ironfish-cli/src/commands/chain/status.ts @@ -3,15 +3,16 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { FileUtils, renderNetworkName } from '@ironfish/sdk' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' import * as ui from '../../ui' export default class ChainStatus extends IronfishCommand { - static description = 'show chain information' + static description = 'show blockchain information' static enableJsonFlag = true static flags = { - [ColorFlagKey]: ColorFlag, + ...JsonFlags, + ...RemoteFlags, } async start(): Promise { diff --git a/ironfish-cli/src/commands/chain/transactions/info.ts b/ironfish-cli/src/commands/chain/transactions/info.ts index 6dfecd004b..3feafe8f70 100644 --- a/ironfish-cli/src/commands/chain/transactions/info.ts +++ b/ironfish-cli/src/commands/chain/transactions/info.ts @@ -5,18 +5,13 @@ import { CurrencyUtils, FileUtils } from '@ironfish/sdk' import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' -import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../../flags' +import { JsonFlags, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' export class TransactionInfo extends IronfishCommand { static description = 'show transaction information' static enableJsonFlag = true - static flags = { - ...RemoteFlags, - [ColorFlagKey]: ColorFlag, - } - static args = { hash: Args.string({ required: true, @@ -24,6 +19,11 @@ export class TransactionInfo extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + ...JsonFlags, + } + async start(): Promise { const { args } = await this.parse(TransactionInfo) diff --git a/ironfish-cli/src/commands/config/edit.ts b/ironfish-cli/src/commands/config/edit.ts index 480cc7249e..c4e9fbfe91 100644 --- a/ironfish-cli/src/commands/config/edit.ts +++ b/ironfish-cli/src/commands/config/edit.ts @@ -15,7 +15,7 @@ const writeFileAsync = promisify(writeFile) const readFileAsync = promisify(readFile) export class EditCommand extends IronfishCommand { - static description = `Edit the config in your configured editor + static description = `interactively edit your config Set the editor in either EDITOR environment variable, or set 'editor' in your ironfish config` diff --git a/ironfish-cli/src/commands/config/get.ts b/ironfish-cli/src/commands/config/get.ts index 0f5a91c1a3..b21f8ccbe7 100644 --- a/ironfish-cli/src/commands/config/get.ts +++ b/ironfish-cli/src/commands/config/get.ts @@ -4,11 +4,11 @@ import { ConfigOptions } from '@ironfish/sdk' import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' import * as ui from '../../ui' export class GetCommand extends IronfishCommand { - static description = `Print out one config value` + static description = `show a single config value` static enableJsonFlag = true static args = { @@ -20,7 +20,7 @@ export class GetCommand extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, user: Flags.boolean({ description: 'Only show config from the users datadir and not overrides', }), diff --git a/ironfish-cli/src/commands/config/index.ts b/ironfish-cli/src/commands/config/index.ts index 5f4a18142e..60e9a64003 100644 --- a/ironfish-cli/src/commands/config/index.ts +++ b/ironfish-cli/src/commands/config/index.ts @@ -3,17 +3,17 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey } from '../../flags' +import { JsonFlags } from '../../flags' import { RemoteFlags } from '../../flags' import * as ui from '../../ui' export class ShowCommand extends IronfishCommand { - static description = `Print out the entire config` + static description = "show the node's config" static enableJsonFlag = true static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, user: Flags.boolean({ description: 'Only show config from the users datadir and not overrides', }), diff --git a/ironfish-cli/src/commands/config/set.ts b/ironfish-cli/src/commands/config/set.ts index 5cd83a7e31..ebc6511b97 100644 --- a/ironfish-cli/src/commands/config/set.ts +++ b/ironfish-cli/src/commands/config/set.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class SetCommand extends IronfishCommand { - static description = `Set a value in the config` + static description = `set a single value in the config` static args = { name: Args.string({ diff --git a/ironfish-cli/src/commands/config/unset.ts b/ironfish-cli/src/commands/config/unset.ts index 3dc81ecd12..86c4fb27f3 100644 --- a/ironfish-cli/src/commands/config/unset.ts +++ b/ironfish-cli/src/commands/config/unset.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class UnsetCommand extends IronfishCommand { - static description = `Unset a value in the config and fall back to default` + static description = `reset a config value to the default` static args = { name: Args.string({ diff --git a/ironfish-cli/src/commands/debug.ts b/ironfish-cli/src/commands/debug.ts index d0d4feeffa..720216cbce 100644 --- a/ironfish-cli/src/commands/debug.ts +++ b/ironfish-cli/src/commands/debug.ts @@ -13,14 +13,19 @@ import { execSync } from 'child_process' import os from 'os' import { getHeapStatistics } from 'v8' import { IronfishCommand } from '../command' - -const SPACE_BUFFER = 8 +import { JsonFlags } from '../flags' +import * as ui from '../ui' export default class Debug extends IronfishCommand { static description = 'Show debug information to help locate issues' static hidden = true + static enableJsonFlag = true + + static flags = { + ...JsonFlags, + } - async start(): Promise { + async start(): Promise { const node = await this.sdk.node({ autoSeed: false }) let dbOpen = true @@ -40,13 +45,15 @@ export default class Debug extends IronfishCommand { let output = this.baseOutput(node) if (dbOpen) { - output = new Map([...output, ...(await this.outputRequiringDB(node))]) + output = { ...output, ...(await this.outputRequiringDB(node)) } } - this.display(output) + this.log(ui.card(output)) + + return output } - baseOutput(node: FullNode): Map { + baseOutput(node: FullNode): Record { const cpus = os.cpus() const cpuNames = [...new Set(cpus.map((c) => c.model))] const cpuThreads = cpus.length @@ -68,26 +75,26 @@ export default class Debug extends IronfishCommand { cmdInPath = false } - return new Map([ - ['Iron Fish version', `${node.pkg.version} @ ${node.pkg.git}`], - ['Iron Fish library', `${IronfishPKG.version} @ ${IronfishPKG.git}`], - ['Operating system', `${os.type()} ${process.arch}`], - ['CPU model(s)', `${cpuNames.toString()}`], - ['CPU threads', `${cpuThreads}`], - ['RAM total', `${memTotal}`], - ['Heap total', `${heapTotal}`], - ['Node version', `${process.version}`], - ['ironfish in PATH', `${cmdInPath.toString()}`], - ['Garbage Collector Exposed', `${String(!!global.gc)}`], - ['Telemetry enabled', `${telemetryEnabled}`], - ['Asset Verification enabled', `${assetVerificationEnabled}`], - ['Node name', `${nodeName}`], - ['Block graffiti', `${blockGraffiti}`], - ]) + return { + 'Iron Fish version': `${node.pkg.version} @ ${node.pkg.git}`, + 'Iron Fish library': `${IronfishPKG.version} @ ${IronfishPKG.git}`, + 'Operating system': `${os.type()} ${process.arch}`, + 'CPU model(s)': cpuNames.toString(), + 'CPU threads': cpuThreads.toString(), + 'RAM total': memTotal, + 'Heap total': heapTotal, + 'Node version': process.version, + 'ironfish in PATH': cmdInPath.toString(), + 'Garbage Collector Exposed': String(!!global.gc), + 'Telemetry enabled': telemetryEnabled, + 'Asset Verification enabled': assetVerificationEnabled, + 'Node name': nodeName, + 'Block graffiti': blockGraffiti, + } } - async outputRequiringDB(node: FullNode): Promise> { - const output = new Map() + async outputRequiringDB(node: FullNode): Promise> { + const output: Record = {} const headHashes = new Map() for await (const { accountId, head } of node.wallet.walletDb.loadHeads()) { @@ -103,33 +110,13 @@ export default class Debug extends IronfishCommand { const shortId = accountId.slice(0, 6) - output.set(`Account ${shortId} uuid`, `${accountId}`) - output.set(`Account ${shortId} name`, `${account?.name || `ACCOUNT NOT FOUND`}`) - output.set( - `Account ${shortId} head hash`, - `${headHash ? headHash.toString('hex') : 'NULL'}`, - ) - output.set(`Account ${shortId} head in chain`, `${headInChain.toString()}`) - output.set(`Account ${shortId} sequence`, `${headSequence}`) + output[`Account ${shortId} uuid`] = accountId + output[`Account ${shortId} name`] = account?.name || `ACCOUNT NOT FOUND` + output[`Account ${shortId} head hash`] = headHash ? headHash.toString('hex') : 'NULL' + output[`Account ${shortId} head in chain`] = headInChain.toString() + output[`Account ${shortId} sequence`] = headSequence ? headSequence.toString() : 'NULL' } return output } - - display(output: Map): void { - // Get the longest key length to determine how big to make the space buffer - let longestStringLength = 0 - for (const key of output.keys()) { - if (key.length > longestStringLength) { - longestStringLength = key.length - } - } - - const maxKeyWidth = longestStringLength + SPACE_BUFFER - output.forEach((value, key) => { - const spaceWidth = maxKeyWidth - key.length - const spaceString = new Array(spaceWidth).join(' ') - this.log(`${key}${spaceString}${value}`) - }) - } } diff --git a/ironfish-cli/src/commands/faucet.ts b/ironfish-cli/src/commands/faucet.ts index 286c9cd3d0..18e6d525b2 100644 --- a/ironfish-cli/src/commands/faucet.ts +++ b/ironfish-cli/src/commands/faucet.ts @@ -12,7 +12,7 @@ import { inputPrompt } from '../ui' const FAUCET_DISABLED = false export class FaucetCommand extends IronfishCommand { - static description = `Receive coins from the Iron Fish official testnet Faucet` + static description = 'get coins from the testnet Faucet' static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/fees.ts b/ironfish-cli/src/commands/fees.ts index fa3b5f073b..6faed84004 100644 --- a/ironfish-cli/src/commands/fees.ts +++ b/ironfish-cli/src/commands/fees.ts @@ -10,20 +10,23 @@ import { } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../command' -import { RemoteFlags } from '../flags' +import { JsonFlags, RemoteFlags } from '../flags' +import * as ui from '../ui' export class FeeCommand extends IronfishCommand { - static description = `Get fee distribution for most recent blocks` + static description = 'show network transaction fees' + static enableJsonFlag = true static flags = { ...RemoteFlags, + ...JsonFlags, explain: Flags.boolean({ default: false, description: 'Explain fee rates', }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(FeeCommand) const client = await this.connectRpc() @@ -35,9 +38,9 @@ export class FeeCommand extends IronfishCommand { const feeRates = await client.chain.estimateFeeRates() this.log('Fee Rates ($ORE/kB)') - this.log(`slow: ${feeRates.content.slow || ''}`) - this.log(`average: ${feeRates.content.average || ''}`) - this.log(`fast: ${feeRates.content.fast || ''}`) + this.log(ui.card(feeRates.content)) + + return feeRates.content } async explainFeeRates(client: RpcClient): Promise { @@ -62,9 +65,13 @@ export class FeeCommand extends IronfishCommand { this.log( 'The slow, average, and fast rates each come from a percentile in the distribution:', ) - this.log(`slow: ${slow}th`) - this.log(`average: ${average}th`) - this.log(`fast: ${fast}th`) + this.log( + ui.card({ + slow: `${slow}th`, + average: `${average}th`, + fast: `${fast}th`, + }), + ) this.log('') } } diff --git a/ironfish-cli/src/commands/logs.ts b/ironfish-cli/src/commands/logs.ts index 93fb0eb0d5..ab97c2f2f8 100644 --- a/ironfish-cli/src/commands/logs.ts +++ b/ironfish-cli/src/commands/logs.ts @@ -1,23 +1,20 @@ /* 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 { ConsoleReporterInstance, FullNode, IJSON } from '@ironfish/sdk' +import { ConsoleReporterInstance, IJSON } from '@ironfish/sdk' import { logType } from 'consola' import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' export default class LogsCommand extends IronfishCommand { - static description = 'Tail server logs' + static description = 'show node logs' static flags = { ...RemoteFlags, } - node: FullNode | null = null - async start(): Promise { await this.parse(LogsCommand) - await this.sdk.client.connect() const response = this.sdk.client.node.getLogStream() diff --git a/ironfish-cli/src/commands/mempool/status.ts b/ironfish-cli/src/commands/mempool/status.ts index 14bb320e1d..06067158c4 100644 --- a/ironfish-cli/src/commands/mempool/status.ts +++ b/ironfish-cli/src/commands/mempool/status.ts @@ -6,14 +6,16 @@ import { GetMempoolStatusResponse } from '@ironfish/sdk' import { Flags } from '@oclif/core' import blessed from 'blessed' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' import * as ui from '../../ui' export default class Status extends IronfishCommand { - static description = 'Show the status of the Mempool' + static description = "show the mempool's status" + static enableJsonFlag = true static flags = { ...RemoteFlags, + ...JsonFlags, follow: Flags.boolean({ char: 'f', default: false, @@ -21,14 +23,15 @@ export default class Status extends IronfishCommand { }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.connectRpc() const response = await client.mempool.getMempoolStatus() this.log(renderStatus(response.content)) - this.exit(0) + + return response.content } // Console log will create display issues with Blessed diff --git a/ironfish-cli/src/commands/mempool/transactions.ts b/ironfish-cli/src/commands/mempool/transactions.ts index a2d82b5829..70f3878cea 100644 --- a/ironfish-cli/src/commands/mempool/transactions.ts +++ b/ironfish-cli/src/commands/mempool/transactions.ts @@ -31,7 +31,7 @@ const parseMinMax = (input: string): MinMax | undefined => { } export class TransactionsCommand extends IronfishCommand { - static description = `List transactions in the mempool` + static description = `list mempool transactions` static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/migrations/index.ts b/ironfish-cli/src/commands/migrations/index.ts index 8a5b047451..6121938c60 100644 --- a/ironfish-cli/src/commands/migrations/index.ts +++ b/ironfish-cli/src/commands/migrations/index.ts @@ -1,15 +1,39 @@ /* 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 { NodeUtils } from '@ironfish/sdk' import { IronfishCommand } from '../../command' +import { JsonFlags } from '../../flags' +import * as ui from '../../ui' export class StatusCommand extends IronfishCommand { - static description = `List all the migration statuses` + static description = `list data migrations and their status` + static enableJsonFlag = true - async start(): Promise { + static flags = { + ...JsonFlags, + } + + async start(): Promise { await this.parse(StatusCommand) const node = await this.sdk.node() - await node.migrator.check() + + // Verify the DB is in a state to be opened by the migrator + await NodeUtils.waitForOpen(node) + await node.closeDB() + + const migrationsStatus = await node.migrator.status() + + const displayData: Record = {} + for (const { name, applied } of migrationsStatus.migrations) { + displayData[name] = applied ? 'APPLIED' : 'WAITING' + } + + this.log(ui.card(displayData)) + + this.log(`\nYou have ${migrationsStatus.unapplied} unapplied migrations.`) + + return migrationsStatus } } diff --git a/ironfish-cli/src/commands/migrations/revert.ts b/ironfish-cli/src/commands/migrations/revert.ts index ba4e1a17f6..39bf7b3780 100644 --- a/ironfish-cli/src/commands/migrations/revert.ts +++ b/ironfish-cli/src/commands/migrations/revert.ts @@ -4,7 +4,7 @@ import { IronfishCommand } from '../../command' export class RevertCommand extends IronfishCommand { - static description = `Revert the last run migration` + static description = `revert the last run migration` static hidden = true diff --git a/ironfish-cli/src/commands/migrations/start.ts b/ironfish-cli/src/commands/migrations/start.ts index 099fc38835..125d7715e3 100644 --- a/ironfish-cli/src/commands/migrations/start.ts +++ b/ironfish-cli/src/commands/migrations/start.ts @@ -5,7 +5,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' export class StartCommand extends IronfishCommand { - static description = `Run migrations` + static description = `run migrations` static flags = { dry: Flags.boolean({ diff --git a/ironfish-cli/src/commands/miners/pools/start.ts b/ironfish-cli/src/commands/miners/pools/start.ts index 53a391f5c0..bba4f85b79 100644 --- a/ironfish-cli/src/commands/miners/pools/start.ts +++ b/ironfish-cli/src/commands/miners/pools/start.ts @@ -19,7 +19,7 @@ import { RemoteFlags } from '../../../flags' import { getExplorer } from '../../../utils/explorer' export class StartPool extends IronfishCommand { - static description = `Start a mining pool that connects to a node` + static description = `start a mining pool` static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/miners/pools/status.ts b/ironfish-cli/src/commands/miners/pools/status.ts index 0d9070a183..61db17f241 100644 --- a/ironfish-cli/src/commands/miners/pools/status.ts +++ b/ironfish-cli/src/commands/miners/pools/status.ts @@ -16,12 +16,15 @@ import { Flags } from '@oclif/core' import blessed from 'blessed' import dns from 'dns' import { IronfishCommand } from '../../../command' +import { JsonFlags } from '../../../flags' import * as ui from '../../../ui' export class PoolStatus extends IronfishCommand { - static description = `Show the status of a mining pool` + static description = `show the mining pool's status` + static enableJsonFlag = true static flags = { + ...JsonFlags, address: Flags.string({ char: 'a', description: 'The public address for which to retrieve pool share data', @@ -41,7 +44,7 @@ export class PoolStatus extends IronfishCommand { }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(PoolStatus) if (flags.address && !isValidPublicAddress(flags.address)) { @@ -71,11 +74,17 @@ export class PoolStatus extends IronfishCommand { } if (!flags.follow) { + let poolStatus stratum.onConnected.on(() => stratum.getStatus(flags.address)) - stratum.onStatus.on((status) => this.log(this.renderStatus(status))) + stratum.onStatus.on((status) => { + this.log(this.renderStatus(status)) + poolStatus = status + }) stratum.start() await waitForEmit(stratum.onStatus) - this.exit(0) + stratum.stop() + + return poolStatus } this.logger.pauseLogs() diff --git a/ironfish-cli/src/commands/miners/start.ts b/ironfish-cli/src/commands/miners/start.ts index 29ad236df5..2e1dca14f2 100644 --- a/ironfish-cli/src/commands/miners/start.ts +++ b/ironfish-cli/src/commands/miners/start.ts @@ -19,7 +19,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class Miner extends IronfishCommand { - static description = `Start a miner and subscribe to new blocks for the node` + static description = `start a miner` updateInterval: SetIntervalToken | null = null diff --git a/ironfish-cli/src/commands/peers/add.ts b/ironfish-cli/src/commands/peers/add.ts index afc5b27693..f0f3422ca6 100644 --- a/ironfish-cli/src/commands/peers/add.ts +++ b/ironfish-cli/src/commands/peers/add.ts @@ -7,7 +7,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class AddCommand extends IronfishCommand { - static description = `Attempt to connect to a peer through websockets` + static description = `attempt to connect to a peer` static args = { address: UrlArg({ diff --git a/ironfish-cli/src/commands/peers/banned.ts b/ironfish-cli/src/commands/peers/banned.ts index 28540c6af1..9344a6efe9 100644 --- a/ironfish-cli/src/commands/peers/banned.ts +++ b/ironfish-cli/src/commands/peers/banned.ts @@ -11,7 +11,7 @@ import { table, TableColumns, TableFlags } from '../../ui' const { sort, ...tableFlags } = TableFlags export class BannedCommand extends IronfishCommand { - static description = `List all banned peers` + static description = `list banned peers` static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/peers/index.ts b/ironfish-cli/src/commands/peers/index.ts index 3a46771f56..f7b13ceffa 100644 --- a/ironfish-cli/src/commands/peers/index.ts +++ b/ironfish-cli/src/commands/peers/index.ts @@ -14,7 +14,7 @@ type GetPeerResponsePeer = GetPeersResponse['peers'][0] const STATE_COLUMN_HEADER = 'STATE' export class ListCommand extends IronfishCommand { - static description = `List all connected peers` + static description = 'list network peers' static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/peers/show.ts b/ironfish-cli/src/commands/peers/info.ts similarity index 78% rename from ironfish-cli/src/commands/peers/show.ts rename to ironfish-cli/src/commands/peers/info.ts index 9e72a8aeab..1a3e0d35ab 100644 --- a/ironfish-cli/src/commands/peers/show.ts +++ b/ironfish-cli/src/commands/peers/info.ts @@ -5,13 +5,15 @@ import { GetPeerMessagesResponse, GetPeerResponse, TimeUtils } from '@ironfish/s import { Args } from '@oclif/core' import colors from 'colors/safe' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' type GetPeerResponsePeer = NonNullable type GetPeerMessagesResponseMessages = GetPeerMessagesResponse['messages'][0] -export class ShowCommand extends IronfishCommand { - static description = `Display info about a peer` +export class PeerInfo extends IronfishCommand { + static description = `show peer information` + static enableJsonFlag = true + static hiddenAliases = ['peers:show'] static args = { identity: Args.string({ @@ -22,16 +24,17 @@ export class ShowCommand extends IronfishCommand { static flags = { ...RemoteFlags, + ...JsonFlags, } - async start(): Promise { - const { args } = await this.parse(ShowCommand) + async start(): Promise { + const { args } = await this.parse(PeerInfo) const { identity } = args - await this.sdk.client.connect() + const client = await this.connectRpc() const [peer, messages] = await Promise.all([ - this.sdk.client.peer.getPeer({ identity }), - this.sdk.client.peer.getPeerMessages({ identity }), + client.peer.getPeer({ identity }), + client.peer.getPeerMessages({ identity }), ]) if (peer.content.peer === null) { @@ -48,7 +51,10 @@ export class ShowCommand extends IronfishCommand { } } - this.exit(0) + return { + ...peer.content, + ...messages.content, + } } renderPeer(peer: GetPeerResponsePeer): string { diff --git a/ironfish-cli/src/commands/repl.ts b/ironfish-cli/src/commands/repl.ts index 7a7d96c2e3..ad7c7409fd 100644 --- a/ironfish-cli/src/commands/repl.ts +++ b/ironfish-cli/src/commands/repl.ts @@ -10,7 +10,7 @@ import path from 'path' import { IronfishCommand } from '../command' export default class Repl extends IronfishCommand { - static description = 'An interactive terminal to the node' + static description = 'start an interactive session' static flags = { opendb: Flags.boolean({ diff --git a/ironfish-cli/src/commands/reset.ts b/ironfish-cli/src/commands/reset.ts index ddd87f37a2..1c001bc794 100644 --- a/ironfish-cli/src/commands/reset.ts +++ b/ironfish-cli/src/commands/reset.ts @@ -8,7 +8,7 @@ import { IronfishCommand } from '../command' import { confirmOrQuit } from '../ui' export default class Reset extends IronfishCommand { - static description = 'Reset the node to its initial state' + static description = 'reset the node database' static flags = { networkId: Flags.integer({ diff --git a/ironfish-cli/src/commands/rpc/status.ts b/ironfish-cli/src/commands/rpc/status.ts index cd4e3ffe2a..e9cbd5b3ec 100644 --- a/ironfish-cli/src/commands/rpc/status.ts +++ b/ironfish-cli/src/commands/rpc/status.ts @@ -5,14 +5,16 @@ import { FileUtils, GetRpcStatusResponse, PromiseUtils } from '@ironfish/sdk' import { Flags } from '@oclif/core' import blessed from 'blessed' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' import * as ui from '../../ui' export default class Status extends IronfishCommand { - static description = 'Show the status of the RPC layer' + static description = "show RPC server's status" + static enableJsonFlag = true static flags = { ...RemoteFlags, + ...JsonFlags, follow: Flags.boolean({ char: 'f', default: false, @@ -20,14 +22,15 @@ export default class Status extends IronfishCommand { }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.connectRpc() const response = await client.rpc.getRpcStatus() this.log(renderStatus(response.content)) - this.exit(0) + + return response.content } // Console log will create display issues with Blessed diff --git a/ironfish-cli/src/commands/rpc/token.ts b/ironfish-cli/src/commands/rpc/token.ts index 5ca6dbf314..006e3fc2e6 100644 --- a/ironfish-cli/src/commands/rpc/token.ts +++ b/ironfish-cli/src/commands/rpc/token.ts @@ -3,18 +3,21 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' +import { JsonFlags } from '../../flags' export default class Token extends IronfishCommand { - static description = 'Get or set the RPC auth token' + static description = 'get or set the RPC auth token' + static enableJsonFlag = true static flags = { + ...JsonFlags, token: Flags.string({ required: false, description: 'Set the RPC auth token to ', }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(Token) const internal = this.sdk.internal @@ -33,5 +36,7 @@ export default class Token extends IronfishCommand { this.log('No RPC auth token found.') } } + + return { rpcAuthToken: token } } } diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 09231246f8..847951f645 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -32,7 +32,7 @@ export const ENABLE_TELEMETRY_CONFIG_KEY = 'enableTelemetry' const DEFAULT_ACCOUNT_NAME = 'default' export default class Start extends IronfishCommand { - static description = 'Start the node' + static description = 'start the node' static flags = { [RpcUseIpcFlagKey]: { ...RpcUseIpcFlag, allowNo: true } as typeof RpcUseIpcFlag, @@ -105,7 +105,7 @@ export default class Start extends IronfishCommand { }), wallet: Flags.boolean({ allowNo: true, - default: true, + default: undefined, description: `Enable the node's wallet to scan transactions and decrypt notes from the blockchain`, }), } @@ -246,6 +246,12 @@ export default class Start extends IronfishCommand { await this.firstRun(node) } + const encrypted = await node.wallet.accountsEncrypted() + if (encrypted) { + this.log('Your wallet is encrypted. Run ironfish wallet:unlock to access your accounts') + this.log() + } + await node.start() this.node = node diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index 14e9013457..b6dbf169cd 100644 --- a/ironfish-cli/src/commands/status.ts +++ b/ironfish-cli/src/commands/status.ts @@ -12,14 +12,16 @@ import { import { Flags } from '@oclif/core' import blessed from 'blessed' import { IronfishCommand } from '../command' -import { RemoteFlags } from '../flags' +import { JsonFlags, RemoteFlags } from '../flags' import * as ui from '../ui' export default class Status extends IronfishCommand { - static description = 'Show the status of the node' + static description = "show the node's status" + static enableJsonFlag = true static flags = { ...RemoteFlags, + ...JsonFlags, follow: Flags.boolean({ char: 'f', default: false, @@ -31,14 +33,15 @@ export default class Status extends IronfishCommand { }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.connectRpc() const response = await client.node.getStatus() this.log(renderStatus(response.content, flags.all)) - this.exit(0) + + return response.content } // Console log will create display issues with Blessed @@ -184,7 +187,9 @@ function renderStatus(content: GetNodeStatusResponse, debugOutput: boolean): str ).toFixed(1)}%)` let accountStatus - if (content.accounts.scanning === undefined) { + if (content.accounts.locked) { + accountStatus = 'LOCKED' + } else if (content.accounts.scanning === undefined) { accountStatus = `IDLE` } else { accountStatus = `SCANNING` @@ -219,7 +224,7 @@ function renderStatus(content: GetNodeStatusResponse, debugOutput: boolean): str Version: `${content.node.version} @ ${content.node.git}`, Node: nodeStatus, 'Node Name': content.node.nodeName, - 'Peed ID': content.peerNetwork.publicIdentity, + 'Peer ID': content.peerNetwork.publicIdentity, 'Block Graffiti': blockGraffiti, Network: network, Memory: memoryStatus, diff --git a/ironfish-cli/src/commands/stop.ts b/ironfish-cli/src/commands/stop.ts index dc696ed86b..af0e8ae4e3 100644 --- a/ironfish-cli/src/commands/stop.ts +++ b/ironfish-cli/src/commands/stop.ts @@ -7,7 +7,7 @@ import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' export default class StopCommand extends IronfishCommand { - static description = 'Stop the node from running' + static description = 'stop the node' static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/wallet/accounts.ts b/ironfish-cli/src/commands/wallet/accounts.ts deleted file mode 100644 index 9b14f58d28..0000000000 --- a/ironfish-cli/src/commands/wallet/accounts.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* 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 { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' - -export class AccountsCommand extends IronfishCommand { - static description = `List all the accounts on the node` - - static flags = { - ...RemoteFlags, - displayName: Flags.boolean({ - default: false, - description: `Display a hash of the account's read-only keys along with the account name`, - }), - } - - async start(): Promise { - const { flags } = await this.parse(AccountsCommand) - - const client = await this.connectRpc() - - const response = await client.wallet.getAccounts({ displayName: flags.displayName }) - - if (response.content.accounts.length === 0) { - this.log('you have no accounts') - } - - for (const name of response.content.accounts) { - this.log(name) - } - } -} diff --git a/ironfish-cli/src/commands/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index e555c689e5..349d5932ae 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -3,16 +3,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Args } from '@oclif/core' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' +import * as ui from '../../ui' export class AddressCommand extends IronfishCommand { - static description = `Display your account address + static description = `show the account's public address The address for an account is the accounts public key, see more here: https://ironfish.network/docs/whitepaper/5_account` - static flags = { - ...RemoteFlags, - } + static enableJsonFlag = true static args = { account: Args.string({ @@ -21,20 +20,28 @@ export class AddressCommand extends IronfishCommand { }), } - async start(): Promise { + static flags = { + ...RemoteFlags, + ...JsonFlags, + } + + async start(): Promise { const { args } = await this.parse(AddressCommand) - const { account } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const response = await client.wallet.getAccountPublicKey({ - account: account, + account: args.account, }) - if (!response) { - this.error(`An error occurred while fetching the public key.`) - } + this.log( + ui.card({ + Account: response.content.account, + Address: response.content.publicKey, + }), + ) - this.log(`Account: ${response.content.account}, public key: ${response.content.publicKey}`) + return response.content } } diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index b21fcfb779..32adc0de6a 100644 --- a/ironfish-cli/src/commands/wallet/assets.ts +++ b/ironfish-cli/src/commands/wallet/assets.ts @@ -9,11 +9,11 @@ import { PUBLIC_ADDRESS_LENGTH, } from '@ironfish/rust-nodejs' import { BufferUtils } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableFlags } from '../../ui' -import { renderAssetWithVerificationStatus } from '../../utils' +import { checkWalletUnlocked, table, TableFlags } from '../../ui' +import { renderAssetWithVerificationStatus, useAccount } from '../../utils' import { TableCols } from '../../utils/table' const MAX_ASSET_METADATA_COLUMN_WIDTH = ASSET_METADATA_LENGTH + 1 @@ -23,7 +23,7 @@ const MAX_ASSET_NAME_COLUMN_WIDTH = ASSET_NAME_LENGTH + 1 const MIN_ASSET_NAME_COLUMN_WIDTH = ASSET_NAME_LENGTH / 2 + 1 export class AssetsCommand extends IronfishCommand { - static description = `Display the wallet's assets` + static description = `list the account's assets` static flags = { ...RemoteFlags, @@ -34,19 +34,14 @@ export class AssetsCommand extends IronfishCommand { }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(AssetsCommand) - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const { flags } = await this.parse(AssetsCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) + const response = client.wallet.getAssets({ account, }) diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index 4e74cf5800..5a0062e311 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -2,18 +2,30 @@ * 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, GetBalanceResponse, isNativeIdentifier, RpcAsset } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import * as ui from '../../ui' -import { renderAssetWithVerificationStatus } from '../../utils' +import { renderAssetWithVerificationStatus, useAccount } from '../../utils' export class BalanceCommand extends IronfishCommand { - static description = - 'Display the account balance\n\ - What is the difference between available to spend balance, and balance?\n\ - Available to spend balance is your coins from transactions that have been mined on blocks on your main chain.\n\ - Balance is your coins from all of your transactions, even if they are on forks or not yet included as part of a mined block.' + static description = `show the account's balance for an asset + +What is the difference between available to spend balance, and balance?\n\ +Available to spend balance is your coins from transactions that have been mined on blocks on your main chain.\n\ +Balance is your coins from all of your transactions, even if they are on forks or not yet included as part of a mined block.` + + static examples = [ + { + description: 'show the balance for $IRON asset', + command: 'ironfish wallet:balance', + }, + { + description: 'show the balance for $IRON asset', + command: + 'ironfish wallet:balance --assetId 51f33a2f14f92735e562dc658a5639279ddca3d5079a6d1242b2a588a9cbf44c', + }, + ] static flags = { ...RemoteFlags, @@ -30,28 +42,20 @@ export class BalanceCommand extends IronfishCommand { description: 'Also show unconfirmed balance', }), confirmations: Flags.integer({ - required: false, description: 'Minimum number of blocks confirmations for a transaction', }), assetId: Flags.string({ - required: false, description: 'Asset identifier to check the balance for', }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get balance for. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(BalanceCommand) - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const { flags } = await this.parse(BalanceCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) const response = await client.wallet.getAccountBalance({ account, @@ -90,9 +94,7 @@ export class BalanceCommand extends IronfishCommand { this.log( ui.card({ Account: response.content.account, - 'Head Hash': response.content.blockHash || 'NULL', - 'Head Sequence': response.content.sequence || 'NULL', - Available: renderedAvailable, + Balance: renderedAvailable, Confirmed: renderedConfirmed, Unconfirmed: renderedUnconfirmed, Pending: renderedPending, @@ -104,7 +106,7 @@ export class BalanceCommand extends IronfishCommand { this.log( ui.card({ Account: response.content.account, - 'Available Balance': renderedAvailable, + Balance: renderedAvailable, }), ) } diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index ca96906f1f..74abc0971b 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -2,16 +2,16 @@ * 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 { BufferUtils, CurrencyUtils, GetBalancesResponse, RpcAsset } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableColumns, TableFlags } from '../../ui' -import { compareAssets, renderAssetWithVerificationStatus } from '../../utils' +import { checkWalletUnlocked, table, TableColumns, TableFlags } from '../../ui' +import { compareAssets, renderAssetWithVerificationStatus, useAccount } from '../../utils' type AssetBalancePairs = { asset: RpcAsset; balance: GetBalancesResponse['balances'][number] } export class BalancesCommand extends IronfishCommand { - static description = `Display the account's balances for all assets` + static description = `show the account's balance for all assets` static flags = { ...RemoteFlags, @@ -25,24 +25,17 @@ export class BalancesCommand extends IronfishCommand { description: `Also show unconfirmed balance, head hash, and head sequence`, }), confirmations: Flags.integer({ - required: false, description: 'Minimum number of blocks confirmations for a transaction', }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get balances for. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(BalancesCommand) + const { flags } = await this.parse(BalancesCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) - // TODO: remove account arg - const account = flags.account ? flags.account : args.account const response = await client.wallet.getAccountBalances({ account, confirmations: flags.confirmations, @@ -66,7 +59,7 @@ export class BalancesCommand extends IronfishCommand { let columns: TableColumns = { assetName: { - header: 'Asset Name', + header: 'Asset', get: ({ asset }) => renderAssetWithVerificationStatus( BufferUtils.toHuman(Buffer.from(asset.name, 'hex')), @@ -76,12 +69,8 @@ export class BalancesCommand extends IronfishCommand { }, ), }, - 'asset.id': { - header: 'Asset Id', - get: ({ asset }) => asset.id, - }, available: { - header: 'Available Balance', + header: 'Balance', get: ({ asset, balance }) => CurrencyUtils.render(balance.available, false, asset.id, asset.verification), }, @@ -90,32 +79,28 @@ export class BalancesCommand extends IronfishCommand { if (flags.all) { columns = { ...columns, - availableNotes: { - header: 'Available Notes', - get: ({ balance }) => balance.availableNoteCount, - }, confirmed: { - header: 'Confirmed Balance', + header: 'Confirmed', get: ({ asset, balance }) => CurrencyUtils.render(balance.confirmed, false, asset.id, asset.verification), }, unconfirmed: { - header: 'Unconfirmed Balance', + header: 'Unconfirmed', get: ({ asset, balance }) => CurrencyUtils.render(balance.unconfirmed, false, asset.id, asset.verification), }, pending: { - header: 'Pending Balance', + header: 'Pending', get: ({ asset, balance }) => CurrencyUtils.render(balance.pending, false, asset.id, asset.verification), }, - blockHash: { - header: 'Head Hash', - get: ({ balance }) => balance.blockHash || 'NULL', + availableNotes: { + header: 'Notes', + get: ({ balance }) => balance.availableNoteCount, }, - sequence: { - header: 'Head Sequence', - get: ({ balance }) => balance.sequence || 'NULL', + 'asset.id': { + header: 'Asset Id', + get: ({ asset }) => asset.id, }, } } diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 5827df6420..1ee14c9777 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -14,7 +14,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' -import { selectAsset } from '../../utils/asset' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -22,7 +22,9 @@ import { selectFee } from '../../utils/fees' import { watchTransaction } from '../../utils/transaction' export class Burn extends IronfishCommand { - static description = 'Burn tokens and decrease supply for a given asset' + static description = `create a transaction to burn tokens + +This will destroy tokens and decrease supply for a given asset.` static examples = [ '$ ironfish wallet:burn --assetId 618c098d8d008c9f78f6155947014901a019d9ec17160dc0f0d1bb1c764b29b4 --amount 1000', @@ -33,28 +35,24 @@ export class Burn extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account to burn from', + char: 'a', + description: 'Name of the account to burn from', }), fee: IronFlag({ - char: 'o', description: 'The fee amount in IRON', minimum: 1n, flagName: 'fee', }), feeRate: IronFlag({ - char: 'r', description: 'The fee rate amount in IRON/Kilobyte', minimum: 1n, flagName: 'fee rate', }), amount: ValueFlag({ - char: 'a', description: 'Amount of coins to burn in the major denomination', flagName: 'amount', }), assetId: Flags.string({ - char: 'i', description: 'Identifier for the asset', }), confirm: Flags.boolean({ @@ -62,10 +60,8 @@ export class Burn extends IronfishCommand { description: 'Confirm without asking', }), confirmations: Flags.integer({ - char: 'c', description: 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', - required: false, }), rawTransaction: Flags.boolean({ default: false, @@ -96,6 +92,7 @@ export class Burn extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(Burn) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!flags.offline) { const status = await client.wallet.getNodeStatus() @@ -107,24 +104,12 @@ export class Burn extends IronfishCommand { } } - let account = flags.account - if (!account) { - const response = await client.wallet.getDefaultAccount() - - if (!response.content.account) { - this.error( - `No account is currently active. - Use ironfish wallet:create to first create an account`, - ) - } - - account = response.content.account.name - } + const account = await useAccount(client, flags.account) let assetId = flags.assetId if (assetId == null) { - const asset = await selectAsset(client, account, { + const asset = await ui.assetPrompt(client, account, { action: 'burn', showNativeAsset: false, showNonCreatorAsset: true, diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index b613651778..879839afc2 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -17,8 +17,7 @@ import { Flags, ux } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../../flags' -import { confirmOrQuit, inputPrompt } from '../../../ui' -import { selectAsset } from '../../../utils' +import * as ui from '../../../ui' import { ChainportBridgeTransaction, ChainportNetwork, @@ -44,36 +43,30 @@ export class BridgeCommand extends IronfishCommand { description: 'Wait for the transaction to be confirmed on Ironfish', }), account: Flags.string({ - char: 'f', - description: 'The account to send the asset from', + char: 'a', + description: 'Name of the account to send the asset from', }), to: Flags.string({ - char: 't', description: 'The Ethereum public address of the recipient', }), amount: ValueFlag({ - char: 'a', description: 'The amount of the asset in the major denomination', flagName: 'amount', }), assetId: HexFlag({ - char: 'i', description: 'The identifier for the asset to use when bridging', }), fee: IronFlag({ - char: 'o', description: 'The fee amount in IRON', minimum: 1n, flagName: 'fee', }), feeRate: IronFlag({ - char: 'r', description: 'The fee rate amount in IRON/Kilobyte', minimum: 1n, flagName: 'fee rate', }), expiration: Flags.integer({ - char: 'e', description: 'The block sequence after which the transaction will be removed from the mempool. Set to 0 for no expiration.', }), @@ -87,6 +80,7 @@ export class BridgeCommand extends IronfishCommand { const { flags } = await this.parse(BridgeCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const networkId = (await client.chain.getNetworkInfo()).content.networkId @@ -118,7 +112,7 @@ export class BridgeCommand extends IronfishCommand { assetData, ) - await confirmOrQuit() + await ui.confirmOrQuit() const postTransaction = await client.wallet.postTransaction({ transaction: RawTransactionSerde.serialize(rawTransaction).toString('hex'), @@ -177,7 +171,7 @@ export class BridgeCommand extends IronfishCommand { } if (!to) { - to = await inputPrompt('Enter the public address of the recipient', true) + to = await ui.inputPrompt('Enter the public address of the recipient', true) } if (!isEthereumAddress(to)) { @@ -191,7 +185,7 @@ export class BridgeCommand extends IronfishCommand { const tokens = await fetchChainportVerifiedTokens(networkId) if (assetId == null) { - const asset = await selectAsset(client, from, { + const asset = await ui.assetPrompt(client, from, { action: 'send', showNativeAsset: true, showNonCreatorAsset: true, diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index fcb9758c6c..56238c933e 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -5,13 +5,13 @@ import { Args } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { inputPrompt } from '../../ui' +import { checkWalletUnlocked, inputPrompt } from '../../ui' export class CreateCommand extends IronfishCommand { - static description = `Create a new account for sending and receiving coins` + static description = `create a new account` static args = { - account: Args.string({ + name: Args.string({ required: false, description: 'Name of the account', }), @@ -23,14 +23,14 @@ export class CreateCommand extends IronfishCommand { async start(): Promise { const { args } = await this.parse(CreateCommand) - let name = args.account + const client = await this.connectRpc() + await checkWalletUnlocked(client) + let name = args.name if (!name) { name = await inputPrompt('Enter the name of the account', true) } - const client = await this.connectRpc() - this.log(`Creating account ${name}`) const result = await client.wallet.createAccount({ name }) @@ -43,5 +43,7 @@ export class CreateCommand extends IronfishCommand { } else { this.log(`Run "ironfish wallet:use ${name}" to set the account as default`) } + + this.exit(0) } } diff --git a/ironfish-cli/src/commands/wallet/decrypt.ts b/ironfish-cli/src/commands/wallet/decrypt.ts new file mode 100644 index 0000000000..177fc07ef6 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/decrypt.ts @@ -0,0 +1,55 @@ +/* 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 { RpcRequestError } from '@ironfish/sdk' +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../command' +import { RemoteFlags } from '../../flags' +import { inputPrompt } from '../../ui' + +export class DecryptCommand extends IronfishCommand { + static hidden = true + + static description = 'decrypt accounts in the wallet' + + static flags = { + ...RemoteFlags, + passphrase: Flags.string({ + description: 'Passphrase to decrypt the wallet with', + }), + } + + async start(): Promise { + const { flags } = await this.parse(DecryptCommand) + + const client = await this.connectRpc() + + const response = await client.wallet.getAccountsStatus() + if (!response.content.encrypted) { + this.log('Wallet is already decrypted') + this.exit(1) + } + + let passphrase = flags.passphrase + if (!passphrase) { + passphrase = await inputPrompt('Enter your passphrase to decrypt the wallet', true, { + password: true, + }) + } + + try { + await client.wallet.decrypt({ + passphrase, + }) + } catch (e) { + if (e instanceof RpcRequestError) { + this.log('Wallet decryption failed') + this.exit(1) + } + + throw e + } + + this.log('Decrypted the wallet') + } +} diff --git a/ironfish-cli/src/commands/wallet/delete.ts b/ironfish-cli/src/commands/wallet/delete.ts index 7aa7569a54..1dd2083347 100644 --- a/ironfish-cli/src/commands/wallet/delete.ts +++ b/ironfish-cli/src/commands/wallet/delete.ts @@ -5,10 +5,10 @@ import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { inputPrompt } from '../../ui' +import * as ui from '../../ui' export class DeleteCommand extends IronfishCommand { - static description = `Permanently delete an account` + static description = `delete an account` static args = { account: Args.string({ @@ -34,18 +34,18 @@ export class DeleteCommand extends IronfishCommand { const { account } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) ux.action.start(`Deleting account '${account}'`) const response = await client.wallet.removeAccount({ account, confirm, wait }) ux.action.stop() if (response.content.needsConfirm) { - const value = await inputPrompt(`Are you sure? Type ${account} to confirm`) - - if (value !== account) { - this.log(`Aborting: ${value} did not match ${account}`) - this.exit(1) - } + await ui.confirmInputOrQuit( + account, + `Are you sure you want to delete "${account}"?\nType ${account} to confirm`, + flags.confirm, + ) ux.action.start(`Deleting account '${account}'`) await client.wallet.removeAccount({ account, confirm: true, wait }) diff --git a/ironfish-cli/src/commands/wallet/encrypt.ts b/ironfish-cli/src/commands/wallet/encrypt.ts new file mode 100644 index 0000000000..2573ceb8c1 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/encrypt.ts @@ -0,0 +1,69 @@ +/* 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 { RpcRequestError } from '@ironfish/sdk' +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../command' +import { RemoteFlags } from '../../flags' +import { inputPrompt } from '../../ui' + +export class EncryptCommand extends IronfishCommand { + static hidden = true + + static description = 'encrypt accounts in the wallet' + + static flags = { + ...RemoteFlags, + passphrase: Flags.string({ + description: 'Passphrase to encrypt the wallet with', + }), + confirm: Flags.boolean({ + description: 'Suppress the passphrase confirmation prompt', + }), + } + + async start(): Promise { + const { flags } = await this.parse(EncryptCommand) + + const client = await this.connectRpc() + + const response = await client.wallet.getAccountsStatus() + if (response.content.encrypted) { + this.log('Wallet is already encrypted') + this.exit(1) + } + + let passphrase = flags.passphrase + if (!passphrase) { + passphrase = await inputPrompt('Enter a passphrase to encrypt the wallet', true, { + password: true, + }) + } + + if (!flags.confirm) { + const confirmedPassphrase = await inputPrompt('Confirm your passphrase', true, { + password: true, + }) + + if (confirmedPassphrase !== passphrase) { + this.log('Passphrases do not match') + this.exit(1) + } + } + + try { + await client.wallet.encrypt({ + passphrase, + }) + } catch (e) { + if (e instanceof RpcRequestError) { + this.log('Wallet encryption failed') + this.exit(1) + } + + throw e + } + + this.log('Encrypted the wallet') + } +} diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 835e9cbacf..5a891d9c3a 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -2,20 +2,25 @@ * 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 { AccountFormat, ErrorUtils, LanguageUtils } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey, EnumLanguageKeyFlag, RemoteFlags } from '../../flags' -import { confirmOrQuit } from '../../ui' +import { EnumLanguageKeyFlag, JsonFlags, RemoteFlags } from '../../flags' +import { checkWalletUnlocked, confirmOrQuit } from '../../ui' +import { useAccount } from '../../utils' export class ExportCommand extends IronfishCommand { - static description = `Export an account` + static description = `export an account` static enableJsonFlag = true static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, + account: Flags.string({ + char: 'a', + description: 'Name of the account to export', + }), local: Flags.boolean({ default: false, description: 'Export an account without an online node', @@ -26,12 +31,10 @@ export class ExportCommand extends IronfishCommand { }), language: EnumLanguageKeyFlag({ description: 'Language to use for mnemonic export', - required: false, choices: LanguageUtils.LANGUAGE_KEYS, }), path: Flags.string({ description: 'The path to export the account to', - required: false, }), viewonly: Flags.boolean({ default: false, @@ -39,17 +42,9 @@ export class ExportCommand extends IronfishCommand { }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to export', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(ExportCommand) + const { flags } = await this.parse(ExportCommand) const { local, path: exportPath, viewonly: viewOnly } = flags - const { account } = args if (flags.language) { flags.mnemonic = true @@ -62,6 +57,10 @@ export class ExportCommand extends IronfishCommand { : AccountFormat.Base64Json const client = await this.connectRpc(local) + await checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) + const response = await client.wallet.exportAccount({ account, viewOnly, diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 535e5eaa11..4ff92863f9 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -10,12 +10,19 @@ import { import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { inputPrompt } from '../../ui' -import { importFile, importPipe, longPrompt } from '../../utils/input' +import { checkWalletUnlocked, inputPrompt } from '../../ui' +import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { Ledger } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { - static description = `Import an account` + static description = `import an account` + + static args = { + blob: Args.string({ + required: false, + description: 'The copy-pasted output of wallet:export; or, a raw spending key', + }), + } static flags = { ...RemoteFlags, @@ -25,33 +32,27 @@ export class ImportCommand extends IronfishCommand { description: 'Rescan the blockchain once the account is imported', }), path: Flags.string({ - description: 'the path to the file containing the account to import', + description: 'The path to the file containing the account to import', }), name: Flags.string({ - description: 'the name to use for the account', + description: 'Name to use for the account', }), createdAt: Flags.integer({ description: 'Block sequence to begin scanning from for the imported account', }), ledger: Flags.boolean({ - description: 'import a view-only account from a ledger device', + description: 'Import a view-only account from a ledger device', default: false, exclusive: ['path'], }), } - static args = { - blob: Args.string({ - required: false, - description: 'The copy-pasted output of wallet:export; or, a raw spending key', - }), - } - async start(): Promise { const { flags, args } = await this.parse(ImportCommand) const { blob } = args const client = await this.connectRpc() + await checkWalletUnlocked(client) let account: string @@ -61,7 +62,7 @@ export class ImportCommand extends IronfishCommand { ((flags.path && flags.path.length !== 0) || flags.ledger) ) { this.error( - `Your command includes an unexpected argument. Please pass only 1 of the following: + `Your command includes an unexpected argument. Please pass only 1 of the following: 1. the output of wallet:export OR 2. --path to import an account from a file OR 3. --ledger to import an account from a ledger device`, @@ -115,7 +116,8 @@ export class ImportCommand extends IronfishCommand { if ( e instanceof RpcRequestError && (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() || - e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString()) + e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString() || + e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString()) ) { const message = 'Enter a name for the account' @@ -124,6 +126,11 @@ export class ImportCommand extends IronfishCommand { this.log(e.codeMessage) } + if (e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString()) { + this.log() + this.log(e.codeMessage) + } + const name = await inputPrompt(message, true) if (name === flags.name) { this.error(`Entered the same name: '${name}'`) diff --git a/ironfish-cli/src/commands/wallet/index.ts b/ironfish-cli/src/commands/wallet/index.ts new file mode 100644 index 0000000000..92ad0dc869 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/index.ts @@ -0,0 +1,82 @@ +/* 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 chalk from 'chalk' +import { IronfishCommand } from '../../command' +import { JsonFlags, RemoteFlags } from '../../flags' +import * as ui from '../../ui' + +export class AccountsCommand extends IronfishCommand { + static description = `list accounts in the wallet` + static enableJsonFlag = true + + static hiddenAliases = ['wallet:accounts'] + + static flags = { + ...RemoteFlags, + ...JsonFlags, + ...ui.TableFlags, + } + + async start(): Promise { + const { flags } = await this.parse(AccountsCommand) + + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const response = await client.wallet.getAccountsStatus() + + if (response.content.locked) { + this.log('Your wallet is locked. Unlock the wallet to access your accounts') + this.exit(0) + } + + if (response.content.accounts.length === 0) { + this.log('you have no accounts') + return [] + } + + ui.table( + response.content.accounts, + { + name: { + get: (row) => row.name, + header: 'Account', + minWidth: 11, + }, + viewOnly: { + get: (row) => (row.viewOnly ? chalk.green('โœ“') : ''), + header: 'View Only', + extended: true, + }, + headInChain: { + get: (row) => (row.head?.inChain ? chalk.green('โœ“') : ''), + header: 'In Chain', + extended: true, + }, + scanningEnabled: { + get: (row) => (row.scanningEnabled ? chalk.green('โœ“') : ''), + header: 'Scanning', + extended: true, + }, + sequence: { + get: (row) => row.head?.sequence ?? '', + header: 'Sequence', + extended: true, + }, + headHash: { + get: (row) => row.head?.hash ?? '', + header: 'Head', + extended: true, + }, + }, + { + ...flags, + printLine: this.log.bind(this), + 'no-header': flags['no-header'] ?? !flags.extended, + }, + ) + + return response.content.accounts + } +} diff --git a/ironfish-cli/src/commands/wallet/lock.ts b/ironfish-cli/src/commands/wallet/lock.ts new file mode 100644 index 0000000000..3fc5ce43a1 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/lock.ts @@ -0,0 +1,40 @@ +/* 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 { RpcRequestError } from '@ironfish/sdk' +import { IronfishCommand } from '../../command' +import { RemoteFlags } from '../../flags' + +export class LockCommand extends IronfishCommand { + static hidden = true + + static description = 'lock accounts in the wallet' + + static flags = { + ...RemoteFlags, + } + + async start(): Promise { + const client = await this.connectRpc() + + const response = await client.wallet.getAccountsStatus() + if (!response.content.encrypted) { + this.log('Wallet is decrypted') + this.exit(1) + } + + try { + await client.wallet.lock() + } catch (e) { + if (e instanceof RpcRequestError) { + this.log('Wallet lock failed') + this.exit(1) + } + + throw e + } + + this.log('Locked the wallet') + this.exit(0) + } +} diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 0557493128..9981524248 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -18,7 +18,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' -import { selectAsset } from '../../utils/asset' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -26,7 +26,9 @@ import { selectFee } from '../../utils/fees' import { watchTransaction } from '../../utils/transaction' export class Mint extends IronfishCommand { - static description = 'Mint tokens and increase supply for a given asset' + static description = `create a transaction to mint tokens + +This will create tokens and increase supply for a given asset.` static examples = [ '$ ironfish wallet:mint --metadata "see more here" --name mycoin --amount 1000', @@ -39,50 +41,39 @@ export class Mint extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account to mint from', + char: 'a', + description: 'Name of the account to mint from', }), fee: IronFlag({ - char: 'o', description: 'The fee amount in IRON', minimum: 1n, flagName: 'fee', }), feeRate: IronFlag({ - char: 'r', description: 'The fee rate amount in IRON/Kilobyte', minimum: 1n, flagName: 'fee rate', }), amount: ValueFlag({ - char: 'a', description: 'Amount of coins to mint in the major denomination', flagName: 'amount', }), assetId: Flags.string({ - char: 'i', description: 'Identifier for the asset', - required: false, }), metadata: Flags.string({ - char: 'm', description: 'Metadata for the asset', - required: false, }), name: Flags.string({ - char: 'n', description: 'Name for the asset', - required: false, }), confirm: Flags.boolean({ default: false, description: 'Confirm without asking', }), confirmations: Flags.integer({ - char: 'c', description: 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', - required: false, }), rawTransaction: Flags.boolean({ default: false, @@ -104,7 +95,6 @@ export class Mint extends IronfishCommand { }), transferOwnershipTo: Flags.string({ description: 'The public address of the account to transfer ownership of this asset to.', - required: false, }), unsignedTransaction: Flags.boolean({ default: false, @@ -117,6 +107,7 @@ export class Mint extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(Mint) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!flags.offline) { const status = await client.wallet.getNodeStatus() @@ -128,19 +119,7 @@ export class Mint extends IronfishCommand { } } - let account = flags.account - if (!account) { - const response = await client.wallet.getDefaultAccount() - - if (!response.content.account) { - this.error( - `No account is currently active. - Use ironfish wallet:create to first create an account`, - ) - } - - account = response.content.account.name - } + const account = await useAccount(client, flags.account) const publicKeyResponse = await client.wallet.getAccountPublicKey({ account }) const accountPublicKey = publicKeyResponse.content.publicKey @@ -168,7 +147,7 @@ export class Mint extends IronfishCommand { const newAsset = new Asset(accountPublicKey, name, metadata) assetId = newAsset.id().toString('hex') } else if (!assetId) { - const asset = await selectAsset(client, account, { + const asset = await ui.assetPrompt(client, account, { action: 'mint', showNativeAsset: false, showNonCreatorAsset: false, diff --git a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts index eca3361fc9..1c7cba014f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -4,6 +4,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import * as ui from '../../../../ui' export class MultisigAccountParticipants extends IronfishCommand { static description = `List all participant identities in the group for a multisig account` @@ -11,8 +12,8 @@ export class MultisigAccountParticipants extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account to list group identities for', + char: 'a', + description: 'Name of the account to list group identities for', }), } @@ -20,6 +21,7 @@ export class MultisigAccountParticipants extends IronfishCommand { const { flags } = await this.parse(MultisigAccountParticipants) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const response = await client.wallet.multisig.getAccountIdentities({ account: flags.account, diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index f09021af7c..5ed54ff9e2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -5,7 +5,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { longPrompt } from '../../../../utils/input' +import * as ui from '../../../../ui' import { MultisigTransactionJson } from '../../../../utils/multisig' export class CreateSigningPackage extends IronfishCommand { @@ -14,9 +14,8 @@ export class CreateSigningPackage extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account to use when creating the signing package', - required: false, + char: 'a', + description: 'Name of the account to use when creating the signing package', }), unsignedTransaction: Flags.string({ char: 'u', @@ -39,24 +38,25 @@ export class CreateSigningPackage extends IronfishCommand { const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) const options = MultisigTransactionJson.resolveFlags(flags, loaded) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let unsignedTransaction = options.unsignedTransaction if (!unsignedTransaction) { - unsignedTransaction = await longPrompt('Enter the unsigned transaction', { + unsignedTransaction = await ui.longPrompt('Enter the unsigned transaction', { required: true, }) } let commitments = options.commitment if (!commitments) { - const input = await longPrompt('Enter the signing commitments, separated by commas', { + const input = await ui.longPrompt('Enter the signing commitments, separated by commas', { required: true, }) commitments = input.split(',') } commitments = commitments.map((s) => s.trim()) - const client = await this.connectRpc() - const signingPackageResponse = await client.wallet.multisig.createSigningPackage({ account: flags.account, unsignedTransaction, diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index fff272cd81..8f3a3614c1 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -5,8 +5,7 @@ import { UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { confirmOrQuit } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' +import * as ui from '../../../../ui' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -16,10 +15,9 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: - 'The account to use for generating the commitment, must be a multisig participant account', - required: false, + 'Name of the account to use for generating the commitment, must be a multisig participant account', }), unsignedTransaction: Flags.string({ char: 'u', @@ -46,9 +44,12 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) const options = MultisigTransactionJson.resolveFlags(flags, loaded) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let identities = options.identity if (!identities || identities.length < 2) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter the identities of all participants who will sign the transaction, separated by commas', { required: true, @@ -64,12 +65,11 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { let unsignedTransactionInput = options.unsignedTransaction if (!unsignedTransactionInput) { - unsignedTransactionInput = await longPrompt('Enter the unsigned transaction', { + unsignedTransactionInput = await ui.longPrompt('Enter the unsigned transaction', { required: true, }) } - const client = await this.connectRpc() const unsignedTransaction = new UnsignedTransaction( Buffer.from(unsignedTransactionInput, 'hex'), ) @@ -81,7 +81,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { this.logger, ) - await confirmOrQuit('Confirm signing commitment creation', flags.confirm) + await ui.confirmOrQuit('Confirm signing commitment creation', flags.confirm) const response = await client.wallet.multisig.createSigningCommitment({ account: flags.account, diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index 1a41a274ca..095ebc0ed7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts @@ -6,8 +6,7 @@ import { AccountImport } from '@ironfish/sdk/src/wallet/exporter' import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { inputPrompt } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' +import * as ui from '../../../../ui' export class MultisigCreateDealer extends IronfishCommand { static description = `Create a set of multisig accounts from participant identities` @@ -37,9 +36,12 @@ export class MultisigCreateDealer extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(MultisigCreateDealer) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let identities = flags.identity if (!identities || identities.length < 2) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter the identities of all participants, separated by commas', { required: true, @@ -55,15 +57,13 @@ export class MultisigCreateDealer extends IronfishCommand { let minSigners = flags.minSigners if (!minSigners) { - const input = await inputPrompt('Enter the number of minimum signers', true) + const input = await ui.inputPrompt('Enter the number of minimum signers', true) minSigners = parseInt(input) if (isNaN(minSigners) || minSigners < 2) { this.error('Minimum number of signers must be at least 2') } } - const client = await this.connectRpc() - const name = await this.getCoordinatorName(client, flags.name?.trim()) const response = await client.wallet.multisig.createTrustedDealerKeyPackage({ @@ -130,7 +130,7 @@ export class MultisigCreateDealer extends IronfishCommand { let name = inputName do { - name = name ?? (await inputPrompt('Enter a name for the coordinator', true)) + name = name ?? (await ui.inputPrompt('Enter a name for the coordinator', true)) if (accountNames.has(name)) { this.log(`Account with name ${name} already exists`) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index d03f4a06f8..b26085cf0c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -4,9 +4,8 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { inputPrompt } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' -import { selectSecret } from '../../../../utils/multisig' +import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -28,21 +27,27 @@ export class DkgRound1Command extends IronfishCommand { char: 'm', description: 'Minimum number of signers to meet signing threshold', }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + hidden: true, + }), } async start(): Promise { const { flags } = await this.parse(DkgRound1Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { - participantName = await selectSecret(client) + participantName = await ui.multisigSecretPrompt(client) } let identities = flags.identity if (!identities || identities.length < 2) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter the identities of all participants, separated by commas', { required: true, @@ -58,13 +63,18 @@ export class DkgRound1Command extends IronfishCommand { let minSigners = flags.minSigners if (!minSigners) { - const input = await inputPrompt('Enter the number of minimum signers', true) + const input = await ui.inputPrompt('Enter the number of minimum signers', true) minSigners = parseInt(input) if (isNaN(minSigners) || minSigners < 2) { this.error('Minimum number of signers must be at least 2') } } + if (flags.ledger) { + await this.performRound1WithLedger() + return + } + const response = await client.wallet.multisig.dkg.round1({ participantName, participants: identities.map((identity) => ({ identity })), @@ -82,4 +92,17 @@ export class DkgRound1Command extends IronfishCommand { this.log('Next step:') this.log('Send the round 1 public package to each participant') } + + async performRound1WithLedger(): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index ebc0f409c6..d2b5b4027b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -4,9 +4,8 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { inputPrompt } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' -import { selectSecret } from '../../../../utils/multisig' +import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -28,21 +27,27 @@ export class DkgRound2Command extends IronfishCommand { 'The public packages that each participant generated during DKG round 1 (may be specified multiple times for multiple participants). Must include your own round 1 public package', multiple: true, }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + hidden: true, + }), } async start(): Promise { const { flags } = await this.parse(DkgRound2Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { - participantName = await selectSecret(client) + participantName = await ui.multisigSecretPrompt(client) } let round1SecretPackage = flags.round1SecretPackage if (!round1SecretPackage) { - round1SecretPackage = await inputPrompt( + round1SecretPackage = await ui.inputPrompt( `Enter the round 1 secret package for participant ${participantName}`, true, ) @@ -50,7 +55,7 @@ export class DkgRound2Command extends IronfishCommand { let round1PublicPackages = flags.round1PublicPackages if (!round1PublicPackages || round1PublicPackages.length < 2) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter round 1 public packages, separated by commas, one for each participant', { required: true }, ) @@ -64,6 +69,11 @@ export class DkgRound2Command extends IronfishCommand { } round1PublicPackages = round1PublicPackages.map((i) => i.trim()) + if (flags.ledger) { + await this.performRound2WithLedger() + return + } + const response = await client.wallet.multisig.dkg.round2({ participantName, round1SecretPackage, @@ -82,4 +92,17 @@ export class DkgRound2Command extends IronfishCommand { this.log('Next step:') this.log('Send the round 2 public package to each participant') } + + async performRound2WithLedger(): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 7cb163bcd3..12034755fd 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -4,9 +4,8 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { inputPrompt } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' -import { selectSecret } from '../../../../utils/multisig' +import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -38,21 +37,27 @@ export class DkgRound3Command extends IronfishCommand { 'The public package that a participant generated during DKG round 2 (may be specified multiple times for multiple participants). Your own round 2 public package is optional; if included, it will be ignored', multiple: true, }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + hidden: true, + }), } async start(): Promise { const { flags } = await this.parse(DkgRound3Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { - participantName = await selectSecret(client) + participantName = await ui.multisigSecretPrompt(client) } let round2SecretPackage = flags.round2SecretPackage if (!round2SecretPackage) { - round2SecretPackage = await inputPrompt( + round2SecretPackage = await ui.inputPrompt( `Enter the round 2 encrypted secret package for participant ${participantName}`, true, ) @@ -60,7 +65,7 @@ export class DkgRound3Command extends IronfishCommand { let round1PublicPackages = flags.round1PublicPackages if (!round1PublicPackages || round1PublicPackages.length < 2) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter round 1 public packages, separated by commas, one for each participant', { required: true, @@ -78,7 +83,7 @@ export class DkgRound3Command extends IronfishCommand { let round2PublicPackages = flags.round2PublicPackages if (!round2PublicPackages) { - const input = await longPrompt( + const input = await ui.longPrompt( 'Enter round 2 public packages, separated by commas, one for each participant', { required: true, @@ -101,6 +106,11 @@ export class DkgRound3Command extends IronfishCommand { } round2PublicPackages = round2PublicPackages.map((i) => i.trim()) + if (flags.ledger) { + await this.performRound3WithLedger() + return + } + const response = await client.wallet.multisig.dkg.round3({ participantName, accountName: flags.accountName, @@ -114,4 +124,17 @@ export class DkgRound3Command extends IronfishCommand { `Account ${response.content.name} imported with public address: ${response.content.publicAddress}`, ) } + + async performRound3WithLedger(): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + } } diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index 5357cfcfc9..1621686631 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -5,7 +5,7 @@ import { RPC_ERROR_CODES, RpcRequestError } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { inputPrompt } from '../../../../ui' +import * as ui from '../../../../ui' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` @@ -20,12 +20,15 @@ export class MultisigIdentityCreate extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(MultisigIdentityCreate) + + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let name = flags.name if (!name) { - name = await inputPrompt('Enter a name for the identity', true) + name = await ui.inputPrompt('Enter a name for the identity', true) } - const client = await this.connectRpc() let response while (!response) { try { @@ -37,7 +40,7 @@ export class MultisigIdentityCreate extends IronfishCommand { ) { this.log() this.log(e.codeMessage) - name = await inputPrompt('Enter a new name for the identity', true) + name = await ui.inputPrompt('Enter a new name for the identity', true) } else { throw e } diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/index.ts b/ironfish-cli/src/commands/wallet/multisig/participant/index.ts index 58241fcf41..c74f31ebfb 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/index.ts @@ -5,6 +5,7 @@ import { Flags } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' +import * as ui from '../../../../ui' export class MultisigIdentity extends IronfishCommand { static description = `Retrieve a multisig participant identity from a name` @@ -21,6 +22,7 @@ export class MultisigIdentity extends IronfishCommand { const { flags } = await this.parse(MultisigIdentity) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (flags.name) { const response = await client.wallet.multisig.getIdentity({ name: flags.name }) diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts index 806e499a28..bbaf0cd33e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { table } from '../../../../ui' +import * as ui from '../../../../ui' export class MultisigParticipants extends IronfishCommand { static description = 'List out all the participant names and identities' @@ -14,12 +14,19 @@ export class MultisigParticipants extends IronfishCommand { async start(): Promise { const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + const response = await client.wallet.multisig.getIdentities() - const participants = [] - for (const { name, identity } of response.content.identities) { + const participants: { + name: string + value: string + hasSecret: boolean + }[] = [] + for (const { name, identity, hasSecret } of response.content.identities) { participants.push({ name, + hasSecret: hasSecret, value: identity, }) } @@ -27,13 +34,17 @@ export class MultisigParticipants extends IronfishCommand { // sort identities by name participants.sort((a, b) => a.name.localeCompare(b.name)) - table( + ui.table( participants, { name: { header: 'Participant Name', get: (p) => p.name, }, + hasSecret: { + header: 'Has Secret', + get: (p) => (p.hasSecret ? 'Yes' : 'No'), + }, identity: { header: 'Identity', get: (p) => p.value, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 7d22da5913..463f63267b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -5,7 +5,7 @@ import { CurrencyUtils, Transaction } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { longPrompt } from '../../../../utils/input' +import * as ui from '../../../../ui' import { MultisigTransactionJson } from '../../../../utils/multisig' import { watchTransaction } from '../../../../utils/transaction' @@ -15,9 +15,8 @@ export class MultisigSign extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'Account to use when aggregating signature shares', - required: false, + char: 'a', + description: 'Name of the account to use when aggregating signature shares', }), signingPackage: Flags.string({ char: 'p', @@ -48,14 +47,17 @@ export class MultisigSign extends IronfishCommand { const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) const options = MultisigTransactionJson.resolveFlags(flags, loaded) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let signingPackage = options.signingPackage if (!signingPackage) { - signingPackage = await longPrompt('Enter the signing package', { required: true }) + signingPackage = await ui.longPrompt('Enter the signing package', { required: true }) } let signatureShares = options.signatureShare if (!signatureShares) { - const input = await longPrompt('Enter the signature shares, separated by commas', { + const input = await ui.longPrompt('Enter the signature shares, separated by commas', { required: true, }) signatureShares = input.split(',') @@ -64,8 +66,6 @@ export class MultisigSign extends IronfishCommand { ux.action.start('Signing the multisig transaction') - const client = await this.connectRpc() - const response = await client.wallet.multisig.aggregateSignatureShares({ account: flags.account, broadcast: flags.broadcast, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 39812cec25..a156964794 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -6,8 +6,7 @@ import { UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' -import { confirmOrQuit } from '../../../../ui' -import { longPrompt } from '../../../../utils/input' +import * as ui from '../../../../ui' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -17,14 +16,12 @@ export class CreateSignatureShareCommand extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account from which the signature share will be created', - required: false, + char: 'a', + description: 'Name of the account from which the signature share will be created', }), signingPackage: Flags.string({ char: 's', description: 'The signing package for which the signature share will be created', - required: false, }), confirm: Flags.boolean({ default: false, @@ -41,13 +38,14 @@ export class CreateSignatureShareCommand extends IronfishCommand { const loaded = await MultisigTransactionJson.load(this.sdk.fileSystem, flags.path) const options = MultisigTransactionJson.resolveFlags(flags, loaded) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + let signingPackageString = options.signingPackage if (!signingPackageString) { - signingPackageString = await longPrompt('Enter the signing package') + signingPackageString = await ui.longPrompt('Enter the signing package') } - const client = await this.connectRpc() - const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) const unsignedTransaction = new UnsignedTransaction( signingPackage.unsignedTransaction().serialize(), @@ -63,7 +61,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { ) if (!flags.confirm) { - await confirmOrQuit('Confirm new signature share creation') + await ui.confirmOrQuit('Confirm new signature share creation') } const signatureShareResponse = await client.wallet.multisig.createSignatureShare({ diff --git a/ironfish-cli/src/commands/wallet/notes/combine.ts b/ironfish-cli/src/commands/wallet/notes/combine.ts index c48a31dd61..e11fb33e32 100644 --- a/ironfish-cli/src/commands/wallet/notes/combine.ts +++ b/ironfish-cli/src/commands/wallet/notes/combine.ts @@ -16,8 +16,8 @@ import { Flags } from '@oclif/core' import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { HexFlag, IronFlag, RemoteFlags } from '../../../flags' -import { confirmOrQuit, inputPrompt, table } from '../../../ui' -import { getAssetsByIDs, selectAsset } from '../../../utils' +import * as ui from '../../../ui' +import { getAssetsByIDs } from '../../../utils' import { getExplorer } from '../../../utils/explorer' import { selectFee } from '../../../utils/fees' import { fetchNotes } from '../../../utils/note' @@ -70,7 +70,7 @@ export class CombineNotesCommand extends IronfishCommand { }), account: Flags.string({ char: 'f', - description: 'The account to send money from', + description: 'Name of the account to send money from', }), benchmark: Flags.boolean({ hidden: true, @@ -132,7 +132,7 @@ export class CombineNotesCommand extends IronfishCommand { // eslint-disable-next-line no-constant-condition while (true) { - const result = await inputPrompt('Enter the number of notes', true) + const result = await ui.inputPrompt('Enter the number of notes', true) const notesToCombine = parseInt(result) @@ -219,6 +219,7 @@ export class CombineNotesCommand extends IronfishCommand { const { flags } = await this.parse(CombineNotesCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let to = flags.to let from = flags.account @@ -246,7 +247,7 @@ export class CombineNotesCommand extends IronfishCommand { } if (!assetId) { - const asset = await selectAsset(client, from, { + const asset = await ui.assetPrompt(client, from, { action: 'combine notes for', showNativeAsset: true, showNonCreatorAsset: true, @@ -286,7 +287,7 @@ export class CombineNotesCommand extends IronfishCommand { const totalAmount = notes.reduce((acc, note) => acc + BigInt(note.value), 0n) - const memo = flags.memo ?? (await inputPrompt('Enter the memo (or leave blank)')) + const memo = flags.memo ?? (await ui.inputPrompt('Enter the memo (or leave blank)')) const expiration = await this.calculateExpiration(client, spendPostTime, numberOfNotes) @@ -356,7 +357,7 @@ export class CombineNotesCommand extends IronfishCommand { })}`, ) - await confirmOrQuit('', flags.confirm) + await ui.confirmOrQuit('', flags.confirm) transactionTimer.start() @@ -449,7 +450,7 @@ export class CombineNotesCommand extends IronfishCommand { if (resultingNotes) { this.log('') - table( + ui.table( resultingNotes, { hash: { diff --git a/ironfish-cli/src/commands/wallet/notes/index.ts b/ironfish-cli/src/commands/wallet/notes/index.ts index a00d62efce..e1ac874f94 100644 --- a/ironfish-cli/src/commands/wallet/notes/index.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -2,15 +2,16 @@ * 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, RpcAsset } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' -import { table, TableFlags } from '../../../ui' +import * as ui from '../../../ui' +import { useAccount } from '../../../utils' import { TableCols } from '../../../utils/table' -const { sort: _, ...tableFlags } = TableFlags +const { sort: _, ...tableFlags } = ui.TableFlags export class NotesCommand extends IronfishCommand { - static description = `Display the account notes` + static description = `list the account's notes` static flags = { ...RemoteFlags, @@ -21,21 +22,15 @@ export class NotesCommand extends IronfishCommand { }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get notes for. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(NotesCommand) - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const { flags } = await this.parse(NotesCommand) const assetLookup: Map = new Map() const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) const response = client.wallet.getAccountNotesStream({ account }) @@ -49,7 +44,7 @@ export class NotesCommand extends IronfishCommand { ) } - table( + ui.table( [note], { memo: { diff --git a/ironfish-cli/src/commands/wallet/prune.ts b/ironfish-cli/src/commands/wallet/prune.ts index 9347b935a1..6a0b16a1b8 100644 --- a/ironfish-cli/src/commands/wallet/prune.ts +++ b/ironfish-cli/src/commands/wallet/prune.ts @@ -4,9 +4,10 @@ import { NodeUtils, TransactionStatus } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' +import { inputPrompt } from '../../ui' export default class PruneCommand extends IronfishCommand { - static description = 'Removes expired transactions from the wallet' + static description = `deletes expired transactions from the wallet` static flags = { dryrun: Flags.boolean({ @@ -40,6 +41,14 @@ export default class PruneCommand extends IronfishCommand { await NodeUtils.waitForOpen(node) ux.action.stop('Done.') + if (node.wallet.locked) { + const passphrase = await inputPrompt('Enter your passphrase to unlock the wallet', true, { + password: true, + }) + + await node.wallet.unlock(passphrase) + } + let accounts if (flags.account) { const account = node.wallet.getAccountByName(flags.account) diff --git a/ironfish-cli/src/commands/wallet/rename.ts b/ironfish-cli/src/commands/wallet/rename.ts index 224913fe3c..84055283f1 100644 --- a/ironfish-cli/src/commands/wallet/rename.ts +++ b/ironfish-cli/src/commands/wallet/rename.ts @@ -4,18 +4,19 @@ import { Args } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { checkWalletUnlocked } from '../../ui' export class RenameCommand extends IronfishCommand { - static description = 'Change the name of an account' + static description = 'rename the name of an account' static args = { - account: Args.string({ + old_name: Args.string({ required: true, - description: 'Name of the account to rename', + description: 'Old account to rename', }), - newName: Args.string({ + new_name: Args.string({ required: true, - description: 'New name to assign to the account', + description: 'New name for the account', }), } @@ -25,10 +26,11 @@ export class RenameCommand extends IronfishCommand { async start(): Promise { const { args } = await this.parse(RenameCommand) - const { account, newName } = args const client = await this.connectRpc() - await client.wallet.renameAccount({ account, newName }) - this.log(`Account ${account} renamed to ${newName}`) + await checkWalletUnlocked(client) + + await client.wallet.renameAccount({ account: args.old_name, newName: args.new_name }) + this.log(`Account ${args.old_name} renamed to ${args.new_name}`) } } diff --git a/ironfish-cli/src/commands/wallet/rescan.ts b/ironfish-cli/src/commands/wallet/rescan.ts index 3761dbb22c..e90470129b 100644 --- a/ironfish-cli/src/commands/wallet/rescan.ts +++ b/ironfish-cli/src/commands/wallet/rescan.ts @@ -6,11 +6,11 @@ import { setLogLevelFromConfig } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { ProgressBar, ProgressBarPresets } from '../../ui' +import { checkWalletUnlocked, ProgressBar, ProgressBarPresets } from '../../ui' import { hasUserResponseError } from '../../utils' export class RescanCommand extends IronfishCommand { - static description = `Rescan the blockchain for transactions. Clears wallet disk caches before rescanning.` + static description = `resets all accounts balance and rescans` static flags = { ...RemoteFlags, @@ -35,6 +35,7 @@ export class RescanCommand extends IronfishCommand { } const client = await this.connectRpc(local) + await checkWalletUnlocked(client) ux.action.start('Asking node to start scanning', undefined, { stdout: true, diff --git a/ironfish-cli/src/commands/wallet/reset.ts b/ironfish-cli/src/commands/wallet/reset.ts index b7e57e3e7c..3936e1d01c 100644 --- a/ironfish-cli/src/commands/wallet/reset.ts +++ b/ironfish-cli/src/commands/wallet/reset.ts @@ -2,16 +2,21 @@ * 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 { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { confirmOrQuit } from '../../ui' +import { checkWalletUnlocked, confirmOrQuit } from '../../ui' +import { useAccount } from '../../utils' export class ResetCommand extends IronfishCommand { - static description = `Resets the transaction of an account but keeps all keys.` + static description = `resets an account's balance and rescans` static flags = { ...RemoteFlags, + account: Flags.string({ + char: 'a', + description: 'Name of the account to reset', + }), resetCreated: Flags.boolean({ default: false, description: 'Reset the accounts birthday', @@ -26,16 +31,13 @@ export class ResetCommand extends IronfishCommand { }), } - static args = { - account: Args.string({ - required: true, - description: 'Name of the account to reset', - }), - } - async start(): Promise { - const { args, flags } = await this.parse(ResetCommand) - const { account } = args + const { flags } = await this.parse(ResetCommand) + + const client = await this.connectRpc() + await checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) await confirmOrQuit( `Are you sure you want to reset the account '${account}'?` + @@ -45,8 +47,6 @@ export class ResetCommand extends IronfishCommand { flags.confirm, ) - const client = await this.connectRpc() - await client.wallet.resetAccount({ account, resetCreatedAt: flags.resetCreated, diff --git a/ironfish-cli/src/commands/wallet/scanning/off.ts b/ironfish-cli/src/commands/wallet/scanning/off.ts index 7dc843fd31..2d0ddee970 100644 --- a/ironfish-cli/src/commands/wallet/scanning/off.ts +++ b/ironfish-cli/src/commands/wallet/scanning/off.ts @@ -4,13 +4,12 @@ import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' export class ScanningOffCommand extends IronfishCommand { - static description = `Turn off scanning for an account. The wallet will no longer scan the blockchain for new account transactions.` + static description = `turn off scanning for an account - static flags = { - ...RemoteFlags, - } +The wallet will no longer scan the blockchain for new account transactions.` static args = { account: Args.string({ @@ -19,11 +18,16 @@ export class ScanningOffCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(ScanningOffCommand) const { account } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) await client.wallet.setScanning({ account: account, diff --git a/ironfish-cli/src/commands/wallet/scanning/on.ts b/ironfish-cli/src/commands/wallet/scanning/on.ts index 58339bdd66..8d350d28a9 100644 --- a/ironfish-cli/src/commands/wallet/scanning/on.ts +++ b/ironfish-cli/src/commands/wallet/scanning/on.ts @@ -4,13 +4,12 @@ import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' export class ScanningOnCommand extends IronfishCommand { - static description = `Turn on scanning for an account. Scanning is on by default. The wallet will scan the blockchain for new account transactions.` + static description = `turn on scanning for an account - static flags = { - ...RemoteFlags, - } +Scanning is on by default. The wallet will scan the blockchain for new account transactions.` static args = { account: Args.string({ @@ -19,11 +18,16 @@ export class ScanningOnCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(ScanningOnCommand) const { account } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) await client.wallet.setScanning({ account: account, diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index fb442012be..66107f8355 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -16,7 +16,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' -import { selectAsset } from '../../utils/asset' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -30,7 +30,7 @@ import { } from '../../utils/transaction' export class Send extends IronfishCommand { - static description = `Send coins to another account` + static description = `create a transaction to send coins` static examples = [ '$ ironfish wallet:send --amount 2.003 --fee 0.00000001 --to 997c586852d1b12da499bcff53595ba37d04e4909dbdb1a75f3bfd90dd7212217a1c2c0da652d187fc52ed', @@ -41,32 +41,27 @@ export class Send extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', - description: 'The account to send money from', + char: 'a', + description: 'Name of the account to send money from', }), amount: ValueFlag({ - char: 'a', description: 'The amount to send in the major denomination', flagName: 'amount', }), to: Flags.string({ - char: 't', description: 'The public address of the recipient', }), fee: IronFlag({ - char: 'o', description: 'The fee amount in IRON', minimum: 1n, flagName: 'fee', }), feeRate: IronFlag({ - char: 'r', description: 'The fee rate amount in IRON/Kilobyte', minimum: 1n, flagName: 'fee rate', }), memo: Flags.string({ - char: 'm', description: 'The memo of transaction', }), confirm: Flags.boolean({ @@ -83,10 +78,8 @@ export class Send extends IronfishCommand { 'The block sequence after which the transaction will be removed from the mempool. Set to 0 for no expiration.', }), confirmations: Flags.integer({ - char: 'c', description: 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', - required: false, }), assetId: HexFlag({ char: 'i', @@ -122,9 +115,9 @@ export class Send extends IronfishCommand { const { flags } = await this.parse(Send) let assetId = flags.assetId let to = flags.to - let from = flags.account const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!flags.offline) { const status = await client.wallet.getNodeStatus() @@ -136,8 +129,10 @@ export class Send extends IronfishCommand { } } + const from = await useAccount(client, flags.account, 'Select an account to send from') + if (assetId == null) { - const asset = await selectAsset(client, from, { + const asset = await ui.assetPrompt(client, from, { action: 'send', showNativeAsset: true, showNonCreatorAsset: true, @@ -191,19 +186,6 @@ export class Send extends IronfishCommand { }) } - if (!from) { - const response = await client.wallet.getDefaultAccount() - - if (!response.content.account) { - this.error( - `No account is currently active. - Use ironfish wallet:create to first create an account`, - ) - } - - from = response.content.account.name - } - if (!to) { to = await ui.inputPrompt('Enter the public address of the recipient', true) } diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index ad53d7d637..7582216e71 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -1,63 +1,67 @@ /* 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 chalk from 'chalk' +import { MathUtils, TimeUtils } from '@ironfish/sdk' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { table, TableFlags } from '../../ui' +import { JsonFlags, RemoteFlags } from '../../flags' +import * as ui from '../../ui' export class StatusCommand extends IronfishCommand { - static description = `Get status of all accounts` + static description = `show wallet information` + static enableJsonFlag = true static flags = { ...RemoteFlags, - ...TableFlags, + ...JsonFlags, } - async start(): Promise { - const { flags } = await this.parse(StatusCommand) + async start(): Promise { + await this.parse(StatusCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) - const response = await client.wallet.getAccountsStatus() - - table( - response.content.accounts, - { - name: { - get: (row) => row.name, - header: 'Account Name', - minWidth: 11, - }, - id: { - get: (row) => row.id, - header: 'Account ID', - }, - viewOnly: { - get: (row) => row.viewOnly, - header: 'View Only', - }, - headHash: { - get: (row) => row.head?.hash ?? 'NULL', - header: 'Head Hash', - }, - headInChain: { - get: (row) => row.head?.inChain ?? 'NULL', - header: 'Head In Chain', - }, - sequence: { - get: (row) => row.head?.sequence ?? 'NULL', - header: 'Head Sequence', - }, - scanningEnabled: { - get: (row) => (row.scanningEnabled ? chalk.green('โœ“') : ''), - header: 'Scanning Enabled', - }, - }, - { - printLine: this.log.bind(this), - ...flags, - }, - ) + const [nodeStatus, walletStatus] = await Promise.all([ + client.node.getStatus(), + client.wallet.getAccounts(), + ]) + + const status: Record = { + Wallet: nodeStatus.content.accounts.enabled ? 'ENABLED' : 'DISABLED', + Accounts: walletStatus.content.accounts.length, + Head: nodeStatus.content.accounts.head.hash, + Sequence: nodeStatus.content.accounts.head.sequence, + Scanner: 'IDLE', + } + + if (nodeStatus.content.accounts.scanning) { + const progress = MathUtils.round( + (nodeStatus.content.accounts.scanning.sequence / + nodeStatus.content.accounts.scanning.endSequence) * + 100, + 2, + ) + + const duration = Date.now() - nodeStatus.content.accounts.scanning.startedAt + const speed = MathUtils.round(nodeStatus.content.accounts.scanning.speed, 2) + + status['Scanner'] = 'SCANNING' + status['Scan Progress'] = progress + '%' + status['Scan Speed'] = `${speed} B/s` + status['Scan Duration'] = TimeUtils.renderSpan(duration, { + hideMilliseconds: true, + forceSecond: true, + }) + status[ + 'Scan Block' + ] = `${nodeStatus.content.accounts.scanning.sequence} -> ${nodeStatus.content.accounts.scanning.endSequence}` + } + + this.log(ui.card(status)) + + return { + ...nodeStatus.content.accounts, + accountsCount: walletStatus.content.accounts.length, + } } } diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 49cff44e0d..07fdedbdb7 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -10,17 +10,17 @@ import { RpcAsset, TransactionType, } from '@ironfish/sdk' -import { Args, Flags } from '@oclif/core' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableColumns, TableFlags } from '../../ui' -import { getAssetsByIDs } from '../../utils' +import { checkWalletUnlocked, table, TableColumns, TableFlags } from '../../ui' +import { getAssetsByIDs, useAccount } from '../../utils' import { extractChainportDataFromTransaction } from '../../utils/chainport' import { Format, TableCols } from '../../utils/table' const { sort: _, ...tableFlags } = TableFlags export class TransactionsCommand extends IronfishCommand { - static description = `Display the account transactions` + static description = `list the account's transactions` static flags = { ...RemoteFlags, @@ -29,8 +29,9 @@ export class TransactionsCommand extends IronfishCommand { char: 'a', description: 'Name of the account to get transactions for', }), - hash: Flags.string({ + transaction: Flags.string({ char: 't', + aliases: ['hash'], description: 'Transaction hash to get details for', }), sequence: Flags.integer({ @@ -52,17 +53,8 @@ export class TransactionsCommand extends IronfishCommand { }), } - static args = { - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(TransactionsCommand) - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const { flags } = await this.parse(TransactionsCommand) const format: Format = flags.csv || flags.output === 'csv' @@ -72,12 +64,15 @@ export class TransactionsCommand extends IronfishCommand { : Format.cli const client = await this.connectRpc() + await checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) const networkId = (await client.chain.getNetworkInfo()).content.networkId const response = client.wallet.getAccountTransactionsStream({ account, - hash: flags.hash, + hash: flags.transaction, sequence: flags.sequence, limit: flags.limit, offset: flags.offset, diff --git a/ironfish-cli/src/commands/wallet/transaction/view.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts similarity index 73% rename from ironfish-cli/src/commands/wallet/transaction/view.ts rename to ironfish-cli/src/commands/wallet/transactions/decode.ts index 6ac2e81332..6708890474 100644 --- a/ironfish-cli/src/commands/wallet/transaction/view.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -5,29 +5,28 @@ import { ErrorUtils, RawTransaction, RawTransactionSerde, - RpcClient, Transaction, UnsignedTransaction, } from '@ironfish/sdk' import { Flags } from '@oclif/core' -import inquirer from 'inquirer' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' -import { longPrompt } from '../../../utils/input' +import * as ui from '../../../ui' import { renderRawTransactionDetails, renderTransactionDetails, renderUnsignedTransactionDetails, } from '../../../utils/transaction' -export class TransactionViewCommand extends IronfishCommand { - static description = `View transaction details` +export class TransactionsDecodeCommand extends IronfishCommand { + static description = `show an encoded transaction's details` + static hiddenAliases = ['wallet:transaction:view'] static flags = { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The name of the account to use to for viewing transaction details', + description: 'Name of the account to use to for viewing transaction details', }), transaction: Flags.string({ char: 't', @@ -37,15 +36,16 @@ export class TransactionViewCommand extends IronfishCommand { } async start(): Promise { - const { flags } = await this.parse(TransactionViewCommand) + const { flags } = await this.parse(TransactionsDecodeCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) - const account = flags.account ?? (await this.selectAccount(client)) + const account = flags.account ?? (await ui.accountPrompt(client)) let transactionString = flags.transaction if (!transactionString) { - transactionString = await longPrompt( + transactionString = await ui.longPrompt( 'Enter the hex-encoded transaction, raw transaction, or unsigned transaction to view', { required: true, @@ -76,33 +76,6 @@ export class TransactionViewCommand extends IronfishCommand { this.error('Unable to deserialize transaction input') } - async selectAccount(client: Pick): Promise { - const accountsResponse = await client.wallet.getAccounts() - - const choices = [] - for (const account of accountsResponse.content.accounts) { - choices.push({ - account, - value: account, - }) - } - - choices.sort((a, b) => a.account.localeCompare(b.account)) - - const selection = await inquirer.prompt<{ - account: string - }>([ - { - name: 'account', - message: 'Select account', - type: 'list', - choices, - }, - ]) - - return selection.account - } - tryDeserializeRawTransaction(transaction: string): RawTransaction | undefined { try { return RawTransactionSerde.deserialize(Buffer.from(transaction, 'hex')) diff --git a/ironfish-cli/src/commands/wallet/transactions/delete.ts b/ironfish-cli/src/commands/wallet/transactions/delete.ts new file mode 100644 index 0000000000..f236579172 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/transactions/delete.ts @@ -0,0 +1,35 @@ +/* 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 { Args } from '@oclif/core' +import { IronfishCommand } from '../../../command' +import * as ui from '../../../ui' + +export default class TransactionsDelete extends IronfishCommand { + static description = 'delete an expired or pending transaction from the wallet' + + static args = { + transaction: Args.string({ + required: true, + description: 'Hash of the transaction to delete from the wallet', + }), + } + + async start(): Promise { + const { args } = await this.parse(TransactionsDelete) + const { transaction } = args + + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const response = await client.wallet.deleteTransaction({ hash: transaction }) + + if (response.content.deleted) { + this.log(`Transaction ${transaction} deleted from wallet`) + } else { + this.error( + `Transaction ${transaction} was not deleted. Either it is on a block already or does not exist`, + ) + } + } +} diff --git a/ironfish-cli/src/commands/wallet/transaction/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts similarity index 81% rename from ironfish-cli/src/commands/wallet/transaction/import.ts rename to ironfish-cli/src/commands/wallet/transactions/import.ts index e9b1232c93..a98ef438fe 100644 --- a/ironfish-cli/src/commands/wallet/transaction/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -4,12 +4,20 @@ import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' -import { importFile, importPipe, longPrompt } from '../../../utils/input' +import * as ui from '../../../ui' +import { importFile, importPipe, longPrompt } from '../../../ui/longPrompt' -export class TransactionImportCommand extends IronfishCommand { - static description = `Import a transaction into your wallet` +export class TransactionsImportCommand extends IronfishCommand { + static description = `import a transaction into the wallet` - static hiddenAliases = ['wallet:transaction:add'] + static hiddenAliases = ['wallet:transaction:add', 'wallet:transaction:import'] + + static args = { + transaction: Args.string({ + required: false, + description: 'The transaction in hex encoding', + }), + } static flags = { ...RemoteFlags, @@ -23,15 +31,8 @@ export class TransactionImportCommand extends IronfishCommand { }), } - static args = { - transaction: Args.string({ - required: false, - description: 'The transaction in hex encoding', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(TransactionImportCommand) + const { flags, args } = await this.parse(TransactionsImportCommand) const { transaction: txArg } = args let transaction @@ -42,6 +43,9 @@ export class TransactionImportCommand extends IronfishCommand { ) } + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + if (txArg) { transaction = txArg } else if (flags.path) { @@ -57,7 +61,6 @@ export class TransactionImportCommand extends IronfishCommand { } ux.action.start(`Importing transaction`) - const client = await this.connectRpc() const response = await client.wallet.addTransaction({ transaction, broadcast: flags.broadcast, diff --git a/ironfish-cli/src/commands/wallet/transaction/index.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts similarity index 92% rename from ironfish-cli/src/commands/wallet/transaction/index.ts rename to ironfish-cli/src/commands/wallet/transactions/info.ts index a55b5c6ef4..ee816378ef 100644 --- a/ironfish-cli/src/commands/wallet/transaction/index.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -18,38 +18,39 @@ import { extractChainportDataFromTransaction, fetchChainportNetworkMap, getAssetsByIDs, + useAccount, } from '../../../utils' import { getExplorer } from '../../../utils/explorer' -export class TransactionCommand extends IronfishCommand { - static description = `Display an account transaction` +export class TransactionInfoCommand extends IronfishCommand { + static description = `show an account transaction's info` - static flags = { - ...RemoteFlags, - account: Flags.string({ - char: 'a', - description: 'Name of the account to get transaction details for', - }), - } + static hiddenAliases = ['wallet:transaction'] static args = { - hash: Args.string({ + transaction: Args.string({ required: true, description: 'Hash of the transaction', }), - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', + } + + static flags = { + ...RemoteFlags, + account: Flags.string({ + char: 'a', + description: 'Name of the account to get transaction details for', }), } async start(): Promise { - const { flags, args } = await this.parse(TransactionCommand) - const { hash } = args - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const { flags, args } = await this.parse(TransactionInfoCommand) + const { transaction: hash } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const account = await useAccount(client, flags.account) + const networkId = (await client.chain.getNetworkInfo()).content.networkId const response = await client.wallet.getAccountTransaction({ diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts similarity index 79% rename from ironfish-cli/src/commands/wallet/post.ts rename to ironfish-cli/src/commands/wallet/transactions/post.ts index 8ff40c6322..e5a0412a95 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -3,14 +3,13 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { RawTransactionSerde, RpcClient, Transaction } from '@ironfish/sdk' import { Args, Flags, ux } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { confirmOrQuit } from '../../ui' -import { longPrompt } from '../../utils/input' -import { renderRawTransactionDetails } from '../../utils/transaction' +import { IronfishCommand } from '../../../command' +import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' +import { renderRawTransactionDetails } from '../../../utils/transaction' -export class PostCommand extends IronfishCommand { - static summary = 'Post a raw transaction' +export class TransactionsPostCommand extends IronfishCommand { + static summary = 'post a raw transaction' static description = `Use this command to post a raw transaction. The output is a finalized posted transaction.` @@ -19,12 +18,19 @@ export class PostCommand extends IronfishCommand { '$ ironfish wallet:post 618c098d8d008c9f78f6155947014901a019d9ec17160dc0f0d1bb1c764b29b4...', ] + static hiddenAliases = ['wallet:post'] + + static args = { + raw_transaction: Args.string({ + description: 'The raw transaction in hex encoding', + }), + } + static flags = { ...RemoteFlags, account: Flags.string({ - description: 'The account that created the raw transaction', + description: 'Name of the account that created the raw transaction', char: 'f', - required: false, deprecated: true, }), confirm: Flags.boolean({ @@ -38,18 +44,15 @@ export class PostCommand extends IronfishCommand { }), } - static args = { - transaction: Args.string({ - description: 'The raw transaction in hex encoding', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(PostCommand) - let transaction = args.transaction + const { flags, args } = await this.parse(TransactionsPostCommand) + let transaction = args.raw_transaction + + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!transaction) { - transaction = await longPrompt('Enter the raw transaction in hex encoding', { + transaction = await ui.longPrompt('Enter the raw transaction in hex encoding', { required: true, }) } @@ -57,8 +60,6 @@ export class PostCommand extends IronfishCommand { const serialized = Buffer.from(transaction, 'hex') const raw = RawTransactionSerde.deserialize(serialized) - const client = await this.connectRpc() - const senderAddress = raw.sender() if (!senderAddress) { this.error('Unable to determine sender for raw transaction') @@ -73,7 +74,7 @@ export class PostCommand extends IronfishCommand { await renderRawTransactionDetails(client, raw, account, this.logger) - await confirmOrQuit('Do you want to post this?', flags.confirm) + await ui.confirmOrQuit('Do you want to post this?', flags.confirm) ux.action.start(`Posting the transaction`) diff --git a/ironfish-cli/src/commands/wallet/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts similarity index 86% rename from ironfish-cli/src/commands/wallet/sign.ts rename to ironfish-cli/src/commands/wallet/transactions/sign.ts index 5c80ca0935..87b57ca0b8 100644 --- a/ironfish-cli/src/commands/wallet/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -4,14 +4,17 @@ import { CurrencyUtils, RpcClient, Transaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' -import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' -import { longPrompt } from '../../utils/input' -import { Ledger } from '../../utils/ledger' -import { renderTransactionDetails, watchTransaction } from '../../utils/transaction' - -export class SignTransaction extends IronfishCommand { - static description = `Sign an unsigned transaction` +import { IronfishCommand } from '../../../command' +import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' +import { Ledger } from '../../../utils/ledger' +import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' + +export class TransactionsSignCommand extends IronfishCommand { + static description = `sign an unsigned transaction` + + static hiddenAliases = ['wallet:sign'] + static flags = { ...RemoteFlags, unsignedTransaction: Flags.string({ @@ -34,8 +37,9 @@ export class SignTransaction extends IronfishCommand { } async start(): Promise { - const { flags } = await this.parse(SignTransaction) + const { flags } = await this.parse(TransactionsSignCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!flags.broadcast && flags.watch) { this.error('Cannot use --watch without --broadcast') @@ -43,7 +47,7 @@ export class SignTransaction extends IronfishCommand { let unsignedTransaction = flags.unsignedTransaction if (!unsignedTransaction) { - unsignedTransaction = await longPrompt('Enter the unsigned transaction', { + unsignedTransaction = await ui.longPrompt('Enter the unsigned transaction', { required: true, }) } diff --git a/ironfish-cli/src/commands/wallet/transaction/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts similarity index 75% rename from ironfish-cli/src/commands/wallet/transaction/watch.ts rename to ironfish-cli/src/commands/wallet/transactions/watch.ts index 46b80853d2..fda93437c5 100644 --- a/ironfish-cli/src/commands/wallet/transaction/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -4,10 +4,23 @@ import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' import { watchTransaction } from '../../../utils/transaction' -export class WatchTxCommand extends IronfishCommand { - static description = `Wait for the status of an account transaction to confirm or expire` +export class TransactionsWatchCommand extends IronfishCommand { + static description = `wait for an account transaction to confirm` + static hiddenAliases = ['wallet:transaction:watch'] + + static args = { + transaction: Args.string({ + required: true, + description: 'Hash of the transaction', + }), + account: Args.string({ + required: false, + description: 'Name of the account. DEPRECATED: use --account flag', + }), + } static flags = { ...RemoteFlags, @@ -16,29 +29,18 @@ export class WatchTxCommand extends IronfishCommand { description: 'Name of the account to get transaction details for', }), confirmations: Flags.integer({ - required: false, description: 'Minimum number of blocks confirmations for a transaction', }), } - static args = { - hash: Args.string({ - required: true, - description: 'Hash of the transaction', - }), - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), - } - async start(): Promise { - const { flags, args } = await this.parse(WatchTxCommand) - const { hash } = args + const { flags, args } = await this.parse(TransactionsWatchCommand) + const { transaction: hash } = args // TODO: remove account arg const account = flags.account ? flags.account : args.account const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) await watchTransaction({ client, diff --git a/ironfish-cli/src/commands/wallet/unlock.ts b/ironfish-cli/src/commands/wallet/unlock.ts new file mode 100644 index 0000000000..bf874f11c6 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/unlock.ts @@ -0,0 +1,69 @@ +/* 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 { DEFAULT_UNLOCK_TIMEOUT_MS, RpcRequestError } from '@ironfish/sdk' +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../command' +import { RemoteFlags } from '../../flags' +import { inputPrompt } from '../../ui' + +export class UnlockCommand extends IronfishCommand { + static hidden = true + + static description = 'unlock accounts in the wallet' + + static flags = { + ...RemoteFlags, + passphrase: Flags.string({ + description: 'Passphrase to unlock the wallet with', + }), + timeout: Flags.integer({ + description: + 'How long to unlock the wallet for in ms. Use -1 to keep the wallet unlocked until the process stops', + }), + } + + async start(): Promise { + const { flags } = await this.parse(UnlockCommand) + + const client = await this.connectRpc() + + const response = await client.wallet.getAccountsStatus() + if (!response.content.encrypted) { + this.log('Wallet is already decrypted') + this.exit(1) + } + + let passphrase = flags.passphrase + if (!passphrase) { + passphrase = await inputPrompt('Enter your passphrase to unlock the wallet', true, { + password: true, + }) + } + + try { + await client.wallet.unlock({ + passphrase, + timeout: flags.timeout, + }) + } catch (e) { + if (e instanceof RpcRequestError) { + this.log('Wallet unlock failed') + this.exit(1) + } + + throw e + } + + const timeout = flags.timeout || DEFAULT_UNLOCK_TIMEOUT_MS + if (timeout === -1) { + this.log( + 'Unlocked the wallet. Call wallet:lock or stop the node to lock the wallet again.', + ) + } else { + this.log(`Unlocked the wallet for ${timeout}ms`) + } + + this.exit(0) + } +} diff --git a/ironfish-cli/src/commands/wallet/use.ts b/ironfish-cli/src/commands/wallet/use.ts index 165f8d6c60..be31581e1b 100644 --- a/ironfish-cli/src/commands/wallet/use.ts +++ b/ironfish-cli/src/commands/wallet/use.ts @@ -1,30 +1,44 @@ /* 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 { Args } from '@oclif/core' +import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { checkWalletUnlocked } from '../../ui' export class UseCommand extends IronfishCommand { - static description = 'Change the default account used by all commands' + static description = 'change the default wallet account' static args = { account: Args.string({ - required: true, description: 'Name of the account', }), } static flags = { ...RemoteFlags, + unset: Flags.boolean({ + description: 'Clear the default account', + }), } async start(): Promise { - const { args } = await this.parse(UseCommand) + const { args, flags } = await this.parse(UseCommand) const { account } = args + const { unset } = flags + + if (!account && !unset) { + this.error('You must provide the name of an account') + } const client = await this.connectRpc() + await checkWalletUnlocked(client) + await client.wallet.useAccount({ account }) - this.log(`The default account is now: ${account}`) + if (account == null) { + this.log('The default account has been unset') + } else { + this.log(`The default account is now: ${account}`) + } } } diff --git a/ironfish-cli/src/commands/wallet/which.ts b/ironfish-cli/src/commands/wallet/which.ts index 3317328aec..c00b459549 100644 --- a/ironfish-cli/src/commands/wallet/which.ts +++ b/ironfish-cli/src/commands/wallet/which.ts @@ -4,9 +4,10 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { checkWalletUnlocked } from '../../ui' export class WhichCommand extends IronfishCommand { - static description = `Show the account currently used. + static description = `show the default wallet account By default all commands will use this account when deciding what keys to use. If no account is specified as the default, you must @@ -24,6 +25,13 @@ export class WhichCommand extends IronfishCommand { const { flags } = await this.parse(WhichCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) + + const response = await client.wallet.getAccountsStatus() + if (response.content.locked) { + this.log('Your wallet is locked. Unlock the wallet to access your accounts') + this.exit(0) + } const { content: { diff --git a/ironfish-cli/src/commands/workers/status.ts b/ironfish-cli/src/commands/workers/status.ts index 69446c663a..0f0c334a75 100644 --- a/ironfish-cli/src/commands/workers/status.ts +++ b/ironfish-cli/src/commands/workers/status.ts @@ -5,13 +5,15 @@ import { GetWorkersStatusResponse, PromiseUtils } from '@ironfish/sdk' import { Flags } from '@oclif/core' import blessed from 'blessed' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' export default class Status extends IronfishCommand { - static description = 'Show the status of the worker pool' + static description = "show worker pool's status" + static enableJsonFlag = true static flags = { ...RemoteFlags, + ...JsonFlags, follow: Flags.boolean({ char: 'f', default: false, @@ -19,14 +21,15 @@ export default class Status extends IronfishCommand { }), } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(Status) if (!flags.follow) { const client = await this.connectRpc() const response = await client.worker.getWorkersStatus() this.log(renderStatus(response.content)) - this.exit(0) + + return response.content } // Console log will create display issues with Blessed diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index f8ee624c88..c1b0eaeab2 100644 --- a/ironfish-cli/src/flags.ts +++ b/ironfish-cli/src/flags.ts @@ -18,6 +18,7 @@ import { Flags } from '@oclif/core' export const VerboseFlagKey = 'verbose' export const ConfigFlagKey = 'config' +export const JsonFlagKey = 'json' export const ColorFlagKey = 'color' export const DataDirFlagKey = 'datadir' export const RpcUseIpcFlagKey = 'rpc.ipc' @@ -37,10 +38,17 @@ export const VerboseFlag = Flags.boolean({ helpGroup: 'GLOBAL', }) +export const JsonFlag = Flags.boolean({ + default: false, + description: 'format output as json', + helpGroup: 'OUTPUT', +}) + export const ColorFlag = Flags.boolean({ default: true, allowNo: true, description: 'Should colorize the output', + helpGroup: 'OUTPUT', }) export const ConfigFlag = Flags.string({ @@ -60,43 +68,52 @@ export const DataDirFlag = Flags.string({ export const RpcUseIpcFlag = Flags.boolean({ default: DEFAULT_USE_RPC_IPC, description: 'Connect to the RPC over IPC (default)', + helpGroup: 'RPC', }) export const RpcUseTcpFlag = Flags.boolean({ default: DEFAULT_USE_RPC_TCP, description: 'Connect to the RPC over TCP', + helpGroup: 'RPC', }) export const RpcTcpHostFlag = Flags.string({ description: 'The TCP host to listen for connections on', + helpGroup: 'RPC', }) export const RpcTcpPortFlag = Flags.integer({ description: 'The TCP port to listen for connections on', + helpGroup: 'RPC', }) export const RpcTcpTlsFlag = Flags.boolean({ default: DEFAULT_USE_RPC_TLS, description: 'Encrypt TCP connection to the RPC over TLS', allowNo: true, + helpGroup: 'RPC', }) export const RpcAuthFlag = Flags.string({ description: 'The RPC auth token', + helpGroup: 'RPC', }) export const RpcHttpHostFlag = Flags.string({ description: 'The HTTP host to listen for connections on', + helpGroup: 'RPC', }) export const RpcHttpPortFlag = Flags.integer({ description: 'The HTTP port to listen for connections on', + helpGroup: 'RPC', }) export const RpcUseHttpFlag = Flags.boolean({ default: DEFAULT_USE_RPC_HTTP, description: 'Connect to the RPC over HTTP', allowNo: true, + helpGroup: 'RPC', }) /** @@ -115,6 +132,15 @@ export const RemoteFlags = { [RpcAuthFlagKey]: RpcAuthFlag, } +/** + * Flags to include if your command returns JSON + * you must also use enableJsonFlag = true + */ +export const JsonFlags = { + [JsonFlagKey]: JsonFlag, + [ColorFlagKey]: ColorFlag, +} + export type IronOpts = { minimum?: bigint; flagName: string } export const IronFlag = Flags.custom({ diff --git a/ironfish-cli/src/ui/index.ts b/ironfish-cli/src/ui/index.ts index 1b524bebe7..9ae9ad0931 100644 --- a/ironfish-cli/src/ui/index.ts +++ b/ironfish-cli/src/ui/index.ts @@ -4,6 +4,9 @@ export * from './card' export * from './json' +export * from './longPrompt' export * from './progressBar' export * from './prompt' +export * from './prompts' export * from './table' +export * from './wallet' diff --git a/ironfish-cli/src/utils/input.ts b/ironfish-cli/src/ui/longPrompt.ts similarity index 100% rename from ironfish-cli/src/utils/input.ts rename to ironfish-cli/src/ui/longPrompt.ts diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index d0b201497e..f670e28044 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -5,29 +5,54 @@ import { ux } from '@oclif/core' import inquirer from 'inquirer' -async function _inputPrompt(message: string): Promise { +async function _inputPrompt(message: string, options?: { password: boolean }): Promise { const result: { prompt: string } = await inquirer.prompt({ - type: 'input', + type: options?.password ? 'password' : 'input', name: 'prompt', message: `${message}:`, }) return result.prompt.trim() } -export async function inputPrompt(message: string, required: boolean = false): Promise { +export async function inputPrompt( + message: string, + required: boolean = false, + options?: { password: boolean }, +): Promise { let userInput: string = '' if (required) { while (!userInput) { - userInput = await _inputPrompt(message) + userInput = await _inputPrompt(message, options) } } else { - userInput = await _inputPrompt(message) + userInput = await _inputPrompt(message, options) } return userInput } +export async function confirmInputOrQuit( + input: string, + message?: string, + confirm?: boolean, +): Promise { + if (confirm) { + return + } + + if (!message) { + message = `Are you sure? Type ${input} to confirm.` + } + + const entered = await inputPrompt(message, true) + + if (entered !== input) { + ux.stdout('Operation aborted.') + ux.exit(0) + } +} + export async function confirmPrompt(message: string): Promise { const result: { prompt: boolean } = await inquirer.prompt({ type: 'confirm', @@ -52,3 +77,30 @@ export async function confirmOrQuit(message?: string, confirm?: boolean): Promis ux.exit(0) } } + +export async function listPrompt( + message: string, + choices: T[], + name: (v: T) => string, + alphebetize: boolean = true, +): Promise { + const values = choices.map((v) => ({ + name: name(v), + value: v, + })) + + if (alphebetize) { + values.sort((a, b) => a.name.localeCompare(b.name)) + } + + const selection = await inquirer.prompt<{ prompt: T }>([ + { + name: 'prompt', + message: message, + type: 'list', + choices: values, + }, + ]) + + return selection.prompt +} diff --git a/ironfish-cli/src/ui/prompts.ts b/ironfish-cli/src/ui/prompts.ts new file mode 100644 index 0000000000..8c2fead622 --- /dev/null +++ b/ironfish-cli/src/ui/prompts.ts @@ -0,0 +1,142 @@ +/* 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 { Asset } from '@ironfish/rust-nodejs' +import { BufferUtils, CurrencyUtils, RpcAsset, RpcClient } from '@ironfish/sdk' +import inquirer from 'inquirer' +import { getAssetsByIDs, renderAssetWithVerificationStatus } from '../utils' +import { listPrompt } from './prompt' + +export async function accountPrompt( + client: Pick, + message: string = 'Select account', +): Promise { + const accountsResponse = await client.wallet.getAccounts() + return listPrompt(message, accountsResponse.content.accounts, (a) => a) +} + +export async function multisigSecretPrompt(client: Pick): Promise { + const identitiesResponse = await client.wallet.multisig.getIdentities() + + const selection = await listPrompt( + 'Select participant secret name', + identitiesResponse.content.identities, + (i) => i.name, + ) + + return selection.name +} + +export async function assetPrompt( + client: Pick, + account: string | undefined, + options: { + action: string + showNativeAsset: boolean + showNonCreatorAsset: boolean + showSingleAssetChoice: boolean + confirmations?: number + filter?: (asset: RpcAsset) => boolean + }, +): Promise< + | { + id: string + name: string + } + | undefined +> { + const balancesResponse = await client.wallet.getAccountBalances({ + account: account, + confirmations: options.confirmations, + }) + + let balances = balancesResponse.content.balances + + const assetLookup = await getAssetsByIDs( + client, + balances.map((b) => b.assetId), + account, + options.confirmations, + ) + if (!options.showNativeAsset) { + balances = balances.filter((b) => b.assetId !== Asset.nativeId().toString('hex')) + } + + if (!options.showNonCreatorAsset) { + const accountResponse = await client.wallet.getAccountPublicKey({ + account: account, + }) + + balances = balances.filter( + (b) => assetLookup[b.assetId].creator === accountResponse.content.publicKey, + ) + } + + if (balances.length === 0) { + return undefined + } + + if (balances.length === 1 && !options.showSingleAssetChoice) { + // If there's only one available asset, showing the choices is unnecessary + return { + id: balances[0].assetId, + name: assetLookup[balances[0].assetId].name, + } + } + + const filter = options.filter + if (filter) { + balances = balances.filter((balance) => filter(assetLookup[balance.assetId])) + } + + // Show verified assets at top of the list + balances = balances.sort((asset1, asset2) => { + const verified1 = assetLookup[asset1.assetId].verification.status === 'verified' + const verified2 = assetLookup[asset2.assetId].verification.status === 'verified' + if (verified1 && verified2) { + return 0 + } + + return verified1 ? -1 : 1 + }) + + const choices = balances.map((balance) => { + const asset = assetLookup[balance.assetId] + + const assetName = BufferUtils.toHuman(Buffer.from(assetLookup[balance.assetId].name, 'hex')) + const assetNameWithVerification = renderAssetWithVerificationStatus(assetName, asset) + + const renderedAvailable = CurrencyUtils.render( + balance.available, + false, + balance.assetId, + asset.verification, + ) + + const name = `${balance.assetId} (${assetNameWithVerification}) (${renderedAvailable})` + + const value = { + id: balance.assetId, + name: asset.name, + } + + return { value, name } + }) + + const response = await inquirer.prompt<{ + asset: { + id: string + name: string + } + }>([ + { + name: 'asset', + message: `Select the asset you wish to ${options.action}`, + type: 'list', + choices, + }, + ]) + + return response.asset +} diff --git a/ironfish-cli/src/ui/table.ts b/ironfish-cli/src/ui/table.ts index c105dd4dc4..a94d45d06f 100644 --- a/ironfish-cli/src/ui/table.ts +++ b/ironfish-cli/src/ui/table.ts @@ -7,6 +7,7 @@ import { Flags, ux } from '@oclif/core' import chalk from 'chalk' import { orderBy } from 'natural-orderby' import stringWidth from 'string-width' +import { json } from './json' const WIDE_DASH = 'โ”€' @@ -157,7 +158,7 @@ class Table> { } renderJson(rows: Record[]) { - this.options.printLine(JSON.stringify(rows, null, 2)) + this.options.printLine(json(rows)) } renderTerminal(rows: Record[]) { diff --git a/ironfish-cli/src/ui/wallet.ts b/ironfish-cli/src/ui/wallet.ts new file mode 100644 index 0000000000..8731b02ad9 --- /dev/null +++ b/ironfish-cli/src/ui/wallet.ts @@ -0,0 +1,18 @@ +/* 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 { RpcClient } from '@ironfish/sdk' +import { inputPrompt } from './prompt' + +export async function checkWalletUnlocked(client: Pick): Promise { + const status = await client.wallet.getAccountsStatus() + if (!status.content.locked) { + return + } + + const passphrase = await inputPrompt('Enter your passphrase to unlock the wallet', true, { + password: true, + }) + + await client.wallet.unlock({ passphrase }) +} diff --git a/ironfish-cli/src/utils/account.ts b/ironfish-cli/src/utils/account.ts new file mode 100644 index 0000000000..a867d86707 --- /dev/null +++ b/ironfish-cli/src/utils/account.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 { RpcClient } from '@ironfish/sdk' +import * as ui from '../ui' + +export async function useAccount( + client: RpcClient, + account: string | undefined, + message?: string, +): Promise { + if (account !== undefined) { + return account + } + + const status = await client.wallet.getAccountsStatus() + if (status.content.locked) { + throw new Error('Wallet is locked. Unlock the wallet to fetch accounts') + } + + const defaultAccount = await client.wallet.getAccounts({ default: true }) + + if (defaultAccount.content.accounts.length) { + return defaultAccount.content.accounts[0] + } + + return ui.accountPrompt(client, message) +} diff --git a/ironfish-cli/src/utils/asset.ts b/ironfish-cli/src/utils/asset.ts index 8cebae636b..a7e3b7a0fd 100644 --- a/ironfish-cli/src/utils/asset.ts +++ b/ironfish-cli/src/utils/asset.ts @@ -2,10 +2,7 @@ * 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 { Asset } from '@ironfish/rust-nodejs' import { - BufferUtils, - CurrencyUtils, RPC_ERROR_CODES, RpcAsset, RpcAssetVerification, @@ -14,7 +11,6 @@ import { StringUtils, } from '@ironfish/sdk' import chalk from 'chalk' -import inquirer from 'inquirer' type RenderAssetNameOptions = { verification?: RpcAssetVerification @@ -67,119 +63,6 @@ export function compareAssets( return 0 } -export async function selectAsset( - client: Pick, - account: string | undefined, - options: { - action: string - showNativeAsset: boolean - showNonCreatorAsset: boolean - showSingleAssetChoice: boolean - confirmations?: number - filter?: (asset: RpcAsset) => boolean - }, -): Promise< - | { - id: string - name: string - } - | undefined -> { - const balancesResponse = await client.wallet.getAccountBalances({ - account: account, - confirmations: options.confirmations, - }) - - let balances = balancesResponse.content.balances - - const assetLookup = await getAssetsByIDs( - client, - balances.map((b) => b.assetId), - account, - options.confirmations, - ) - if (!options.showNativeAsset) { - balances = balances.filter((b) => b.assetId !== Asset.nativeId().toString('hex')) - } - - if (!options.showNonCreatorAsset) { - const accountResponse = await client.wallet.getAccountPublicKey({ - account: account, - }) - - balances = balances.filter( - (b) => assetLookup[b.assetId].creator === accountResponse.content.publicKey, - ) - } - - if (balances.length === 0) { - return undefined - } - - if (balances.length === 1 && !options.showSingleAssetChoice) { - // If there's only one available asset, showing the choices is unnecessary - return { - id: balances[0].assetId, - name: assetLookup[balances[0].assetId].name, - } - } - - const filter = options.filter - if (filter) { - balances = balances.filter((balance) => filter(assetLookup[balance.assetId])) - } - - // Show verified assets at top of the list - balances = balances.sort((asset1, asset2) => { - const verified1 = assetLookup[asset1.assetId].verification.status === 'verified' - const verified2 = assetLookup[asset2.assetId].verification.status === 'verified' - if (verified1 && verified2) { - return 0 - } - - return verified1 ? -1 : 1 - }) - - const choices = balances.map((balance) => { - const asset = assetLookup[balance.assetId] - - const assetName = BufferUtils.toHuman(Buffer.from(assetLookup[balance.assetId].name, 'hex')) - const assetNameWithVerification = renderAssetWithVerificationStatus(assetName, asset) - - const renderedAvailable = CurrencyUtils.render( - balance.available, - false, - balance.assetId, - asset.verification, - ) - - const name = `${balance.assetId} (${assetNameWithVerification}) (${renderedAvailable})` - - const value = { - id: balance.assetId, - name: asset.name, - } - - return { value, name } - }) - - const response = await inquirer.prompt<{ - asset: { - id: string - name: string - } - }>([ - { - name: 'asset', - message: `Select the asset you wish to ${options.action}`, - type: 'list', - choices, - }, - ]) - - return response.asset -} - export async function getAssetVerificationByIds( client: Pick, assetIds: string[], diff --git a/ironfish-cli/src/utils/index.ts b/ironfish-cli/src/utils/index.ts index 3c0863b5ae..ddf1749ac3 100644 --- a/ironfish-cli/src/utils/index.ts +++ b/ironfish-cli/src/utils/index.ts @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export * from './asset' +export * from './account' export * from './chainport' export * from './editor' export * from './platform' diff --git a/ironfish-cli/src/utils/multisig.ts b/ironfish-cli/src/utils/multisig.ts index 97791419f0..f41367eefc 100644 --- a/ironfish-cli/src/utils/multisig.ts +++ b/ironfish-cli/src/utils/multisig.ts @@ -1,8 +1,7 @@ /* 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 { FileSystem, RpcClient, YupUtils } from '@ironfish/sdk' -import inquirer from 'inquirer' +import { FileSystem, YupUtils } from '@ironfish/sdk' import * as yup from 'yup' export type MultisigTransactionOptions = { @@ -69,30 +68,3 @@ export const MultisigTransactionJson = { load, resolveFlags, } - -export async function selectSecret(client: Pick): Promise { - const identitiesResponse = await client.wallet.multisig.getIdentities() - - const choices = [] - for (const { name } of identitiesResponse.content.identities) { - choices.push({ - name, - value: name, - }) - } - - choices.sort((a, b) => a.name.localeCompare(b.name)) - - const selection = await inquirer.prompt<{ - name: string - }>([ - { - name: 'name', - message: 'Select participant secret name', - type: 'list', - choices, - }, - ]) - - return selection.name -} diff --git a/ironfish-rust-nodejs/Cargo.toml b/ironfish-rust-nodejs/Cargo.toml index 0850f2b6c0..69455959fa 100644 --- a/ironfish-rust-nodejs/Cargo.toml +++ b/ironfish-rust-nodejs/Cargo.toml @@ -31,8 +31,8 @@ base64 = "0.13.0" fish_hash = "0.3.0" ironfish = { path = "../ironfish-rust" } ironfish-frost = { git = "https://github.com/iron-fish/ironfish-frost.git", branch = "main" } -napi = { version = "2.13.2", features = ["napi6"] } -napi-derive = "2.13.0" +napi = { version = "2.14.4", features = ["napi6"] } +napi-derive = "2.14.6" jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs", features = ["multiply-many"] } rand = "0.8.5" num_cpus = "1.16.0" diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index cb4be65e4f..eb4ed16352 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -3,25 +3,42 @@ /* auto-generated by NAPI-RS */ +export interface PublicPackage { + identity: string + frostPackage: string + groupSecretKeyShardEncrypted: string + checksum: string +} +export declare function deserializePublicPackage(round1PublicPackage: string): PublicPackage +export interface Round2PublicPackage { + senderIdentity: string + recipientIdentity: string + frostPackage: string + checksum: string +} +export interface Round2CombinedPublicPackage { + packages: Array +} +export declare function deserializeRound2CombinedPublicPackage(round2CombinedPublicPackage: string): Round2CombinedPublicPackage export const KEY_LENGTH: number export const NONCE_LENGTH: number -export function randomBytes(bytesLength: number): Uint8Array +export declare function randomBytes(bytesLength: number): Uint8Array export interface BoxedMessage { nonce: string boxedMessage: string } -export function boxMessage(plaintext: string, senderSecretKey: Uint8Array, recipientPublicKey: string): BoxedMessage -export function unboxMessage(boxedMessage: string, nonce: string, senderPublicKey: string, recipientSecretKey: Uint8Array): string +export declare function boxMessage(plaintext: string, senderSecretKey: Uint8Array, recipientPublicKey: string): BoxedMessage +export declare function unboxMessage(boxedMessage: string, nonce: string, senderPublicKey: string, recipientSecretKey: Uint8Array): string /** * # Safety * This is unsafe, it calls libc functions */ -export function initSignalHandler(): void +export declare function initSignalHandler(): void /** * # Safety * This is unsafe, it intentionally crashes */ -export function triggerSegfault(): void +export declare function triggerSegfault(): void export const ASSET_ID_LENGTH: number export const ASSET_METADATA_LENGTH: number export const ASSET_NAME_LENGTH: number @@ -46,7 +63,7 @@ export const TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH: number export const TRANSACTION_EXPIRATION_LENGTH: number export const TRANSACTION_FEE_LENGTH: number export const LATEST_TRANSACTION_VERSION: number -export function verifyTransactions(serializedTransactions: Array): boolean +export declare function verifyTransactions(serializedTransactions: Array): boolean export const enum LanguageCode { English = 0, ChineseSimplified = 1, @@ -65,13 +82,13 @@ export interface Key { publicAddress: string proofAuthorizingKey: string } -export function generateKey(): Key -export function spendingKeyToWords(privateKey: string, languageCode: LanguageCode): string -export function wordsToSpendingKey(words: string, languageCode: LanguageCode): string -export function generatePublicAddressFromIncomingViewKey(ivkString: string): string -export function generateKeyFromPrivateKey(privateKey: string): Key -export function initializeSapling(): void -export function isValidPublicAddress(hexAddress: string): boolean +export declare function generateKey(): Key +export declare function spendingKeyToWords(privateKey: string, languageCode: LanguageCode): string +export declare function wordsToSpendingKey(words: string, languageCode: LanguageCode): string +export declare function generatePublicAddressFromIncomingViewKey(ivkString: string): string +export declare function generateKeyFromPrivateKey(privateKey: string): Key +export declare function initializeSapling(): void +export declare function isValidPublicAddress(hexAddress: string): boolean /** * Return the number of processing units available to the system and to the current process. * @@ -85,8 +102,8 @@ export function isValidPublicAddress(hexAddress: string): boolean * Also note that these numbers may not be accurate when running in a virtual machine or in a * sandboxed environment. */ -export function getCpuCount(): CpuCount -export function generateRandomizedPublicKey(viewKeyString: string, publicKeyRandomnessString: string): string +export declare function getCpuCount(): CpuCount +export declare function generateRandomizedPublicKey(viewKeyString: string, publicKeyRandomnessString: string): string export class FishHashContext { constructor(full: boolean) prebuildDataset(threads: number): void @@ -314,12 +331,20 @@ export namespace multisig { proofAuthorizingKey: string } export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array): Buffer + export type NativeSignatureShare = SignatureShare + export class SignatureShare { + constructor(jsBytes: Buffer) + static fromFrost(frostSignatureShare: Buffer, identity: Buffer): NativeSignatureShare + identity(): Buffer + frostSignatureShare(): Buffer + } export class ParticipantSecret { constructor(jsBytes: Buffer) serialize(): Buffer static random(): ParticipantSecret toIdentity(): ParticipantIdentity decryptData(jsBytes: Buffer): Buffer + decryptLegacyData(jsBytes: Buffer): Buffer } export class ParticipantIdentity { constructor(jsBytes: Buffer) @@ -329,13 +354,19 @@ export namespace multisig { export type NativePublicKeyPackage = PublicKeyPackage export class PublicKeyPackage { constructor(value: string) + static fromFrost(frostPublicKeyPackage: Buffer, identities: Array, minSigners: number): NativePublicKeyPackage + serialize(): Buffer identities(): Array + frostPublicKeyPackage(): Buffer minSigners(): number } export type NativeSigningCommitment = SigningCommitment export class SigningCommitment { constructor(jsBytes: Buffer) + static fromRaw(identity: string, rawCommitments: Buffer, transactionHash: Buffer, signers: Array): NativeSigningCommitment + serialize(): Buffer identity(): Buffer + rawCommitments(): Buffer verifyChecksum(transactionHash: Buffer, signerIdentities: Array): boolean } export type NativeSigningPackage = SigningPackage @@ -343,5 +374,25 @@ export namespace multisig { constructor(jsBytes: Buffer) unsignedTransaction(): NativeUnsignedTransaction signers(): Array + frostSigningPackage(): Buffer + } +} +export namespace xchacha20poly1305 { + export const XKEY_LENGTH: number + export const XSALT_LENGTH: number + export const XNONCE_LENGTH: number + export type NativeXChaCha20Poly1305Key = XChaCha20Poly1305Key + export class XChaCha20Poly1305Key { + constructor(passphrase: string) + static fromParts(passphrase: string, salt: Buffer, nonce: Buffer): XChaCha20Poly1305Key + deriveKey(salt: Buffer, nonce: Buffer): XChaCha20Poly1305Key + deriveNewKey(): XChaCha20Poly1305Key + static deserialize(jsBytes: Buffer): NativeXChaCha20Poly1305Key + destroy(): void + salt(): Buffer + nonce(): Buffer + key(): Buffer + encrypt(plaintext: Buffer): Buffer + decrypt(ciphertext: Buffer): Buffer } } diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 6ca64090b1..e4d1006a92 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,9 +252,11 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding +const { FishHashContext, deserializePublicPackage, deserializeRound2CombinedPublicPackage, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig, xchacha20poly1305 } = nativeBinding module.exports.FishHashContext = FishHashContext +module.exports.deserializePublicPackage = deserializePublicPackage +module.exports.deserializeRound2CombinedPublicPackage = deserializeRound2CombinedPublicPackage module.exports.KEY_LENGTH = KEY_LENGTH module.exports.NONCE_LENGTH = NONCE_LENGTH module.exports.BoxKeyPair = BoxKeyPair @@ -304,3 +306,4 @@ module.exports.CpuCount = CpuCount module.exports.getCpuCount = getCpuCount module.exports.generateRandomizedPublicKey = generateRandomizedPublicKey module.exports.multisig = multisig +module.exports.xchacha20poly1305 = xchacha20poly1305 diff --git a/ironfish-rust-nodejs/npm/darwin-arm64/package.json b/ironfish-rust-nodejs/npm/darwin-arm64/package.json index b7c2e79307..eeddf59ab5 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.5.0", + "version": "2.6.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index 37a498d4c4..d34397a1f7 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.5.0", + "version": "2.6.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 e137fa5f06..9a3ab7c0f6 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.5.0", + "version": "2.6.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 49287e2e3c..fabe279e18 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.5.0", + "version": "2.6.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 33b833b861..0041a8bae1 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.5.0", + "version": "2.6.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 07598dd27a..0f10be1936 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.5.0", + "version": "2.6.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 a5a8b11320..2a7e344884 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.5.0", + "version": "2.6.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index 49a8e9ddce..4b61d3bad9 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "2.5.0", + "version": "2.6.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-nodejs/src/lib.rs b/ironfish-rust-nodejs/src/lib.rs index e21182f4f1..31cdc60cc5 100644 --- a/ironfish-rust-nodejs/src/lib.rs +++ b/ironfish-rust-nodejs/src/lib.rs @@ -26,6 +26,7 @@ pub mod nacl; pub mod rolling_filter; pub mod signal_catcher; pub mod structs; +pub mod xchacha20poly1305; #[cfg(feature = "stats")] pub mod stats; diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 957676b23a..dde0171e5f 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -4,7 +4,14 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ - frost::{keys::KeyPackage, round2, Randomizer}, + frost::{ + frost::{ + keys::PublicKeyPackage as FrostPublicKeyPackage, round1::SigningCommitments, + round2::SignatureShare as FrostSignatureShare, + }, + keys::KeyPackage, + round2, Randomizer, + }, frost_utils::{ account_keys::derive_account_keys, signing_package::SigningPackage, split_spender_key::split_spender_key, @@ -14,13 +21,16 @@ use ironfish::{ SaplingKey, }; use ironfish_frost::{ - dkg, keys::PublicKeyPackage, multienc, nonces::deterministic_signing_nonces, - signature_share::SignatureShare, signing_commitment::SigningCommitment, + dkg::{self, round3::PublicKeyPackage}, + multienc, + nonces::deterministic_signing_nonces, + signature_share::SignatureShare, + signing_commitment::SigningCommitment, }; use napi::{bindgen_prelude::*, JsBuffer}; use napi_derive::napi; use rand::thread_rng; -use std::io; +use std::fmt::Display; use std::ops::Deref; #[napi(namespace = "multisig")] @@ -29,11 +39,12 @@ pub const IDENTITY_LEN: u32 = ironfish::frost_utils::IDENTITY_LEN as u32; #[napi(namespace = "multisig")] pub const SECRET_LEN: u32 = ironfish_frost::participant::SECRET_LEN as u32; -fn try_deserialize(items: I, deserialize_item: F) -> Result> +fn try_deserialize(items: I, deserialize_item: F) -> Result> where I: IntoIterator, S: Deref, - F: for<'a> Fn(&'a [u8]) -> io::Result, + E: Display, + F: for<'a> Fn(&'a [u8]) -> std::result::Result, { items .into_iter() @@ -76,7 +87,8 @@ pub fn create_signing_commitment( key_package.signing_share(), &transaction_hash, &signers, - ); + ) + .map_err(to_napi_err)?; let bytes = signing_commitment.serialize(); Ok(bytes_to_hex(&bytes[..])) @@ -131,6 +143,55 @@ pub fn create_signature_share( Ok(bytes_to_hex(&bytes[..])) } +#[napi(js_name = "SignatureShare", namespace = "multisig")] +pub struct NativeSignatureShare { + signature_share: SignatureShare, +} + +#[napi(namespace = "multisig")] +impl NativeSignatureShare { + #[napi(constructor)] + pub fn new(js_bytes: JsBuffer) -> Result { + let bytes = js_bytes.into_value()?; + SignatureShare::deserialize_from(bytes.as_ref()) + .map(|signature_share| NativeSignatureShare { signature_share }) + .map_err(to_napi_err) + } + + #[napi(factory)] + pub fn from_frost( + frost_signature_share: JsBuffer, + identity: JsBuffer, + ) -> Result { + let frost_signature_share = frost_signature_share.into_value()?; + let frost_signature_share = + FrostSignatureShare::deserialize(frost_signature_share.as_ref()) + .map_err(to_napi_err)?; + + let identity = identity.into_value()?; + let identity = Identity::deserialize_from(&identity[..]).map_err(to_napi_err)?; + + let signature_share = SignatureShare::from_frost(frost_signature_share, identity); + + Ok(NativeSignatureShare { signature_share }) + } + + #[napi] + pub fn identity(&self) -> Buffer { + Buffer::from(self.signature_share.identity().serialize().as_slice()) + } + + #[napi] + pub fn frost_signature_share(&self) -> Buffer { + Buffer::from( + self.signature_share + .frost_signature_share() + .serialize() + .as_slice(), + ) + } +} + #[napi(namespace = "multisig")] pub struct ParticipantSecret { secret: Secret, @@ -165,10 +226,18 @@ impl ParticipantSecret { #[napi] pub fn decrypt_data(&self, js_bytes: JsBuffer) -> Result { + let bytes = js_bytes.into_value()?; + multienc::decrypt(&self.secret, &bytes) + .map(Buffer::from) + .map_err(to_napi_err) + } + + #[napi] + pub fn decrypt_legacy_data(&self, js_bytes: JsBuffer) -> Result { let bytes = js_bytes.into_value()?; let encrypted_blob = multienc::MultiRecipientBlob::deserialize_from(bytes.as_ref()).map_err(to_napi_err)?; - multienc::decrypt(&self.secret, &encrypted_blob) + multienc::decrypt_legacy(&self.secret, &encrypted_blob) .map(Buffer::from) .map_err(to_napi_err) } @@ -197,11 +266,11 @@ impl ParticipantIdentity { #[napi] pub fn encrypt_data(&self, js_bytes: JsBuffer) -> Result { let bytes = js_bytes.into_value()?; - let encrypted_blob = multienc::encrypt(&bytes, [&self.identity], thread_rng()); - encrypted_blob - .serialize() - .map(Buffer::from) - .map_err(to_napi_err) + Ok(Buffer::from(multienc::encrypt( + &bytes, + [&self.identity], + thread_rng(), + ))) } } @@ -285,6 +354,28 @@ impl NativePublicKeyPackage { Ok(NativePublicKeyPackage { public_key_package }) } + #[napi(factory)] + pub fn from_frost( + frost_public_key_package: JsBuffer, + identities: Vec, + min_signers: u16, + ) -> Result { + let frost_public_key_package = + FrostPublicKeyPackage::deserialize(frost_public_key_package.into_value()?.as_ref()) + .map_err(to_napi_err)?; + let identities = try_deserialize_identities(identities)?; + + let public_key_package = + PublicKeyPackage::from_frost(frost_public_key_package, identities, min_signers); + + Ok(NativePublicKeyPackage { public_key_package }) + } + + #[napi] + pub fn serialize(&self) -> Buffer { + Buffer::from(self.public_key_package.serialize()) + } + #[napi] pub fn identities(&self) -> Vec { self.public_key_package @@ -294,6 +385,17 @@ impl NativePublicKeyPackage { .collect() } + #[napi] + pub fn frost_public_key_package(&self) -> Result { + Ok(Buffer::from( + self.public_key_package + .frost_public_key_package() + .serialize() + .map_err(to_napi_err)? + .as_slice(), + )) + } + #[napi] pub fn min_signers(&self) -> u16 { self.public_key_package.min_signers() @@ -315,11 +417,55 @@ impl NativeSigningCommitment { .map_err(to_napi_err) } + #[napi(factory)] + pub fn from_raw( + identity: String, + raw_commitments: JsBuffer, + transaction_hash: JsBuffer, + signers: Vec, + ) -> Result { + let identity = + Identity::deserialize_from(&hex_to_vec_bytes(&identity).map_err(to_napi_err)?[..]) + .map_err(to_napi_err)?; + + let raw_commitments = + SigningCommitments::deserialize(raw_commitments.into_value()?.as_ref()) + .map_err(to_napi_err)?; + + let signers = try_deserialize_identities(signers)?; + + let signing_commitment = SigningCommitment::from_raw( + raw_commitments, + identity, + transaction_hash.into_value()?.as_ref(), + &signers[..], + ) + .map_err(to_napi_err)?; + + Ok(NativeSigningCommitment { signing_commitment }) + } + + #[napi] + pub fn serialize(&self) -> Buffer { + Buffer::from(self.signing_commitment.serialize().as_slice()) + } + #[napi] pub fn identity(&self) -> Buffer { Buffer::from(self.signing_commitment.identity().serialize().as_slice()) } + #[napi] + pub fn raw_commitments(&self) -> Result { + Ok(Buffer::from( + self.signing_commitment + .raw_commitments() + .serialize() + .map_err(to_napi_err)? + .as_slice(), + )) + } + #[napi] pub fn verify_checksum( &self, @@ -365,6 +511,17 @@ impl NativeSigningPackage { .map(|signer| Buffer::from(&signer.serialize()[..])) .collect() } + + #[napi] + pub fn frost_signing_package(&self) -> Result { + Ok(Buffer::from( + &self + .signing_package + .frost_signing_package + .serialize() + .map_err(to_napi_err)?[..], + )) + } } #[napi(namespace = "multisig")] @@ -429,7 +586,7 @@ pub struct DkgRound2Packages { pub round2_public_package: String, } -#[napi(object, namespace = "multisig")] +#[napi(namespace = "multisig")] pub fn dkg_round3( secret: &ParticipantSecret, round2_secret_package: String, @@ -452,7 +609,8 @@ pub fn dkg_round3( ) .map_err(to_napi_err)?; - let account_keys = derive_account_keys(public_key_package.verifying_key(), &group_secret_key); + let account_keys = derive_account_keys(public_key_package.verifying_key(), &group_secret_key) + .map_err(to_napi_err)?; Ok(DkgRound3Packages { public_address: account_keys.public_address.hex_public_address(), @@ -465,6 +623,67 @@ pub fn dkg_round3( }) } +#[napi(object)] +pub struct PublicPackage { + pub identity: String, + pub frost_package: String, + pub group_secret_key_shard_encrypted: String, + pub checksum: String, +} + +#[napi] +pub fn deserialize_public_package(round1_public_package: String) -> Result { + let serialized_item = hex_to_vec_bytes(&round1_public_package).map_err(to_napi_err)?; + let pkg = + dkg::round1::PublicPackage::deserialize_from(&serialized_item[..]).map_err(to_napi_err)?; + + Ok(PublicPackage { + identity: pkg.identity().to_string(), + frost_package: bytes_to_hex(&pkg.frost_package().serialize().map_err(to_napi_err)?), + group_secret_key_shard_encrypted: bytes_to_hex(pkg.group_secret_key_shard_encrypted()), + checksum: pkg.checksum().to_string(), + }) +} + +#[napi(object)] +pub struct Round2PublicPackage { + pub sender_identity: String, + pub recipient_identity: String, + pub frost_package: String, + pub checksum: String, +} + +#[napi(object)] +pub struct Round2CombinedPublicPackage { + pub packages: Vec, +} + +#[napi] +pub fn deserialize_round2_combined_public_package( + round2_combined_public_package: String, +) -> Result { + let serialized_item = hex_to_vec_bytes(&round2_combined_public_package).map_err(to_napi_err)?; + let pkg = dkg::round2::CombinedPublicPackage::deserialize_from(&serialized_item[..]) + .map_err(to_napi_err)?; + + let mut packages: Vec = Vec::new(); + + for round2_public_package in pkg.packages() { + packages.push(Round2PublicPackage { + sender_identity: round2_public_package.sender_identity().to_string(), + recipient_identity: round2_public_package.recipient_identity().to_string(), + frost_package: bytes_to_hex( + &round2_public_package + .frost_package() + .serialize() + .map_err(to_napi_err)?, + ), + checksum: round2_public_package.checksum().to_string(), + }) + } + + Ok(Round2CombinedPublicPackage { packages }) +} #[napi(object, namespace = "multisig")] pub struct DkgRound3Packages { pub public_address: String, diff --git a/ironfish-rust-nodejs/src/structs/note.rs b/ironfish-rust-nodejs/src/structs/note.rs index 2af093177a..161fd5c8f2 100644 --- a/ironfish-rust-nodejs/src/structs/note.rs +++ b/ironfish-rust-nodejs/src/structs/note.rs @@ -6,6 +6,7 @@ use std::cmp; use ironfish::{ assets::asset::ID_LENGTH as ASSET_ID_LENGTH, + keys::PUBLIC_ADDRESS_SIZE, note::{AMOUNT_VALUE_SIZE, MEMO_SIZE, SCALAR_SIZE}, ViewKey, }; @@ -14,8 +15,6 @@ use napi_derive::napi; use ironfish::Note; -use ironfish::keys::PUBLIC_ADDRESS_SIZE; - use crate::to_napi_err; #[napi] diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 14219c6c75..17070949cd 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -24,7 +24,7 @@ use ironfish::{ MerkleNoteHash, OutgoingViewKey, ProposedTransaction, PublicAddress, SaplingKey, Transaction, ViewKey, }; -use ironfish_frost::keys::PublicKeyPackage; +use ironfish_frost::dkg::round3::PublicKeyPackage; use ironfish_frost::signature_share::SignatureShare; use ironfish_frost::signing_commitment::SigningCommitment; use napi::{ diff --git a/ironfish-rust-nodejs/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs new file mode 100644 index 0000000000..da3a5f6820 --- /dev/null +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -0,0 +1,141 @@ +/* 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 ironfish::xchacha20poly1305::{ + XChaCha20Poly1305Key, KEY_LENGTH as KEY_SIZE, SALT_LENGTH as SALT_SIZE, + XNONCE_LENGTH as XNONCE_SIZE, +}; +use napi::{bindgen_prelude::*, JsBuffer}; +use napi_derive::napi; + +use crate::to_napi_err; + +#[napi{namespace = "xchacha20poly1305"}] +pub const XKEY_LENGTH: u32 = KEY_SIZE as u32; + +#[napi{namespace = "xchacha20poly1305"}] +pub const XSALT_LENGTH: u32 = SALT_SIZE as u32; + +#[napi{namespace = "xchacha20poly1305"}] +pub const XNONCE_LENGTH: u32 = XNONCE_SIZE as u32; + +#[napi(js_name = "XChaCha20Poly1305Key", namespace = "xchacha20poly1305")] +pub struct NativeXChaCha20Poly1305Key { + pub(crate) key: XChaCha20Poly1305Key, +} + +#[napi{namespace = "xchacha20poly1305"}] +impl NativeXChaCha20Poly1305Key { + #[napi(constructor)] + pub fn generate(passphrase: String) -> Result { + let key = XChaCha20Poly1305Key::generate(passphrase.as_bytes()).map_err(to_napi_err)?; + + Ok(NativeXChaCha20Poly1305Key { key }) + } + + #[napi] + pub fn from_parts( + passphrase: String, + salt: JsBuffer, + nonce: JsBuffer, + ) -> Result { + let salt_buffer = salt.into_value()?; + let salt_vec = salt_buffer.as_ref(); + let mut salt_bytes = [0u8; SALT_SIZE]; + salt_bytes.clone_from_slice(&salt_vec[0..SALT_SIZE]); + + let nonce_buffer = nonce.into_value()?; + let nonce_vec = nonce_buffer.as_ref(); + let mut nonce_bytes = [0; XNONCE_SIZE]; + nonce_bytes.clone_from_slice(&nonce_vec[0..XNONCE_SIZE]); + + let key = XChaCha20Poly1305Key::from_parts(passphrase.as_bytes(), salt_bytes, nonce_bytes) + .map_err(to_napi_err)?; + + Ok(NativeXChaCha20Poly1305Key { key }) + } + + #[napi] + pub fn derive_key( + &self, + salt: JsBuffer, + nonce: JsBuffer, + ) -> Result { + let salt_buffer = salt.into_value()?; + let salt_vec = salt_buffer.as_ref(); + let mut salt_bytes = [0; SALT_SIZE]; + salt_bytes.clone_from_slice(&salt_vec[0..SALT_SIZE]); + + let derived_key = self.key.derive_key(salt_bytes).map_err(to_napi_err)?; + + let nonce_buffer = nonce.into_value()?; + let nonce_vec = nonce_buffer.as_ref(); + let mut nonce_bytes = [0; XNONCE_SIZE]; + nonce_bytes.clone_from_slice(&nonce_vec[0..XNONCE_SIZE]); + + let key = XChaCha20Poly1305Key { + key: derived_key, + nonce: nonce_bytes, + salt: salt_bytes, + }; + + Ok(NativeXChaCha20Poly1305Key { key }) + } + + #[napi] + pub fn derive_new_key(&self) -> Result { + let key = self.key.derive_new_key().map_err(to_napi_err)?; + + Ok(NativeXChaCha20Poly1305Key { key }) + } + + #[napi(factory)] + pub fn deserialize(js_bytes: JsBuffer) -> Result { + let byte_vec = js_bytes.into_value()?; + + let key = XChaCha20Poly1305Key::read(byte_vec.as_ref()).map_err(to_napi_err)?; + + Ok(NativeXChaCha20Poly1305Key { key }) + } + + #[napi] + pub fn destroy(&mut self) -> Result<()> { + self.key.destroy(); + Ok(()) + } + + #[napi] + pub fn salt(&self) -> Buffer { + Buffer::from(self.key.salt.to_vec()) + } + + #[napi] + pub fn nonce(&self) -> Buffer { + Buffer::from(self.key.nonce.to_vec()) + } + + #[napi] + pub fn key(&self) -> Buffer { + Buffer::from(self.key.key.to_vec()) + } + + #[napi] + pub fn encrypt(&self, plaintext: JsBuffer) -> Result { + let plaintext_bytes = plaintext.into_value()?; + let result = self + .key + .encrypt(plaintext_bytes.as_ref()) + .map_err(to_napi_err)?; + + Ok(Buffer::from(&result[..])) + } + + #[napi] + pub fn decrypt(&self, ciphertext: JsBuffer) -> Result { + let byte_vec = ciphertext.into_value()?; + let result = self.key.decrypt(byte_vec.to_vec()).map_err(to_napi_err)?; + + Ok(Buffer::from(&result[..])) + } +} diff --git a/ironfish-rust/Cargo.toml b/ironfish-rust/Cargo.toml index 1f5f52f9d1..2ecfa87512 100644 --- a/ironfish-rust/Cargo.toml +++ b/ironfish-rust/Cargo.toml @@ -39,7 +39,7 @@ blake2s_simd = "1.0.0" blake3 = "1.5.0" blstrs = { version = "0.6.0", features = ["portable"] } byteorder = "1.4.3" -chacha20poly1305 = "0.9.0" +chacha20poly1305 = "0.10.1" crypto_box = { version = "0.9", features = ["std"] } ff = "0.12.0" group = "0.12.0" @@ -52,6 +52,9 @@ libc = "0.2.126" # sub-dependency that needs a pinned version until a new releas rand = "0.8.5" tiny-bip39 = "1.0" xxhash-rust = { version = "0.8.5", features = ["xxh3"] } +argon2 = { version = "0.5.3", features = ["password-hash"] } +hkdf = "0.12.4" +sha2 = "0.10" [dev-dependencies] hex-literal = "0.4" diff --git a/ironfish-rust/src/errors.rs b/ironfish-rust/src/errors.rs index 446e1ea077..0de86e44fc 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -27,8 +27,12 @@ pub enum IronfishErrorKind { BellpersonSynthesis, CryptoBox, FrostLibError, + FailedArgon2Hash, FailedSignatureAggregation, FailedSignatureVerification, + FailedXChaCha20Poly1305Decryption, + FailedXChaCha20Poly1305Encryption, + FailedHkdfExpansion, IllegalValue, InconsistentWitness, InvalidAssetIdentifier, diff --git a/ironfish-rust/src/frost_utils/account_keys.rs b/ironfish-rust/src/frost_utils/account_keys.rs index bb5380ce39..b40b872844 100644 --- a/ironfish-rust/src/frost_utils/account_keys.rs +++ b/ironfish-rust/src/frost_utils/account_keys.rs @@ -2,7 +2,10 @@ * 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::{IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey}; +use crate::{ + errors::{IronfishError, IronfishErrorKind}, + IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey, +}; use group::GroupEncoding; use ironfish_frost::frost::VerifyingKey; use ironfish_zkp::constants::PROOF_GENERATION_KEY_GENERATOR; @@ -23,7 +26,7 @@ pub struct MultisigAccountKeys { /// Derives the account keys for a multisig account, realizing the following key hierarchy: /// -/// ``` +/// ```ignore /// ak โ”€โ” /// โ”œโ”€ ivk โ”€โ”€ pk /// gsk โ”€โ”€ nsk โ”€โ”€ nk โ”€โ”˜ @@ -31,14 +34,17 @@ pub struct MultisigAccountKeys { pub fn derive_account_keys( authorizing_key: &VerifyingKey, group_secret_key: &[u8; 32], -) -> MultisigAccountKeys { +) -> Result { // Group secret key (gsk), obtained from the multisig setup process let group_secret_key = SaplingKey::new(*group_secret_key).expect("failed to derive group secret key"); // Authorization key (ak), obtained from the multisig setup process - let authorizing_key = Option::from(SubgroupPoint::from_bytes(&authorizing_key.serialize())) - .expect("failied to derive authorizing key"); + let mut bytes: [u8; 32] = [0; 32]; + bytes.copy_from_slice(&authorizing_key.serialize()?); + let authorizing_key = Option::from(SubgroupPoint::from_bytes(&bytes)).ok_or( + IronfishError::new_with_source(IronfishErrorKind::InvalidData, "invalid authorizing_key"), + )?; // Nullifier keys (nsk and nk), derived from the gsk let proof_authorizing_key = group_secret_key.sapling_proof_generation_key().nsk; @@ -50,8 +56,7 @@ pub fn derive_account_keys( nullifier_deriving_key, }; let incoming_viewing_key = IncomingViewKey { - view_key: SaplingKey::hash_viewing_key(&authorizing_key, &nullifier_deriving_key) - .expect("failed to derive view key"), + view_key: SaplingKey::hash_viewing_key(&authorizing_key, &nullifier_deriving_key)?, }; // Outgoing view key (ovk), derived from the gsk @@ -60,11 +65,11 @@ pub fn derive_account_keys( // Public address (pk), derived from the ivk let public_address = incoming_viewing_key.public_address(); - MultisigAccountKeys { + Ok(MultisigAccountKeys { proof_authorizing_key, outgoing_viewing_key, view_key, incoming_viewing_key, public_address, - } + }) } diff --git a/ironfish-rust/src/frost_utils/mod.rs b/ironfish-rust/src/frost_utils/mod.rs index f35df12885..92cbe7b45d 100644 --- a/ironfish-rust/src/frost_utils/mod.rs +++ b/ironfish-rust/src/frost_utils/mod.rs @@ -7,5 +7,5 @@ pub mod signing_package; pub mod split_secret; pub mod split_spender_key; -pub use ironfish_frost::keys::PublicKeyPackage; +pub use ironfish_frost::frost::keys::PublicKeyPackage; pub use ironfish_frost::participant::IDENTITY_LEN; diff --git a/ironfish-rust/src/frost_utils/split_secret.rs b/ironfish-rust/src/frost_utils/split_secret.rs index 6112c50740..d96f328d24 100644 --- a/ironfish-rust/src/frost_utils/split_secret.rs +++ b/ironfish-rust/src/frost_utils/split_secret.rs @@ -4,12 +4,12 @@ use ironfish_frost::participant::Identity; use ironfish_frost::{ + dkg::round3::PublicKeyPackage, frost::{ frost::keys::split, keys::{IdentifierList, KeyPackage}, SigningKey, }, - keys::PublicKeyPackage, }; use rand::{CryptoRng, RngCore}; use std::{ @@ -69,7 +69,7 @@ pub(crate) fn split_secret( let frost_ids = frost_id_map.keys().cloned().collect::>(); let identifier_list = IdentifierList::Custom(&frost_ids[..]); - let secret_key = SigningKey::deserialize(spender_key.spend_authorizing_key.to_bytes())?; + let secret_key = SigningKey::deserialize(&spender_key.spend_authorizing_key.to_bytes()[..])?; let max_signers: u16 = num_identities.try_into()?; let (shares, pubkeys) = split( diff --git a/ironfish-rust/src/frost_utils/split_spender_key.rs b/ironfish-rust/src/frost_utils/split_spender_key.rs index 86c2ad2b5e..7bee66eaf7 100644 --- a/ironfish-rust/src/frost_utils/split_spender_key.rs +++ b/ironfish-rust/src/frost_utils/split_spender_key.rs @@ -6,7 +6,9 @@ use crate::{ errors::IronfishError, IncomingViewKey, OutgoingViewKey, PublicAddress, SaplingKey, ViewKey, }; use group::GroupEncoding; -use ironfish_frost::{frost::keys::KeyPackage, keys::PublicKeyPackage, participant::Identity}; +use ironfish_frost::{ + dkg::round3::PublicKeyPackage, frost::keys::KeyPackage, participant::Identity, +}; use rand::thread_rng; use std::collections::HashMap; @@ -33,7 +35,7 @@ pub fn split_spender_key( split_secret(spender_key, identities, min_signers, thread_rng())?; assert_eq!( - public_key_package.verifying_key().serialize(), + public_key_package.verifying_key().serialize()?, spender_key.view_key().authorizing_key.to_bytes() ); diff --git a/ironfish-rust/src/lib.rs b/ironfish-rust/src/lib.rs index eae216ffc0..352e0fbef8 100644 --- a/ironfish-rust/src/lib.rs +++ b/ironfish-rust/src/lib.rs @@ -20,6 +20,8 @@ pub mod signal_catcher; pub mod transaction; pub mod util; pub mod witness; +pub mod xchacha20poly1305; + pub use { ironfish_frost::frost, ironfish_frost::participant, diff --git a/ironfish-rust/src/serializing/aead.rs b/ironfish-rust/src/serializing/aead.rs index 5bff971365..c98ee5e14d 100644 --- a/ironfish-rust/src/serializing/aead.rs +++ b/ironfish-rust/src/serializing/aead.rs @@ -3,8 +3,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::errors::{IronfishError, IronfishErrorKind}; -use chacha20poly1305::aead::{AeadInPlace, NewAead}; -use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use chacha20poly1305::aead::AeadInPlace; +use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; pub const MAC_SIZE: usize = 16; diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index fa3e05ae9a..5190481263 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -6,9 +6,8 @@ use std::collections::{BTreeMap, HashMap}; #[cfg(test)] use super::internal_batch_verify_transactions; - use super::{ProposedTransaction, Transaction, TRANSACTION_PUBLIC_KEY_SIZE}; - +use crate::frost_utils::account_keys::derive_account_keys; use crate::test_util::create_multisig_identities; use crate::transaction::tests::split_spender_key::split_spender_key; use crate::{ @@ -27,6 +26,8 @@ use crate::{ }; use ff::Field; use group::GroupEncoding; +use ironfish_frost::dkg::{round1 as round1_dkg, round2 as round2_dkg, round3 as round3_dkg}; +use ironfish_frost::participant::Secret; use ironfish_frost::{ frost::{round2, round2::SignatureShare, Identifier, Randomizer}, nonces::deterministic_signing_nonces, @@ -915,3 +916,196 @@ fn test_add_signature_by_building_transaction() { verify_transaction(&signed).expect("should be able to verify transaction"); } + +#[test] +fn test_dkg_signing() { + let secret1 = Secret::random(thread_rng()); + let secret2 = Secret::random(thread_rng()); + let secret3 = Secret::random(thread_rng()); + let identity1 = secret1.to_identity(); + let identity2 = secret2.to_identity(); + let identity3 = secret3.to_identity(); + let identities = &[identity1.clone(), identity2.clone(), identity3.clone()]; + + let (round1_secret_package_1, package1) = round1_dkg::round1( + &identity1, + 2, + [&identity1, &identity2, &identity3], + thread_rng(), + ) + .expect("round 1 failed"); + + let (round1_secret_package_2, package2) = round1_dkg::round1( + &identity2, + 2, + [&identity1, &identity2, &identity3], + thread_rng(), + ) + .expect("round 1 failed"); + + let (round1_secret_package_3, package3) = round1_dkg::round1( + &identity3, + 2, + [&identity1, &identity2, &identity3], + thread_rng(), + ) + .expect("round 1 failed"); + + let (encrypted_secret_package_1, round2_public_packages_1) = round2_dkg::round2( + &secret1, + &round1_secret_package_1, + [&package1, &package2, &package3], + thread_rng(), + ) + .expect("round 2 failed"); + + let (encrypted_secret_package_2, round2_public_packages_2) = round2_dkg::round2( + &secret2, + &round1_secret_package_2, + [&package1, &package2, &package3], + thread_rng(), + ) + .expect("round 2 failed"); + + let (encrypted_secret_package_3, round2_public_packages_3) = round2_dkg::round2( + &secret3, + &round1_secret_package_3, + [&package1, &package2, &package3], + thread_rng(), + ) + .expect("round 2 failed"); + + let (key_package_1, public_key_package, group_secret_key) = round3_dkg::round3( + &secret1, + &encrypted_secret_package_1, + [&package1, &package2, &package3], + [&round2_public_packages_2, &round2_public_packages_3], + ) + .expect("round 3 failed"); + + let (key_package_2, _, _) = round3_dkg::round3( + &secret2, + &encrypted_secret_package_2, + [&package1, &package2, &package3], + [&round2_public_packages_1, &round2_public_packages_3], + ) + .expect("round 3 failed"); + + let (key_package_3, _, _) = round3_dkg::round3( + &secret3, + &encrypted_secret_package_3, + [&package1, &package2, &package3], + [&round2_public_packages_1, &round2_public_packages_2], + ) + .expect("round 3 failed"); + + let account_keys = derive_account_keys(public_key_package.verifying_key(), &group_secret_key) + .expect("account key derivation failed"); + let public_address = account_keys.public_address; + + // create raw/proposed transaction + let in_note = Note::new(public_address, 42, "", NATIVE_ASSET, public_address); + let out_note = Note::new(public_address, 40, "", NATIVE_ASSET, public_address); + let asset = Asset::new(public_address, "Testcoin", "A really cool coin") + .expect("should be able to create an asset"); + let value = 5; + let mint_out_note = Note::new(public_address, value, "", *asset.id(), public_address); + let witness = make_fake_witness(&in_note); + + let mut transaction = ProposedTransaction::new(TransactionVersion::latest()); + transaction + .add_spend(in_note, &witness) + .expect("add spend to transaction"); + assert_eq!(transaction.spends.len(), 1); + transaction + .add_output(out_note) + .expect("add output to transaction"); + assert_eq!(transaction.outputs.len(), 1); + transaction + .add_mint(asset, value) + .expect("add mint to transaction"); + transaction + .add_output(mint_out_note) + .expect("add mint output to transaction"); + + let intended_fee = 1; + transaction + .add_change_notes(Some(public_address), public_address, intended_fee) + .expect("should be able to add change notes"); + + // build UnsignedTransaction without signing + let mut unsigned_transaction = transaction + .build( + account_keys.proof_authorizing_key, + account_keys.view_key, + account_keys.outgoing_viewing_key, + intended_fee, + Some(account_keys.public_address), + ) + .expect("should be able to build unsigned transaction"); + + let transaction_hash = unsigned_transaction + .transaction_signature_hash() + .expect("should be able to compute transaction hash"); + + let mut commitments = HashMap::new(); + + // simulate signing + // commitment generation + let identity_keypackages = [ + (identity1, key_package_1), + (identity2, key_package_2), + (identity3, key_package_3), + ]; + for (identity, key_package) in identity_keypackages.iter() { + let nonces = deterministic_signing_nonces( + key_package.signing_share(), + &transaction_hash, + identities, + ); + commitments.insert(identity.clone(), (&nonces).into()); + } + + let signing_package = unsigned_transaction + .signing_package(commitments) + .expect("should be able to create signing package"); + + // simulate round 2 + let mut signature_shares: BTreeMap = BTreeMap::new(); + let randomizer = + Randomizer::deserialize(&unsigned_transaction.public_key_randomness.to_bytes()) + .expect("should be able to deserialize randomizer"); + + for (identity, key_package) in identity_keypackages.iter() { + let nonces = deterministic_signing_nonces( + key_package.signing_share(), + &transaction_hash, + identities, + ); + let signature_share = round2::sign( + &signing_package.frost_signing_package, + &nonces, + key_package, + randomizer, + ) + .expect("should be able to create signature share"); + signature_shares.insert(identity.to_frost_identifier(), signature_share); + } + + // coordinator creates signed transaction + let signed_transaction = unsigned_transaction + .aggregate_signature_shares( + &public_key_package, + &signing_package.frost_signing_package, + signature_shares, + ) + .expect("should be able to sign transaction"); + + assert_eq!(signed_transaction.spends.len(), 1); + assert_eq!(signed_transaction.outputs.len(), 3); + assert_eq!(signed_transaction.mints.len(), 1); + assert_eq!(signed_transaction.burns.len(), 0); + + // verify transaction + verify_transaction(&signed_transaction).expect("should be able to verify transaction"); +} diff --git a/ironfish-rust/src/transaction/unsigned.rs b/ironfish-rust/src/transaction/unsigned.rs index 2ff8fd90e2..eae47bf51e 100644 --- a/ironfish-rust/src/transaction/unsigned.rs +++ b/ironfish-rust/src/transaction/unsigned.rs @@ -5,11 +5,11 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use group::GroupEncoding; use ironfish_frost::{ + dkg::round3::PublicKeyPackage, frost::{ aggregate, round1::SigningCommitments, round2::SignatureShare, Identifier, RandomizedParams, Randomizer, SigningPackage as FrostSigningPackage, }, - keys::PublicKeyPackage, participant::Identity, }; @@ -229,7 +229,9 @@ impl UnsignedTransaction { let serialized_signature = authorizing_group_signature.serialize(); - let transaction = self.add_signature(serialized_signature)?; + let mut bytes = [0; 64]; + bytes.copy_from_slice(&serialized_signature?); + let transaction = self.add_signature(bytes)?; Ok(transaction) } diff --git a/ironfish-rust/src/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs new file mode 100644 index 0000000000..cefb0fb2c3 --- /dev/null +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -0,0 +1,193 @@ +/* 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 std::io; + +use argon2::Argon2; +use argon2::RECOMMENDED_SALT_LEN; +use chacha20poly1305::aead::Aead; +use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; +use hkdf::Hkdf; +use rand::{thread_rng, RngCore}; +use sha2::Sha256; + +use crate::errors::{IronfishError, IronfishErrorKind}; + +pub const KEY_LENGTH: usize = 32; +pub const SALT_LENGTH: usize = RECOMMENDED_SALT_LEN; +pub const XNONCE_LENGTH: usize = 24; + +#[derive(Debug)] +pub struct XChaCha20Poly1305Key { + pub key: [u8; KEY_LENGTH], + + pub nonce: [u8; XNONCE_LENGTH], + + pub salt: [u8; SALT_LENGTH], +} + +impl XChaCha20Poly1305Key { + pub fn generate(passphrase: &[u8]) -> Result { + let mut nonce = [0u8; XNONCE_LENGTH]; + thread_rng().fill_bytes(&mut nonce); + + let mut salt = [0u8; SALT_LENGTH]; + thread_rng().fill_bytes(&mut salt); + + XChaCha20Poly1305Key::from_parts(passphrase, salt, nonce) + } + + pub fn from_parts( + passphrase: &[u8], + salt: [u8; SALT_LENGTH], + nonce: [u8; XNONCE_LENGTH], + ) -> Result { + let mut key = [0u8; KEY_LENGTH]; + let argon2 = Argon2::default(); + + argon2 + .hash_password_into(passphrase, &salt, &mut key) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedArgon2Hash))?; + + Ok(XChaCha20Poly1305Key { key, salt, nonce }) + } + + pub fn derive_key(&self, salt: [u8; SALT_LENGTH]) -> Result<[u8; KEY_LENGTH], IronfishError> { + let hkdf = Hkdf::::new(None, &self.key); + + let mut okm = [0u8; KEY_LENGTH]; + hkdf.expand(&salt, &mut okm) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedHkdfExpansion))?; + + Ok(okm) + } + + pub fn derive_new_key(&self) -> Result { + let mut nonce = [0u8; XNONCE_LENGTH]; + thread_rng().fill_bytes(&mut nonce); + + let mut salt = [0u8; SALT_LENGTH]; + thread_rng().fill_bytes(&mut salt); + + let hkdf = Hkdf::::new(None, &self.key); + + let mut okm = [0u8; KEY_LENGTH]; + hkdf.expand(&salt, &mut okm) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedHkdfExpansion))?; + + Ok(XChaCha20Poly1305Key { + key: okm, + salt, + nonce, + }) + } + + pub fn read(mut reader: R) -> Result { + let mut salt = [0u8; SALT_LENGTH]; + reader.read_exact(&mut salt)?; + + let mut nonce = [0u8; XNONCE_LENGTH]; + reader.read_exact(&mut nonce)?; + + let mut key = [0u8; KEY_LENGTH]; + reader.read_exact(&mut key)?; + + Ok(XChaCha20Poly1305Key { salt, nonce, key }) + } + + pub fn write(&self, mut writer: W) -> Result<(), IronfishError> { + writer.write_all(&self.salt)?; + writer.write_all(&self.nonce)?; + writer.write_all(&self.key)?; + + Ok(()) + } + + pub fn destroy(&mut self) { + self.key.fill(0); + self.nonce.fill(0); + self.salt.fill(0); + } + + pub fn encrypt(&self, plaintext: &[u8]) -> Result, IronfishError> { + let nonce = XNonce::from_slice(&self.nonce); + let key = Key::from(self.key); + let cipher = XChaCha20Poly1305::new(&key); + + let ciphertext = cipher.encrypt(nonce, plaintext).map_err(|_| { + IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption) + })?; + + Ok(ciphertext) + } + + pub fn decrypt(&self, ciphertext: Vec) -> Result, IronfishError> { + let nonce = XNonce::from_slice(&self.nonce); + let key = Key::from(self.key); + let cipher = XChaCha20Poly1305::new(&key); + + cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Decryption)) + } +} + +#[cfg(test)] +mod test { + use crate::xchacha20poly1305::XChaCha20Poly1305Key; + + #[test] + fn test_valid_passphrase() { + let plaintext = "thisissensitivedata"; + let passphrase = "supersecretpassword"; + + let key = + XChaCha20Poly1305Key::generate(passphrase.as_bytes()).expect("should generate key"); + + let encrypted_output = key + .encrypt(plaintext.as_bytes()) + .expect("should successfully encrypt"); + let decrypted = key + .decrypt(encrypted_output) + .expect("should decrypt successfully"); + + assert_eq!(decrypted, plaintext.as_bytes()); + } + + #[test] + fn test_invalid_passphrase() { + let plaintext = "thisissensitivedata"; + let passphrase = "supersecretpassword"; + let incorrect_passphrase = "foobar"; + + let key = + XChaCha20Poly1305Key::generate(passphrase.as_bytes()).expect("should generate key"); + + let encrypted_output = key + .encrypt(plaintext.as_bytes()) + .expect("should successfully encrypt"); + + let incorrect_key = XChaCha20Poly1305Key::generate(incorrect_passphrase.as_bytes()) + .expect("should generate key"); + + incorrect_key + .decrypt(encrypted_output) + .expect_err("should fail decryption"); + } + + #[test] + fn test_derive_key() { + let passphrase = "supersecretpassword"; + + let encryption_key = XChaCha20Poly1305Key::generate(passphrase.as_bytes()) + .expect("should successfully generate key"); + + let key = encryption_key.derive_new_key().expect("should derive key"); + let derived_key = encryption_key + .derive_key(key.salt) + .expect("should derive key"); + + assert_eq!(key.key, derived_key); + } +} diff --git a/ironfish/package.json b/ironfish/package.json index 813a98b780..2e3e59653b 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "2.5.0", + "version": "2.6.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -22,7 +22,7 @@ "dependencies": { "@ethersproject/bignumber": "5.7.0", "@fast-csv/format": "4.3.5", - "@ironfish/rust-nodejs": "2.5.0", + "@ironfish/rust-nodejs": "2.6.0", "@napi-rs/blake-hash": "1.3.3", "axios": "1.7.2", "bech32": "2.0.0", diff --git a/ironfish/src/migrations/migrator.ts b/ironfish/src/migrations/migrator.ts index 527f8d15d7..b8448fcf25 100644 --- a/ironfish/src/migrations/migrator.ts +++ b/ironfish/src/migrations/migrator.ts @@ -8,10 +8,11 @@ import { Assert } from '../assert' import { Logger } from '../logger' import { IDatabaseTransaction } from '../storage/database/transaction' import { StrEnumUtils } from '../utils' -import { ErrorUtils } from '../utils/error' import { MIGRATIONS } from './data' import { Database, Migration, MigrationContext } from './migration' +type MigrationStatus = { name: string; applied: boolean } + export class Migrator { readonly context: MigrationContext readonly logger: Logger @@ -172,31 +173,28 @@ export class Migrator { logger.info(`Successfully ${dryRun ? 'dry ran' : 'applied'} ${unapplied.length} migrations`) } - async check(): Promise { + async status(): Promise<{ + migrations: MigrationStatus[] + unapplied: number + }> { let unapplied = 0 - - this.logger.info('Checking migrations:') - + const migrations: MigrationStatus[] = [] for (const migration of this.migrations) { - process.stdout.write(` Checking ${migration.name.slice(0, 35)}...`.padEnd(50, ' ')) + const applied = await this.isApplied(migration) - try { - const applied = await this.isApplied(migration) - process.stdout.write(` ${applied ? 'APPLIED' : 'WAITING'}\n`) + migrations.push({ + name: migration.name, + applied, + }) - if (!applied) { - unapplied++ - } - } catch (e) { - process.stdout.write(` ERROR\n`) - this.logger.error(ErrorUtils.renderError(e, true)) - throw e + if (!applied) { + unapplied++ } } - if (unapplied > 0) { - this.logger.info('') - this.logger.info(`You have ${unapplied} unapplied migrations.`) + return { + migrations, + unapplied, } } } diff --git a/ironfish/src/mining/stratum/clients/client.ts b/ironfish/src/mining/stratum/clients/client.ts index 1712d3f22c..a69c227bcd 100644 --- a/ironfish/src/mining/stratum/clients/client.ts +++ b/ironfish/src/mining/stratum/clients/client.ts @@ -37,6 +37,7 @@ export abstract class StratumClient { readonly version: number private started: boolean + private isClosing = false private id: number | null private connected: boolean private connectWarned: boolean @@ -85,6 +86,10 @@ export abstract class StratumClient { } private async startConnecting(): Promise { + if (this.isClosing) { + return + } + if (this.disconnectUntil && this.disconnectUntil > Date.now()) { this.connectTimeout = setTimeout(() => void this.startConnecting(), 60 * 1000) return @@ -114,6 +119,7 @@ export abstract class StratumClient { } stop(): void { + this.isClosing = true void this.close() if (this.connectTimeout) { @@ -191,11 +197,13 @@ export abstract class StratumClient { } this.logger.info(message) - } else { + } else if (!this.isClosing) { this.logger.info('Disconnected from pool unexpectedly. Reconnecting.') } - this.connectTimeout = setTimeout(() => void this.startConnecting(), 5000) + if (!this.isClosing) { + this.connectTimeout = setTimeout(() => void this.startConnecting(), 5000) + } } protected onError = (error: unknown): void => { diff --git a/ironfish/src/mining/stratum/clients/tcpClient.ts b/ironfish/src/mining/stratum/clients/tcpClient.ts index 8ae6bc617f..b6d7458757 100644 --- a/ironfish/src/mining/stratum/clients/tcpClient.ts +++ b/ironfish/src/mining/stratum/clients/tcpClient.ts @@ -19,7 +19,7 @@ export class StratumTcpClient extends StratumClient { protected onSocketDisconnect = (): void => { this.client?.off('error', this.onError) - this.client?.off('close', this.onDisconnect) + this.client?.off('close', this.onSocketDisconnect) this.client?.off('data', this.onSocketData) this.onDisconnect() } diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts new file mode 100644 index 0000000000..f07eaa92c6 --- /dev/null +++ b/ironfish/src/multisig.test.slow.ts @@ -0,0 +1,186 @@ +/* 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 { Asset, multisig, Note as NativeNote, verifyTransactions } from '@ironfish/rust-nodejs' +import { Note, RawTransaction } from './primitives' +import { Transaction, TransactionVersion } from './primitives/transaction' +import { makeFakeWitness } from './testUtilities' + +describe('multisig', () => { + describe('dkg', () => { + it('should create multisig accounts and sign transactions', () => { + const participantSecrets = [ + multisig.ParticipantSecret.random(), + multisig.ParticipantSecret.random(), + multisig.ParticipantSecret.random(), + ] + + const secrets = participantSecrets.map((secret) => secret.serialize().toString('hex')) + const identities = participantSecrets.map((secret) => + secret.toIdentity().serialize().toString('hex'), + ) + + const minSigners = 2 + + const round1Packages = secrets.map((_, index) => + multisig.dkgRound1(identities[index], minSigners, identities), + ) + + const round1PublicPackages = round1Packages.map( + (packages) => packages.round1PublicPackage, + ) + + const round2Packages = secrets.map((secret, index) => + multisig.dkgRound2( + secret, + round1Packages[index].round1SecretPackage, + round1PublicPackages, + ), + ) + + const round2PublicPackages = round2Packages.map( + (packages) => packages.round2PublicPackage, + ) + + const round3Packages = participantSecrets.map((participantSecret, index) => + multisig.dkgRound3( + participantSecret, + round2Packages[index].round2SecretPackage, + round1PublicPackages, + round2PublicPackages, + ), + ) + + const publicAddress = round3Packages[0].publicAddress + + // Ensure that we can construct PublicKeyPackage from the raw frost public + // key package + for (const round3Package of round3Packages) { + const deserializedPublicKeyPackage = new multisig.PublicKeyPackage( + round3Package.publicKeyPackage, + ) + + const publicKeyPackage = multisig.PublicKeyPackage.fromFrost( + deserializedPublicKeyPackage.frostPublicKeyPackage(), + deserializedPublicKeyPackage.identities().map((i) => i.toString('hex')), + deserializedPublicKeyPackage.minSigners(), + ) + + expect(publicKeyPackage.serialize().toString('hex')).toEqual( + round3Package.publicKeyPackage, + ) + } + + const raw = new RawTransaction(TransactionVersion.V1) + + const inNote = new NativeNote( + publicAddress, + 42n, + Buffer.from(''), + Asset.nativeId(), + publicAddress, + ) + const outNote = new NativeNote( + publicAddress, + 40n, + Buffer.from(''), + Asset.nativeId(), + publicAddress, + ) + const asset = new Asset(publicAddress, 'Testcoin', 'A really cool coin') + const mintOutNote = new NativeNote( + publicAddress, + 5n, + Buffer.from(''), + asset.id(), + publicAddress, + ) + + const witness = makeFakeWitness(new Note(inNote.serialize())) + + raw.spends.push({ note: new Note(inNote.serialize()), witness }) + raw.outputs.push({ note: new Note(outNote.serialize()) }) + raw.outputs.push({ note: new Note(mintOutNote.serialize()) }) + raw.mints.push({ + creator: asset.creator().toString('hex'), + name: asset.name().toString(), + metadata: asset.metadata().toString(), + value: mintOutNote.value(), + }) + raw.fee = 1n + + const proofAuthorizingKey = round3Packages[0].proofAuthorizingKey + const viewKey = round3Packages[0].viewKey + const outgoingViewKey = round3Packages[0].outgoingViewKey + + const unsignedTransaction = raw.build(proofAuthorizingKey, viewKey, outgoingViewKey) + const transactionHash = unsignedTransaction.hash() + + const commitments = secrets.map((secret, index) => + multisig.createSigningCommitment( + secret, + round3Packages[index].keyPackage, + transactionHash, + identities, + ), + ) + + // Simulates receiving raw commitments from Ledger + // Ledger app generates raw commitments, not wrapped SigningCommitment + const signingCommitments: string[] = [] + for (const commitment of commitments) { + const deserializedSigningCommitment = new multisig.SigningCommitment( + Buffer.from(commitment, 'hex'), + ) + + const signingCommitment = multisig.SigningCommitment.fromRaw( + deserializedSigningCommitment.identity().toString('hex'), + deserializedSigningCommitment.rawCommitments(), + transactionHash, + identities, + ) + + signingCommitments.push(signingCommitment.serialize().toString('hex')) + } + + const signingPackage = unsignedTransaction.signingPackage(signingCommitments) + + // Ensure that we can extract deserialize and extract frost signing package + // Ledger app needs frost signing package to generate signature shares + const frostSigningPackage = new multisig.SigningPackage( + Buffer.from(signingPackage, 'hex'), + ).frostSigningPackage() + expect(frostSigningPackage).not.toBeUndefined() + + const signatureShares = secrets.map((secret, index) => + multisig.createSignatureShare(secret, round3Packages[index].keyPackage, signingPackage), + ) + + // Ensure we can construct SignatureShare from parts + // Ledger app returns raw frost signature shares + for (const share of signatureShares) { + const signatureShare = new multisig.SignatureShare(Buffer.from(share, 'hex')) + const reconstructed = multisig.SignatureShare.fromFrost( + signatureShare.frostSignatureShare(), + signatureShare.identity(), + ) + expect(reconstructed.frostSignatureShare()).toEqual( + signatureShare.frostSignatureShare(), + ) + expect(reconstructed.identity()).toEqual(signatureShare.identity()) + } + + const serializedTransaction = multisig.aggregateSignatureShares( + round3Packages[0].publicKeyPackage, + signingPackage, + signatureShares, + ) + const transaction = new Transaction(serializedTransaction) + + expect(verifyTransactions([serializedTransaction])).toBeTruthy() + + expect(transaction.unsignedHash().equals(transactionHash)).toBeTruthy() + }) + }) +}) diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index de84887961..5835d90c0d 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -12,8 +12,11 @@ export enum RPC_ERROR_CODES { UNAUTHENTICATED = 'unauthenticated', NOT_FOUND = 'not-found', DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name', + DUPLICATE_IDENTITY_NAME = 'duplicate-identity-name', IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required', MULTISIG_SECRET_NOT_FOUND = 'multisig-secret-not-found', + WALLET_ALREADY_DECRYPTED = 'wallet-already-decrypted', + WALLET_ALREADY_ENCRYPTED = 'wallet-already-encrypted', } /** diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index f8b70ac435..06e3760519 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -35,12 +35,18 @@ import type { CreateTransactionResponse, CreateTrustedDealerKeyPackageRequest, CreateTrustedDealerKeyPackageResponse, + DecryptWalletRequest, + DecryptWalletResponse, + DeleteTransactionRequest, + DeleteTransactionResponse, DkgRound1Request, DkgRound1Response, DkgRound2Request, DkgRound2Response, DkgRound3Request, DkgRound3Response, + EncryptWalletRequest, + EncryptWalletResponse, EstimateFeeRateRequest, EstimateFeeRateResponse, EstimateFeeRatesRequest, @@ -132,9 +138,13 @@ import type { GetWorkersStatusRequest, GetWorkersStatusResponse, ImportAccountRequest, + ImportParticipantRequest, + ImportParticipantResponse, ImportResponse, IsValidPublicAddressRequest, IsValidPublicAddressResponse, + LockWalletRequest, + LockWalletResponse, MintAssetRequest, MintAssetResponse, OnGossipRequest, @@ -166,6 +176,8 @@ import type { StopNodeResponse, SubmitBlockRequest, SubmitBlockResponse, + UnlockWalletRequest, + UnlockWalletResponse, UnsetConfigRequest, UnsetConfigResponse, UploadConfigRequest, @@ -265,6 +277,15 @@ export abstract class RpcClient { ).waitForEnd() }, + importParticipant: ( + params: ImportParticipantRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/importParticipant`, + params, + ).waitForEnd() + }, + getIdentity: ( params: GetIdentityRequest, ): Promise> => { @@ -586,6 +607,15 @@ export abstract class RpcClient { ).waitForEnd() }, + deleteTransaction: ( + params: DeleteTransactionRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/deleteTransaction`, + params, + ).waitForEnd() + }, + estimateFeeRates: ( params?: EstimateFeeRatesRequest, ): Promise> => { @@ -627,6 +657,38 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + encrypt: ( + params: EncryptWalletRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/encrypt`, + params, + ).waitForEnd() + }, + + decrypt: ( + params: DecryptWalletRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/decrypt`, + params, + ).waitForEnd() + }, + + unlock: (params: UnlockWalletRequest): Promise> => { + return this.request( + `${ApiNamespace.wallet}/unlock`, + params, + ).waitForEnd() + }, + + lock: (params?: LockWalletRequest): Promise> => { + return this.request( + `${ApiNamespace.wallet}/lock`, + params, + ).waitForEnd() + }, } mempool = { @@ -962,7 +1024,7 @@ export abstract class RpcClient { isValidPublicAddress: ( params: IsValidPublicAddressRequest, - ): Promise> => { + ): Promise> => { return this.request( `${ApiNamespace.chain}/isValidPublicAddress`, params, @@ -971,7 +1033,7 @@ export abstract class RpcClient { broadcastTransaction: ( params: BroadcastTransactionRequest, - ): Promise> => { + ): Promise> => { return this.request( `${ApiNamespace.chain}/broadcastTransaction`, params, diff --git a/ironfish/src/rpc/clients/memoryClient.ts b/ironfish/src/rpc/clients/memoryClient.ts index 69998ce565..cd602a8420 100644 --- a/ironfish/src/rpc/clients/memoryClient.ts +++ b/ironfish/src/rpc/clients/memoryClient.ts @@ -30,5 +30,7 @@ export class RpcMemoryClient extends RpcClient { return RpcMemoryAdapter.requestStream(this.router, route, data) } - close(): void {} + async close(): Promise { + await this.node.stopNode() + } } diff --git a/ironfish/src/rpc/routes/node/getStatus.test.ts b/ironfish/src/rpc/routes/node/getStatus.test.ts index 7f4f34097f..5f8e01373a 100644 --- a/ironfish/src/rpc/routes/node/getStatus.test.ts +++ b/ironfish/src/rpc/routes/node/getStatus.test.ts @@ -25,6 +25,9 @@ describe('Route node/getStatus', () => { blockSyncer: { status: 'stopped', }, + accounts: { + locked: expect.any(Boolean), + }, }) }) }) diff --git a/ironfish/src/rpc/routes/node/getStatus.ts b/ironfish/src/rpc/routes/node/getStatus.ts index f26c2b9007..16f41610e9 100644 --- a/ironfish/src/rpc/routes/node/getStatus.ts +++ b/ironfish/src/rpc/routes/node/getStatus.ts @@ -97,6 +97,7 @@ export type GetNodeStatusResponse = { } accounts: { enabled: boolean + locked: boolean scanning?: { hash: string sequence: number @@ -236,6 +237,7 @@ export const GetStatusResponseSchema: yup.ObjectSchema = }) .defined(), enabled: yup.boolean().defined(), + locked: yup.boolean().defined(), scanning: yup .object({ hash: yup.string().defined(), @@ -366,6 +368,7 @@ async function getStatus(node: FullNode): Promise { }, accounts: { enabled: node.config.get('enableWallet'), + locked: node.wallet.locked, head: { hash: walletHead?.hash.toString('hex') ?? '', sequence: walletHead?.sequence ?? -1, diff --git a/ironfish/src/rpc/routes/node/stopNode.ts b/ironfish/src/rpc/routes/node/stopNode.ts index e95c0229dc..4ed2eced3d 100644 --- a/ironfish/src/rpc/routes/node/stopNode.ts +++ b/ironfish/src/rpc/routes/node/stopNode.ts @@ -24,7 +24,7 @@ routes.register( async (request, context): Promise => { AssertHasRpcContext(request, context, 'shutdown', 'logger') - context.logger.withTag('stopnode').info('Shutting down') + context.logger.withTag('stopnode').debug('Shutting down') request.end() await context.shutdown() }, diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/decrypt.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/decrypt.test.ts.fixture new file mode 100644 index 0000000000..6cf14eedb0 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/decrypt.test.ts.fixture @@ -0,0 +1,182 @@ +{ + "Route wallet/encrypt decrypts accounts": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "bd48f7e3-1fdc-444c-8da2-a69b5929c266", + "name": "A", + "spendingKey": "b650292782c859e02bb02ecc72b5a652c8d4e06b212fc48daa9826a84ec29eed", + "viewKey": "c58ef0ecc3ddb83e70c16595ff4507ebed969bd2d4717b0223cfda5fb3e3c12eeaaa2bee08f33a8f71e79a7546a9201e7c2803136d4d1559ee0c7d061edd0c18", + "incomingViewKey": "ed46f4be99df94d4ff73fdff53ce14f723c8875873271ba7677f0e0bfcff8d01", + "outgoingViewKey": "e9a4c430af2b19a1f459e8f5bbc236b1f50c88eae932e7f24c81b750bb6ccbb3", + "publicAddress": "17002598ba11259a7f825aa9097aad74e21020c30ac005ed5a9b452eccacbee5", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "7147af8c39390e803e4b83618878a369070910086119c011f4485d46932ef308" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "b8aaf95a-4fc8-4779-9651-c4a66958d68b", + "name": "B", + "spendingKey": "ed3aa48b6fa3dba1eb9023ce609a724629dcd9a6cd1f6749b9c7052a9616f8a5", + "viewKey": "d498fa26c41423f21042a65035c891652407e9b72ec5815bd55462ac7546969715468bd5b13b8a062cbb5ff83317f9327b30e62c2da1cb87b7c3d57a00ae529a", + "incomingViewKey": "9f1d2adc095b549d2e0939064c2d3c16b09a207c6778c68b8aa84a1a90c7ee04", + "outgoingViewKey": "075d6b5eddd608da7adf58d0d20e291fbcf33b06e13e381c56072627e6bb91a3", + "publicAddress": "8288102c344d529a9ad80062bc4e9fc12e96c0837f8502764b22abe4275e91dd", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "10917f752c51a7ac8aec4a43aa1dd531b78ffb17709a54646f026243f7810d08" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/encrypt throws if wallet is already decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "5e904ab8-0299-478a-b8ac-3140bd22e49b", + "name": "A", + "spendingKey": "23016779802774aafd71b9a65a692784a72a3519956e42096c91d7f07c9feb2e", + "viewKey": "5b1e183effe90ba7046f3a5016fb4caada071566a92494e96558c8b644e5758111bc2bd5e471322192b12525c40f71cc60ed1c0e98c40059f24962920a33982b", + "incomingViewKey": "1dd148bb07ab6628456d97c72d4c498b2261de06315fc7276cc49d5d90222303", + "outgoingViewKey": "8c38ad5428fb3af935c47055a31a57b5d494710a4fe952b47b0d9c9b3f31e207", + "publicAddress": "32a97426553a305863356aa0415b17850c2fc1a378cff4489a4977a526724cd8", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "33a2cfdd1531e2384981e98f8044b579b1578981d87a47f82376ce19a378720e" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "a3e01e93-e5b1-4a24-9581-03038220a909", + "name": "B", + "spendingKey": "dde3954bd4d8a97017bd99be3ba40774949bff30814090e20deda0858b51fe37", + "viewKey": "ef2530818f5fb4b2a21c25788193e927ad24cb8ba988edb31b18603d4c29433a9b8e0c57ef33073f6822efa6fc828631fc69d26c437897035ac59ca7b8c89bed", + "incomingViewKey": "3d9782327454977b5671a144eb4662aadbc0348010d1cd5153d4dd75bdffba03", + "outgoingViewKey": "c13d7a59bbf9aa6ad5b21e10f20fe95f46beae789f966fcab12c1d69c61d7e24", + "publicAddress": "d90f0656660eb92683818c18ff66f7bcf445a3c9fff0a7b703e5dc74e349a630", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0c22a2440735d1754388208f74d20ef3ddb5e9b2393ffc2e33b4cd4f50cb6f02" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/encrypt throws if wallet decryption fails": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "8dbed752-3b7c-45b4-b967-afebffecd45b", + "name": "A", + "spendingKey": "20a319f5c4cc53cb09ba3a9c75d5563f1223b7f05b0dcae7cdd839f7c6e271b5", + "viewKey": "3418a81c59d6c253d0e0d04f13fee14c7742430f6776856c1ef355b29ecc111d2ab615f0024143ecfe0cb031cbb7728f54c9b78a799d9fa59457f9e68c6c5298", + "incomingViewKey": "48f265e4afe6074fc6691f07f1da7071b61307a6ea559b5b95a4ccd6d55d4903", + "outgoingViewKey": "f77e9066200ec9990edd3b79478cdadf576879049f00b284d827abe03e08ed30", + "publicAddress": "4a2ebb0b985d39f97f3eab809688e317b615070c36e6e3cf0fa95409caa0f1af", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "80e1ba98c7409e9c63bba9ae390eef3a2960cf07727774d0a30b548448df0201" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "890f2d68-48da-4bdd-ab5c-084a8e9fd307", + "name": "B", + "spendingKey": "297dc206316d47b1e26ad8b6b773dcbb4ade162ccf78ee4c91cf35203c3518b9", + "viewKey": "5f7734257759f5af6117c2bd2afe2ee5cd6a210fab0ef6dde4d87a0017b1e971f7704f47c44084051a56ec9a68cfdff31911776beb103fe3508b4e73d8a2dd9f", + "incomingViewKey": "09c08e34a5d9b758d1ef255fddd61a6f97ad5e43c300f1191c1d4d15f6b5cb04", + "outgoingViewKey": "b85737c7ef8337d4fb0d02398e6dc4cc0723f43ae1746b1cf6cc869c03f67d01", + "publicAddress": "5b261e39488361a0420eaffc9c96917c38b1d80e04352a66dd85a4f6948f4590", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "5d5110516992247f029e1f861e9e956bda6cf550aabbab7afc002b3a12263709" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/deleteTransaction.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/deleteTransaction.test.ts.fixture new file mode 100644 index 0000000000..1a8d501952 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/deleteTransaction.test.ts.fixture @@ -0,0 +1,154 @@ +{ + "Route wallet/deleteTransaction should return true when deleting a transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4c199f86-625f-4d70-9ef1-5416197ba2a0", + "name": "existingAccount", + "spendingKey": "42fecb76b31eb44609b56dbfe517f82507f918227213fa79232ea79eef2db753", + "viewKey": "5641e4f8e724d3c6b5bf9da8d157b0f700ab9c75ca549079f9405124b12108dead5fd34c339019d3981ed36ab2fd303cd4d63d58222fb2982d9e529aba157412", + "incomingViewKey": "ef622236566f1b70dfcf2047bc7662080c599bd10b1a37955eb9acdab30f3507", + "outgoingViewKey": "a52cdc2178c2d247296fd30ced237cfafcc982e9f287bca21157f1d94b3832ba", + "publicAddress": "39f13996a551619bf22d0055ac3bb815ca8b1c1d9d36d43a1a72526286664027", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "6ef02a141b510095c7c13d79ce631e7801b19ba145dadeca57957462cc40c708" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:tSJWkx+7V9qY04v4Au4Um2pKF1qKK6VCC4Gq3V9yCjQ=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:ovVuTirVK5+zRp8E31C8Te8+ybxM4uXM1+U6OKn41sc=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724101482787, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAvSSGna5IX1qe2USicxdfVZ/QnSPcUKF22K4SNWq9RQmCClfrCxW+rge/LSe/Y9mojk/VzUseMRxhjSa2mlc1PYJGTA++vBSxzPHqL6tV9zCkcig9pv9Mhtl0yD2lbQJFOtVm7rN0GGjQ2A2/AvjJxd5wIiImUU/tyTrkBVQtB30J0j8ZX+ZOMK7C4QR7QOWkcg9XgYQAgVKxErCWmkfCfTMnxtzn8pIde6EnlUw+sVaXwssuvoEoM1lyVmlgmYqqhv8hQrX/is4AvL38NxFwkrpjPvzY0UXEC+WVwUNVaT0pEPPoCjoL31ZEhlxIt94MIqVBYcj1P87pYvZhkFUUlLlKZFZU7fEyPmfK3uK94tN1krktpik2HzC4iTj4PrlRZeLrDCnA1TREfLVcwusiEmWVY+nfK9P8ojuHqWvUsrtkC1c6micFWNF+F3f5J5q+mV1tL55dQUWD5xjVYIPRw3WJDFL7OlEHsu1AJGh8c35IAq/FYUYRmy9Jg1ArjNk88TW3HkU0NFUviCIJizWLp4TC4M+OC8qq/qeaLZTXzg0oZzhsFDzGCKKx+bz/q3x0Hz2EiTJRYRqRK8jnTo//G/t9K/IztrTLj/PoLBQXLpsSnczmYkl4qElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw5PWUUWTxkg/aG55GiCwAs8LK9rccClxxZcxgTrpEWtCMRC28oL/J7dxme3XbEBGMI+HO9kI5YApJVbeQr3RvBg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjloaWWidq7LycElVC6DAUOttLxDSuTkYSCq/sIjqBlC3Atcg5pf8sDfKGw6NID/kC1qsInNNRp9Sfc/XodWfyfRDeB30fr6WpzA+RvknjiK51lGFtud1qgJ+BC9g7RW9Qzzj0dPJMSaabE5nq1d1w4H7Ai+vJRdnGvWpO+a//qUO37rDp5eK2TorJ35CHqf/uf0xFrYGWykb5Pk00t3pDYv+lSvf1gYIrr3CDz3NVgGjPaeBVUs/2UsSjAcJv4kam+h5Fjof2oyZUGG8keqgnYDoSXI+jXTqDWbJLO6H09N5DMOAAjNxgbmi2akJkRS+F4zJB+SrDrN26WmcZ616Y7UiVpMfu1famNOL+ALuFJtqShdaiiulQguBqt1fcgo0BAAAAPNLIjwGkrJapeH96XeKk6GJ925d1HL+xz3wVHkQYqgJMC87x6Yojq5aho1ZAJuGppLOZ/laHtsLNYtzJ6pVeDg/ixoG88Ej/8VmmCgTPoc7XOxtgCPsyuqbvxeTPljEAbQuQeXXN9OsSTlL/c0VzFe0M4vJCgJTZ6XH5kTlotQaOI7SnP0Zj7jcbP1SX0FSNZL0BXvPELznpZMjZSGmYlJXlbAng0b+NVUxlicMrV5DYL2aERrpNnYwI9xzZ1P3Qw79IXwKB+NI8x6yzLfaug6Tpk5d1oDJKyEX0cSClvJfTqX4WdwyzsJsI/2APRkWp5ZYZ1g9axJQvmoLOT42FGmvjWwnqd2R0lMFiFEQgwxNMAtl8k9iz+TJgWZIf7U0dvsNUw9tieE/ZVILCDK/NmmlfI+ZfPXk6GpBp7iEJLjq7NyDPwlq1kEUPj3EJcQiD4XhWwWlhVCzkW0bg1D6Ejx4W9ReuWQL7P0c/Ka/eNIRUlonwi3cRd4ZftVuQeVHWUiZVuzcdBMxSZRpBCSy2/AmqW3ips7UUQViZYnYDXbiOfeYiaXd2VzZYTSekAABgm0LVIlE3L4Y9Q4GIsissoRFU8VG0rPnTnJIiyK+TwyHcJBbxQcTFruSP1eNUycbrF11DoSVVHkW9nzApW3XQ2eMS49+uQaAKdQAJrJvFVGNIfFFZ2/AiLV60BsZCzOQTD0UIAFNmf3ric1dDFDjyzuJdjF6gVleSBmqyfvgPFpP9/nmPP5rZ/mYk0X3vyw5Z5qtxNxwTfHTBz1TEzeuW4kGEkJzd/suCM1t4rVZSwYRARdKM6GuriSu4imptViHolr2HajZPIOoajwbGF94K+gL/rYfrad1BCh6hdF7s3D+6quipcqiepqg32qhsfVYMuNiNCCB5DQm60FSr8h56TrdXI685It5NfDZTuVS9D6oxA7nBwhfCRQFVuHKWyjpzBrHsOHtWdGurpqROffJNXi6stZQA6QedTYrtLpZSxGcyuIcEwePFj6ygUUYnA6L87+onfxE4lsfqNjobIehI7PW+eqUv8YXJka5EMVkmoYGCjqoDNJ4WtqaHOzsFWEd+lAKGcfmeihx+K/xwxGoVnZ2g13b66kXo/jFRykmb5Uh9ZA3optXBv8BNYdGoycsJ+34hZCddLoX4kXqo3/iYlxXRtNPnwyT8Y7SejWgGK3ubwiwyMNYPy6gszyvyghHGUwMg4RpxOYdi6vzkBbwf8Fd/cjdzhTs3bpV/KUmYr//hzhsQkfHCyEsuxJU0HGgXqzQyJa0qZEX5IgliI9bYiC1kxg13WdsbkKD9nkbHy+gPQbj/UcQB1h/KtQCPuMe6+M3/w59CqT3j+zwLrx+9lqlXByU8iaZLt1ZqfuHD4Rf6+rKP+TxbYVrn/dkXwr2FlD7UCzwIud/npFMZ/3OVUHf6QmTCK50ZiuQjJoCSHF8dc76VUGljErhAXLOI+5ug4KN2MN54OfFHaEGFT13I+vJkAL9Lc+Jp+xlKY4jMEre2kb+lHUemHmmvK2ILxmrrfApmWV9bbwnHXMtTC4isWnE9txfhFNgaF51g2TpSR/IqAiCRvdoGgj/92/7wz8JBg==" + } + ], + "Route wallet/deleteTransaction should return false when not deleting a transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f7c6bf85-5f42-446c-b127-fcd4077dcdef", + "name": "existingAccount", + "spendingKey": "8b91a476376d7def7a5db2e4cc74d54213b94210eab698f3bc7a5121a956c906", + "viewKey": "3db6a3ab0140021a8e8ac37727b9e426d2e2684ed7b3c13cb27befea04a78361f2e32de664d29af5d6688da28b569276ce408016bce0c0df35601a0d6083ed46", + "incomingViewKey": "2923ffcb0a770ba4e1febe690f3be2cf781042c6227e65a33b9314fd7d2c4d05", + "outgoingViewKey": "1a6649a9026c6bd95a758c4653d92bc1873864b8d96df5eeccd84c9037c9ba41", + "publicAddress": "efc9ad599a974b9113b810855012e4d07e5c76ec5262a081de05cadbb2b4cf3f", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "6b4fc75057816604fffc79d70cd9baaa732fa2476c8be084260cb2d573553009" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:v/C0+lNxuE90GJgCKOYazIlvy+U2APGs+wVplr6CyGc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:cuzMHsViFGG1s+d7SdtxKoXoNM21VFP5o/61OCFvvb8=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724101484308, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAySF5vigNwaUcBefzDh/9DrmS+7Rrd8ZJNGqFEjkJazy1oFzvCAG5ZD5rAPftHr7o92s506p3oxHqnTA6O1jUAG/VWifg4KUKX57rQAzRsC+jaG/7BhQYlg5539pcZvF8W7E8gpfeI0YlI7ds9OyxuxH0f5KZAhaDT0D2tk5y3LgRlWzY3oMsRSm6xO8eEmydneRorBhL0jxD5G5YNnQqFm86lsPXCFjNby1lPNqWzxyP6nS1yK8a+zU/Gqnpt541x0vOnauY+rzhElgNDTUAsAuuXi7wEiZtDMDmc5INr63z0UN7J2qNCykLLMByBSredVra5vqHLMYzDHFAJeVfJkP0hIJIRZVdZze7A19huML6PSxpOkzGZdNTnGIytbBqMifsLhZu5EB49Qk+f4Df1Hf+ZunJDvGfIPSYIQYgOpN5OZ9iWCx8YK5F8Tt6jY18mWj7UIr8W1kT9VdBs5A2EgeHIcJuJiBRgLYi1d42xEAji+gSpQ1hy529n94jNB2CdN50vBff0/WJ5hj1W/Rj22H1aRV9vYw52gSnxzjcMNnuQVgI8iyipQur7vljYYkJzarluBKISqL62QITPIDxuUXGZVLgbbK18i1tLrl3s11OLhfXx47xhElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwHOfghlQZuvd3ZWFIk9uNBOT/wcp2Kylc5QGSObUHCs2982aMRnbTKV9PzMANJlP6UrXHX3nQS1pj5NLWDaAVDg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgUZNPZM48VIhvU2oL9GJuEAi5e90zusgOVVjRj8k6OKtX07xcjgBHM2MYhdnd/zDcREVB7ydgaVF67IaUkBvNPLCqcuEaEFNTlD++J0rZ0m31+sW905Z4z1LqyYD/3G3g/kOP1ho8sllLa7U3ISsgRXs+Aax9DDogZMK+AB8kFQHWaE12vU0ybm4VBK0s4xe6HeakPMe+4+xBx59+6A8Uj/dRgaE8u20oDLmGviARqKHdyKy278hBHfXJM9/7R2VBBYuZzb5MEMl+CrL2gH3jKYx+u+28MkRYMUiRA3/rqV1LxWWix5ozhbX7L2aWMjxRbLhTMOcoaL+u62sbVCPJL/wtPpTcbhPdBiYAijmGsyJb8vlNgDxrPsFaZa+gshnBAAAALh5zUmRv90W2jPooRCJExB2b+vvps4eVXLlQY8U6haWLl5GdJ7m9MNcEgucACxzMPdgYTZKXn2p86spVkMGKLG6C7ZIGLp6XQ/cJmP+MUsjboo2a1pWBTQ7mru/9d26Bq2eZe6S7EAThFkhVvyvd3lMeVTb/zrZqEJuWQz3hl+pGub+8ocfjDud76a+VnCwrq58qpRvSuJWDek2Dc0Cr/yqWtxVnFIiZWPLUMMwM5w6so+tWxrCVAUBxVe05Nh4rAAaS4s31D0TRpyFxKApdWO/jLZ0MLcZHp99LxhYJRP7ZKZVcu7DVJxDhWyCu8Qiiqdi/LpKEu0x/Arhhkg0ihRAHpTl83mKf3hNETP7Rv8SotHfCHXpVbnQ+ZXCXNu/lz9E5vsqnfnEkcncWK4JzE2Nhk4c9xdkJ/Bq1OjMQG3tt4kDC1I5bE0eulWDt+fifXEOh1XRa/UEJgm1g6whNhDpYZCn+INg/nBlfbenn1DwL1mqONURQsdfjDMDEaUET9BKV5pPs0Jug6gkKxq62lb1CW5vVo1aMRvWuyTK3Yi+50+73gDGUmx1YDn1Wm4bPsJ7/yALIEcTkOCXiz4ba2/vE4FlGcG7KE/TniyMQRxmw443N4iWFKDP9oB+VGd1zCUTLE9t4MChHyUEoayozaCxxuq0wEZiH27HQ0g79b2rGQNG2pUNOKGjTFOUkQl3WG5JwOlzZ7diy1ngrgtZZh112Xf9ay/9SPoNM75bLiZC8WWgg4W60E3Y6+LfQtwdsT8wkiSz7Lh1cPyxSm6WJP2qtHd6aplvC6U0Tzw7GZYfGyJhxLSMZjGT/drGsG6KjqnZbDn6zZxkR1+CXxvxweVOSZ0FoB5rpi3kXm+PZ0L7B0pHRupZcp+qAwgmFsJGlygnV8qA3HlXv3V+ZdInkZMteNpZTAvqkpYA0uHhvL8I9WdIe8W4GIIDjZa+MRnt7K/Sle0Mvc4FNWy0m2DaQZsfRfsc03RdyzwCJErsVN9S/ACl9cuOCCmrmAEMcFk1sjk4o+5txVlYw0uCCSW3tdRaByD8D2UCl4GNESSsXPg9aOd3kUaCZFu3ozXj0qhSdvTG2psz5igJGd+gvDbwAWb4fgHgc044MK11HNGlbyMOjGXMu0UYOewF/pvMIuU0FDqiFKII6lkqHnKzGb6rB0Vi/DyLmVKtIR5G1EMfQYQvzuUS3omgNbXbUgNPYiPDdtjsyNCnoT3g438YzYwLzMW87PfxbQr91zayw+JqKbwEPUwPYHxhfmrpovuoDOpN1VS1YaWDsQSMsP3E0N7vY7DSap6Otn/Yvo0B9FvOcae0LSqY5hv/fM+tUbdL8ExD3WtSWv+/MHkTQKvl1NZvB+OmNvyaRVujancJzIEfRVrmWI7eauA/aJdUGcV0zsaXGHHLLWugbXFbHic1pha+kcGenC1CRzP0i8rv1NZyuyZloekvKcz1k/hnHxBRgEZ99KMZc2itHEyR35+2dZw/cTrEUUk5mERbDMgaIDJjcEbPA146fqVX4aTm4y1ro7Fe2sJxGqdPAwqmgLjDhH0moduFz4/2QnqNXcA6Uw4rYxC/RaEuUwZb48REsbm05ZM9DQ==" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "D0D4749863C99A069CE5778049D18438AF3FDBB823F8FEABFC69E6E6FDE23938", + "noteCommitment": { + "type": "Buffer", + "data": "base64:H3+z95bu5SibcW3MeSd/8JV5EWN8dr64T8QmnYbSFAI=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:WJO8qQqSda/5PSZHMepvSmnkpk3jMPFzhJSWMaz6PAw=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1724101485748, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAKfamQt03AlTN7ezYLEIwTyZTmERzm7h3VV2uvUdsSoe06f+UxuaFp7A0w0HllDuN0ab6kbpjsp7EMMixgk3JQazBdWJ9jq1m/qneU8nep5WKp5SB38IWfNjaH9tH13C4nXC69lSGg46LK4wqvQQG3fue4VrzXj+j617W+bq+e3cDX4a1MChp7IBye7OgJRiKD0/P9DxRT6/w4wncZZi8MWP76tIh7G3wk2dHKurnQf+Ve+RW4cMHDNqf/4m1wapeX/nbR7LFpRexxtAfRkxWWHscfrweJsmbcSwDe5GWlUQcXhadkg6PAJD+zV4+LpddAOW+CHYbu072lGwnM7DZFPHVTnDHVKergfW5Vh4G9a1VsFB89c1afvlrrqvZXw9nyZj/Ozx7Dki+EDad21xXatOHOEMnHuHrP+gjViY0lRYycsW5UiseZim/pBvS4YlDTwNrNX09g01q7pQonRr42t8mQbsJyLQt8veMUKo5tncUhetOdb27e1RdPnkKKnP1aKMwc3hihO7QPiaMUSlR9/YHba+gE3MTx4TYFgBOSi5soH29D2ydRLausrCzN+Z+gdPSDIR+wxPAsoewkaNT3ngOPL+fmq5fBs7YoD1M0na+NkwnZqvUvElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwU32KfIQDpXNIzkvETpwBeIlW4VvwQBUu9K3zZmtQlfPU51FGHPkCdkTg8ZZbT9Nv89jd+VOCKZuOh6F0OMjSCQ==" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgUZNPZM48VIhvU2oL9GJuEAi5e90zusgOVVjRj8k6OKtX07xcjgBHM2MYhdnd/zDcREVB7ydgaVF67IaUkBvNPLCqcuEaEFNTlD++J0rZ0m31+sW905Z4z1LqyYD/3G3g/kOP1ho8sllLa7U3ISsgRXs+Aax9DDogZMK+AB8kFQHWaE12vU0ybm4VBK0s4xe6HeakPMe+4+xBx59+6A8Uj/dRgaE8u20oDLmGviARqKHdyKy278hBHfXJM9/7R2VBBYuZzb5MEMl+CrL2gH3jKYx+u+28MkRYMUiRA3/rqV1LxWWix5ozhbX7L2aWMjxRbLhTMOcoaL+u62sbVCPJL/wtPpTcbhPdBiYAijmGsyJb8vlNgDxrPsFaZa+gshnBAAAALh5zUmRv90W2jPooRCJExB2b+vvps4eVXLlQY8U6haWLl5GdJ7m9MNcEgucACxzMPdgYTZKXn2p86spVkMGKLG6C7ZIGLp6XQ/cJmP+MUsjboo2a1pWBTQ7mru/9d26Bq2eZe6S7EAThFkhVvyvd3lMeVTb/zrZqEJuWQz3hl+pGub+8ocfjDud76a+VnCwrq58qpRvSuJWDek2Dc0Cr/yqWtxVnFIiZWPLUMMwM5w6so+tWxrCVAUBxVe05Nh4rAAaS4s31D0TRpyFxKApdWO/jLZ0MLcZHp99LxhYJRP7ZKZVcu7DVJxDhWyCu8Qiiqdi/LpKEu0x/Arhhkg0ihRAHpTl83mKf3hNETP7Rv8SotHfCHXpVbnQ+ZXCXNu/lz9E5vsqnfnEkcncWK4JzE2Nhk4c9xdkJ/Bq1OjMQG3tt4kDC1I5bE0eulWDt+fifXEOh1XRa/UEJgm1g6whNhDpYZCn+INg/nBlfbenn1DwL1mqONURQsdfjDMDEaUET9BKV5pPs0Jug6gkKxq62lb1CW5vVo1aMRvWuyTK3Yi+50+73gDGUmx1YDn1Wm4bPsJ7/yALIEcTkOCXiz4ba2/vE4FlGcG7KE/TniyMQRxmw443N4iWFKDP9oB+VGd1zCUTLE9t4MChHyUEoayozaCxxuq0wEZiH27HQ0g79b2rGQNG2pUNOKGjTFOUkQl3WG5JwOlzZ7diy1ngrgtZZh112Xf9ay/9SPoNM75bLiZC8WWgg4W60E3Y6+LfQtwdsT8wkiSz7Lh1cPyxSm6WJP2qtHd6aplvC6U0Tzw7GZYfGyJhxLSMZjGT/drGsG6KjqnZbDn6zZxkR1+CXxvxweVOSZ0FoB5rpi3kXm+PZ0L7B0pHRupZcp+qAwgmFsJGlygnV8qA3HlXv3V+ZdInkZMteNpZTAvqkpYA0uHhvL8I9WdIe8W4GIIDjZa+MRnt7K/Sle0Mvc4FNWy0m2DaQZsfRfsc03RdyzwCJErsVN9S/ACl9cuOCCmrmAEMcFk1sjk4o+5txVlYw0uCCSW3tdRaByD8D2UCl4GNESSsXPg9aOd3kUaCZFu3ozXj0qhSdvTG2psz5igJGd+gvDbwAWb4fgHgc044MK11HNGlbyMOjGXMu0UYOewF/pvMIuU0FDqiFKII6lkqHnKzGb6rB0Vi/DyLmVKtIR5G1EMfQYQvzuUS3omgNbXbUgNPYiPDdtjsyNCnoT3g438YzYwLzMW87PfxbQr91zayw+JqKbwEPUwPYHxhfmrpovuoDOpN1VS1YaWDsQSMsP3E0N7vY7DSap6Otn/Yvo0B9FvOcae0LSqY5hv/fM+tUbdL8ExD3WtSWv+/MHkTQKvl1NZvB+OmNvyaRVujancJzIEfRVrmWI7eauA/aJdUGcV0zsaXGHHLLWugbXFbHic1pha+kcGenC1CRzP0i8rv1NZyuyZloekvKcz1k/hnHxBRgEZ99KMZc2itHEyR35+2dZw/cTrEUUk5mERbDMgaIDJjcEbPA146fqVX4aTm4y1ro7Fe2sJxGqdPAwqmgLjDhH0moduFz4/2QnqNXcA6Uw4rYxC/RaEuUwZb48REsbm05ZM9DQ==" + } + ] + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/encrypt.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/encrypt.test.ts.fixture new file mode 100644 index 0000000000..b31691823b --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/encrypt.test.ts.fixture @@ -0,0 +1,122 @@ +{ + "Route wallet/encrypt encrypts accounts": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "86003dbd-7d93-472f-879c-21a81fcb4fcc", + "name": "A", + "spendingKey": "810407957cd5d03dc263291acaf61883e3fcd5d0f6d9d33c71e727d02be4980b", + "viewKey": "ee2a5899b2bcf104f3e0c96383ced37dc96a750b616b6111cbbbdb42cd81df6cffdba56303ef07be834051ade514bb55b8fc33e9b5f1f6d8a65d207fa354dca0", + "incomingViewKey": "ae71cad0dab16916dc266816a2eec97e6a56880278a744739f9129697b524401", + "outgoingViewKey": "0cd5582c6a4cc98748957a315e96e87eacad49dd5fc4d1a3fa3e329e5710186b", + "publicAddress": "550e4bab2719759b3ba48eef4164addefb5ab1ea709827a8d2f242a62da4b9c6", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "d68f9ab66a90fb80ee341323ed38eb09fe0ac2fad9c16834f0c41160e6dbf304" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "8f86ead0-93f8-4bd5-9775-2935c1a1e1ed", + "name": "B", + "spendingKey": "acc89c2e183b12b9183f2feaa5d8e1528834da3a18baecddd6ee2d96a8bac2f6", + "viewKey": "a62b094ac064211bfcba30354b4feff5bc4aedd982064952b6724483f7c09c9738f4efe5b87424211600edc193bc07c89be7e8548d5615cc10d5d572faeeed3f", + "incomingViewKey": "02979895d4a3b7219f000100ec029a9919bb2f23c4d7fb1f7b14eef0241b6104", + "outgoingViewKey": "9c1282d1758dec787ced4ada55735ad1cef92535ca80e38c72dadd55230bde85", + "publicAddress": "31ddaa091da2f2384de1355b5861cf6a4458d05e9bcee77e8dce3c62ecf0724e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "9a2f0550b162e559e07df44c4517eb1c8b37930af68b2768d5a6349db8db960d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/encrypt throws if wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "e7b0cdf4-ebf7-4647-ac4a-970c161b8f67", + "name": "A", + "spendingKey": "c8698709f51e8b8b27366ac47773a84e103286a04f95ed5c74861fcc5b14f1e8", + "viewKey": "d660a67d58a6f26435ca6993a4013de10d56a6607b67c0e94fdfb4e7d2392e99b8058e548f657fa72d9bc41ece07dbbd07811444f334b96d824a3029b640cb3c", + "incomingViewKey": "e628ba4f58057825e21dfeefb5b652db4c8c4dead57958d46062261c8188a000", + "outgoingViewKey": "aed6efb86774d5292dd6b6851d6011fb2d76648694748baa1685b93cce2e0980", + "publicAddress": "867706d9da5632afc0cfdf7044c759d9b05579781c3c1743500aa42d6ede1c18", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "547970bb590fde11a35a47cb39cab5f7df609987ea53e566095db73763fa2903" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "b637868f-b7a1-466c-b14e-05f3fc493ad5", + "name": "B", + "spendingKey": "8a80e1564e85b27a572b52b32334e9a26dd917e8054afe9c3df65ab502b5d268", + "viewKey": "a85b813148406d81c5efa836026f9c946e3544de3e11316c9ba352b186c8c9730c89e1faf4863c5c6c85d1bad6774c43cf3d6ed6a5442d218cda74cdad36bc16", + "incomingViewKey": "857e9d97a89b07ffb5eef5774ea6e3a808e2eba90d375be8fa023addb1d50605", + "outgoingViewKey": "5d2d37392995f438b03d7d83842b6565799991ed23438c51a615f3baecc465ac", + "publicAddress": "410c313cec38ce1ab3805104802c133c7353ff1b2584aa71c556215c2caabcee", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "5686d627e789c93c8dfd0f2546e099873a167b65834a4007b3b03728d01d260e" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/importAccount.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/importAccount.test.ts.fixture index f16089c7c0..fd0ddb3eff 100644 --- a/ironfish/src/rpc/routes/wallet/__fixtures__/importAccount.test.ts.fixture +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/importAccount.test.ts.fixture @@ -6,15 +6,15 @@ "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", "noteCommitment": { "type": "Buffer", - "data": "base64:OrFh1gNPebpwyD0EOBJXhvWSWcNIqvOofpmIUdQuMxg=" + "data": "base64:ZXWDsqog2eRQq6WZ70lqE9cVSUINpf1J8m6T/NS5UTQ=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:o67WawihP0SziWZCyKRf8E9IJLHEAKJRxtHegUciYYU=" + "data": "base64:iIFwx36ZjBXyt1DsFBf5Av/sNYw4+Z5ZMXV1m0bNLm4=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1718920385337, + "timestamp": 1726274775730, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -22,7 +22,7 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAKVqg+LxkGFUdgVMSHhIIrp1oG0U1gg3WxwKonNL3I16NB/NqRoZxzc+mlx6IX3v2Ch+S+aHHTzXnVwJoMNsy5KGWbqGPvb1RP+fM/H6mmbCnik9BEtu8uYv12x4XjDqAohBRAPoUG0AjYAAhpQLSXnvQ7bF6grHEj+agByDw45kVAnz+nVhTFp3ODOCH4HGMqylTTG7329MZgUGXXOSIqWKCLYjIpACsiyEsz1Y/XP+54h9oHmUwm2ObkGOLFXb6D0PHxfKTEWg+jJNU6XVGUDY9BVt6SgYyWE+qA8ForJYDgxHly6kPsDy1WMYhAHpLpvSF4oyvwUoVa8qQDmVhtS8k/F7aRjCICP94DaPs/IvvesJkXI/gmp6mBsZyS/hxwQUNPi/OOqUZzDKvVthYcKwTIJcxt2W99bVKv2UnIc9+ay/WZ6m/Tm8zm0EO2lCqZXOer6eR7sQUStydjRgl8TYyHyl4htkK8gsS5hWQarJLIgGClFAYs+zicp/me4Pn8aK+8G1h4BuLApM+TJotvIzbISyMnkOB9c9x1H01T3txQYAeAgoLtLhNbI6Pz6VPNTes9GYWCiCH6FBudyyW+7ug95kJSU54xrOvdkBjHNX6bd17okg8Rklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwDj/3ANmFakpfmgyAW39nTxSIjLTJION9L6HdYthgi2QyZ4ClO1gzlXZKRnMfu8E6JLAtC7M6ZEfe0m+JysV5Ag==" + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAKmTHIfoCqB1LPml+WWhSb5/fnq2hk48D9yavNpWu6VeJkv9RrOYq6mpE0ItseXRXF8J6Nl4s4StLeuv4wLiCrAI+n4gmZX8+b+DEUnHSUDaT6iroKN3gPhYrj2ljrGdPO5BtMIu2OAss1FuOkri17p3ABqHUbGr23HVypBuMacMVmaSpbmpctyz3zmAF+bSq24oCCm7tmQUpuK3tc4YH41QYD5vrETduQmdq0Fln5zeSpH40uEA3GegClTHrQ486PHxekbx2I2CMpC/XSZABupohuoZX81a62qm2t9rni07Y4hJtREUiK4Zt2d3+jj6HRZVnn7SeB0J+NEv6CI4vVZfk3K561jIq/tEUdFWexO5U5YZ8ylhFlDkvd4+P20o1544GTAmy9eFOcIjwkn7JBniZHpjcgZY7+H8h/CHHC95CsU8XT3rR/VLaWhy8CvTTx1PGc+X5XK0a66UfI0Pv4bEKjr+ecB4FXk7Y3DPbe4nXTAKnNoh+EbltJGZMhpGC60OQgJ8vURFVmJ/KYtwMwjyD6YFSaFD7FrsjxTIs76GEHuPsNnvEDkBknJk8ngczOqy8j9hu4/uRtBygXZyLRC00SeyfsX+FSK4xWEfKr5pXYBGu9j8LVUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw/ZIbYQski4H101WQ64c3yvUoZtTFl5YQAyAsxoD2Sce86qNzWDY02qg0qQ9OYQHEKC3h5Gyumwk347ivnSLTCg==" } ] } diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/lock.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/lock.test.ts.fixture new file mode 100644 index 0000000000..37fcf87236 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/lock.test.ts.fixture @@ -0,0 +1,122 @@ +{ + "Route wallet/lock does nothing if the wallet is decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "9374e12f-9da1-4ed0-9992-179a78bbbf44", + "name": "A", + "spendingKey": "a4145ffbb03d8b66f80f27761cff36c1cacb583e5b09086bea79ef46884538fd", + "viewKey": "29b2cc1d84fbd7944127148f7b8a92aed187ebad05c9494b5f92a95e726bdb430f2ffb722ccda7bb2d95da01fd336cce0874c700db44c41d9f4f3955225819ae", + "incomingViewKey": "a58eb026316a992d578632f05667bc63a3e84365662758ba64307fec8067e005", + "outgoingViewKey": "6ea17f4ed69dbaa60e771df2efa46ebad3f3a683cc660be74d96eb09b9f304a2", + "publicAddress": "59d05112781447a20a9129f86ca31a8f9eae4a28da238d76b3e73dbbf86517a6", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "602989a565416bd3fc015a7bc3a085c8911c4eea8484854614fd02528ee9e108" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "adb72bdc-4216-4d8d-96a5-a42209243b73", + "name": "B", + "spendingKey": "74786da4227610d50c99525e1e63e5fdfa647952cbe29185c37e72a508a90463", + "viewKey": "b62c45bee3cff9769378da7bc809f822778fb1d9ef799ac4f695476db9d48316e9cb447bcff80e4ea54573b59b308a15666b0579aed21430547d0bd845b2de19", + "incomingViewKey": "a6936e0c99b1172ff9f76dfafff18694ce9e57c02bf6a632085991e4982c9003", + "outgoingViewKey": "69846586a824f6a927c4465bb90520794b25574ffd92f93147a75eba9657c554", + "publicAddress": "7f990dd2606ab6e718a19e87fe848a8e9ba917424e9f07ea87ec4704f0651ee4", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "517cc469b43f901a89454df9d287bda126f775b15172424f4fcfbf459cafc800" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/lock locks the wallet": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "2a75f388-bce2-4e15-aec8-265c8413c501", + "name": "A", + "spendingKey": "361761823e0cc1af973072d48b013216d15468c5946a8ff7200249a024499cfb", + "viewKey": "466f05770f70e964393210e2b0d71bb3315c61a761d87597dfdca24855b8bd1ccb3dcc69b24019f92d73d4312ccb3c8ee37cde3b976aed1f4a2ab911fa07a3e4", + "incomingViewKey": "ec2d7f8626701a8f28b75306e65544d1158e3b4ff544023a9b5b1220d918a504", + "outgoingViewKey": "25e124b435374aff46a01201be3ccfc1afbbab70c13465ac82af38778a6cd950", + "publicAddress": "9735e4531c754982e6a59bfbdbfa43c6236a3fae37b281da2e1d88213831be66", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0d441f53c61cd43a56955fcef78d6c1cb52c4b463e16812752b7c5e777ff240a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "747df61f-cb52-4419-aa73-857c9f0bfce5", + "name": "B", + "spendingKey": "c0f9e1b439485b03d19d25eb06937201dea447cff67b5483e556fec4816c3be6", + "viewKey": "b2d78fb858d9cbf950270923f18c8de6c3b2b60e66e3658951f86ed0b65712e1640b556c858699c26de7fdc11b60131fbe1d9ed947493d08ea34d30193c1fcb3", + "incomingViewKey": "e9725a3336d4f65346b4e5c68b7e6f50a2c00d015be64d8e1cc2a6fb8ce75a01", + "outgoingViewKey": "5a17bdeefebe53add6747ae4e3b32c0229d6b9023953a2f1bf1d0664cc36506b", + "publicAddress": "257deef75e2eb67584ec560c44fb06d254f2d5576eb43e1788e50d65b80ccd27", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "456290795a50d3602d12aa856c78b1a09098f971cc05bbabd905f42008b95e04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__fixtures__/unlock.test.ts.fixture b/ironfish/src/rpc/routes/wallet/__fixtures__/unlock.test.ts.fixture new file mode 100644 index 0000000000..b976fc73a3 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__fixtures__/unlock.test.ts.fixture @@ -0,0 +1,182 @@ +{ + "Route wallet/unlock does nothing if the wallet is decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f9a7e6ae-d45b-4015-a42b-f57514584dbf", + "name": "A", + "spendingKey": "68d0226a9b4875fe2a6a67dd62fa9d4001a9e7450679358ca102e19ec0ee9e28", + "viewKey": "1fc4232036da00819a0d596b899c47b210f19b7d1adb580c0d7e7da79695ff64ba010b4b7430b18c3cc6863e1b6f63ee8a4d9a157be39e90cc9c1325f0ec2865", + "incomingViewKey": "f6e111a6d3436b9f62aa233de637cff50a5c1004bb5c185f533c88c671ac7f03", + "outgoingViewKey": "f3bc0a30d0acea319d1ba4287372d8c15395fbdf2bf9637fba6327c4d9b63e5d", + "publicAddress": "c97a6caf06c0a73c7bddc88637acb718912278bd29b6baa444cc99587c2bc172", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "a193591a4b6f2923670fcf2401a6cd078d5afd6aa1ee08e68b64205e0ed6330a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "2da0c285-bd6a-4d67-b2d3-1198e33959c0", + "name": "B", + "spendingKey": "a57081cebe037dbb959681d13043c5c3b5f704bb1dfcaef7ccffc6d518504b41", + "viewKey": "aef088f4000312c703b5d57da6271a848b2610d1124269e142714e957f33b045fab63ae3bf5ee5bef2ad3a0c0aff505d0839909cf5491ad095456a8de012b70e", + "incomingViewKey": "fcd6f4d1d02199744d5041b69848a9c611ea8d3bf28f23d9e741afd058e9fb06", + "outgoingViewKey": "a0303aa1820a398b62781c3fe915414e3b1d2d5248d03342d8e375442a6f6fbb", + "publicAddress": "01d70038bb5cb4dfc6a9ea1355d7d2e0937cadb419590e3f3b8ed9ab044ff29a", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1f489a416d31b598f56b357206fceac690d31b8150912ce22bca1bcdc64a270a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/unlock throws if wallet decryption fails": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "597c39fc-0ef6-4a68-af95-e6f750ecd55e", + "name": "A", + "spendingKey": "62a2a8a953f0f8a94a137b1a2d78397f45262c929d2e0a2337644ac1bff89e52", + "viewKey": "7005f9c44311d9d9088ada9d5bfedf8c092fcddbb2ce5c2c0cec6d1b1a3a42358c9658d664a2cc4107b3791c440797862c675856c4cf7c92f608e5f043de8f6b", + "incomingViewKey": "7cc00267f1b60ccb7f83c7ab68d1391842a869c3cb247eafbd73d3f92c222801", + "outgoingViewKey": "9ee6d7bba6b3a935e69ff74a4f2d5faf6725ec8580d09312a5f14d1a451b4425", + "publicAddress": "59361460ad98475b51effed3d7b7bdb6fcb9986841ceb3474abeb7b412f8886b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "7ba509dc63bc599f778568c99f13a62a0e5a22bab958533db1538aff17daf007" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "49333fd0-1500-4bfb-ae05-a779ea9c2e55", + "name": "B", + "spendingKey": "7151b4531fe3e196ca8eb4e35fa5c54216eadbb4f0a77979a68d8d9bc6c6384a", + "viewKey": "ad31c32965c9d02a9925473f8b5326747aabe523ea84968878f80bfd67e257ecdff668db58e215cd48fc9a9d57276c3f621e570b7b7c330362b09b138cd2745c", + "incomingViewKey": "aa8e8848fac84f52bccb95889e6e8bda170a9d170c1f80005f60b81802349a00", + "outgoingViewKey": "6d2ab57ca77aa68644a5a9157b1594f9e7a97f0629c11a1fe90d4b2c3f9c874f", + "publicAddress": "7d1534f616082473aee55a872dfc1392ec606c09fad5c8a3065dd44323ee7639", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0e21aaef7c8c0ff8ba997e5c6a01368df4769fad8b021863db1689af6af8e502" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Route wallet/unlock unlocks the wallet with the correct passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "050c72be-0d59-4f4c-8400-29363febff73", + "name": "A", + "spendingKey": "8d92b5c7c8479998e070a4d1aad62b328e4ec4e1073bdd240c0f3408e614a088", + "viewKey": "241bbe035cd6ba6cf4bea1a70d9e0c65da5abc192309040691d72b5f3072ebbca650139c67ae85084fbb6587d23b57f30d006e59d656f1e07d88bcd135592d4b", + "incomingViewKey": "ff2946c5714f74658b6fdab386ba606b2f342d431cfb207872060169d2b3f605", + "outgoingViewKey": "65077e7e22c13885ee78f2757cebad8352b6eeec7ef1ba46a778b27b007e6941", + "publicAddress": "56e37f6f80a2d84c575e9507a2a3eb3365a9a85e170ea31ccf6ae00c61aa41c9", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c8dcfc8ffaa7f72c406a8ce102922cb5a17c8896391c0c09d03f76f931857a0d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "b3d877f2-98c9-48fe-a05d-50ca51b16534", + "name": "B", + "spendingKey": "5a3a3e126577611ab0fbbc421ff428f32fcb672f6541cc18dc0a78a392d6bfbf", + "viewKey": "19304c39803ae2dbd54f521c832620960b82a7cb862281216c768c295b41ad493d340dde3b60904b06afdce789f6f02d7baf2534a69342eb7bf794979513253a", + "incomingViewKey": "b5534c304f50008704173871ef8df9f3cd59a8598fa702371479f2c08fac6e03", + "outgoingViewKey": "a9eb4e2a74ff89abc47479d5dfd2808aeba619548765eed7d1e87c562ecd9420", + "publicAddress": "ba7ccf4ece289fbd56a7aee03790620e003185e7d6164b7bbea33ecb663779ef", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1d1519a3b10a0307ff2eb34d6c9d3064cd3c620adf1961ee7998ba9f158d1805" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/2p6p0_json_multisig.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/2p6p0_json_multisig.txt new file mode 100644 index 0000000000..fa5508d71a --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/2p6p0_json_multisig.txt @@ -0,0 +1 @@ +{"version":4,"name":"multisig-test-0","spendingKey":null,"viewKey":"293d96b68049736f3cf75eae4b34e0cb7ef4123e0a932d0950ed29993c8f320732217bef15f3b4fb91604456e36d473036b2b71c55ae2792154164849894cfb2","incomingViewKey":"cf53348203c66c3196580d47e32f57e0c9096b4a1e15d5ddc7f4d0c51374d703","outgoingViewKey":"3ceb1aae767b95524ef4352c95ed5e45fb654d9466d22c00157723a3f03d1fb2","publicAddress":"4d740a396b33cea2755342847a3869c1f7b693f5b3839298344c9b0548151c98","createdAt":{"hash":"000000df3bee7c0486981e1c41776bd2e3272a96bf2ca65f5b77a855b8effdf8","sequence":66},"multisigKeys":{"publicKeyPackage":"a600000000c3d2051e02fa165dea65cfb13778b4b6b2ef4fad6045f1ac72e33d122c303ff08b1cf101067bf9c0e2f1cd4b422e930531173f2baaf865b61afaecffcfae20354445635f57a38ba803ede7410f01bba3e18cc043ad504690c88dff3b45d2ba64d05c92c50a23caf37a66cf7a18bfb7390b94fc508a5c8c5027c4f1b39f4f0336f9740a8a69293d96b68049736f3cf75eae4b34e0cb7ef4123e0a932d0950ed29993c8f32070200000072c60942feac595aa83bb0856c264dfc070b8f3c765ca9434054fde21a2806938184b7c5421c5efa9e8f4ba3295ffc229fa05925f08f6ae9837ba4ac6b720a2a44a10c8e761faa2534149c33c7864c4b780d4d83409e5ca3d9428f1f09b0b0476137feb8a8f73cb3d2515110f1b82e88a2af2708a2961ef6c9e15541e288dcad03723729d1b6af022a4c5d67e126a3a79700d5b346577d2ecb1d3b543f4f4f18f2bb43850fb4fd8b554732a11de1b8f0ab65717f01b79c8fd5b459507a65e78c5432454de9de1722f33846ce2bead0b9ad50290edb96b065f2cbc80762518d39a209de1c3e7d998314b3fc9380cdd3b98bd363ec486317bd1e72b8628e61163403040200","identity":"723729d1b6af022a4c5d67e126a3a79700d5b346577d2ecb1d3b543f4f4f18f2bb43850fb4fd8b554732a11de1b8f0ab65717f01b79c8fd5b459507a65e78c5432454de9de1722f33846ce2bead0b9ad50290edb96b065f2cbc80762518d39a209de1c3e7d998314b3fc9380cdd3b98bd363ec486317bd1e72b8628e6116340304"},"proofAuthorizingKey":"c1ecf0f5baa2656f358154decb9011383dccbee08e06f1d9d308d992d03fcf0a"} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/decrypt.test.ts b/ironfish/src/rpc/routes/wallet/decrypt.test.ts new file mode 100644 index 0000000000..e347ebecb9 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/decrypt.test.ts @@ -0,0 +1,64 @@ +/* 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 { useAccountFixture } from '../../../testUtilities' +import { createRouteTest } from '../../../testUtilities/routeTest' +import { RPC_ERROR_CODES } from '../../adapters/errors' + +describe('Route wallet/encrypt', () => { + const routeTest = createRouteTest() + + it('decrypts accounts', async () => { + const passphrase = 'foobar' + + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase }) + + let status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + await routeTest.client.wallet.decrypt({ passphrase }) + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(false) + expect(status.content.locked).toBe(false) + }) + + it('throws if wallet is already decrypted', async () => { + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await expect(routeTest.client.wallet.decrypt({ passphrase: 'foobar' })).rejects.toThrow( + expect.objectContaining({ + message: expect.any(String), + status: 400, + code: RPC_ERROR_CODES.WALLET_ALREADY_DECRYPTED, + }), + ) + }) + + it('throws if wallet decryption fails', async () => { + const passphrase = 'foobar' + const invalidPassphrase = 'baz' + + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase }) + + let status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + await expect( + routeTest.client.wallet.decrypt({ passphrase: invalidPassphrase }), + ).rejects.toThrow('Request failed (400) error: Failed to decrypt wallet') + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/decrypt.ts b/ironfish/src/rpc/routes/wallet/decrypt.ts new file mode 100644 index 0000000000..0e944b8538 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/decrypt.ts @@ -0,0 +1,41 @@ +/* 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 * as yup from 'yup' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' + +export type DecryptWalletRequest = { passphrase: string } +export type DecryptWalletResponse = undefined + +export const DecryptWalletRequestSchema: yup.ObjectSchema = yup + .object({ + passphrase: yup.string().defined(), + }) + .defined() + +export const DecryptWalletResponseSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +routes.register( + `${ApiNamespace.wallet}/decrypt`, + DecryptWalletRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const encrypted = await context.wallet.accountsEncrypted() + if (!encrypted) { + throw new RpcValidationError( + 'Wallet is already decrypted', + 400, + RPC_ERROR_CODES.WALLET_ALREADY_DECRYPTED, + ) + } + + await context.wallet.decrypt(request.data.passphrase) + request.end() + }, +) diff --git a/ironfish/src/rpc/routes/wallet/deleteTransaction.test.ts b/ironfish/src/rpc/routes/wallet/deleteTransaction.test.ts new file mode 100644 index 0000000000..2625414dfb --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/deleteTransaction.test.ts @@ -0,0 +1,65 @@ +/* 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 { useAccountFixture, useMinerBlockFixture, useTxFixture } from '../../../testUtilities' +import { createRouteTest } from '../../../testUtilities/routeTest' + +describe('Route wallet/deleteTransaction', () => { + const routeTest = createRouteTest() + + it('should return true when deleting a transaction', async () => { + const accountA = await useAccountFixture(routeTest.node.wallet, 'existingAccount') + + const block2 = await useMinerBlockFixture( + routeTest.chain, + undefined, + accountA, + routeTest.node.wallet, + ) + await expect(routeTest.node.chain).toAddBlock(block2) + await routeTest.node.wallet.scan() + + const transaction = await useTxFixture(routeTest.node.wallet, accountA, accountA) + + const response = await routeTest.client.wallet.deleteTransaction({ + hash: transaction.hash().toString('hex'), + }) + + expect(response.status).toBe(200) + expect(response.content.deleted).toBe(true) + }) + + it('should return false when not deleting a transaction', async () => { + const accountA = await useAccountFixture(routeTest.node.wallet, 'existingAccount') + + const block2 = await useMinerBlockFixture( + routeTest.chain, + undefined, + accountA, + routeTest.node.wallet, + ) + await expect(routeTest.node.chain).toAddBlock(block2) + await routeTest.node.wallet.scan() + + const transaction = await useTxFixture(routeTest.node.wallet, accountA, accountA) + + const block3 = await useMinerBlockFixture( + routeTest.node.chain, + undefined, + accountA, + routeTest.node.wallet, + [transaction], + ) + await routeTest.node.chain.addBlock(block3) + + await routeTest.node.wallet.scan() + + const response = await routeTest.client.wallet.deleteTransaction({ + hash: transaction.hash().toString('hex'), + }) + + expect(response.status).toBe(200) + expect(response.content.deleted).toBe(false) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/deleteTransaction.ts b/ironfish/src/rpc/routes/wallet/deleteTransaction.ts new file mode 100644 index 0000000000..24d0ab4f94 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/deleteTransaction.ts @@ -0,0 +1,42 @@ +/* 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 * as yup from 'yup' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' + +export type DeleteTransactionRequest = { + hash: string +} + +export type DeleteTransactionResponse = { + deleted: boolean +} + +export const DeleteTransactionRequestSchema: yup.ObjectSchema = yup + .object({ + hash: yup.string().defined(), + }) + .defined() +export const DeleteTransactionResponseSchema: yup.ObjectSchema = yup + .object({ + deleted: yup.boolean().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/deleteTransaction`, + DeleteTransactionRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const hash = Buffer.from(request.data.hash, 'hex') + + const deleted = await context.wallet.deleteTransaction(hash) + + request.end({ + deleted, + }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/encrypt.test.ts b/ironfish/src/rpc/routes/wallet/encrypt.test.ts new file mode 100644 index 0000000000..9c2ca962c1 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/encrypt.test.ts @@ -0,0 +1,37 @@ +/* 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 { useAccountFixture } from '../../../testUtilities' +import { createRouteTest } from '../../../testUtilities/routeTest' +import { RPC_ERROR_CODES } from '../../adapters/errors' + +describe('Route wallet/encrypt', () => { + const routeTest = createRouteTest() + + it('encrypts accounts', async () => { + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase: 'foobar' }) + + const status = await routeTest.client.wallet.getAccountsStatus() + + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + }) + + it('throws if wallet is encrypted', async () => { + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase: 'foobar' }) + + await expect(routeTest.client.wallet.encrypt({ passphrase: 'foobar' })).rejects.toThrow( + expect.objectContaining({ + message: expect.any(String), + status: 400, + code: RPC_ERROR_CODES.WALLET_ALREADY_ENCRYPTED, + }), + ) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/encrypt.ts b/ironfish/src/rpc/routes/wallet/encrypt.ts new file mode 100644 index 0000000000..9e71e1912d --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/encrypt.ts @@ -0,0 +1,41 @@ +/* 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 * as yup from 'yup' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' + +export type EncryptWalletRequest = { passphrase: string } +export type EncryptWalletResponse = undefined + +export const EncryptWalletRequestSchema: yup.ObjectSchema = yup + .object({ + passphrase: yup.string().defined(), + }) + .defined() + +export const EncryptWalletResponseSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +routes.register( + `${ApiNamespace.wallet}/encrypt`, + EncryptWalletRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + const encrypted = await context.wallet.accountsEncrypted() + if (encrypted) { + throw new RpcValidationError( + 'Wallet is already encrypted', + 400, + RPC_ERROR_CODES.WALLET_ALREADY_ENCRYPTED, + ) + } + + await context.wallet.encrypt(request.data.passphrase) + request.end() + }, +) diff --git a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts index 747de34c8a..285f33ff84 100644 --- a/ironfish/src/rpc/routes/wallet/exportAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/exportAccount.test.ts @@ -127,10 +127,10 @@ describe('Route wallet/exportAccount', () => { ), ) - const multisigSecret = await routeTest.node.wallet.walletDb.getMultisigSecret( + const multisigIdentity = await routeTest.node.wallet.walletDb.getMultisigIdentity( Buffer.from(participants[0].identity, 'hex'), ) - Assert.isNotUndefined(multisigSecret) + Assert.isNotUndefined(multisigIdentity) // Initialize the group though TDK and import one of the accounts generated const trustedDealerPackage = ( @@ -157,6 +157,9 @@ describe('Route wallet/exportAccount', () => { }) expect(response.status).toBe(200) + + Assert.isNotUndefined(multisigIdentity.secret) + expect(JSON.parse(response.content.account)).toMatchObject({ name: accountNames[0], spendingKey: null, @@ -165,7 +168,7 @@ describe('Route wallet/exportAccount', () => { outgoingViewKey: trustedDealerPackage.outgoingViewKey, publicAddress: trustedDealerPackage.publicAddress, multisigKeys: { - secret: multisigSecret.secret.toString('hex'), + secret: multisigIdentity.secret.toString('hex'), publicKeyPackage: trustedDealerPackage.publicKeyPackage, }, }) @@ -185,10 +188,10 @@ describe('Route wallet/exportAccount', () => { ), ) - const multisigSecret = await routeTest.node.wallet.walletDb.getMultisigSecret( + const multisigIdentity = await routeTest.node.wallet.walletDb.getMultisigIdentity( Buffer.from(participants[0].identity, 'hex'), ) - Assert.isNotUndefined(multisigSecret) + Assert.isNotUndefined(multisigIdentity) // Initialize the group though TDK and import one of the accounts generated const trustedDealerPackage = ( diff --git a/ironfish/src/rpc/routes/wallet/getAccounts.ts b/ironfish/src/rpc/routes/wallet/getAccounts.ts index 608beb46f9..b0d063ac12 100644 --- a/ironfish/src/rpc/routes/wallet/getAccounts.ts +++ b/ironfish/src/rpc/routes/wallet/getAccounts.ts @@ -14,6 +14,7 @@ export type GetAccountsResponse = { accounts: string[] } export const GetAccountsRequestSchema: yup.ObjectSchema = yup .object({ default: yup.boolean().optional(), + displayName: yup.boolean().optional(), }) .notRequired() .default({}) diff --git a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts index d91a976566..b41d202c11 100644 --- a/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts +++ b/ironfish/src/rpc/routes/wallet/getAccountsStatus.ts @@ -12,6 +12,8 @@ export type GetAccountsStatusRequest = Record | undefined export type GetAccountsStatusResponse = { accounts: RpcAccountStatus[] + encrypted: boolean + locked: boolean } export const GetAccountsStatusRequestSchema: yup.ObjectSchema = yup @@ -22,6 +24,8 @@ export const GetAccountsStatusRequestSchema: yup.ObjectSchema = yup .object({ accounts: yup.array(RpcAccountStatusSchema).defined(), + encrypted: yup.boolean().defined(), + locked: yup.boolean().defined(), }) .defined() @@ -35,6 +39,10 @@ routes.register serializeRpcAccountStatus(node.wallet, account)), ) - request.end({ accounts }) + request.end({ + accounts, + encrypted: await node.wallet.accountsEncrypted(), + locked: node.wallet.locked, + }) }, ) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index 1948e81d07..549fbf4817 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -4,15 +4,22 @@ import { generateKey, LanguageCode, multisig, spendingKeyToWords } from '@ironfish/rust-nodejs' import fs from 'fs' import path from 'path' +import { Assert } from '../../../assert' import { createTrustedDealerKeyPackages, useMinerBlockFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { JsonEncoder } from '../../../wallet' +import { + decodeAccountImport, + isMultisigHardwareSignerImport, + isMultisigSignerImport, +} from '../../../wallet/exporter' import { AccountFormat, encodeAccountImport } from '../../../wallet/exporter/account' import { AccountImport } from '../../../wallet/exporter/accountImport' import { Bech32Encoder } from '../../../wallet/exporter/encoders/bech32' import { Bech32JsonEncoder } from '../../../wallet/exporter/encoders/bech32json' import { encryptEncodedAccount } from '../../../wallet/exporter/encryption' -import { RpcClient } from '../../clients' +import { RPC_ERROR_CODES } from '../../adapters' +import { RpcClient, RpcRequestError } from '../../clients' describe('Route wallet/importAccount', () => { const routeTest = createRouteTest(true) @@ -51,7 +58,7 @@ describe('Route wallet/importAccount', () => { }) it('should import a multisig account that has no spending key', async () => { - const trustedDealerPackages = createTrustedDealerKeyPackages() + const { dealer: trustedDealerPackages } = createTrustedDealerKeyPackages() const account: AccountImport = { version: 1, @@ -320,6 +327,24 @@ describe('Route wallet/importAccount', () => { expect(response.content.name).not.toBeNull() await routeTest.client.wallet.removeAccount({ account: testCaseFile }) + + const account = decodeAccountImport(testCase, { + name: testCaseFile, + }) + + if (account.multisigKeys && isMultisigHardwareSignerImport(account.multisigKeys)) { + await routeTest.node.wallet.walletDb.deleteMultisigIdentity( + Buffer.from(account.multisigKeys.identity, 'hex'), + ) + } + + if (account.multisigKeys && isMultisigSignerImport(account.multisigKeys)) { + await routeTest.node.wallet.walletDb.deleteMultisigIdentity( + new multisig.ParticipantSecret(Buffer.from(account.multisigKeys.secret, 'hex')) + .toIdentity() + .serialize(), + ) + } } }) @@ -370,7 +395,7 @@ describe('Route wallet/importAccount', () => { const secret = new multisig.ParticipantSecret(Buffer.from(key, 'hex')) const identity = secret.toIdentity() - await routeTest.node.wallet.walletDb.putMultisigSecret(identity.serialize(), { + await routeTest.node.wallet.walletDb.putMultisigIdentity(identity.serialize(), { secret: secret.serialize(), name: testCaseFile, }) @@ -412,4 +437,209 @@ describe('Route wallet/importAccount', () => { const accountHead = await account?.getHead() expect(accountHead?.sequence).toEqual(createdAtSequence - 1) }) + + it('should not import account with duplicate name', async () => { + const name = 'duplicateNameTest' + const spendingKey = generateKey().spendingKey + + await routeTest.client.wallet.importAccount({ + account: spendingKey, + name, + rescan: false, + }) + + try { + await routeTest.client.wallet.importAccount({ + account: spendingKey, + name, + rescan: false, + }) + } catch (e: unknown) { + if (!(e instanceof RpcRequestError)) { + throw e + } + expect(e.status).toBe(400) + expect(e.code).toBe(RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) + } + + expect.assertions(2) + }) + + it('should not import multisig account with secret with the same identity name', async () => { + const name = 'duplicateIdentityNameTest' + + const { + dealer: trustedDealerPackages, + secrets, + identities, + } = createTrustedDealerKeyPackages() + + await routeTest.node.wallet.walletDb.putMultisigIdentity( + Buffer.from(identities[0], 'hex'), + { + secret: secrets[0].serialize(), + name, + }, + ) + + const indentityCountBefore = (await routeTest.client.wallet.multisig.getIdentities()) + .content.identities.length + + const account: AccountImport = { + version: 1, + name, + viewKey: trustedDealerPackages.viewKey, + incomingViewKey: trustedDealerPackages.incomingViewKey, + outgoingViewKey: trustedDealerPackages.outgoingViewKey, + publicAddress: trustedDealerPackages.publicAddress, + spendingKey: null, + createdAt: null, + proofAuthorizingKey: trustedDealerPackages.proofAuthorizingKey, + multisigKeys: { + publicKeyPackage: trustedDealerPackages.publicKeyPackage, + keyPackage: trustedDealerPackages.keyPackages[1].keyPackage.toString(), + secret: secrets[1].serialize().toString('hex'), + }, + } + + try { + await routeTest.client.wallet.importAccount({ + account: new JsonEncoder().encode(account), + name, + rescan: false, + }) + } catch (e: unknown) { + if (!(e instanceof RpcRequestError)) { + throw e + } + + /** + * These assertions ensures that we cannot import multiple identities with the same name. + * This is done by creating an identity, storing it and attempting to import another identity but give it the same name. + */ + expect(e.status).toBe(400) + expect(e.code).toBe(RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME) + } + + if (account.multisigKeys && isMultisigSignerImport(account.multisigKeys)) { + account.multisigKeys.secret = secrets[0].serialize().toString('hex') + } else { + throw new Error('Invalid multisig keys') + } + + const response = await routeTest.client.wallet.importAccount({ + account: new JsonEncoder().encode(account), + name: 'account2', + rescan: false, + }) + + expect(response.status).toBe(200) + expect(response.content.name).toEqual('account2') + + const identitiesAfter = (await routeTest.client.wallet.multisig.getIdentities()).content + .identities + const newIdentity = identitiesAfter.find((identity) => identity.name === name) + + /** + * These assertions ensure that if we try to import an identity with the same secret but different name, it will pass. + * However, the identity name will remain the same as the original identity that was imported first. + */ + expect(identitiesAfter.length).toBe(indentityCountBefore) + expect(newIdentity).toBeDefined() + expect(newIdentity?.name).toBe(name) + + expect.assertions(7) + }) + + it('should not import hardware multisig account with same identity name', async () => { + const name = 'duplicateIdentityNameTest' + + const { + dealer: trustedDealerPackages, + secrets, + identities, + } = createTrustedDealerKeyPackages() + + const identity = identities[0] + const nextIdentity = identities[1] + + await routeTest.node.wallet.walletDb.putMultisigIdentity(Buffer.from(identity, 'hex'), { + secret: secrets[0].serialize(), + name, + }) + + const account: AccountImport = { + version: 1, + name, + viewKey: trustedDealerPackages.viewKey, + incomingViewKey: trustedDealerPackages.incomingViewKey, + outgoingViewKey: trustedDealerPackages.outgoingViewKey, + publicAddress: trustedDealerPackages.publicAddress, + proofAuthorizingKey: trustedDealerPackages.proofAuthorizingKey, + spendingKey: null, + createdAt: null, + multisigKeys: { + publicKeyPackage: trustedDealerPackages.publicKeyPackage, + identity: nextIdentity, + }, + } + + try { + await routeTest.client.wallet.importAccount({ + account: new JsonEncoder().encode(account), + name, + rescan: false, + }) + } catch (e: unknown) { + if (!(e instanceof RpcRequestError)) { + throw e + } + + expect(e.status).toBe(400) + expect(e.code).toBe(RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME) + } + + expect.assertions(2) + }) + + it('should not modify existing identity if a new one is being imported with a different name', async () => { + const { dealer: trustedDealerPackages, identities } = createTrustedDealerKeyPackages() + + const identity = identities[0] + + await routeTest.node.wallet.walletDb.putMultisigIdentity(Buffer.from(identity, 'hex'), { + name: 'existingIdentity', + }) + + const account: AccountImport = { + version: 1, + name: 'newIdentity', + viewKey: trustedDealerPackages.viewKey, + incomingViewKey: trustedDealerPackages.incomingViewKey, + outgoingViewKey: trustedDealerPackages.outgoingViewKey, + publicAddress: trustedDealerPackages.publicAddress, + proofAuthorizingKey: trustedDealerPackages.proofAuthorizingKey, + spendingKey: null, + createdAt: null, + multisigKeys: { + publicKeyPackage: trustedDealerPackages.publicKeyPackage, + identity: identity, + }, + } + + const response = await routeTest.client.wallet.importAccount({ + account: new JsonEncoder().encode(account), + name: 'newIdentity', + rescan: false, + }) + + expect(response.status).toBe(200) + expect(response.content.name).toEqual('newIdentity') + + const existingIdentity = await routeTest.wallet.walletDb.getMultisigIdentity( + Buffer.from(identity, 'hex'), + ) + Assert.isNotUndefined(existingIdentity) + expect(existingIdentity.name).toEqual('existingIdentity') + }) }) diff --git a/ironfish/src/rpc/routes/wallet/importAccount.ts b/ironfish/src/rpc/routes/wallet/importAccount.ts index 0663381257..165ed8b410 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import * as yup from 'yup' import { DecodeInvalidName } from '../../../wallet' -import { DuplicateAccountNameError } from '../../../wallet/errors' +import { DuplicateAccountNameError, DuplicateIdentityNameError } from '../../../wallet/errors' import { decodeAccountImport } from '../../../wallet/exporter/account' import { decryptEncodedAccount } from '../../../wallet/exporter/encryption' import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters' @@ -70,6 +70,9 @@ routes.register( isDefaultAccount, }) } catch (e) { + if (e instanceof DuplicateIdentityNameError) { + throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME) + } if (e instanceof DuplicateAccountNameError) { throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) } else if (e instanceof DecodeInvalidName) { diff --git a/ironfish/src/rpc/routes/wallet/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index 775f5ebbe5..7ab200afc7 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -9,7 +9,10 @@ export * from './burnAsset' export * from './create' export * from './createAccount' export * from './createTransaction' +export * from './decrypt' +export * from './deleteTransaction' export * from './estimateFeeRates' +export * from './encrypt' export * from './exportAccount' export * from './getAccountNotesStream' export * from './getAccountStatus' @@ -28,6 +31,7 @@ export * from './getPublicKey' export * from './getTransactionNotes' export * from './getUnsignedTransactionNotes' export * from './importAccount' +export * from './lock' export * from './mintAsset' export * from './multisig' export * from './postTransaction' @@ -42,5 +46,6 @@ export * from './setAccountHead' export * from './setScanning' export * from './signTransaction' export * from './types' +export * from './unlock' export * from './use' export * from './useAccount' diff --git a/ironfish/src/rpc/routes/wallet/lock.test.ts b/ironfish/src/rpc/routes/wallet/lock.test.ts new file mode 100644 index 0000000000..e668959918 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/lock.test.ts @@ -0,0 +1,51 @@ +/* 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 { useAccountFixture } from '../../../testUtilities' +import { createRouteTest } from '../../../testUtilities/routeTest' + +describe('Route wallet/lock', () => { + const routeTest = createRouteTest() + + it('does nothing if the wallet is decrypted', async () => { + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.lock() + + const status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(false) + expect(status.content.locked).toBe(false) + }) + + it('locks the wallet', async () => { + const passphrase = 'foobar' + + const accountA = await useAccountFixture(routeTest.node.wallet, 'A') + const accountB = await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase }) + + let status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + await routeTest.client.wallet.unlock({ passphrase }) + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(false) + + let decryptedAccounts = await routeTest.client.wallet.getAccounts() + expect(decryptedAccounts.content.accounts.sort()).toEqual([accountA.name, accountB.name]) + + await routeTest.client.wallet.lock() + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + decryptedAccounts = await routeTest.client.wallet.getAccounts() + expect(decryptedAccounts.content.accounts).toHaveLength(0) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/lock.ts b/ironfish/src/rpc/routes/wallet/lock.ts new file mode 100644 index 0000000000..2ea9c284d9 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/lock.ts @@ -0,0 +1,28 @@ +/* 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 * as yup from 'yup' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' + +export type LockWalletRequest = undefined +export type LockWalletResponse = undefined + +export const LockWalletRequestSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +export const LockWalletResponseSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +routes.register( + `${ApiNamespace.wallet}/lock`, + LockWalletRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + await context.wallet.lock() + request.end() + }, +) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts index 1390a84c0e..546ba09e0d 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts @@ -45,7 +45,7 @@ describe('Route wallet/multisig/createParticipant', () => { const name = 'identity' const response = await routeTest.client.wallet.multisig.createParticipant({ name }) - const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecret( + const secretValue = await routeTest.node.wallet.walletDb.getMultisigIdentity( Buffer.from(response.content.identity, 'hex'), ) Assert.isNotUndefined(secretValue) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts index 552812283f..6d19f58776 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.test.ts @@ -33,7 +33,7 @@ describe('Route multisig/dkg/round1', () => { participantName, ) Assert.isNotUndefined(secretValue) - const secret = new multisig.ParticipantSecret(secretValue.secret) + const secret = new multisig.ParticipantSecret(secretValue) secret.decryptData(Buffer.from(response.content.round1SecretPackage, 'hex')) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts index 5d599e6e4f..bc407c3ccf 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round1.ts @@ -55,7 +55,7 @@ routes.register( } const participantIdentities = participants.map((p) => p.identity) - const selfIdentity = new multisig.ParticipantSecret(multisigSecret.secret) + const selfIdentity = new multisig.ParticipantSecret(multisigSecret) .toIdentity() .serialize() .toString('hex') diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts index 3d87ae5b8b..54d444afe4 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round2.ts @@ -51,7 +51,7 @@ routes.register( ) } - const secret = multisigSecret.secret.toString('hex') + const secret = multisigSecret.toString('hex') const packages = multisig.dkgRound2(secret, round1SecretPackage, round1PublicPackages) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts index a3e9ea1f25..9fbba85f09 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -249,6 +249,6 @@ describe('Route multisig/dkg/round3', () => { round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), }), - ).rejects.toThrow('decryption error: aead::Error') + ).rejects.toThrow('decryption error: ciphertext could not be decrypted') }) }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index 35ce045e73..0912b324db 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -57,7 +57,7 @@ routes.register( ) } - const secret = new multisig.ParticipantSecret(multisigSecret.secret) + const secret = new multisig.ParticipantSecret(multisigSecret) const identity = secret.toIdentity().serialize().toString('hex') const { diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts index ea7ba49f94..ef722ad507 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts @@ -12,6 +12,7 @@ export type GetIdentitiesResponse = { identities: Array<{ name: string identity: string + hasSecret: boolean }> } @@ -27,6 +28,7 @@ export const GetIdentitiesResponseSchema: yup.ObjectSchema( for await (const [ identity, - { name }, - ] of context.wallet.walletDb.multisigSecrets.getAllIter()) { + { name, secret }, + ] of context.wallet.walletDb.multisigIdentities.getAllIter()) { identities.push({ name, identity: identity.toString('hex'), + hasSecret: secret ? true : false, }) } diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts index 59c0d16325..881b559970 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts @@ -1,7 +1,6 @@ /* 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 { multisig } from '@ironfish/rust-nodejs' import * as yup from 'yup' import { RpcValidationError } from '../../../adapters/errors' import { ApiNamespace } from '../../namespaces' @@ -36,14 +35,11 @@ routes.register( const { name } = request.data - const record = await context.wallet.walletDb.getMultisigSecretByName(name) - if (record === undefined) { + const identity = await context.wallet.walletDb.getMultisigIdentityByName(name) + if (identity === undefined) { throw new RpcValidationError(`No identity found with name ${name}`, 404) } - const secret = new multisig.ParticipantSecret(record.secret) - const identity = secret.toIdentity() - - request.end({ identity: identity.serialize().toString('hex') }) + request.end({ identity: identity.toString('hex') }) }, ) diff --git a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts new file mode 100644 index 0000000000..6d7e782aec --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts @@ -0,0 +1,106 @@ +/* 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 { multisig } from '@ironfish/rust-nodejs' +import { Assert } from '../../../../assert' +import { createRouteTest } from '../../../../testUtilities/routeTest' + +describe('Route wallet/multisig/importParticipant', () => { + const routeTest = createRouteTest() + + it('should fail for an identity that exists', async () => { + const name = 'name' + + const secret = multisig.ParticipantSecret.random() + const identity = secret.toIdentity() + + await routeTest.wallet.walletDb.putMultisigIdentity(identity.serialize(), { + name, + secret: undefined, + }) + + await expect( + routeTest.client.wallet.multisig.importParticipant({ + identity: identity.serialize().toString('hex'), + name: 'new-name', + secret: secret.serialize().toString('hex'), + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + `Multisig identity ${identity.serialize().toString('hex')} already exists`, + ), + status: 400, + }), + ) + }) + it('should fail for a participant name that exists', async () => { + const name = 'name' + + const secret = multisig.ParticipantSecret.random() + const identity = secret.toIdentity() + + await routeTest.wallet.walletDb.putMultisigIdentity(identity.serialize(), { + name, + secret: undefined, + }) + + const newSecret = multisig.ParticipantSecret.random() + const newIdentity = newSecret.toIdentity() + + await expect( + routeTest.client.wallet.multisig.importParticipant({ + identity: newIdentity.serialize().toString('hex'), + name, + secret: newSecret.serialize().toString('hex'), + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + `Multisig identity already exists with the name ${name}`, + ), + status: 400, + }), + ) + }) + + it('should fail for an account name that exists', async () => { + const name = 'existing-account' + await routeTest.client.wallet.createAccount({ name }) + + const secret = multisig.ParticipantSecret.random() + const identity = secret.toIdentity() + + await expect( + routeTest.client.wallet.multisig.importParticipant({ + identity: identity.serialize().toString('hex'), + name, + secret: secret.serialize().toString('hex'), + }), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining(`Account already exists with the name ${name}`), + status: 400, + }), + ) + }) + + it('should import a new identity', async () => { + const name = 'identity' + + const secret = multisig.ParticipantSecret.random() + const identity = secret.toIdentity() + + await routeTest.client.wallet.multisig.importParticipant({ + identity: identity.serialize().toString('hex'), + name, + secret: secret.serialize().toString('hex'), + }) + + const secretValue = await routeTest.node.wallet.walletDb.getMultisigIdentity( + identity.serialize(), + ) + Assert.isNotUndefined(secretValue) + expect(secretValue.name).toEqual(name) + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts new file mode 100644 index 0000000000..0e1de10590 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts @@ -0,0 +1,77 @@ +/* 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 * as yup from 'yup' +import { RPC_ERROR_CODES, RpcValidationError } from '../../../adapters' +import { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' + +export type ImportParticipantRequest = { + name: string + identity: string + secret?: string +} + +export type ImportParticipantResponse = { + identity: string +} + +export const ImportParticipantRequestSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().defined(), + identity: yup.string().defined(), + secret: yup.string().optional(), + }) + .defined() + +export const ImportParticipantResponseSchema: yup.ObjectSchema = yup + .object({ + identity: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/importParticipant`, + ImportParticipantRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + if (await context.wallet.walletDb.hasMultisigSecretName(request.data.name)) { + throw new RpcValidationError( + `Multisig identity already exists with the name ${request.data.name}`, + 400, + RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME, + ) + } + + if ( + await context.wallet.walletDb.getMultisigIdentity( + Buffer.from(request.data.identity, 'hex'), + ) + ) { + throw new RpcValidationError( + `Multisig identity ${request.data.identity} already exists`, + 400, + ) + } + + if (context.wallet.getAccountByName(request.data.name)) { + throw new RpcValidationError( + `Account already exists with the name ${request.data.name}`, + 400, + RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME, + ) + } + + await context.wallet.walletDb.putMultisigIdentity( + Buffer.from(request.data.identity, 'hex'), + { + name: request.data.name, + secret: request.data.secret ? Buffer.from(request.data.secret, 'hex') : undefined, + }, + ) + + request.end({ identity: request.data.identity }) + }, +) diff --git a/ironfish/src/rpc/routes/wallet/multisig/index.ts b/ironfish/src/rpc/routes/wallet/multisig/index.ts index ec6f0dd92f..c141f997fd 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/index.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/index.ts @@ -11,4 +11,5 @@ export * from './createParticipant' export * from './getIdentity' export * from './getIdentities' export * from './getAccountIdentities' +export * from './importParticipant' export * from './dkg' diff --git a/ironfish/src/rpc/routes/wallet/rename.ts b/ironfish/src/rpc/routes/wallet/rename.ts index 760c00124f..1d07190a8c 100644 --- a/ironfish/src/rpc/routes/wallet/rename.ts +++ b/ironfish/src/rpc/routes/wallet/rename.ts @@ -21,7 +21,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName) + await context.wallet.setName(account, request.data.newName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/renameAccount.ts b/ironfish/src/rpc/routes/wallet/renameAccount.ts index 5268ea4430..6c5de737ef 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.ts @@ -28,7 +28,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName) + await context.wallet.setName(account, request.data.newName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/serializers.ts b/ironfish/src/rpc/routes/wallet/serializers.ts index 477256564f..c477c27b68 100644 --- a/ironfish/src/rpc/routes/wallet/serializers.ts +++ b/ironfish/src/rpc/routes/wallet/serializers.ts @@ -5,6 +5,7 @@ import { Config } from '../../../fileStores' import { BufferUtils, CurrencyUtils } from '../../../utils' import { Account, Wallet } from '../../../wallet' import { + isMultisigHardwareSignerImport, isMultisigSignerImport, isMultisigSignerTrustedDealerImport, MultisigKeysImport, @@ -107,6 +108,13 @@ export function deserializeRpcAccountMultisigKeys( } } + if (isMultisigHardwareSignerImport(rpcMultisigKeys)) { + return { + publicKeyPackage: rpcMultisigKeys.publicKeyPackage, + identity: rpcMultisigKeys.identity, + } + } + if (isMultisigSignerTrustedDealerImport(rpcMultisigKeys)) { return { publicKeyPackage: rpcMultisigKeys.publicKeyPackage, diff --git a/ironfish/src/rpc/routes/wallet/setScanning.ts b/ironfish/src/rpc/routes/wallet/setScanning.ts index 3b5eceb5af..499a6cb56b 100644 --- a/ironfish/src/rpc/routes/wallet/setScanning.ts +++ b/ironfish/src/rpc/routes/wallet/setScanning.ts @@ -28,7 +28,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.updateScanningEnabled(request.data.enabled) + await context.wallet.setScanningEnabled(account, request.data.enabled) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/unlock.test.ts b/ironfish/src/rpc/routes/wallet/unlock.test.ts new file mode 100644 index 0000000000..9e2ddf9e6d --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/unlock.test.ts @@ -0,0 +1,75 @@ +/* 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 { useAccountFixture } from '../../../testUtilities' +import { createRouteTest } from '../../../testUtilities/routeTest' + +describe('Route wallet/unlock', () => { + const routeTest = createRouteTest() + + it('does nothing if the wallet is decrypted', async () => { + const passphrase = 'foobar' + + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.unlock({ passphrase }) + + const status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(false) + expect(status.content.locked).toBe(false) + }) + + it('throws if invalid timeout is provided', async () => { + const timeout = -2 + await expect( + routeTest.client.wallet.unlock({ passphrase: 'foobar', timeout }), + ).rejects.toThrow(`Request failed (400) validation: Invalid timeout value: ${timeout}`) + }) + + it('throws if wallet decryption fails', async () => { + const passphrase = 'foobar' + const invalidPassphrase = 'baz' + + await useAccountFixture(routeTest.node.wallet, 'A') + await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase }) + + let status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + await expect( + routeTest.client.wallet.unlock({ passphrase: invalidPassphrase }), + ).rejects.toThrow('Request failed (400) error: Failed to decrypt wallet') + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + }) + + it('unlocks the wallet with the correct passphrase', async () => { + const passphrase = 'foobar' + + const accountA = await useAccountFixture(routeTest.node.wallet, 'A') + const accountB = await useAccountFixture(routeTest.node.wallet, 'B') + + await routeTest.client.wallet.encrypt({ passphrase }) + + let status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(true) + + await routeTest.client.wallet.unlock({ passphrase }) + + status = await routeTest.client.wallet.getAccountsStatus() + expect(status.content.encrypted).toBe(true) + expect(status.content.locked).toBe(false) + + const decryptedAccounts = await routeTest.client.wallet.getAccounts() + expect(decryptedAccounts.content.accounts.sort()).toEqual([accountA.name, accountB.name]) + + await routeTest.client.wallet.lock() + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/unlock.ts b/ironfish/src/rpc/routes/wallet/unlock.ts new file mode 100644 index 0000000000..694c875829 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/unlock.ts @@ -0,0 +1,41 @@ +/* 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 * as yup from 'yup' +import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters/errors' +import { ApiNamespace } from '../namespaces' +import { routes } from '../router' +import { AssertHasRpcContext } from '../rpcContext' + +export type UnlockWalletRequest = { passphrase: string; timeout?: number } +export type UnlockWalletResponse = undefined + +export const UnlockWalletRequestSchema: yup.ObjectSchema = yup + .object({ + passphrase: yup.string().defined(), + timeout: yup.number().optional(), + }) + .defined() + +export const UnlockWalletResponseSchema: yup.MixedSchema = yup + .mixed() + .oneOf([undefined] as const) + +routes.register( + `${ApiNamespace.wallet}/unlock`, + UnlockWalletRequestSchema, + async (request, context): Promise => { + AssertHasRpcContext(request, context, 'wallet') + + if (request.data.timeout && request.data.timeout < -1) { + throw new RpcValidationError( + `Invalid timeout value: ${request.data.timeout}`, + 400, + RPC_ERROR_CODES.VALIDATION, + ) + } + + await context.wallet.unlock(request.data.passphrase, request.data.timeout) + request.end() + }, +) diff --git a/ironfish/src/rpc/routes/wallet/useAccount.ts b/ironfish/src/rpc/routes/wallet/useAccount.ts index 7413869967..65b6ffdb05 100644 --- a/ironfish/src/rpc/routes/wallet/useAccount.ts +++ b/ironfish/src/rpc/routes/wallet/useAccount.ts @@ -7,12 +7,12 @@ import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' -export type UseAccountRequest = { account: string } +export type UseAccountRequest = { account?: string } export type UseAccountResponse = undefined export const UseAccountRequestSchema: yup.ObjectSchema = yup .object({ - account: yup.string().defined(), + account: yup.string().optional(), }) .defined() @@ -26,8 +26,12 @@ routes.register( async (request, context): Promise => { AssertHasRpcContext(request, context, 'wallet') - const account = getAccount(context.wallet, request.data.account) - await context.wallet.setDefaultAccount(account.name) + let accountName = null + if (request.data.account) { + accountName = getAccount(context.wallet, request.data.account).name + } + + await context.wallet.setDefaultAccount(accountName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/utils.ts b/ironfish/src/rpc/routes/wallet/utils.ts index 4e7b343d83..67a7843f71 100644 --- a/ironfish/src/rpc/routes/wallet/utils.ts +++ b/ironfish/src/rpc/routes/wallet/utils.ts @@ -12,6 +12,10 @@ import { serializeRpcWalletNote } from './serializers' import { RpcWalletNote } from './types' export function getAccount(wallet: Wallet, name?: string): Account { + if (wallet.locked) { + throw new RpcValidationError('Wallet is locked. Unlock the wallet to fetch accounts') + } + if (name) { const account = wallet.getAccountByName(name) if (account) { diff --git a/ironfish/src/testUtilities/fixtures/account.ts b/ironfish/src/testUtilities/fixtures/account.ts index 14626f4b50..c99de7a6f3 100644 --- a/ironfish/src/testUtilities/fixtures/account.ts +++ b/ironfish/src/testUtilities/fixtures/account.ts @@ -2,7 +2,8 @@ * 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 { Blockchain } from '../../blockchain' -import { AccountValue, AssertSpending, SpendingAccount, Wallet } from '../../wallet' +import { AssertSpending, SpendingAccount, Wallet } from '../../wallet' +import { DecryptedAccountValue } from '../../wallet/walletdb/accountValue' import { HeadValue } from '../../wallet/walletdb/headValue' import { useMinerBlockFixture } from './blocks' import { FixtureGenerate, useFixture } from './fixture' @@ -26,7 +27,7 @@ export function useAccountFixture( serialize: async ( account: SpendingAccount, ): Promise<{ - value: AccountValue + value: DecryptedAccountValue head: HeadValue | null }> => { return { @@ -39,7 +40,7 @@ export function useAccountFixture( value, head, }: { - value: AccountValue + value: DecryptedAccountValue head: HeadValue | null }): Promise => { const createdAt = value.createdAt diff --git a/ironfish/src/testUtilities/keys.ts b/ironfish/src/testUtilities/keys.ts index 1677cc5efb..a7c6bbc1cc 100644 --- a/ironfish/src/testUtilities/keys.ts +++ b/ironfish/src/testUtilities/keys.ts @@ -6,9 +6,22 @@ import { multisig } from '@ironfish/rust-nodejs' export function createTrustedDealerKeyPackages( minSigners: number = 2, maxSigners: number = 2, -): multisig.TrustedDealerKeyPackages { - const identities = Array.from({ length: maxSigners }, () => - multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'), - ) - return multisig.generateAndSplitKey(minSigners, identities) +): { + dealer: multisig.TrustedDealerKeyPackages + identities: string[] + secrets: multisig.ParticipantSecret[] +} { + const secrets = Array.from({ length: maxSigners }, () => multisig.ParticipantSecret.random()) + + const identities = secrets.map((secret) => { + return secret.toIdentity().serialize().toString('hex') + }) + + const dealer = multisig.generateAndSplitKey(minSigners, identities) + + return { + dealer, + identities, + secrets, + } } diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 53339cb8d4..ce480f7ba5 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -6898,5 +6898,1210 @@ } ] } + ], + "Wallet encrypt saves encrypted blobs to disk and updates the wallet account fields": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f49b047c-5140-4b7b-9527-ddc262783e91", + "name": "A", + "spendingKey": "0db21b7c8a42e1690a20a0a5fc7522d5e818e219cb1fdce371525d1b4787f2fa", + "viewKey": "024dbfadd8740380a505fd6038604c72f8796bde51cc6c94631cdd626656379c449f9953ceb45323ba12fe175b4b9897715c0f183dacd6a2eb7e8f04a5e9d4c9", + "incomingViewKey": "b34d1189c00ee074ecceffee7f5b61272a625df2023dd383af36b1212f576a04", + "outgoingViewKey": "1af19d206b9ef93946f1f052ac0bb7375ac3f2e8777ca2e3abeb4104c40d44d0", + "publicAddress": "1fbddcc770972a6c3503b7a6fdd174044cf35d3ef171d68e8cf1e5702c16271b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c24c3fe541c7f6ea59d3c8a98199dc0b3696928a5abfd4e6314de1b933589a04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "666561d9-6ff7-4ae1-a5e3-e0c3775a0a4b", + "name": "B", + "spendingKey": "61837c88868454f64f096c139cd7f1dc44e2aba494fa0be78491f65a2dd81b85", + "viewKey": "caf7b5e225d20021beea59c56ba2844c07ed646434bed5546c731a1eb7914412c92e38c6695ddbaa28f5818057d3757407b71e59479f1a13f4947a6d4930fa2d", + "incomingViewKey": "1b234f2e510f10540665686dcfb850dc00eaf56363a6cfeffcf614ce5d73b301", + "outgoingViewKey": "d44acb51dd32912ec1c6d709afbdd8701ee1ea821c019c13da4e0cc5f352b1ed", + "publicAddress": "a6186075a14d9d6ead67f208d6329cc5582b0ec77e154bca7e1550e25aa19b4e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0f8be35575f3df948fd6f4eba41e378c87d4588b69825e65c6319bd754835e0a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet decrypt saves decrypted accounts to disk and updates the wallet account fields": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "333e7293-8495-415d-a23c-c5c42299a28c", + "name": "A", + "spendingKey": "ab109f4b6f709d52325e3fdc9eb91ad5c5aea8c32241c0d51c59b3079534e5eb", + "viewKey": "daa445d831d064fd6663448c17e097b4e698e41775e982bd15a6de060bf2cb1ce89a5cd36099ea86618ad38a8a53648ecccdd190d5fb04432c711c2628ce7a23", + "incomingViewKey": "99495a5e0c7615935cfb8564d87323e5cf50e159892afe966fed576ca9f08e04", + "outgoingViewKey": "4211e27a7965b806e822ebe99a5a1980bb7aebd5cc6b011dc94e365531ef8674", + "publicAddress": "e88dc4d5956ff1754dac3f4b9e1acb6a6c31786af3e3cc1e7e9aaf6b6901f3a2", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "60d383fbce19b29d1dc49be91ad51729295fa23c321503828fca0c9987f10f0d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "414ef1bb-88c3-42e8-9cce-73258f6395d6", + "name": "B", + "spendingKey": "44617ebb8ee9299500684dd8aea996764609fca2c2bb8b2bf4da1194535fc3e1", + "viewKey": "9e5035501da8cf3f174533deb8592c1f65930442b09bdf1a610e1441b79fa7ee063662e65ee08477ea938146dd94145ae6a21f5c68b683f9d487ae28f0dbd336", + "incomingViewKey": "44f517e6d7133af416ff1f733ba99bdf31d6fd6e3dd82e567e82bff872c57803", + "outgoingViewKey": "8b7cecb7efc2448273d25eacf2124354dc7ad39ac0a81dade10ceecfd7fd5187", + "publicAddress": "c5a35f571c155bf9c0481c1e82c6a3d0769b0eb5628f31abd9e60f1e9f4c9509", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "ca6de858a4cbb527e9f8d33b64a946ea56840f57baffd5534128f3e325c1c20d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet decrypt fails with an invalid passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f55cd33d-3bae-405d-bde9-ff86d80bbf86", + "name": "A", + "spendingKey": "e33273c1a8d8c99dc30a87be0be68fc5420e5db5c2143734c03be1bb3d4bcdf7", + "viewKey": "1a59c5e8c21bf4024f1d175896602d4777a3616ded5d05a3b7316a23290482bd96870ad88d4d85603198f768aeb0ece799bcf090d1c8250c0aa6a338c31f3ad5", + "incomingViewKey": "0ab4f8a7800a3b2a25bf5e1171b9b14b25318ad31998ce01fe45c7bf815b6603", + "outgoingViewKey": "579103a34bc8c7d9f1f876bf505ab8bbc75709e84a845de307248d63edd4fe56", + "publicAddress": "aa31dc223dee0b7190c94f057c9ae1ebfca0604e39696c70f32b70f009142527", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c796c91e34e0005a2be596751f07f5bb1438591a3ddc538f2ec83dbdf6318005" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "21900bff-d690-4b0f-939b-ba3f80b25495", + "name": "B", + "spendingKey": "095ef9fd35272a49371ebd0d65e1d3cac64c1e941f4e878ef9d7deaf41530853", + "viewKey": "fd924ce7abfbc4d67badd5e06d2eb91814090d1f6322da0bef55f1bd76d137b963399defe0138174624cd5f5ea368016668cf145bd039c9b2292598f86c07219", + "incomingViewKey": "c348f1f2e555cf44b0d9146e0ce1bb2ac9f90fce724a0813b67846d6d187f005", + "outgoingViewKey": "8e85ca189a708b86328cf9353bc40df764c391c2f1b98ad91470c00681b3c8c9", + "publicAddress": "105b2517120c59c26e4562662ae02a18a571f8689e55762a60a74fd775814ae7", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "17d26f648e1b94cd89ee8deb24c698632632c6806b21b92212f20de1a3f72d01" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet lock does nothing if the wallet is decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "8af0e641-fc15-4a89-9768-83cf64abeeeb", + "name": "A", + "spendingKey": "237de7df9ed8a722d3bf4328d9739a38475db3bd5f362d17f28910b5e39c9bd9", + "viewKey": "c3f792f13567a87d7c0b04118aa366ec44923b531a49d52cf52a4e5de607c48b97e719eaeea93b8e2bae4ee0ea644769fc648a19e6e3b3daeee3720055901fd6", + "incomingViewKey": "ee50b5c9a64f8608b812c8bfcd28ab4f1dbc647dbff1a9da0fbba8eec7eab701", + "outgoingViewKey": "7d18c8f25ac2e37eae0172130d7c10d04a15e90b7b61d8c622fc8734e71d6548", + "publicAddress": "901e29364e8bc674a3055462243e0de8679c063ca7edb22c8ecadf21c8529809", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "019b4ea6bf29fdbf550e308aefc0334c6ae78e73b7df201866700b11df8a5003" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "24f155b2-d22c-4572-960a-cb4ee61b6e61", + "name": "B", + "spendingKey": "2af84f62d481371999754e429f659c0c0ac7fd1af7c2c28a0dd0409f0d9dfbdb", + "viewKey": "7fef125b2a47015c9ce0f08155a0f014b1e700c2903c0a3226d9d8695098456850139aec9d0e43304c6fe1ef9ff2861d433ca376d8ad0f33c3cd610c72810ebd", + "incomingViewKey": "4e732036137814a2fba27fa409b98f3244a79b00dc29d13dacd27aed47d90604", + "outgoingViewKey": "5547943b200ede2c93710d96bcea2f5aec9982e606450d8ee6a4711a080a6df7", + "publicAddress": "bc843e766ba2c59ad68b23edcb12deb833ef878f855c21fdfab8810f62457ec5", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "7f543a79769a544ca8050ae64370d2da59e6c87d163133d5da38572d0ff2b009" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet lock clears decrypted accounts if the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4531af42-9639-47b3-afef-ce575774946e", + "name": "A", + "spendingKey": "e50fa8a0307352ca6577427a64abd6196180520d7c0df0c003e7a35d8deed329", + "viewKey": "bf55a55c74df77f2d96d23bc35d7974977514d0b4a3e78c26a827b547e96c52e43e9f623029f41f241a6a92e6fca6877c363098eda2279b721706ae060e44d64", + "incomingViewKey": "b2053da877ec8db680dcb5ac943e10315a65e8fb3965f10531f6c718fa57c505", + "outgoingViewKey": "877c69e7bc0a77bc675420a639ab446bd760870abaf2af3deb076d7fc87fe159", + "publicAddress": "64785cec540aa64f8723a07132a6ce7d03bdd35461cf9262a1fa6451a00fe41c", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "75e533bdc944c8a07f56526147faabf6624b71651e13f3ed625184dd24c0e707" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "b85c7b21-6f6a-482a-bfd6-320ef989d30a", + "name": "B", + "spendingKey": "b5882a9f81ecc018d06b5dd857fcc25f3fcbf4116df81fcb421e15084960569b", + "viewKey": "49a836adf8e591451cff38acf5aff4a753cd54951cbe116ec58054141a09d95335ba0a48bb2b4f19a753c8900aca17c3c89c649a076bc99aea71bc7cafdb4eef", + "incomingViewKey": "df437af36f0edb32c899086e455a54c10799d4a587c74ab23436907484b49703", + "outgoingViewKey": "54fbf88518b8de5d45fb691b9076f770ad40ae3d1928c28dc67be28889fa4a70", + "publicAddress": "62d4847dbbc77bd2c447e1c550159a937da0d558cb6e694e38f1ec0e9114520d", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "bea22593c255a3863cddf2b5edee06e67b8398e0eb785e590f3db9575ce45c00" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet unlock does nothing if the wallet is decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "334e8487-aa79-45de-89ab-b5dd5fd0244d", + "name": "A", + "spendingKey": "cf4dde47c8e4b50b1b51c92a2959fbe4bd525053dd900538ff6b97221e048572", + "viewKey": "f76cd2fde34f98c0f3b327ff93eaa872004aab787c4eb115a36faeb83c0db244723d34416ef53d669e8bd42e3b8a9b023e067a4a2cb8a37b942233682fe94842", + "incomingViewKey": "c4be4f5641a692f71a39335cbcb6f0f6040dd28a6d7be48cb5e985c630933e06", + "outgoingViewKey": "ecb5401f16b185bfa92afaa4486c3c3aa568e3484afa5827fd93915c9ac30ab4", + "publicAddress": "6b257f729fd7e7724d3a14f35b3fb087dddf2b81e2575a24f8c061e051e3392e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "4d96678f22835d12fde65e43dfd032debb821bd3e19223b8e363cc455a0b7e0c" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "c76feb88-27cf-4f1e-b0c4-14b2fdf481f6", + "name": "B", + "spendingKey": "dcc22de1010f28f8b467c9461f79a7f35ef4d74f89840148eb2b31e2bc7cdbc2", + "viewKey": "e908e1709c9e658bfc470b0ec29cb07a27e3d251391049ed5d0425642fb9dd6f2b9441d3d543292dfe5c0749d2fd319879ec54fa252c264966b6aa4302891eef", + "incomingViewKey": "3cc8da1c2a9cb66aaa5cf6645f58c2bb68c7324b61c6c9e7920d67f00ebfbf00", + "outgoingViewKey": "222dbc4f3b70bd6a544216b0c2eab8005664b9cae1a55791f2273415df6737ac", + "publicAddress": "5a53b85abd7a26f85735c113e3d49051f2b8ad3757c05c83eaa64a34c76e7e20", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e9674e3b1c1cfc895f4c40f4fca18a62557cead575a59f0a5148ed9b7e533007" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet unlock does not unlock the wallet with an invalid passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4db9e167-a9ae-44de-8f7f-4483e84decf9", + "name": "A", + "spendingKey": "d4633b8108f2480a7f23e3038c4b40f5db53dfceedaec1e73d50324f82898ab4", + "viewKey": "26226d3aa338516e3591ed701fc4c5e0b311c58e2204b870678d11e69939c9d67c29ccc06b1c502ead09fd703853a848c0c9bec130e93bdbf9598c216b0aeeaf", + "incomingViewKey": "6f21d7fa508f24bb3487dbdba4e26f96a1c670d9fa18ba237ac9f36c5617d700", + "outgoingViewKey": "758af14295174291a8685fbdca6c9f8c21bc270c2be5bcf0fa57290980bba1db", + "publicAddress": "1d1ed34db0d1892bc146dfaffad521666aa6ecfcc8a03ccbbe41e46e11c639b3", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "aa19d0c4ea71c7d31e3efbe4b0872b6f7781e68eb28dc063a00741cf91f29d04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "9162afb0-8a0f-4e98-af83-835a5e16b258", + "name": "B", + "spendingKey": "828175233697311072b278977e76e92bd49898c4a8397e8e031b4b81839e4137", + "viewKey": "4f5d82e80111257fb0b6bbd337e12366de1edce52cb773755a93920f9ef7896bff18f5394234095d3ecb11d509dbcc58335b41ab3ec60818f26bd75f2354bd15", + "incomingViewKey": "ecac98b848e00e9619b47cbe47cfe1dc20c2cb932fe2f1fac5bbd0c7c747fa03", + "outgoingViewKey": "64f2e79345ceeac1f66f1b3e02e306afe32a15df94e50b7566d69f7982e51467", + "publicAddress": "6d528f8071d9992f33838144f2a8659e3dc244b93061b3f8375dc20ca487f656", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "9b9ceed7867b16ff969a5d96f4b585c5b3a138d0bdab0c0c0133864780d96e05" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet unlock saves decrypted accounts to memory with a valid passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "384fe5b9-3160-4664-aed2-ac4d006b704f", + "name": "A", + "spendingKey": "f15eda5e36aa16bc6089fa3af4559d73586f46e98000450542d8b9be1c0140fc", + "viewKey": "2a81a45c51c715b8e65f1f460efa1378819f244f982ccbd6d9af642a5f9abfb27eb45969f2d44eda21b47b114c5dc232ecf10ef339154294ba5a49cb84c25ef3", + "incomingViewKey": "780c27a6fe9008e627b118c90d435bad1494380ca22fd780faa09df060e2d204", + "outgoingViewKey": "56d91dd686547a1108ea92570ad74b5aa9913be43bafe9d7de2a85fabd1ac9e6", + "publicAddress": "4946e7af9b10183cde7fe74747652af25fb04092558abac7d201a683d251b5e9", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "a572f7aac3975180f4324907111b1f49c1870308ba2e3914f5d1646cad2ee500" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "113965fb-4d82-4775-a8e0-ab53dd486aae", + "name": "B", + "spendingKey": "de48a3a36c8f24aac3fb9c9fc068e0313b49ef51996b06a15d5c05fb58d49093", + "viewKey": "4f2d6835e2e7c89c634e9fef094974a106e0ffde0b7379f68f130646cef857df641be33dcae1aa687353a6d10265ea4df6ce9b7d1272617c92270b84581f8d44", + "incomingViewKey": "aba10ce211826f14de8a4436b1e3c8b7a78006bc8748bfa807e4c9e4e7af8006", + "outgoingViewKey": "13dae0f5e3b229a8d47c489844d9ad9c3f55c500cf7e4fd7acdb5c4d1f55a911", + "publicAddress": "57b793afa5f829d9fa84f12540116d31bfc6bd23508719e1b38e31f7a6721c38", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1f4d67591e3fe5de62930c5cd066e3a08456dac92497eb354cb8c71f805ad608" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet deleteTransaction should delete a pending transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "9298fcf2-a079-48b0-b117-dccf085bc139", + "name": "accountA", + "spendingKey": "b5515f7ed1dbc32d6508f6a4cf3ccab20022a8dcd04838ea5e87c64658a0c44a", + "viewKey": "47fd97c80179feb4b3e9a14fa7068a8f2b514b969de1daff06baa81988ecdcd2416a8152ac80bf2e6491ab4657a58c45bb18f8d10eb115382dec65ff4dbeba9c", + "incomingViewKey": "41ae68a798b824830a3399c36660bbc3ec3be5ea2687258138cd6cdfd90efc03", + "outgoingViewKey": "739e390f643f3aeb97df69d238f2f231bb8c65c60c3b0955313a5c0bd2c12156", + "publicAddress": "b6a2a5c5d5bda6ae86a4ed6be4188c093a467ac65fc80a5b5a50454615d274f0", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "562af8d84c8b03dd1e9ee45d4ba2fa489d1671eb7d1b3e9e00a5ab178f85ff02" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "ac0ea86d-73ec-4dc6-a53c-43827c068416", + "name": "accountB", + "spendingKey": "d15e3d1e3025450e94f94d8b8b5a609d7180f7e22e123a069b3379368b909e21", + "viewKey": "91e583329c137391c88f6e863cc4a7c5ad9dde30b1eadb1957274726fbec96c8631263a888c691e754710034cb138759be67df8e17b652dac6745e30a77c0736", + "incomingViewKey": "edecdda6920f01f04100013b9c1df43e468104bafd6b0368ab8962f297a4b703", + "outgoingViewKey": "bcdb7d24322a7a9ce66448ed0faebb54c9f8c5e981be4fa756790c69dfd98615", + "publicAddress": "8f5445f7de1eb951e62e58b5a70b025632650ff89452e3943cd51b8905dd931e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "9be05afa8583038ae73e5afa138948418c996faa7998378a390f49977c335d04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:wLrqkbGBzeCb9GqEdr20AT4sLUOmK0zZmF6Qd6hSTBA=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:NZzZvHi2Vu2WlTexqNWeL+uBEEOGQHvK50zi3d25V2c=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724100965066, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAARoNwhuFWjC0lcbQMWkCUusqkHx/gRPtdSmJM9UImwZ6E4AX7nub7eoeT8ekyTi1EtRNt6XwBFIlc3sGvE2SzGPqTUYTSdcwMfI2Il4+RKX6pVhotabGoXIb0QskMUJUkVoj+rl/qrmI1d6BymkWP0zEMxZXaTz1hkQvRkDpOfJ0CE7XfZd4k6yth7SgLiXkfKwcjf9l3kJdPRSlvIBbWekok9KIxXLlufcsB8N+K+sO3DDzRytMUM2aNdpr5SHU3UtZYgXKPzZs0U+aQ9kj3TTkDAc4v9t33Fso8RQ075Kvp07DDQ6oQoIAjee+Mv0lKl8CLIFVkMxrGWJFRIU4YUg7lGFY2rJi8X3isYxNCAZvl39JpUTxicK+ufR3kQzpS/U3+hOVdx4aXCdYcvvMBZoEkSrTV073D0D2EPDi4wimmJLSRDHl1dW8b1zzrvBgf9Dmgo8RAtqgoMkvDqqqg6cjeX0I3eezTNJTawQwzV08SpLZqZ0GwHX+IybuiVz/wdZePtgIzAs3EY8TsagqgObkwtuu3u51dxzLBuqmP58XatJBY158VRuDikwBQYTYY1dr3cWPvkD+i5QTfhb0BGjrMxmRyXPmVV0NxEnoyxPM+PE7gnqTtkElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwWPPGWHq+gbRrHTKY6UJngUViLlvnkWKQtYfhFYX1Ec9SRI5Ym2ny7+qeZKtOTBwXCy2DNq9nJkVijGwU88i2Ag==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2tQ3nJUyvBGyLyDY5i/6rc1ek71s0hsXmDgevO4U+AmGJK3Qg++ej3P8Z+3WpM1HZe6gzsI+aEDW9ac1dvwobSFL+LgugmNps7e39nWx1/uQ+cEgk+PDpafoyt8PTsyC6ogHDKFkxL0vi44aHchf22UH6K4Gkh86bdIRrAVVrLYEUtjyTlX/3i303osaCFviHqrBA4g77nPgTnzuLPMjJVfrCQolRMhRzLvNx1CwjPi37s2lKYAmrojzElYwNCIzPGmlBK3qpRwxmrhlMW+JuQYgaxEOj0En/IxbbieWwL/Wdyj0KYj66trGIGONys9T0VxsLtfBUiT3Pf3WNYHdnMC66pGxgc3gm/RqhHa9tAE+LC1DpitM2ZhekHeoUkwQBAAAAMLOSOjfkUJVUxsIOhM7TGLfZXdL4ceMse+kqpBZuaowiRUYiKBicmxkaKw5ykxB1B9owy5W5nAxPaUKDKKPSg1SaJc6VMnNAkkzsRZDFPD/Jkr3LD9oTj9TlEnIieJZDqgOblqvVkhre7btrfTX1NI55g/0ZQPL4TiGudgG45AXwSh7Iz5sSM8d+gq/7IONNJXLLVr9nP8Su38Xb0U7gAEET4iayQmQYuWHQVe1qfQJhM0NEragfAZ0lgU1/6dU4wDswCcd/eQ3TR5jHlyc6QPdBN9fjLTVJB7IleAriqy0ubrd5m+rA2q4MeuxwK8opJYCBhjxqURvbs1BqVgEqmLvwEwDDk/k/1p8tstXtqLvU/ncp+lJ8IcZu1NJ2zyjkWocfdTiSrTFWSlDzhZ2DmI4Erx6Lr2ppmHEboi6ZQxne5/tMIQysUH6gOTDPlpx77ocrfzij8Y9bL47XCyECQ+vbqdsx5ZFsBYeRm/gz9L/cnYOJ+hEHEUQEo39RGeBLf1aSuztqyMg4iAnVWk5vxE1kzI72cZ3KsrSpCfyhhjvNCjoSY2Anz8zko5Q5cedJ9pkEI2/9NisQ0VJTvXCPvMa1e62RgHAmt8zUzkoufeRRruO3LzHys2XhDfPWpLnIXfwWiXxWucau1Jfe7o2mapal9587bmHIxPQvVqRmEOm8i+PtUe7/aFx1D/VtgItaXQyE2uThj+Wh+bfB8tJi57a5ixKcK6I9wOA1fsfyXPA0/WBZHpKgTDNkYHtUr0XpQpJNHg3eKb1s588gyLGelvamTf9QS8CLWQYRnqtfZqN5nGMeqkWpymNGBLzm7VNEfxCsgvoY85Oe3raSK2ibc7rmG1vlU2yl4aXInJAaxLtiGJ/yyT0zbioGhjNbLmySCH0N1IEcHhUD0aaTS1fOakrzv5D3fCxnos3qf1YDxBWNCflOOfiiTYMfH6/LVMTuxIk+B5NDVyucJjOyEuInVWxZpQU1tGmWhkrkXnkVQbHAURwiDmeC5+w5BLtm4M+Y/Q/LypPxyeO9zjUVKz1y2d/SeobxCQNJGyGFjBkauV9WX5M7jxHWRut1QbRqhADjedSnfokb3UZDmdd9sRyTr+n+gAT40nVWTbjkS3+mwBAs/IJPFGcjpPdoCzbX12agMhIGnm0Jf1AQtpaQRwPdEeR8GnarhpomtSDL1+UARl64PS/HT6rwXFWoDtkfAqEdlLcW365JUTCfsS5RDQs2wNc3ZFFAeYkmRGh134UjZxpQkty0u3YXEbtPxdhd7RfwSq6WPhF1Q5/5Dx3iIKnBj6hca8Uit0P59jlGXAv9fP1A8GO5+dv29fNhURuF1aljy+y+cM4xs1zCbFAE5LSxeGy0CSPS0xn3ng/RsnVrCGNUHtqtROE1S+O1e5VnREjJkTTaLBwIc14CfX+hn04KTBzluN2YQmGO18sLLjUKzvnarZZEi9p3UpxA7mLkQMiMg+wiOkHZc+ApzEV9McW/WBVxwnsACc5G0ECwIPzt87lrkeXPUOfeB2mdzQp93EZTQclITnIOrdAXxRFL0JUPBOO9+7pWnw37oEP3iaMGcQ3aDVUCKWEooKa2i/JAf8yBQ==" + } + ], + "Wallet deleteTransaction should delete an expired transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "38aab35e-5980-4776-9ecb-a3cb7eb09538", + "name": "accountA", + "spendingKey": "8232647e841239cac7cbc2b7585a76ea0701b7fe1e31bbd3f8acf1782a05cc26", + "viewKey": "2d14962cbf935de87bf2569fc731efa5506e5266c5de16a768dfc03f85e8917363ac7bee2fff2405f7100141e62e4fde21f3c97c6ab2213d9b9cb9a9ed9900da", + "incomingViewKey": "d479a3c9640bbede3e2ed2024681e712ab24fee45976cc6f6c3b6916e0383c05", + "outgoingViewKey": "9a6c1e997bc259b196afe13eb4344d543a9d741b149368d1c24b7c1da925dae9", + "publicAddress": "2eb9558b3e016e0f4de1bb9653a5a58929e22b675720700b2e3cf70fea78ee44", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "7fdaab7910b100d0f8238ac96850a1cdeb162aeca0e022b67bfc33df0ed23102" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "2dbd6571-c831-4b4c-b085-f7bffa6b197c", + "name": "accountB", + "spendingKey": "81a3bf6a6b81fe33f04b1e5875be04177e3f1cbc70d1440fd8e58e37d75525df", + "viewKey": "cc7304b6f2698135242d37f74a8c3906b3002abcd80d5b575993a7caa378c042a7fac91a9618261d5ff52580972d4ff0eb7453f86812b96828cbda01b99c59cb", + "incomingViewKey": "f2b39fbbf6f5c350b1f28cb666cb0d5d3bcddef0f94d1bfb1baaa7a9dc853c04", + "outgoingViewKey": "05c71ecc608c9e0599fbdcf425763323e6743ec8ddb7c12a7fde5e74e018de0c", + "publicAddress": "e0c91fd96a52af2ac2a3a7146d919959d06d2e361e6c401420a4935355f61035", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "443a31903e4f6522bc358e40bb7e239cbc2073c5e9e45d28128cab1d2b87cc08" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:pUAoKu/QQdf1LbBGN7RjVDlSC3o+qY8GFSrg+4dxS20=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:YWnU20srVrXmXKIUPTHUXjoKTSwTN4geHpBDBMlf6kg=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724100967063, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAaH7GTXP3mm8KOKRXAKf3IoZh2/JGl9H/+DjMnCNi4SyYlHGR+RtX0ZUVVHRZZZ7oDWi6HjhUa2GhcT1ZmT2YySE9oneY1KdZCooiGzDKPiiYpzjoe8rnTp88wnaeczmRmQzGSlj4jKMBLALyOpwa1mcupSEnI2N9qsL692px8NUPUoY6hbSKc81bsOPlX6puIgKj0P+9TM95M4J+hW74pw4ZAkUDnNxv/IySQHOL5VKrtqbMADXBbWikU1nYLXNfSzywMixQa9borqmJY2ZVB72zabQvZSA/3zBOWDjJWjt+pcWNeW5QPy0ySog94cYVSCkc9T2po8z6yNmC6JEhi77nBWqRYGH21uAGtPe/EG1gJo8VwbBPX1Qortc2upYZAxZk+w++zMkylmtyZp8jbLL7Y6PXkbyF2ePXrPs1KHBhdY/EDPNroVWv8I0uEa0JGplKS27imfTYgDhZpm5KdWeTnb0GQexL/0hoDJXYKNqBegx2ZO8XBhqqWzoI9eznP4FCufmdbNiVEMqsjRPsfmlSGxssxHo/QpHx+UZJNCaqOL6JIUFCtQ9t2EhTC2Z5UxWA7y8OlAQmAKbzw5cJbMzCXykh9H2RFSOXnTK6WPighm8VkBc42klyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwwRxsSMwG+rNTD2W+HfsEk3FftCZihXNl1gfNEYoWHnHopf/nPt7ExrjnPwOYJE63BqzmcmV5wQG72RH8GpgqDQ==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAkn/hp9Jv/8AXt9XBBziSVMBqqOqu3FcrHZ6sZJ179xi4gsuJOH/pQQil95epQVypmBWlnMoJOlxQG1391lWDNZmO9XWryiHL4eXD0FRMHQ6yvcDT3Q9Eqj+wHkgCDX/f0QKcq95EtsJQTXIQfFCpDt5aIfZZsF2dRz0qVKEBZY4T7tRhGkbiIAg0wkRwjxP0EWooGXwNhQLFyI8aA7/IF4DBjb2x8uqgyY0SigFKoiaybFqQ7eL0CBxiJWdA4iSF9R3i3w3Y0rP5ClVAuaw9GLue2Ci4u3rKa6fY7HDbDOwhnK7qdKSJ6wZmh8FizW5eSwpwI7tYzYMmj+zVwaTcQaVAKCrv0EHX9S2wRje0Y1Q5Ugt6PqmPBhUq4PuHcUttBAAAAOoOygZhqbTkkb71tHPyE/u4DzS0nH/2lc2LX7SS2F1YpsjD3Nz3/02YvzsXXdC5Cm+JRdTYfViLan/C1kZ1U0iBBfGKPSXWD5U53kdDjYOFlaKAWvBlkq69TP8I49oZBIy9jjPoGQa3JhgOAZo8q5jLYx+zFJOAQFZhfpTwEIlJUoao7jQo2p8d3mWE7wsCq4lqJ4tt2QMZiyxfiijcwB1lGGHx2lQCEOc+85f/BBEhySw18yREPckwivOQ3tG65RPCCvcLL7MMzqDQ2CPOjb187uoMaR2obFlUYJDEI1MliT+uKmQoXQJamnSJE3i57JTuQSRirHawdxoAaF6wWvOv/GVMf8Pa8g1pJxbAeNMb7UwgJIKZlIuorQ0O+pVPzzS9I9tHEQRAdZ4r1EXIBp/5Qc+5DtA6+CsMOz90P+Lry7wK8I57ViOEKY8Pn+5u51HUCqDq85Tb+vq8PJ5peVwwPN+taIiqdziwRV9dAgqGY8GSA2pcWEn3U7Kz/VjiLZs8/7Y0zxBJ2F12fQLatvfs4FNitQGzE8FK1MUruPZm0vEKjaYdPIe3PKKtZ05Tn/64lm3gB4bbZxS2kJKqSKLi7QMrXa7YXVlHQyXcMIr+hSYMWqQljzCAaUNcgDF/BqAjOVxijdQe5p4LvZahls2VLg8oPCRVdoFPdERBL3+pfDF3PhuRW+ylYej72VomapgfPqYb4N+5N8cTCkLNLGtD3TbmULuGPQnxvB0udXz3B9qrbmTa0XsT1n/LkmjIAEgmcUPnqwv98T6q7CefmRaC7pKDtlm9S2bZzaCMkjJ/xo2+xOD6zsez1OVY09HjCYMUJbNTkuxxIfcv0FrtuNwxltXypLEKkrk4vobkrD2phLCFboZ6Vju5y3MuFqoNFzgDkuwU2iXPBVIJP8mPL9cQpeb7NQexDMBdHobQKafniGTfla96t5YJ//hrCFx0McqsVe9J26v/tyt0c04U0teQ2nRlgPXhe8mVkB4uoZfep5MowQSAIZmXyTMGRkzpi/cexW7xEh/Crlu1prKtbFgjQDxUGpJBq1ns3EmA7qf/PWU4E4CPtKYo+AOKs9Ni6d7Ssl2H2SktIwPdXNQQzzg4YLBX0lPcEkJn7RjgAg1mKZ4jBBoG64ZXNdwsTjoJizlM8YRaQkVULfZ/f+vx3tye8/CR9GoR6xpbvk9ZsO9/7aaYt7d1X3DVM7UNgejsOmrbRnu3xRewHfvLLMOmVaCBaSu7KLNe3uwM+KGT48gu//YdmMS59fkbNWZ+9MLhowOuZuOqjx6YdPlwxb7eS4rMXEniOAqYGpejx0/lW/rn+9Ig4gooIkqJT8yLA7VOkOLEIsPjrQcgv5Nd4GOVPuhYvuJ05y0ws13OpiVMCAmDOXmYeN1a09mz2p6IHxeTXIhQhB+Bmxh1XRZd/WAnrjyKH66tnWc8tQRxVi/hZRX0SL4wQiPvGgMQ767GMsPqS3EPi736FUXyTx1Fxx3npnR96wd8YHs9HQCB27eahu0ThlszmSfDO+IWjMr5DEtmTPExqtjYnuOxtnfb+sLriRGso+I/3Bc77DZSV+ztnfW1H9vnOzGE/kqmffc75syJAw==" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "A2DD0C56F72918F96F2F89EDF9EBFA608966F8FE499D14D10759AAA5971B09F6", + "noteCommitment": { + "type": "Buffer", + "data": "base64:RXAUT0iYB+5W6/TuTTjwm78Zop7ulcthwJHvSknl6yc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:2Njk1LecucLbwq6DBSF9sW6xEHlczpm2cJ8ioL+6WGc=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1724100968743, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 5, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAACXZ6cM4aaaJBUgEZhPs7nYzH1K/Vvph5dlayMmaAcAC2xVDm4p6PukI58uoUWymoqVheHN+AitilvNM/0MhwqtKJ4TAyekORYC+Iwe7hRf+YW1Y1B0YxWXfy43ROsPWFrBt3gIV74a6KY1e6G8xrrJuUDY9pCNuOdTJtyI2TL2AWiHplehTJpRl9kiFLOEzx6Rfs9svEZxnXJlpWMZX2Z9r9k7l5U7BV+IfPHyhBUUaDSxmdeKRQHpe1yngemRYHJxemB8tKWvEtAIMiC3n5MiPsU/WjrQJPtOVt3zS9Zv707bWyUqXn005KYwonznrBLWl+I96kJeglbmPsdJcpMh5xRXBmPVCmlCVvWaZ/EgkyLHN51S8SlaWbXqJlYlo+eREQw/OFT8v8o+aPqPYI5KJ8uvQeTnWbuOGCU41G/LS+/jM5WCPRm2yM4vstvnLf2eeJ7x8LWal2Oqls/27YWpnSHWhm42cB23DyY0sDh/UhObMbe+i1txreo5MetRiSkJByLUzpSSyulqhVBy/KUsNn/zdKmGqg1TaLaczxs4GRz7m3H23zs/IqhS50qcHD5zrjzbe9EBNdK4xndW/YxJIc7zwAd6+onAVrtDjl9vMyeg0E8/3UBklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwDW3MD1Ai3RimwLuhmH17g6DCqnRC12dItRMRqdamYR2xCn/DnOgCzUZt5jkM4p+eHetx4aWwzWe1HyhnwQI/Dg==" + } + ] + } + ], + "Wallet deleteTransaction should not delete an unconfirmed transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "dee72893-bed1-4eaa-a87a-f0fc9b67e2d3", + "name": "accountA", + "spendingKey": "5ef7a6782a63da0a30eaf2603e0bc7f944b686f5a9b18c33108b3574730fc779", + "viewKey": "4562776b3e18207e5d7646ee8aac9ddec3241b74b773100a6b107ccde01cdc530ccf09843e61615d5406b8a6ea183e785894bf6809e481280c0d94d8951316d1", + "incomingViewKey": "6ba3a58f6634efca56b248ef63b40f663385182c82f136c69200e4a87446f600", + "outgoingViewKey": "693fd07b99cadba67dac5e6bdfc700f1d644fc57b330db75a35e5c91b96e3332", + "publicAddress": "af9a8901aae7ac3f2f877aae3887cc6709f9a97ae4289dd4408cea5751e7401c", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "ed51efe8f5c0cb08c7dbc37d16a132085a00923b002dc131e3f2cc41b8614004" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "9bb06817-6052-4fb7-8662-2df9151b0de9", + "name": "accountB", + "spendingKey": "b4dd73b6072a9158999330f2d761f557a3a3f87f2134fe7e709edbced2ea3702", + "viewKey": "8fe421ef6bbbe0d9f6790f402e27b176fc297170fd23c948379705678d69222819fa62f8094e33392f95c5c22f43c0028029c65b391c33eec2c17f236b93edc8", + "incomingViewKey": "989ea06e8ca92e887e83f8e79691a3e1781c9b7f663bdbb4bd0aa604a0e5a707", + "outgoingViewKey": "27db7e9a2351732239bea52f5c2a6d23d369272731997066955cbbdc84c746a4", + "publicAddress": "de6b4bd98e7300bdf44cfe446dce4ff03e9c3134d798448b1d571f39d807e764", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "d1d9b98fc5970371df0c56a317eda876de0acebe22916c3a46424ccc923af50c" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:v0D3ij6hu6PRuSP8Nc1U99SX+UCrqKY+vJsC1b0+xRw=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:0xvYggH6C84XDvVTOxCrgEiUixqXvn7OlZBKLJ3OYLo=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724100969461, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAeB7oPIjsBursuXrk9K+0/ledNRQfEuyRytKBJxY0wwS58gSufhs/HRecqLeFsPtt6Iyzo/jGY0XnsVhylTmP/XU6+ZQg2kXkrvnQTNkMrQep7YPBpyA+3NgrOD6K49Dw5+swLe3dwe3JZyh3lEpxJv3R3dazdgrm/VEhSVHOqHYIL7jo9C0O2A8xErBy+nahquSRtGxd/8zNO4weT3I9PlFGRr6Mqc6R3hkEbE54mVKQB/dy5tRidCEIRBAU2csRG7mP3Dg0pQ02s9TbHyUn5ATyoqoj2iHp858RlqdfmJy7kWXKLopjYoFTK5tEQPWxqALDJqKfxQrWWBbvhpnIFjFWAL8INc8alJuNFUmw9Hm51SaRv/iARueEG0g3aGEqgnZvTg8u/VTHebZWYuZOUCU5yHfznQub4NFMn9LsIFjxedM7gM+ZDAjNA4ubNmc+cGXkyPE6zyPRhAUuF5VKzkWWfZwgm4dKz7aAPTr31YZC3GFDMv6U6BynafyW4vbPzQYqLHIVvznn4KZtOPJwH93l+sSZRA2Ae7i9Is/UJKC7ZiuYm+Ubt22OsdVjQtrwKcDXr/qCGzgDanl5vNvolQckoLGvKm2kg/DaVuKQbiVys0pnsnkBJElyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwDF+CQlFndjM0Zi6xhYmDfFY2kYG88koo9o7QxX9GCCjdftiSZ2PTj4RLXTRnp+hL2v+M3wekw0z/W14qiGqGDA==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvttXVThk2n3ChFPyQ4tsWL51MYWNWVV2aXXy+/HaX9mnS8CAWJ/5hbWetBdntDVv72m5m1stXRQrgZuQ5bdl/9azXlO+syP2r+hmhNkIBzGEL3eCT9BXCullQj4kfJf5pGDD1El/Z70bK546hZ5YZ6Y/jaP2Yd3zUmwRzv3po5QFC8HE0ThZZ8FensaDFxArsaJSCX2co5YLalOzNUvREtdEwzaIPMJW6yFy/78kKNmJdQDukjfbm1iPuiG7sPq4NCskngu3Wj2q/CSk6dAMj7paOydw3Td74faIrE47a7Ovo/iRE3HgSnUa3u7AX/J2FfJ2P5B7la/aEesjyXE7mL9A94o+obuj0bkj/DXNVPfUl/lAq6imPrybAtW9PsUcBAAAALJQdqv1f3dFSVXrKaIiO/Hz3zwdMyHRu4liNyemGP1e95AqhkGwxSVbukUMtb8OwSwO7KRQOyZcLIAyZEs7cSZBTZTKkdMd2PvkFtlvaugahRpj/UWZ4T4655wbp9fcAoRvdUph/QRgQ1hd/dg6H+EfaqdboOeapImPjMsXaLAZgrymcfgPWc1zAV3nMcu5ZKSzEmre625IwgMc9C7GXi49rloG+i4zRE6tdQh4fiSJEib7cboFMnpezyIytb0NeQTTgr7QwcdQYKZv7QGqPEwIH4JNvqzEfCH99FGlCQstGbQ3MI87tEiXbaDbJis5baidvLhuvOKTYEz5WYRG6pQQi63t+2YOoo6/4g31F5ksIfXxT6sQMNYoh67wi0qmuUpIRHTmbEV9Tuo5wUiQnQO9b9kfWOHOjuRzDXL9LtLgfhKilB93LibVEmR82CrVXKWWVOeJcKouB9ni0ZbJO2bgsPE18w8gN/xKL0oJ6qP1ZuIwcZy0uxTmxK4FOYhXTNxzR5Ja/QNVCUHhxr+XNNwe6EPI7hm7tPfv/NbmAW8qkVWMXkiS6eWmwe33aV4wk2Z+M6cmYlTDIj9hdHYQGSLzUF/LDK4UdSsdnfF1scTPFQweHip8LnJDXyz+21WKSEzsB59PVNwnJkNJBzMzKdydJNXXnmoZ+UvA7xiI1bLTRZTioJccNFadHcS/PHB2Nzg+PYfOn4jTPLJijsrPy6kYoVr/E5XrVGUDdyp4bQBAAD9Y70S+t+yQfCc9iAI4pwL1aNF43PuohVtoR4YHgVOglxVV4Ih/LwnEPE8InxW8etaBAc3JVyWItNtXLpWSSKArhidC43yBon6XzS5V4k5NiwN3gqzAI/thWgo6Sf0/CqAx8OWxAhizwpc9I/313re8+rWvp9BlvAqc7BM1GXLHFIkN1DY+Bok3zwJBi+eTtj7CEuhTbtQF7Sg2KDdZyBNrjQ+sUirA+aILIrmf5HrHVcvhEZw2PrHePslsn5ilqw8EtjSQJaq5cTTnIpQqPgSnursVx9uaTRThbJGx/nTeKLk/rQykamLuRF/fOnHq99a3v+g0H4mZiApfLTmD9UqPZ7iTnq50a7hIZb/fFHGPzL0DjIV1D8bhx006sjufiDRg5ChE0a68D3eSf2o7l62BUsyMEyJfZmQUfjyH7t2gPHJXt5REFpVX5NKh0KPgw0gdnmW/jsY9g8/Mt5cd9MAdSku5Q84/WAyLHmRezOL8l6QRMtDdpCV2rQhu7fzYwO4qcrKMC3yWfSSlKERYkASmzDUbqvCAfUppUw5n1d+UeR77FXmYMzU0cz4MwgfflveYCKBLMvhxk7sMwi86lBrFgQSeAhE+aLBcNbejrU8F0vSYpL1Ev+6MRkNTe3hCh2Emf56XNWiOhF5tq4F3Obufv91bETjfQNeSi1XNsa99SrVAU1xouSa85bhh6Xxj/MG5v1FPXZuN800YBrK1VUMAKRVYgwaKE5JVkHtKTCrBzAwh3EjjmCHzI40ukYcqdbW1zKTVfL74mhUOjbcIy+WKDkrwvv5oU1fdtuAcelYQyNUfx9J/7g9S/rnxQoocl+BwJhH1xknm+NqGF+YmDA==" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "F2300EC6976E42141BE76C2CE4431B2F9E52416F364751531DAA4D518ACDC263", + "noteCommitment": { + "type": "Buffer", + "data": "base64:h22WzyQM28FdJwz/5+pdskBv4M5M9VSQ5Esnd9PptWc=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:ADXSeNDKREYcMrm2aojZKeUZ5y6xpkyApOnN2+Rtcvo=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1724100971333, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAWuXyFyURf7TI49aQu7GhkswV4nuhYoi2mBfOK7MLo9qDZAee8uASIWVyjq215+2bSEqHCf/Req5va/8Li4SDxGJMy8sspgu9XZesD/jGPBKRXKJkGBqcvfilx/l07eQ1rmQgK5OPPuND7W4uui5JckguPNv+Ms8iwPZA+kHa6OwNo6isziINTn5Ly8bTZeu085pPi5mg1SPybYusZdaSgO4epZT5xyGYuo0kS7J9somEBXmeUSzrOXwlEol3BOftlBjlbCQoylsbfbvD5iWT8+uFVCsestaaxrwYjnz8vGCDwH87UhbrEKtHDAYMaw2ZibLq8bOAbVUcKY6G6NvvNYDD1xSMZj7jyw84cVThcEqm6hh2O/Z5A+ktqmfX6uVrVcQZ8Yq8mfH7Egd9Au2Moi9isUzU2CksBOy/xStizNQ8xjKYAIPEeXIqiLEZxIlnJD1JokaiMMj58ySNhabxF84cy/v0hdgeqHBNv0APj6KHxJqqDHGIkeuOpTbn1CAs1mqwYSIEDvgvYnGvObcfEl2hZEPiTURrfAEQSs1ITIknQFgNz4ZFgXJxu5Ym5iYDB4nIr6f0Q2DJY8uuD/LdU/WruV/x6HXGk9rWjmnSA1vvpKSv/g2eAUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwJLlsyvR4YHwp0t+d//ENNvGl3kw6nwLxrXNbWEWIiW40iKL7h9c3CjEfv1GpO1rJigB8nkbEWzhy4rGYXpXlCw==" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvttXVThk2n3ChFPyQ4tsWL51MYWNWVV2aXXy+/HaX9mnS8CAWJ/5hbWetBdntDVv72m5m1stXRQrgZuQ5bdl/9azXlO+syP2r+hmhNkIBzGEL3eCT9BXCullQj4kfJf5pGDD1El/Z70bK546hZ5YZ6Y/jaP2Yd3zUmwRzv3po5QFC8HE0ThZZ8FensaDFxArsaJSCX2co5YLalOzNUvREtdEwzaIPMJW6yFy/78kKNmJdQDukjfbm1iPuiG7sPq4NCskngu3Wj2q/CSk6dAMj7paOydw3Td74faIrE47a7Ovo/iRE3HgSnUa3u7AX/J2FfJ2P5B7la/aEesjyXE7mL9A94o+obuj0bkj/DXNVPfUl/lAq6imPrybAtW9PsUcBAAAALJQdqv1f3dFSVXrKaIiO/Hz3zwdMyHRu4liNyemGP1e95AqhkGwxSVbukUMtb8OwSwO7KRQOyZcLIAyZEs7cSZBTZTKkdMd2PvkFtlvaugahRpj/UWZ4T4655wbp9fcAoRvdUph/QRgQ1hd/dg6H+EfaqdboOeapImPjMsXaLAZgrymcfgPWc1zAV3nMcu5ZKSzEmre625IwgMc9C7GXi49rloG+i4zRE6tdQh4fiSJEib7cboFMnpezyIytb0NeQTTgr7QwcdQYKZv7QGqPEwIH4JNvqzEfCH99FGlCQstGbQ3MI87tEiXbaDbJis5baidvLhuvOKTYEz5WYRG6pQQi63t+2YOoo6/4g31F5ksIfXxT6sQMNYoh67wi0qmuUpIRHTmbEV9Tuo5wUiQnQO9b9kfWOHOjuRzDXL9LtLgfhKilB93LibVEmR82CrVXKWWVOeJcKouB9ni0ZbJO2bgsPE18w8gN/xKL0oJ6qP1ZuIwcZy0uxTmxK4FOYhXTNxzR5Ja/QNVCUHhxr+XNNwe6EPI7hm7tPfv/NbmAW8qkVWMXkiS6eWmwe33aV4wk2Z+M6cmYlTDIj9hdHYQGSLzUF/LDK4UdSsdnfF1scTPFQweHip8LnJDXyz+21WKSEzsB59PVNwnJkNJBzMzKdydJNXXnmoZ+UvA7xiI1bLTRZTioJccNFadHcS/PHB2Nzg+PYfOn4jTPLJijsrPy6kYoVr/E5XrVGUDdyp4bQBAAD9Y70S+t+yQfCc9iAI4pwL1aNF43PuohVtoR4YHgVOglxVV4Ih/LwnEPE8InxW8etaBAc3JVyWItNtXLpWSSKArhidC43yBon6XzS5V4k5NiwN3gqzAI/thWgo6Sf0/CqAx8OWxAhizwpc9I/313re8+rWvp9BlvAqc7BM1GXLHFIkN1DY+Bok3zwJBi+eTtj7CEuhTbtQF7Sg2KDdZyBNrjQ+sUirA+aILIrmf5HrHVcvhEZw2PrHePslsn5ilqw8EtjSQJaq5cTTnIpQqPgSnursVx9uaTRThbJGx/nTeKLk/rQykamLuRF/fOnHq99a3v+g0H4mZiApfLTmD9UqPZ7iTnq50a7hIZb/fFHGPzL0DjIV1D8bhx006sjufiDRg5ChE0a68D3eSf2o7l62BUsyMEyJfZmQUfjyH7t2gPHJXt5REFpVX5NKh0KPgw0gdnmW/jsY9g8/Mt5cd9MAdSku5Q84/WAyLHmRezOL8l6QRMtDdpCV2rQhu7fzYwO4qcrKMC3yWfSSlKERYkASmzDUbqvCAfUppUw5n1d+UeR77FXmYMzU0cz4MwgfflveYCKBLMvhxk7sMwi86lBrFgQSeAhE+aLBcNbejrU8F0vSYpL1Ev+6MRkNTe3hCh2Emf56XNWiOhF5tq4F3Obufv91bETjfQNeSi1XNsa99SrVAU1xouSa85bhh6Xxj/MG5v1FPXZuN800YBrK1VUMAKRVYgwaKE5JVkHtKTCrBzAwh3EjjmCHzI40ukYcqdbW1zKTVfL74mhUOjbcIy+WKDkrwvv5oU1fdtuAcelYQyNUfx9J/7g9S/rnxQoocl+BwJhH1xknm+NqGF+YmDA==" + } + ] + } + ], + "Wallet deleteTransaction should not delete a confirmed transaction": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "acca6d83-f008-41b1-8d40-dc9a97663b5c", + "name": "accountA", + "spendingKey": "c756d38921a46c1d55ca6a0448725886a3993806ce60053dbac252dd33e5d50c", + "viewKey": "12d84f9b163ad48517b4a05ed652c745f84fa65a09e79458dbf9fda4e660196e6576e7ec77103f48ead592a0f34596252cfc9ec476ff3042b4d9eb25f78151ae", + "incomingViewKey": "91d48dc8142707ec98c14c3e48d777ba6734c4e5c1d53b42e4a82fe05f73fa07", + "outgoingViewKey": "e1e5c7ada22d37327207f40eb1fa52ce1db2dcd5e3b80351e846c42dffdf9c9a", + "publicAddress": "84ebcb862a655cdfabbb0bbb02d658dbbb3dd49df5841dcf68ea5304f5e8aa51", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "d431c4e1ffd3c3453cbb17c48358c4e3b98932361f7c7c680736ebc542172c01" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "b8b3740b-145e-4053-adac-490ea46c60ae", + "name": "accountB", + "spendingKey": "d32165bf91624c164c6f60ec3a4f1ca8e1c9a1a01b941bd7c1fe997a40a4c8b3", + "viewKey": "889200a0b629ece12c6b39b96b035c48cb3b5dbf628026ae8ebcbee0858bbd3fd308734614cd9d05640ed66fac51982b24768791e9272189c739dbfee67e64db", + "incomingViewKey": "0601dfb90e1041464efcb33b47995e33c3e554de2e1dacac7bd6a99c6ffc0406", + "outgoingViewKey": "e9556a3bd75f8543ba605bee1438e90212470811e213bca650aa2e543f5b582e", + "publicAddress": "a6b3cee0444f0d898bfc2f992a526c340a2191e981269f0924ad682b7efb6156", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "58707418a3de3e872e964f03487105907508bcc627aef4b576c8238d6207e50d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "header": { + "sequence": 2, + "previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86", + "noteCommitment": { + "type": "Buffer", + "data": "base64:Bd5SAql/hUmGTdJ2N61gbJnqDhl/BfhRNC+oTUD5MkI=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:xQfm61+YatBK+iBtnu/Q2AK6zbJxX1E6h/ayzF3Oz6E=" + }, + "target": "9282972777491357380673661573939192202192629606981189395159182914949423", + "randomness": "0", + "timestamp": 1724100971960, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 4, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAzun/Nqui8jL55f3iArrAu4zaR3jY4g9yJ385X/5n7S2QInwUvlX+FqXNICJxzoJJwHu8+RVkq7HPplT2RQ36dtsQT9izxs7Hs7rwcOpHTe+nGJt+qrHTk87eiizTtct681FBD5CP1XlfvTbqnJ8GfwNbBcW4+SrGYyCjZeXgVSkU5qU0f7jj6nFjJUfGHiG9iqu2rqmi377QjJzegXsu25Z0t1cSxC0nzQCC74mmMDuSS9InrVaIu1pI7KsnpnWJZLBNTvZCuQWAf+hSOjI0bZau0Bpgaqo1zl1R33KxACj/mXgHnS3W59cTrxXGUgeEK0tVLmUh19J3nc50qHWjVHeRL/ZYMgz0SUR0Jactqc5sdR1kIW87BpjTbpblxFNWnehKszn1/MDgtiF1WkgvedwWOd23u0/6L7cmD6Nj9RC55Rjxk/DQQypSqNkemJJihLSKN3oaIPgbM8ztbAF2I6qo6qAHGisjb7mwtEz7NR92IYec7yUMnEVdvy7rUznheqAO8vsn0cEoQ5d9f34tAUG2+w6KyRpP+qtcYBWH1XA41LY5omLe3jaqiHRl073ZFpQ8E8aR0rf1TRJV5ziuTcEoGAaN1jHEmYyni+5+N+GVsPQp+KMJfklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwxzHYas8i9jKqt5xn6RWsp+vbNAjHqsECxw5gfgQKlEwtS+wMdFKy+9cFj2uDCqM44HjrxCPWi4rOwn1gQ3a7Cg==" + } + ] + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFZDyQdtPQjBlgJgtldneLw/Hwukm7EBSCBVVPjCOaqkapZlceNGdXibJQuERzh2LQ/H4AJvJiEARo/EiAcOBC3aLsWC4SjvZy6Sh6vaLbyN+qkyKZJ7XJaL+objKdry5MxY3UIlOMbMFNfPJbtp80HMQHxXzYPVTSiJG6usQ98PIqEpyAU7a8jw/AcoGCEnH+6ucbvz1+UeZF953BqeTj8HGOWchq/IlsWEOFOzfE2SFd9MPriL8jK2YKD4ejGxiidLp6OLG+m9xnOHKE9e4HLD3aQ6H6su8gcGL0mvqk+dhORzodSvq2miK5czzmiuYmnzk3M9F4+FcIfyEVu0lQXeUgKpf4VJhk3SdjetYGyZ6g4ZfwX4UTQvqE1A+TJCBAAAANCGeKQ6mKGoj7j7x46qgiX7oAp1QJjuq8P0/gkxCCGRFYWnAoE745yCk5NPTB9yOUcO89/HVa0Ucw7Y56btqIdkGDUGopuoTSwwxB0hw+y8MttVZww/jEI6o5L91gk4A5FuyAS+iKNycQ009yyiEMrFldXmUfEz/j0j1NoXxwI8uK6fjt29xgGqUSA0cW6hvoSFKuoUYkUt5VM6aPOoa+uVLPU3kOXRVLfoQ9McGi5sVyLX4a3NVWuK+/g7ygL4nwyMYpXxBpO0PSgRukZzA/Dulc3VfyoSpl9nm9aA31LuO3rzGsnEI8jMCJnFsWb2s7PFozml1WsCT48KaYYGENRaJw+8KI30pczVRzXoEQZNIpKPKCBiuXhbXH1my2HCae2w/oJyph1To/4KUU82Ytuoso4JHkmrGlRnYnOsX5cG1p5H8ocazxfyXeg2BDiG+TTj1lrurjAPVtT91ejpgCasOd9kStU4sK7SrCMLxFddeJ8blUdcMuvMDo2TIo8htIAk+TO51y5RywVDS+sgIoQyBl9oBOw4yH6Cb+GUkXHV60Lah6Zj8fIfybqsQiXDIABXd/Tg11Yk21xTSfwgv9GnXtq6P1iYrXxoHqlpQO47jQAkCYeuyl5rB+lFmhophUKPxeL8El57lw4J4jl+zbToENWlR8/WB73hsIlFjSePE3MNYeMBP28nBfOGf9slzVB06E1rbnZEi3ZKUbrc859+JvNjmCr4cxNHibewKYwphk6KUy5GYTMnEk/iYVpl///FzzrQaEZhz7u/VWQGjMANAaQVYvszTVrm+NtX0y0rBahCGILqbLytZOOcX1Fuw3mNnYnxrP8IsH6akx1J3ddTDnKqjUySi5GhnamOSHMPVY7sqVcgmNuzNzNmBuYucRAx/TCWiZAlnvaJB8knOUY0HGuAX5G/tRbAHR9ZHwJIP5F2mESqUPcRdSsgZRwH3W/QQB5UWk0DfHJkG1C1yhEOd5O1C491qBo2SgRvb/DIOg77UqKLyty0MKWDeX0pxKcS9zVYxoestQMHrP3wxeB717gaHx+IZ6j3FQn/1fYPjQo7qO49Vl9Ci6pnmclKjnHg0sDsGE6HrTrbV/GmZqugHRIkQ8xnaT0UNJs+r20tpjeVS3q+hO1iRYvUwr8ht0dBrqCFSj43fkA3t4jQBGZE+MuXmlSaJC7lexHYsPScVFjhSsf6rkKEPQIscz5usmKE5c4OqRjhLEViYnkTNe6qWLh/nBNkci5Vt+KgledP67okJJZBKjdRrPyYrG6OtR3+Y+R8TARjbES+oCAHxaZnhtYzF31Z7fYBKxF61eh2EDjmTSIHvArNhI4ZxLkZq0t70FSD8znZrTkfEkrw/zBByxjwLc1Gp2Fjv5xDcKEC0PKEHBs2e3fbDcg3dMkFyCgqnG7KwZ/D3RT8OhtoU/9wWX1l3ZhhndRBgVYXMCwqPQ/OFH8yinCkJOjkYR9j2Kfk4As5zgr3q6lxiXIf9WBihacgtd0aTQrS9d8+ReCUI5jHQsTjm7b7ftVytt1pV0RdYgBIEmdEE+Bo4xR+OFjuDCi87qjTDZM4DGGd59yHM7HKFLLo2E8ujLYwIpLMCA==" + }, + { + "header": { + "sequence": 3, + "previousBlockHash": "781548CE5FD03C6D168EF77DC67124DE7D56BAC14307ADF4CBA0C4E9699DE4DA", + "noteCommitment": { + "type": "Buffer", + "data": "base64:1fh9L0r9ikvGSJc7aZfQUvp5hDfFNUQtStrDTGKMsQQ=" + }, + "transactionCommitment": { + "type": "Buffer", + "data": "base64:xjzofuNy07Pasq14U5aEyu444juTuEyOJvxvxf6kIds=" + }, + "target": "9255858786337818395603165512831024101510453493377417362192396248796027", + "randomness": "0", + "timestamp": 1724100973876, + "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", + "noteSize": 7, + "work": "0" + }, + "transactions": [ + { + "type": "Buffer", + "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAANTu+S0UWAVMJKt07vEuPyxIIgbTJpojSAVDDPRuXPbCyjL9XfR76aM9P74HMHJ1pA9s+6KZw7x1E4PKM0FRvqS/ccJMqapDvWRquaFvnKbSSi9PW+zJ70pbrTSvOOwx9BLvkkgJTkBgy6qymW9eVhmlVKStMZwpFqjdP7QyeDnUWMuoFLJFEPceBYQItsgqs4fL0YI1ukGjJDO003HQNntOjrg22IdddtgCp5yx+sEGDr+BkGKf0GPWogDsqehF9rkefFW0y6NQU8c4fKECAA/uGz4aEfr5oSrbx5GgFjgRmdL6hIjySMVp+v/O4pM7S9UgbZHlRTHZAEgVKqUWakYGdH6tsD1USDTiGSjb9I2j/AZxXLDCeRBZMHvY+iT8EI1lvw6cKnwnZBxN7CBi67Ob42Y42BcLKoC4Gst57TiKSp57Erj2T+cwA8jJ5tAI/u946jw6uVa1dDIjhNt6ocDoD/qkmwqoNghm2Hu7e4ZstMqc66M2e13EN4HuUeXONV7N3g1z94nFFWqOIuIVPxdP8bEliOlRlOU6HJYUrV55S2IYLRopjAs1VMtx/zAKhtwJvrQF304NYQKxPHmwwA92qw6lxpbIxyvyMFNEgHL1B1iSjd8XpiUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcc3PL5yTsUhdrmHot0sIefDAHWFs5fjUKhj7AfcC18o0faPZGb6bUBvRQNfNzD5alUwXArubL0oNzO5KSHzBCw==" + }, + { + "type": "Buffer", + "data": "base64:AQEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFZDyQdtPQjBlgJgtldneLw/Hwukm7EBSCBVVPjCOaqkapZlceNGdXibJQuERzh2LQ/H4AJvJiEARo/EiAcOBC3aLsWC4SjvZy6Sh6vaLbyN+qkyKZJ7XJaL+objKdry5MxY3UIlOMbMFNfPJbtp80HMQHxXzYPVTSiJG6usQ98PIqEpyAU7a8jw/AcoGCEnH+6ucbvz1+UeZF953BqeTj8HGOWchq/IlsWEOFOzfE2SFd9MPriL8jK2YKD4ejGxiidLp6OLG+m9xnOHKE9e4HLD3aQ6H6su8gcGL0mvqk+dhORzodSvq2miK5czzmiuYmnzk3M9F4+FcIfyEVu0lQXeUgKpf4VJhk3SdjetYGyZ6g4ZfwX4UTQvqE1A+TJCBAAAANCGeKQ6mKGoj7j7x46qgiX7oAp1QJjuq8P0/gkxCCGRFYWnAoE745yCk5NPTB9yOUcO89/HVa0Ucw7Y56btqIdkGDUGopuoTSwwxB0hw+y8MttVZww/jEI6o5L91gk4A5FuyAS+iKNycQ009yyiEMrFldXmUfEz/j0j1NoXxwI8uK6fjt29xgGqUSA0cW6hvoSFKuoUYkUt5VM6aPOoa+uVLPU3kOXRVLfoQ9McGi5sVyLX4a3NVWuK+/g7ygL4nwyMYpXxBpO0PSgRukZzA/Dulc3VfyoSpl9nm9aA31LuO3rzGsnEI8jMCJnFsWb2s7PFozml1WsCT48KaYYGENRaJw+8KI30pczVRzXoEQZNIpKPKCBiuXhbXH1my2HCae2w/oJyph1To/4KUU82Ytuoso4JHkmrGlRnYnOsX5cG1p5H8ocazxfyXeg2BDiG+TTj1lrurjAPVtT91ejpgCasOd9kStU4sK7SrCMLxFddeJ8blUdcMuvMDo2TIo8htIAk+TO51y5RywVDS+sgIoQyBl9oBOw4yH6Cb+GUkXHV60Lah6Zj8fIfybqsQiXDIABXd/Tg11Yk21xTSfwgv9GnXtq6P1iYrXxoHqlpQO47jQAkCYeuyl5rB+lFmhophUKPxeL8El57lw4J4jl+zbToENWlR8/WB73hsIlFjSePE3MNYeMBP28nBfOGf9slzVB06E1rbnZEi3ZKUbrc859+JvNjmCr4cxNHibewKYwphk6KUy5GYTMnEk/iYVpl///FzzrQaEZhz7u/VWQGjMANAaQVYvszTVrm+NtX0y0rBahCGILqbLytZOOcX1Fuw3mNnYnxrP8IsH6akx1J3ddTDnKqjUySi5GhnamOSHMPVY7sqVcgmNuzNzNmBuYucRAx/TCWiZAlnvaJB8knOUY0HGuAX5G/tRbAHR9ZHwJIP5F2mESqUPcRdSsgZRwH3W/QQB5UWk0DfHJkG1C1yhEOd5O1C491qBo2SgRvb/DIOg77UqKLyty0MKWDeX0pxKcS9zVYxoestQMHrP3wxeB717gaHx+IZ6j3FQn/1fYPjQo7qO49Vl9Ci6pnmclKjnHg0sDsGE6HrTrbV/GmZqugHRIkQ8xnaT0UNJs+r20tpjeVS3q+hO1iRYvUwr8ht0dBrqCFSj43fkA3t4jQBGZE+MuXmlSaJC7lexHYsPScVFjhSsf6rkKEPQIscz5usmKE5c4OqRjhLEViYnkTNe6qWLh/nBNkci5Vt+KgledP67okJJZBKjdRrPyYrG6OtR3+Y+R8TARjbES+oCAHxaZnhtYzF31Z7fYBKxF61eh2EDjmTSIHvArNhI4ZxLkZq0t70FSD8znZrTkfEkrw/zBByxjwLc1Gp2Fjv5xDcKEC0PKEHBs2e3fbDcg3dMkFyCgqnG7KwZ/D3RT8OhtoU/9wWX1l3ZhhndRBgVYXMCwqPQ/OFH8yinCkJOjkYR9j2Kfk4As5zgr3q6lxiXIf9WBihacgtd0aTQrS9d8+ReCUI5jHQsTjm7b7ftVytt1pV0RdYgBIEmdEE+Bo4xR+OFjuDCi87qjTDZM4DGGd59yHM7HKFLLo2E8ujLYwIpLMCA==" + } + ] + } + ], + "Wallet importAccount should throw an error when the wallet is encrypted and there is no passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "a01a5314-b884-444b-8eec-c47aa8492756", + "name": "A", + "spendingKey": "ad2eddc5a1dc1230df218496213ca6c6382118d96d2c8ffaf384cf76bb832fbe", + "viewKey": "564c0f3f9b408d26e472300f7db570f08255df2b90b97dc41438f0ed9c85b698c5626317e4209ac92e44ef4f824ae9606e721bbfcc6410be388c0d5800188768", + "incomingViewKey": "34cb22d6ee1babef87814ab2c2ad4289eb1aa26273a8726379c5e7b1f06d4b03", + "outgoingViewKey": "8c474949652b7890d821edce628b53c753c1f96d558d3bca80ab2ed75bf44a76", + "publicAddress": "e46acb643c937f7d370046d710383c84cbab530f979c650f4f692121ffdd730e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "2cd5ee2aa54f226ef01b6e50882c8d366fc6e20106a6165995b29cd7feb64609" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet importAccount should encrypt and store the account if the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "49e2a0d9-797c-4e00-be2e-30bb721c7abc", + "name": "A", + "spendingKey": "4a7bb3f99b2179be0c2d8ed2e64c777db53373fe7591ce3f7af1f646645f2669", + "viewKey": "b6be34f85957f7da7c357b96295b8456f8d6abd7e00c9fa6b804990a0f3c5c066899f395efc2e88d1d2f61b6e1ef1be4c3c7914005bf08e295153c3a634f9d00", + "incomingViewKey": "6fd24b8877ab029879f6c185d089a57fa155f13cdf4d3fe2608e849c2e7dce04", + "outgoingViewKey": "babbc58950db472c941fc697cc167e8095770b29c33de07c389b54e4ed26a012", + "publicAddress": "1b542a7505e93228fca0e5c31708fb0e1da57f5c992b3acc594ca7db1679e86a", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "443dff9087d0f23a139cd3ef3bb5065436f4a7cbe0fd68ed73f6be3cce0f400e" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet importAccount should throw an error when the wallet is encrypted and the passphrase is incorrect": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f314a1b1-2f72-4cdb-b642-1846639885df", + "name": "A", + "spendingKey": "d27ae281a962d050b624cc11f0c575e19e270b095e94fa7f2e225984263bb98d", + "viewKey": "8e56b51f4f2f9bdc084ba04b192550458114750dca61942b8f148ae8af0c2320647305a1a1770c56fc2bde2b36e7f5ed7927b6ffe272db5def306aa2df9639c3", + "incomingViewKey": "2629fceae11c0b2d2b56a1f918947533f1c1fbe7b45be4c77527970aa877e701", + "outgoingViewKey": "a90418c4fb17dd151a5678c58cbc0cd0134d9245b1597d48709464a0154a07c2", + "publicAddress": "dc4182b4a354566727d3b8e41287b6c89028cae6129360447f40dcb2212a284d", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e47274a4b3cd00b89727106857ad3dfe8ed1c2b94b95721a6d08bdd702d73101" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet resetAccount should throw an error if the wallet is encrypted and the passphrase is not provided": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "cfadf46a-dc13-4e7f-81e7-d74128b1cdbb", + "name": "A", + "spendingKey": "852f1b373ef781f1d29d9e9f39c9473f70f7603d871e442f76b227bba6d52cda", + "viewKey": "925996d92ca97ce6a76a9b9e4b167e2ba091d692205b6f70a160c1b355f59008a8b20cded41314b7b015450ad61c854badff212a6f210ca43937fb5755a2b024", + "incomingViewKey": "d098c5ff87958c2b3a855376ed8c6d41ddc23cf526ed5a352401ca91bdd5fa05", + "outgoingViewKey": "11ff38cfea5f9cd8be6f3bc04db871b77abc396b0cc3a1f01c944cb379ffcb0b", + "publicAddress": "ffd363ed6c09d9f3bee23c9da20f344e122c1f57cf07d163d38696d89c042785", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1523258397a7359553b9a39cdd226eb92b11f2dd6d2376b3e33441b3db2c9803" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet resetAccount should throw an error if the wallet is encrypted and the passphrase is incorrect": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "e20a4166-8927-4d6e-8334-c38f14865a77", + "name": "A", + "spendingKey": "e0b052fdbab001478ffeb1fba279f7f89da1e4ff10e0fcd832b7be3263377e1f", + "viewKey": "d10d54e76cf646764de5bcc2761bbb7dd6d4585d3dabd11e3aa6736c0ee578876dfd7f312f32b12e9ff937956a4c24a595fc733bc18918c87f474f8fef57a046", + "incomingViewKey": "840566453558f7effbf43575d083203a24c7658995befab02927343a1515fa00", + "outgoingViewKey": "e5e2d8e98d4aa87be1faec3ed52b7ac48b4521f9c94a12910d562edc31fa8dfd", + "publicAddress": "3c47269fd0adc92a752ab55c85a49525a89ed48d46ccefa99dc2ed232d0f7556", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "4b1edb19d77dfeb2806d1a7bab98e562c75d0e9569eba5088b4e2f498d8b5707" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet resetAccount save the encrypted account when the wallet is encrypted and passphrase is valid": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "e75f5d8e-0338-400b-b166-31d0ba82b827", + "name": "A", + "spendingKey": "d4fd92b66615797fe372d90b5124b655d4b98489bc51de6ab62438640f8271e4", + "viewKey": "8997adb9c4271b2ce8c56d9444cad37bbcc8eba3a7aa55577a88d9661c097fdadee9ae53f5830acbccb39d7567bd21867132bf831ef40bd9542562304b64980c", + "incomingViewKey": "d589e64e471a7c588ae4906cd370c6fdc1d7eb25d6bab676fe17952625bab104", + "outgoingViewKey": "2d37c9ff2e401a3000c1f5c19463708923bc143c947a77fc9413f5852e40d34a", + "publicAddress": "31df6b319421f1108114f0efe9ac6b8479ec3a4cfe52f10fb0f4a7131e1ef948", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "a27afb3eb769e16db0fb0a4feddc63c881da6bd194d6d0ea505971000468b203" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet createAccount should throw an error if the wallet is encrypted and no passphrase is provided": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "1c6e07eb-c169-4b57-abf9-265edf76e1fa", + "name": "A", + "spendingKey": "d943511a50b8462f22b2dfd211af1cb259d35d863ba9bf951a0cbedd7e31da00", + "viewKey": "dd73ab5660d0d0a0e284cb8bf7ff12b426359088cf80e8b060ce004f4dbc37468a268af4b8d099aa7f1b28fe916a43f9d1d453c2c39998695f4bc9cbbff17f60", + "incomingViewKey": "dfae17b21ba416ec45f6d501ad18b357b0f8a74e0d5c634d8916f87214dec902", + "outgoingViewKey": "cbec4ca0886e0f2f9463b49f30d72f82c5b785c7f93735bfa873cd02447294dc", + "publicAddress": "c189195086f945f7eb47761b5c76186885a3ed80e0f4e004e0bec391d0a2d362", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "58d69b65852dcbc009694caf64e01350a114c3dd3184a187dcb9836be4577d0e" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet createAccount should throw an error if the wallet is encrypted and an incorrect passphrase is provided": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "bf34ed08-531f-426a-ad4e-04850e4ec30d", + "name": "A", + "spendingKey": "5e0dc425fe146cfae4ff69ae152befc60d63a43ae25e61dfec42f80f96ab3a66", + "viewKey": "65fb139017e246a2deab7dd168383e4d1e2199ec5dbdc3ff4818e22b7aeb265f58f220bf5059c259f99771b03ffb1e7f66162bc7d4191342b134632768f96b08", + "incomingViewKey": "e4763f3e32f92f949ae618837d88d3a8b4ebc78ee6edf66634a1f2e755ea3d03", + "outgoingViewKey": "b5c437917dc0e319f76be255520e59bbdaf84eb753321a6524014fabc469275a", + "publicAddress": "a9e24ca9cc0a6ac7ea1c0aa62fcc5219ceb79cb1534ddd4b1b406995baa985b7", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "dfcf2e74f203fec5c6101bf1cab647d01c36864cd87b5eb9f2e21f554f7aa801" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Wallet createAccount should save a new encrypted account with the correct passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4485f7db-a46d-4647-aa6b-fa076ea71a35", + "name": "A", + "spendingKey": "4e9af29010a71416b5430e32162d2fc62a0ad33b933067e7c96e585cb7643334", + "viewKey": "ef434cfa62b32cd927dae4a767c8f1f5c87029b85aa57e7df0d0aa025bb84471022e1c0c36d1acc95d916740d89dce5469169af479bd0862914b0cf8a1c9691b", + "incomingViewKey": "ebab5cdb70bde85d171a9db146ef8bb6bf99e60518c89ac53c122790a21e5701", + "outgoingViewKey": "a18924829aa89073d7b2e81982868c4c41cb3989914e26c6feb03b0372b2f419", + "publicAddress": "71bc93dd9eebf50402bfaedc63f302a61bdc95fef7630bd5526c259dd353542f", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "acb375d98a7f82e9b72b9a8b9448d2db409ad4b089bc8296f1fc4e019888af0a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index 71b3bf6140..b0e82282cf 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5810,5 +5810,191 @@ } ] } + ], + "Accounts encrypt returns an encrypted account that can be decrypted into the original account": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "1bbc8bfa-2393-41fd-b1ad-a3ca03aee8ac", + "name": "test", + "spendingKey": "6a0ae63bd630bb5b205737f49cb0778a872fab11f7e797935adbb9716917c612", + "viewKey": "0c1888cfbe1d0f02c3069df3350665c86d2f56cfc64242d53c43925d09e1012c3f0cf9098c9a9fb7f826509d15d7140eb6d76ccfcb2fff81a3dacd382cf7fe00", + "incomingViewKey": "781829745831663347945ff2ac60e0c6181ffa8d73d9bcc4f5fd24a40f1dcd06", + "outgoingViewKey": "1b3ace62d49d5c0c55773264e78aeb6125b451595cf33f8033dcce01d9507d2a", + "publicAddress": "a634ebb5367d5ef7196b7226a7cf6583980b689992c2f595febc4648d2ee7449", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "cc1913dba277eb64857337a5144a5c2f7f4ebc4af1915b667aa1a46b3e49fe07" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should rename an account": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4d934926-259d-4926-958d-a4d6b3e4d50c", + "name": "accountA", + "spendingKey": "d680a4a56c52fee0832800067948d66c0296f06970252a01cfb8368be8a8d141", + "viewKey": "b21cec78809681a5ca376220c7b75aeaff2e7b6159bb07db194a072e568f56beb2a8804428dc4fb36fa8dd99347312e8570386a5f55a9844555778149a352d4e", + "incomingViewKey": "9aad17ed39340920700c12392762953da2c7a14df0d2afea783d84cb19401b03", + "outgoingViewKey": "0ba463093a0f32b0b163bbfc88a398eb22386363457a9f786cc40d226f80ce18", + "publicAddress": "abd628bdcf088f7465f98628de06723514415b9511c859b943db519738f70552", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e839d3ed4a71d618b48bac49b51b94766437410813775eaf976d3e4c29cdb909" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should not allow blank names": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "a4d623c5-1816-4dcd-b152-8581e3184482", + "name": "accountA", + "spendingKey": "184d9512d2e35b4ef90ad6bce36874da8e2570bd16705ecd7b2e3bc3b8b2790c", + "viewKey": "9b88c9881b603e5599fbd8cc3d6e2538e291b325a2d85c6d863f4dc8e5e65d0422f8d8b14eb0ac9714d9e90dfc84d94e3aab030100a71c06c7840f1ddca5454e", + "incomingViewKey": "4c72a84c56a21765950d6e835b250277590bfec11f0311ee162f48ac563c4007", + "outgoingViewKey": "d8fce77c8c7660c7028246828316029e6eede4767688533b3d4ef83e97d63d99", + "publicAddress": "d4e04322213119f855d6ba0e56ef4da36f5d72d3abbf81ed2ff951a6d022b6a0", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "7f0eabcd1ab1efe3818ad7d20cae55a6def4bb2850baacdca3aca03a18bbf30a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should throw an error if the passphrase is missing and the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "aea047be-8c73-448e-8e8d-22844f49736f", + "name": "accountA", + "spendingKey": "431a4e45c614fd41d7cee2de809e4464e92a125752d6f6839c0af7c706a01f67", + "viewKey": "4be28bbdca174c4e7499d913ea02642d74136eea9058f4e6eb7586dd5c864e905128a5aa5d25356934e5f4cf36575445b8dc60bfa672e1c135521cb5610e9b4c", + "incomingViewKey": "78b6e7887d55853b84d5d0b3b80d787379750b8839ddbc738882124453a5c004", + "outgoingViewKey": "3ce3ee26d7ab6c76838b6920eb6b5cf8fa3aa1ca54940d3b3cbf20532be34e41", + "publicAddress": "09d7f58ee5ae406b19e714b7fab882b83334e5b566a64c9d9707fcc513426163", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c95606e98895f390f247f0c9e4beb22a039adfdab5cd11dac28263870dfe4802" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should throw an error if there is no master key and the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "df4836f9-2889-471c-b9c3-c1aaa38fc595", + "name": "accountA", + "spendingKey": "433f92654fe57940d18e87481c52588fec36fa552f64dca434b1726e022722cd", + "viewKey": "1d6e8a7faea61bdfd77d6e3c3b63951b3e613a8907bd6786fe0466e3490a8558eec35c9e08b3abbbaa0b596f427801bfc306297d0c3bf1b50ad02aa62cae702f", + "incomingViewKey": "88328774271cf9b9437e0b76ffc05c5697742d2004b9430a257590cceccbb000", + "outgoingViewKey": "5cb8a083108e4f3aa7d5db9a6ddce584d9b3a941cf707ee691b016ddf6c0fc87", + "publicAddress": "534ba54bbf1721cb16e4c8a702f25d96a0c99c2f02c38431be8870a28fc04914", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "25079aaa8548a644fc43ba77584bb0cb53adb60e3b2a033865df05569ee44302" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should save the encrypted account if the passphrase is correct and the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "de02e4f1-ad01-464d-9e5b-9b4e83c57f0c", + "name": "accountA", + "spendingKey": "4c946561f929bfc55faf90cda2f3c8bc6881b0cf96f0a422acb4c2240d5b995b", + "viewKey": "3120c5db86dd17d6b11f14df5f652878306aba202c74db71aad778e45ae4888843eaa4e8fa18ee78a762c2ed3b024c29d0ccd4163b0e49dba1e9c0b2055fd5b5", + "incomingViewKey": "2b1bcb2a5821e8f0485740aaec3157d43af552e853c97fa09dedb86442095003", + "outgoingViewKey": "dff7ee410247d8d2b63cd0da40509bd8fc3e96a4d26c04ec9fdb016da2fbb2ea", + "publicAddress": "9a3f5703d2cf40fc2899bf25793249b44e187acbd5c1f6f4f20ad4143408e42e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "8726a9a636670be8e1fd12a0b4b42a1d0e30a526b28cae014afc7b241ad1fb0d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/account/__fixtures__/encryptedAccount.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/encryptedAccount.test.ts.fixture new file mode 100644 index 0000000000..00f0a6dc88 --- /dev/null +++ b/ironfish/src/wallet/account/__fixtures__/encryptedAccount.test.ts.fixture @@ -0,0 +1,64 @@ +{ + "EncryptedAccount can decrypt an encrypted account": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "0332daf1-4ba3-42b3-9d72-6803529df295", + "name": "test", + "spendingKey": "f7bff5aabef137c1231d16fdaee2c0b7da61e81cebd08e8be35d0cd0dea1f015", + "viewKey": "c8ee06884cc6aa31002c058a511aeea219059579350cbe6deb07416ad049e7cfef7877b730dd1e4d982d5228e8badf0c7020b1d07ff0eb23b298d9c97b0d5e1c", + "incomingViewKey": "d9753f22ba723bdc381cd3dba675ef36247a6807e5c0d082f88948496f687601", + "outgoingViewKey": "03268cd3770209e043742554a83f6944a768980309ba2beec2bc8995fe446130", + "publicAddress": "7742fa58b9e1efc337b05d54dea06d1dabd89c78cd98c7656f667314da865d04", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "67e962ba0d98e71f351534ad8d9bb8cfe01c76f291b06933b694b57b7808c10a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "EncryptedAccount throws an error when an invalid passphrase is used": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "2710f42e-f848-4d67-b0a2-9e4eac56df7d", + "name": "test", + "spendingKey": "eb527038b1b452c2abb13c1c7135309d6acd9e4737a7f18aa75bb4363141e077", + "viewKey": "b05272abd89b4eb48435b336d638771e69854fd36947971d22b3bd9cde7a2a5c0d83d88ccc8ef8259693c1391eedc1a25e5fccdf554ff605b593c15a619aa851", + "incomingViewKey": "fd48d3ddf807a6d1a881af440f2b3b23fc670b70323ecbb08f8dd54ff9220401", + "outgoingViewKey": "62603f4717d3a7f7ab00887ad77c1567b103f06fc885aa4a000625cf4b278f81", + "publicAddress": "40fd3b67d0abeacda175781f1bb97d5a08dff7b3850cc94ce0b3c3dfee742a41", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "c0159378c279f74958da06b63f3db79a85e84abf4919b1c9b321392de5be9a06" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/wallet/account/account.test.ts b/ironfish/src/wallet/account/account.test.ts index 0d24f6fc4d..47cb5b61e0 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -17,8 +17,10 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils/async' +import { MasterKey } from '../masterKey' import { BalanceValue } from '../walletdb/balanceValue' import { Account } from './account' +import { EncryptedAccount } from './encryptedAccount' describe('Accounts', () => { const nodeTest = createNodeTest() @@ -167,6 +169,76 @@ describe('Accounts', () => { await expect(account.getTransaction(tx.hash())).resolves.toBeDefined() }) + describe('setName', () => { + it('should rename an account', async () => { + const { node } = nodeTest + + const account = await useAccountFixture(node.wallet, 'accountA') + + await account.setName('newName') + + expect(node.wallet.getAccountByName('newName')).toBeDefined() + }) + + it('should not allow blank names', async () => { + const { node } = nodeTest + + const account = await useAccountFixture(node.wallet, 'accountA') + + await expect(account.setName('')).rejects.toThrow('Account name cannot be blank') + await expect(account.setName(' ')).rejects.toThrow('Account name cannot be blank') + }) + + it('should throw an error if the passphrase is missing and the wallet is encrypted', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + const account = await useAccountFixture(node.wallet, 'accountA') + await node.wallet.encrypt(passphrase) + + await expect(account.setName('B')).rejects.toThrow() + }) + + it('should throw an error if there is no master key and the wallet is encrypted', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + const account = await useAccountFixture(node.wallet, 'accountA') + await node.wallet.encrypt(passphrase) + + await expect(account.setName('B')).rejects.toThrow() + }) + + it('should save the encrypted account if the passphrase is correct and the wallet is encrypted', async () => { + const { node } = nodeTest + const passphrase = 'foo' + const newName = 'B' + + const account = await useAccountFixture(node.wallet, 'accountA') + await node.wallet.encrypt(passphrase) + + await node.wallet.unlock(passphrase) + await node.wallet.setName(account, newName) + await node.wallet.lock() + + const accountValue = await node.wallet.walletDb.accounts.get(account.id) + Assert.isNotUndefined(accountValue) + Assert.isTrue(accountValue.encrypted) + + const encryptedAccount = new EncryptedAccount({ + accountValue, + walletDb: node.wallet.walletDb, + }) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) + + expect(decryptedAccount.name).toEqual(newName) + }) + }) + describe('loadPendingTransactions', () => { it('should load pending transactions', async () => { const { node } = nodeTest @@ -2596,4 +2668,20 @@ describe('Accounts', () => { expect(accountTransactionHashes).toEqual(blockTransactionHashes) }) }) + + describe('encrypt', () => { + it('returns an encrypted account that can be decrypted into the original account', async () => { + const { node } = nodeTest + const account = await useAccountFixture(node.wallet) + const passphrase = 'foo' + + const masterKey = MasterKey.generate(passphrase) + const key = await masterKey.unlock(passphrase) + const encryptedAccount = account.encrypt(masterKey) + + const decryptedAccount = encryptedAccount.decrypt(key) + + expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 861627fbec..fc21d97f2d 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -1,8 +1,7 @@ /* 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 { multisig } from '@ironfish/rust-nodejs' -import { Asset } from '@ironfish/rust-nodejs' +import { Asset, multisig } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' import MurmurHash3 from 'imurmurhash' import { Assert } from '../../assert' @@ -15,7 +14,8 @@ import { WithNonNull, WithRequired } from '../../utils' import { DecryptedNote } from '../../workerPool/tasks/decryptNotes' import { AssetBalances } from '../assetBalances' import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' -import { AccountValue } from '../walletdb/accountValue' +import { MasterKey } from '../masterKey' +import { AccountValueEncoding, DecryptedAccountValue } from '../walletdb/accountValue' import { AssetValue } from '../walletdb/assetValue' import { BalanceValue } from '../walletdb/balanceValue' import { DecryptedNoteValue } from '../walletdb/decryptedNoteValue' @@ -23,6 +23,7 @@ import { HeadValue } from '../walletdb/headValue' import { isSignerMultisig } from '../walletdb/multisigKeys' import { TransactionValue } from '../walletdb/transactionValue' import { WalletDB } from '../walletdb/walletdb' +import { EncryptedAccount } from './encryptedAccount' export const ACCOUNT_KEY_LENGTH = 32 @@ -76,7 +77,13 @@ export class Account { readonly multisigKeys?: MultisigKeys readonly proofAuthorizingKey: string | null - constructor({ accountValue, walletDb }: { accountValue: AccountValue; walletDb: WalletDB }) { + constructor({ + accountValue, + walletDb, + }: { + accountValue: DecryptedAccountValue + walletDb: WalletDB + }) { this.id = accountValue.id this.name = accountValue.name this.spendingKey = accountValue.spendingKey @@ -102,8 +109,9 @@ export class Account { return this.spendingKey !== null } - serialize(): AccountValue { + serialize(): DecryptedAccountValue { return { + encrypted: false, version: this.version, id: this.id, name: this.name, @@ -119,10 +127,26 @@ export class Account { } } - async setName(name: string, tx?: IDatabaseTransaction): Promise { + async setName( + name: string, + options?: { masterKey: MasterKey | null }, + tx?: IDatabaseTransaction, + ): Promise { + if (!name.trim()) { + throw new Error('Account name cannot be blank') + } + + const walletEncrypted = await this.walletDb.accountsEncrypted(tx) + this.name = name - await this.walletDb.setAccount(this, tx) + if (walletEncrypted) { + Assert.isNotUndefined(options) + Assert.isNotNull(options?.masterKey) + await this.walletDb.setEncryptedAccount(this, options.masterKey, tx) + } else { + await this.walletDb.setAccount(this, tx) + } } async *getNotes( @@ -934,40 +958,64 @@ export class Account { sequence: number | null }> { const head = await this.getHead() - if (!head) { - return - } - const pendingByAsset = await this.getPendingDeltas(head.sequence, tx) - const unconfirmedByAsset = await this.getUnconfirmedDeltas(head.sequence, confirmations, tx) + let hasNative = false - for await (const { assetId, balance } of this.walletDb.getUnconfirmedBalances(this, tx)) { - const { delta: unconfirmedDelta, count: unconfirmedCount } = unconfirmedByAsset.get( - assetId, - ) ?? { - delta: 0n, - count: 0, - } + if (head) { + const pendingByAsset = await this.getPendingDeltas(head.sequence, tx) + const unconfirmedByAsset = await this.getUnconfirmedDeltas( + head.sequence, + confirmations, + tx, + ) - const { delta: pendingDelta, count: pendingCount } = pendingByAsset.get(assetId) ?? { - delta: 0n, - count: 0, - } + for await (const { assetId, balance } of this.walletDb.getUnconfirmedBalances(this, tx)) { + const { delta: unconfirmedDelta, count: unconfirmedCount } = unconfirmedByAsset.get( + assetId, + ) ?? { + delta: 0n, + count: 0, + } + + const { delta: pendingDelta, count: pendingCount } = pendingByAsset.get(assetId) ?? { + delta: 0n, + count: 0, + } - const { balance: available, noteCount: availableNoteCount } = - await this.calculateAvailableBalance(head.sequence, assetId, confirmations, tx) + const { balance: available, noteCount: availableNoteCount } = + await this.calculateAvailableBalance(head.sequence, assetId, confirmations, tx) + + if (!hasNative && Asset.nativeId().equals(assetId)) { + hasNative = true + } + yield { + assetId, + unconfirmed: balance.unconfirmed, + unconfirmedCount, + confirmed: balance.unconfirmed - unconfirmedDelta, + pending: balance.unconfirmed + pendingDelta, + pendingCount, + available, + availableNoteCount, + blockHash: balance.blockHash, + sequence: balance.sequence, + } + } + } + + if (!hasNative) { yield { - assetId, - unconfirmed: balance.unconfirmed, - unconfirmedCount, - confirmed: balance.unconfirmed - unconfirmedDelta, - pending: balance.unconfirmed + pendingDelta, - pendingCount, - available, - availableNoteCount, - blockHash: balance.blockHash, - sequence: balance.sequence, + assetId: Asset.nativeId(), + unconfirmed: 0n, + unconfirmedCount: 0, + confirmed: 0n, + pending: 0n, + pendingCount: 0, + available: 0n, + availableNoteCount: 0, + blockHash: head?.hash ?? null, + sequence: head?.sequence ?? null, } } } @@ -1251,10 +1299,20 @@ export class Account { async updateScanningEnabled( scanningEnabled: boolean, + options?: { masterKey: MasterKey | null }, tx?: IDatabaseTransaction, ): Promise { + const walletEncrypted = await this.walletDb.accountsEncrypted(tx) + this.scanningEnabled = scanningEnabled - await this.walletDb.setAccount(this, tx) + + if (walletEncrypted) { + Assert.isNotUndefined(options) + Assert.isNotNull(options?.masterKey) + await this.walletDb.setEncryptedAccount(this, options.masterKey, tx) + } else { + await this.walletDb.setAccount(this, tx) + } } async getTransactionNotes( @@ -1282,6 +1340,23 @@ export class Account { const publicKeyPackage = new multisig.PublicKeyPackage(this.multisigKeys.publicKeyPackage) return publicKeyPackage.identities() } + + encrypt(masterKey: MasterKey): EncryptedAccount { + const encoder = new AccountValueEncoding() + const serialized = encoder.serialize(this.serialize()) + const derivedKey = masterKey.deriveNewKey() + const data = derivedKey.encrypt(serialized) + + return new EncryptedAccount({ + accountValue: { + encrypted: true, + data, + salt: derivedKey.salt(), + nonce: derivedKey.nonce(), + }, + walletDb: this.walletDb, + }) + } } export function calculateAccountPrefix(id: string): Buffer { diff --git a/ironfish/src/wallet/account/encryptedAccount.test.ts b/ironfish/src/wallet/account/encryptedAccount.test.ts new file mode 100644 index 0000000000..6dac099c3b --- /dev/null +++ b/ironfish/src/wallet/account/encryptedAccount.test.ts @@ -0,0 +1,41 @@ +/* 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 { useAccountFixture } from '../../testUtilities/fixtures/account' +import { createNodeTest } from '../../testUtilities/nodeTest' +import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' + +describe('EncryptedAccount', () => { + const nodeTest = createNodeTest() + + it('can decrypt an encrypted account', async () => { + const passphrase = 'foobarbaz' + const { node } = nodeTest + const account = await useAccountFixture(node.wallet) + + const masterKey = MasterKey.generate(passphrase) + const key = await masterKey.unlock(passphrase) + + const encryptedAccount = account.encrypt(masterKey) + const decryptedAccount = encryptedAccount.decrypt(key) + + expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) + }) + + it('throws an error when an invalid passphrase is used', async () => { + const passphrase = 'foobarbaz' + const invalidPassphrase = 'fakepassphrase' + const { node } = nodeTest + const account = await useAccountFixture(node.wallet) + + const masterKey = MasterKey.generate(passphrase) + const invalidMasterKey = MasterKey.generate(invalidPassphrase) + const invalidKey = await invalidMasterKey.unlock(passphrase) + + await masterKey.unlock(passphrase) + const encryptedAccount = account.encrypt(masterKey) + + expect(() => encryptedAccount.decrypt(invalidKey)).toThrow(AccountDecryptionFailedError) + }) +}) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts new file mode 100644 index 0000000000..44f321d4d7 --- /dev/null +++ b/ironfish/src/wallet/account/encryptedAccount.ts @@ -0,0 +1,52 @@ +/* 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 { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { AccountDecryptionFailedError } from '../errors' +import { AccountValueEncoding, EncryptedAccountValue } from '../walletdb/accountValue' +import { WalletDB } from '../walletdb/walletdb' +import { Account } from './account' + +export class EncryptedAccount { + private readonly walletDb: WalletDB + readonly salt: Buffer + readonly nonce: Buffer + readonly data: Buffer + + constructor({ + accountValue, + walletDb, + }: { + accountValue: EncryptedAccountValue + walletDb: WalletDB + }) { + this.salt = accountValue.salt + this.nonce = accountValue.nonce + this.data = accountValue.data + this.walletDb = walletDb + } + + decrypt(masterKey: xchacha20poly1305.XChaCha20Poly1305Key): Account { + try { + const key = masterKey.deriveKey(this.salt, this.nonce) + const decryptedAccountValue = key.decrypt(this.data) + key.destroy() + + const encoder = new AccountValueEncoding() + const accountValue = encoder.deserializeDecrypted(decryptedAccountValue) + + return new Account({ accountValue, walletDb: this.walletDb }) + } catch { + throw new AccountDecryptionFailedError() + } + } + + serialize(): EncryptedAccountValue { + return { + encrypted: true, + salt: this.salt, + nonce: this.nonce, + data: this.data, + } + } +} diff --git a/ironfish/src/wallet/errors.ts b/ironfish/src/wallet/errors.ts index 342b79c235..e0d9c0c16e 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -43,6 +43,24 @@ export class DuplicateAccountNameError extends Error { } } +export class DuplicateIdentityNameError extends Error { + name = this.constructor.name + + constructor(name: string) { + super() + this.message = `Multisig identity already exists with the name ${name}` + } +} + +export class DuplicateIdentityError extends Error { + name = this.constructor.name + + constructor(identity: string) { + super() + this.message = `Multisig participant already exists for the identity ${identity}` + } +} + export class DuplicateSpendingKeyError extends Error { name = this.constructor.name @@ -60,3 +78,12 @@ export class DuplicateMultisigSecretNameError extends Error { this.message = `Multisig secret already exists with the name ${name}` } } + +export class AccountDecryptionFailedError extends Error { + name = this.constructor.name + + constructor() { + super() + this.message = 'Failed to decrypt wallet' + } +} diff --git a/ironfish/src/wallet/exporter/accountImport.ts b/ironfish/src/wallet/exporter/accountImport.ts index 57ca171f84..3652cc0f47 100644 --- a/ironfish/src/wallet/exporter/accountImport.ts +++ b/ironfish/src/wallet/exporter/accountImport.ts @@ -11,6 +11,7 @@ import { isValidSpendingKey, isValidViewKey, } from '../validator' +import { isSignerMultisig } from '../walletdb/multisigKeys' import { MultisigKeysImport } from './multisig' export type AccountImport = { @@ -58,7 +59,7 @@ export function toAccountImport( if (viewOnly) { value.spendingKey = null - if (value.multisigKeys) { + if (value.multisigKeys && isSignerMultisig(value.multisigKeys)) { value.multisigKeys = { publicKeyPackage: value.multisigKeys.publicKeyPackage, } diff --git a/ironfish/src/wallet/exporter/encoders/bech32.ts b/ironfish/src/wallet/exporter/encoders/bech32.ts index a73b44e9cd..07961220ec 100644 --- a/ironfish/src/wallet/exporter/encoders/bech32.ts +++ b/ironfish/src/wallet/exporter/encoders/bech32.ts @@ -1,11 +1,11 @@ /* 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 { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import { KEY_LENGTH, PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' import bufio, { EncodingError } from 'bufio' import { Bech32m } from '../../../utils' import { ACCOUNT_SCHEMA_VERSION } from '../../account/account' -import { KEY_LENGTH, VIEW_KEY_LENGTH } from '../../walletdb/accountValue' +import { VIEW_KEY_LENGTH } from '../../walletdb/accountValue' import { AccountImport } from '../accountImport' import { AccountDecodingOptions, AccountEncoder, DecodeFailed, DecodeInvalid } from '../encoder' import { MultisigKeysEncoding } from './multisigKeys' diff --git a/ironfish/src/wallet/exporter/encryption.ts b/ironfish/src/wallet/exporter/encryption.ts index 9e1a293117..4f1e6d3715 100644 --- a/ironfish/src/wallet/exporter/encryption.ts +++ b/ironfish/src/wallet/exporter/encryption.ts @@ -33,7 +33,11 @@ export async function decryptEncodedAccount( if (encrypted.startsWith(BASE64_JSON_MULTISIG_ENCRYPTED_ACCOUNT_PREFIX)) { const encoded = encrypted.slice(BASE64_JSON_MULTISIG_ENCRYPTED_ACCOUNT_PREFIX.length) - for await (const { secret: secretBuffer } of wallet.walletDb.getMultisigSecrets()) { + for await (const { secret: secretBuffer } of wallet.walletDb.getMultisigIdentities()) { + if (secretBuffer === undefined) { + continue + } + const secret = new multisig.ParticipantSecret(secretBuffer) const decrypted = decryptEncodedAccountWithMultisigSecret(encoded, secret) @@ -59,7 +63,11 @@ export function decryptEncodedAccountWithMultisigSecret( try { return secret.decryptData(encoded).toString('utf8') } catch (e: unknown) { - return null + try { + return secret.decryptLegacyData(encoded).toString('utf8') + } catch (e: unknown) { + return null + } } } diff --git a/ironfish/src/wallet/exporter/multisig.ts b/ironfish/src/wallet/exporter/multisig.ts index 4215bc44e1..df2c871744 100644 --- a/ironfish/src/wallet/exporter/multisig.ts +++ b/ironfish/src/wallet/exporter/multisig.ts @@ -1,7 +1,11 @@ /* 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 { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' +import { + MultisigHardwareSigner, + MultisigKeys, + MultisigSigner, +} from '../interfaces/multisigKeys' export interface MultisigSignerTrustedDealerImport { identity: string @@ -18,8 +22,14 @@ export function isMultisigSignerImport(data: MultisigKeysImport): data is Multis return 'secret' in data } +export function isMultisigHardwareSignerImport( + data: MultisigKeysImport, +): data is MultisigHardwareSigner { + return 'identity' in data && !('keyPackage' in data) +} + export function isMultisigSignerTrustedDealerImport( data: MultisigKeysImport, ): data is MultisigSignerTrustedDealerImport { - return 'identity' in data + return 'identity' in data && 'keyPackage' in data } diff --git a/ironfish/src/wallet/interfaces/multisigKeys.ts b/ironfish/src/wallet/interfaces/multisigKeys.ts index 6832c7d2ce..7686bcb59b 100644 --- a/ironfish/src/wallet/interfaces/multisigKeys.ts +++ b/ironfish/src/wallet/interfaces/multisigKeys.ts @@ -8,8 +8,13 @@ export interface MultisigSigner { publicKeyPackage: string } +export interface MultisigHardwareSigner { + identity: string + publicKeyPackage: string +} + export interface MultisigCoordinator { publicKeyPackage: string } -export type MultisigKeys = MultisigSigner | MultisigCoordinator +export type MultisigKeys = MultisigSigner | MultisigHardwareSigner | MultisigCoordinator diff --git a/ironfish/src/wallet/masterKey.test.ts b/ironfish/src/wallet/masterKey.test.ts new file mode 100644 index 0000000000..7af4ec390b --- /dev/null +++ b/ironfish/src/wallet/masterKey.test.ts @@ -0,0 +1,37 @@ +/* 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 { MasterKey } from './masterKey' + +describe('MasterKey', () => { + it('can regenerate the master key from parts', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + const duplicate = new MasterKey({ nonce: masterKey.nonce, salt: masterKey.salt }) + + const key = await masterKey.unlock(passphrase) + const reconstructed = await duplicate.unlock(passphrase) + expect(key.key().equals(reconstructed.key())).toBe(true) + }) + + it('can regenerate the child key from parts', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + await masterKey.unlock(passphrase) + + const childKey = masterKey.deriveNewKey() + const duplicate = masterKey.deriveKey(childKey.salt(), childKey.nonce()) + expect(childKey.key().equals(duplicate.key())).toBe(true) + }) + + it('can save and remove the xchacha20poly1305 in memory', async () => { + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + + await masterKey.unlock(passphrase) + expect(masterKey['masterKey']).not.toBeNull() + + await masterKey.lock() + expect(masterKey['masterKey']).toBeNull() + }) +}) diff --git a/ironfish/src/wallet/masterKey.ts b/ironfish/src/wallet/masterKey.ts new file mode 100644 index 0000000000..c75eb1c3d5 --- /dev/null +++ b/ironfish/src/wallet/masterKey.ts @@ -0,0 +1,97 @@ +/* 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 { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { Assert } from '../assert' +import { Mutex } from '../mutex' +import { MasterKeyValue } from './walletdb/masterKeyValue' + +/** + * A Master Key implementation for XChaCha20Poly1305. This class can be used + * to derive child keys deterministically given the child key's salt and nonces. + * + * This master key does not automatically lock or unlock. You must call those + * explicitly if you would like any default timeout behavior. + */ +export class MasterKey { + private mutex: Mutex + private locked: boolean + + readonly salt: Buffer + readonly nonce: Buffer + + private masterKey: xchacha20poly1305.XChaCha20Poly1305Key | null + + constructor(masterKeyValue: MasterKeyValue) { + this.mutex = new Mutex() + + this.salt = masterKeyValue.salt + this.nonce = masterKeyValue.nonce + + this.locked = true + this.masterKey = null + } + + static generate(passphrase: string): MasterKey { + const key = new xchacha20poly1305.XChaCha20Poly1305Key(passphrase) + return new MasterKey({ salt: key.salt(), nonce: key.nonce() }) + } + + async lock(): Promise { + const unlock = await this.mutex.lock() + + try { + if (this.masterKey) { + this.masterKey.destroy() + this.masterKey = null + } + + this.locked = true + } finally { + unlock() + } + } + + async unlock(passphrase: string): Promise { + const unlock = await this.mutex.lock() + + try { + this.masterKey = xchacha20poly1305.XChaCha20Poly1305Key.fromParts( + passphrase, + this.salt, + this.nonce, + ) + this.locked = false + + return this.masterKey + } catch (e) { + if (this.masterKey) { + this.masterKey.destroy() + this.masterKey = null + } + + this.locked = true + throw e + } finally { + unlock() + } + } + + deriveNewKey(): xchacha20poly1305.XChaCha20Poly1305Key { + Assert.isFalse(this.locked) + Assert.isNotNull(this.masterKey) + + return this.masterKey.deriveNewKey() + } + + deriveKey(salt: Buffer, nonce: Buffer): xchacha20poly1305.XChaCha20Poly1305Key { + Assert.isFalse(this.locked) + Assert.isNotNull(this.masterKey) + + return this.masterKey.deriveKey(salt, nonce) + } + + async destroy(): Promise { + await this.lock() + } +} diff --git a/ironfish/src/wallet/wallet.test.slow.ts b/ironfish/src/wallet/wallet.test.slow.ts index 754c7ac025..cae2f9263b 100644 --- a/ironfish/src/wallet/wallet.test.slow.ts +++ b/ironfish/src/wallet/wallet.test.slow.ts @@ -1141,7 +1141,7 @@ describe('Wallet', () => { const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() - await node.wallet.walletDb.putMultisigSecret(identity.serialize(), { + await node.wallet.walletDb.putMultisigIdentity(identity.serialize(), { name, secret: secret.serialize(), }) @@ -1340,7 +1340,7 @@ describe('Wallet', () => { const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() - await node.wallet.walletDb.putMultisigSecret(identity.serialize(), { + await node.wallet.walletDb.putMultisigIdentity(identity.serialize(), { name, secret: secret.serialize(), }) diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 71e3357ad8..f6b1213e59 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -22,13 +22,16 @@ import { } from '../testUtilities' import { AsyncUtils, BufferUtils, ORE_TO_IRON } from '../utils' import { Account, TransactionStatus, TransactionType } from '../wallet' +import { EncryptedAccount } from './account/encryptedAccount' import { + AccountDecryptionFailedError, DuplicateAccountNameError, DuplicateSpendingKeyError, MaxMemoLengthError, } from './errors' import { toAccountImport } from './exporter' import { AssetStatus, Wallet } from './wallet' +import { DecryptedAccountValue } from './walletdb/accountValue' describe('Wallet', () => { const nodeTest = createNodeTest() @@ -650,6 +653,57 @@ describe('Wallet', () => { expect(accountBImport.createdAt).toBeDefined() }) + + it('should throw an error when the wallet is encrypted and there is no passphrase', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '0', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + + await expect(node.wallet.importAccount(accountValue)).rejects.toThrow() + }) + + it('should encrypt and store the account if the wallet is encrypted', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '0', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + + await node.wallet.unlock(passphrase) + const account = await node.wallet.importAccount(accountValue) + await node.wallet.lock() + + expect(account.name).toEqual(accountValue.name) + expect(account.viewKey).toEqual(key.viewKey) + expect(account.incomingViewKey).toEqual(key.incomingViewKey) + expect(account.outgoingViewKey).toEqual(key.outgoingViewKey) + expect(account.spendingKey).toEqual(key.spendingKey) + expect(account.publicAddress).toEqual(key.publicAddress) + }) }) describe('expireTransactions', () => { @@ -783,6 +837,166 @@ describe('Wallet', () => { }) }) + describe('deleteTransaction', () => { + it('should delete a pending transaction', async () => { + const { node, wallet } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block) + + await node.wallet.scan() + + const transaction = await useTxFixture(node.wallet, accountA, accountB) + + // ensure account A has the transaction as pending + const txValueA = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueA) + const statusA = await wallet.getTransactionStatus(accountA, txValueA) + expect(statusA).toEqual(TransactionStatus.PENDING) + + // ensure account B has the transaction as pending + const txValueB = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueB) + const statusB = await wallet.getTransactionStatus(accountA, txValueB) + expect(statusB).toEqual(TransactionStatus.PENDING) + + const deleted = await wallet.deleteTransaction(transaction.hash()) + expect(deleted).toEqual(true) + + expect(await accountA.getTransaction(transaction.hash())).toBeUndefined() + expect(await accountB.getTransaction(transaction.hash())).toBeUndefined() + }) + + it('should delete an expired transaction', async () => { + const { node } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block2 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block2) + + await node.wallet.scan() + + const transaction = await useTxFixture( + node.wallet, + accountA, + accountB, + undefined, + undefined, + 3, + ) + + const block3 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block3) + + await node.wallet.scan() + + await node.wallet.expireTransactions(block3.header.sequence) + + // ensure account A has the transaction as expired + const txValueA = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueA) + const statusA = await node.wallet.getTransactionStatus(accountA, txValueA) + expect(statusA).toEqual(TransactionStatus.EXPIRED) + + // ensure account B has the transaction as expired + const txValueB = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueB) + const statusB = await node.wallet.getTransactionStatus(accountA, txValueB) + expect(statusB).toEqual(TransactionStatus.EXPIRED) + + const deleted = await node.wallet.deleteTransaction(transaction.hash()) + expect(deleted).toEqual(true) + + expect(await accountA.getTransaction(transaction.hash())).toBeUndefined() + expect(await accountB.getTransaction(transaction.hash())).toBeUndefined() + }) + + it('should not delete an unconfirmed transaction', async () => { + const { node, wallet } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block) + + await node.wallet.scan() + + const transaction = await useTxFixture(node.wallet, accountA, accountB) + + const block3 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet, [ + transaction, + ]) + await node.chain.addBlock(block3) + + await node.wallet.scan() + + node.config.set('confirmations', 1) + + // ensure account A has the transaction as pending + const txValueA = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueA) + const statusA = await wallet.getTransactionStatus(accountA, txValueA) + expect(statusA).toEqual(TransactionStatus.UNCONFIRMED) + + // ensure account B has the transaction as pending + const txValueB = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueB) + const statusB = await wallet.getTransactionStatus(accountA, txValueB) + expect(statusB).toEqual(TransactionStatus.UNCONFIRMED) + + const deleted = await wallet.deleteTransaction(transaction.hash()) + expect(deleted).toEqual(false) + + expect(await accountA.getTransaction(transaction.hash())).toBeDefined() + expect(await accountB.getTransaction(transaction.hash())).toBeDefined() + }) + + it('should not delete a confirmed transaction', async () => { + const { node, wallet } = nodeTest + + const accountA = await useAccountFixture(node.wallet, 'accountA') + const accountB = await useAccountFixture(node.wallet, 'accountB') + + const block = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet) + await node.chain.addBlock(block) + + await node.wallet.scan() + + const transaction = await useTxFixture(node.wallet, accountA, accountB) + + const block3 = await useMinerBlockFixture(node.chain, undefined, accountA, node.wallet, [ + transaction, + ]) + await node.chain.addBlock(block3) + + await node.wallet.scan() + + // ensure account A has the transaction as pending + const txValueA = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueA) + const statusA = await wallet.getTransactionStatus(accountA, txValueA) + expect(statusA).toEqual(TransactionStatus.CONFIRMED) + + // ensure account B has the transaction as pending + const txValueB = await accountA.getTransaction(transaction.hash()) + Assert.isNotUndefined(txValueB) + const statusB = await wallet.getTransactionStatus(accountA, txValueB) + expect(statusB).toEqual(TransactionStatus.CONFIRMED) + + const deleted = await wallet.deleteTransaction(transaction.hash()) + expect(deleted).toEqual(false) + + expect(await accountA.getTransaction(transaction.hash())).toBeDefined() + expect(await accountB.getTransaction(transaction.hash())).toBeDefined() + }) + }) + describe('createAccount', () => { it('should set createdAt to the chain head', async () => { const node = nodeTest.node @@ -808,6 +1022,57 @@ describe('Wallet', () => { expect(head?.hash).toEqualHash(block2.header.hash) expect(head?.sequence).toEqual(block2.header.sequence) }) + + it('should not allow blank names', async () => { + const node = nodeTest.node + + await expect(node.wallet.createAccount('')).rejects.toThrow( + 'Account name cannot be blank', + ) + + await expect(node.wallet.createAccount(' ')).rejects.toThrow( + 'Account name cannot be blank', + ) + }) + + it('should throw an error if the wallet is encrypted and no passphrase is provided', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + await expect(node.wallet.createAccount('B')).rejects.toThrow() + }) + + it('should save a new encrypted account with the correct passphrase', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + await node.wallet.unlock(passphrase) + const account = await node.wallet.createAccount('B') + + const accountValue = await node.wallet.walletDb.accounts.get(account.id) + Assert.isNotUndefined(accountValue) + Assert.isTrue(accountValue.encrypted) + + const encryptedAccount = new EncryptedAccount({ + accountValue, + walletDb: node.wallet.walletDb, + }) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) + await node.wallet.lock() + + expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) + expect(decryptedAccount.name).toEqual(account.name) + }) }) describe('removeAccount', () => { @@ -2176,6 +2441,42 @@ describe('Wallet', () => { expect(newAccount.scanningEnabled).toBe(true) }) + + it('should throw an error if the wallet is encrypted and the passphrase is not provided', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + const account = await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + await expect(node.wallet.resetAccount(account)).rejects.toThrow() + }) + + it('save the encrypted account when the wallet is encrypted and passphrase is valid', async () => { + const { node } = await nodeTest.createSetup() + const passphrase = 'foo' + + const account = await useAccountFixture(node.wallet, 'A') + await node.wallet.encrypt(passphrase) + + await node.wallet.unlock(passphrase) + await node.wallet.resetAccount(account) + + const newAccount = node.wallet.getAccountByName(account.name) + Assert.isNotNull(newAccount) + + const encryptedAccount = node.wallet.encryptedAccountById.get(newAccount.id) + Assert.isNotUndefined(encryptedAccount) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) + await node.wallet.lock() + + expect(decryptedAccount.name).toEqual(account.name) + expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) + }) }) describe('getTransactionType', () => { @@ -2374,4 +2675,182 @@ describe('Wallet', () => { expect(node.wallet.shouldDecryptForAccount(block.header, account)).toBe(true) }) }) + + describe('encrypt', () => { + it('saves encrypted blobs to disk and updates the wallet account fields', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + + await node.wallet.encrypt(passphrase) + + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + + const encryptedAccountA = node.wallet.encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(key) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = node.wallet.encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(key) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) + + describe('decrypt', () => { + it('saves decrypted accounts to disk and updates the wallet account fields', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + await node.wallet.decrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + + const decryptedAccountA = node.wallet.accountById.get(accountA.id) + Assert.isNotUndefined(decryptedAccountA) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const decryptedAccountB = node.wallet.accountById.get(accountB.id) + Assert.isNotUndefined(decryptedAccountB) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + + it('fails with an invalid passphrase', async () => { + const { node } = nodeTest + const passphrase = 'foo' + const invalidPassphrase = 'bar' + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + await expect(node.wallet.decrypt(invalidPassphrase)).rejects.toThrow( + AccountDecryptionFailedError, + ) + + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + }) + }) + + describe('lock', () => { + it('does nothing if the wallet is decrypted', async () => { + const { node } = nodeTest + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + + await node.wallet.lock() + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + }) + + it('clears decrypted accounts if the wallet is encrypted', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + await node.wallet.unlock(passphrase) + expect(node.wallet.accounts).toHaveLength(2) + + await node.wallet.lock() + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.locked).toBe(true) + }) + }) + + describe('unlock', () => { + it('does nothing if the wallet is decrypted', async () => { + const { node } = nodeTest + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + + await node.wallet.unlock('foobar') + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(0) + }) + + it('does not unlock the wallet with an invalid passphrase', async () => { + const { node } = nodeTest + const passphrase = 'foo' + const invalidPassphrase = 'bar' + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + await expect(node.wallet.unlock(invalidPassphrase)).rejects.toThrow( + AccountDecryptionFailedError, + ) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + expect(node.wallet.locked).toBe(true) + }) + + it('saves decrypted accounts to memory with a valid passphrase', async () => { + const { node } = nodeTest + const passphrase = 'foo' + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + await node.wallet.unlock(passphrase) + expect(node.wallet.accounts).toHaveLength(2) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + expect(node.wallet.locked).toBe(false) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + + for (const [id, account] of node.wallet.accountById.entries()) { + const encryptedAccount = node.wallet.encryptedAccountById.get(id) + Assert.isNotUndefined(encryptedAccount) + const decryptedAccount = encryptedAccount.decrypt(key) + + expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) + } + + await node.wallet.lock() + }) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index cb5215c514..b37c278120 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -42,17 +42,24 @@ import { import { WorkerPool } from '../workerPool' import { DecryptedNote, DecryptNotesItem } from '../workerPool/tasks/decryptNotes' import { Account, ACCOUNT_SCHEMA_VERSION } from './account/account' +import { EncryptedAccount } from './account/encryptedAccount' import { AssetBalances } from './assetBalances' import { DuplicateAccountNameError, + DuplicateIdentityNameError, DuplicateMultisigSecretNameError, DuplicateSpendingKeyError, MaxMemoLengthError, NotEnoughFundsError, } from './errors' +import { isMultisigSignerImport } from './exporter' import { AccountImport, validateAccountImport } from './exporter/accountImport' -import { isMultisigSignerTrustedDealerImport } from './exporter/multisig' +import { + isMultisigHardwareSignerImport, + isMultisigSignerTrustedDealerImport, +} from './exporter/multisig' import { MintAssetOptions } from './interfaces/mintAssetOptions' +import { MasterKey } from './masterKey' import { ScanState } from './scanner/scanState' import { WalletScanner } from './scanner/walletScanner' import { AssetValue } from './walletdb/assetValue' @@ -89,11 +96,14 @@ export type TransactionOutput = { assetId: Buffer } +export const DEFAULT_UNLOCK_TIMEOUT_MS = 24 * 60 * 60 * 1000 + export class Wallet { readonly onAccountImported = new Event<[account: Account]>() readonly onAccountRemoved = new Event<[account: Account]>() - protected readonly accountById = new Map() + readonly accountById = new Map() + readonly encryptedAccountById = new Map() readonly walletDb: WalletDB private readonly logger: Logger readonly workerPool: WorkerPool @@ -102,13 +112,16 @@ export class Wallet { private readonly config: Config private readonly consensus: Consensus readonly networkId: number + private masterKey: MasterKey | null protected rebroadcastAfter: number protected defaultAccount: string | null = null protected isStarted = false protected isOpen = false protected isSyncingTransactionGossip = false + locked: boolean protected eventLoopTimeout: SetTimeoutToken | null = null + protected lockTimeout: SetTimeoutToken | null private readonly createTransactionMutex: Mutex private readonly eventLoopAbortController: AbortController private eventLoopPromise: Promise | null = null @@ -142,6 +155,9 @@ export class Wallet { this.networkId = networkId this.nodeClient = nodeClient || null this.rebroadcastAfter = rebroadcastAfter ?? 10 + this.locked = false + this.lockTimeout = null + this.masterKey = null this.createTransactionMutex = new Mutex() this.eventLoopAbortController = new AbortController() @@ -208,9 +224,30 @@ export class Wallet { } private async load(): Promise { - for await (const accountValue of this.walletDb.loadAccounts()) { - const account = new Account({ accountValue, walletDb: this.walletDb }) - this.accountById.set(account.id, account) + this.encryptedAccountById.clear() + this.accountById.clear() + this.masterKey = null + + const masterKeyValue = await this.walletDb.loadMasterKey() + if (masterKeyValue) { + this.masterKey = new MasterKey(masterKeyValue) + } + + for await (const [id, accountValue] of this.walletDb.loadAccounts()) { + if (accountValue.encrypted) { + const encryptedAccount = new EncryptedAccount({ + walletDb: this.walletDb, + accountValue, + }) + this.encryptedAccountById.set(id, encryptedAccount) + + this.locked = true + } else { + const account = new Account({ accountValue, walletDb: this.walletDb }) + this.accountById.set(account.id, account) + + this.locked = false + } } const meta = await this.walletDb.loadAccountsMeta() @@ -218,6 +255,7 @@ export class Wallet { } private unload(): void { + this.encryptedAccountById.clear() this.accountById.clear() this.defaultAccount = null @@ -242,6 +280,12 @@ export class Wallet { } async stop(): Promise { + if (this.masterKey) { + await this.masterKey.destroy() + } + + this.stopUnlockTimeout() + if (!this.isStarted) { return } @@ -264,18 +308,20 @@ export class Wallet { const [promise, resolve] = PromiseUtils.split() this.eventLoopPromise = promise - if (!this.scanner.running) { - void this.scan() - } + if (!this.locked) { + if (!this.scanner.running) { + void this.scan() + } - void this.syncTransactionGossip() - await this.cleanupDeletedAccounts() + void this.syncTransactionGossip() + await this.cleanupDeletedAccounts() - const head = await this.getLatestHead() + const head = await this.getLatestHead() - if (head) { - await this.expireTransactions(head.sequence) - await this.rebroadcastTransactions(head.sequence) + if (head) { + await this.expireTransactions(head.sequence) + await this.rebroadcastTransactions(head.sequence) + } } if (this.isStarted) { @@ -326,16 +372,18 @@ export class Wallet { options?: { resetCreatedAt?: boolean resetScanningEnabled?: boolean + passphrase?: string }, tx?: IDatabaseTransaction, ): Promise { await this.resetAccounts(options, tx) } - resetAccounts( + async resetAccounts( options?: { resetCreatedAt?: boolean resetScanningEnabled?: boolean + passphrase?: string }, tx?: IDatabaseTransaction, ): Promise { @@ -1169,6 +1217,48 @@ export class Wallet { } } + /** + * Delete a transaction from all accounts in the wallet if it has not yet been + * added to a block + */ + async deleteTransaction(hash: Buffer, tx?: IDatabaseTransaction): Promise { + let deleted = false + + await this.walletDb.db.withTransaction(tx, async (tx) => { + for (const account of this.accountById.values()) { + const transactionValue = await account.getTransaction(hash, tx) + + if (transactionValue == null) { + continue + } + + const transactionStatus = await this.getTransactionStatus( + account, + transactionValue, + undefined, + tx, + ) + + if ( + transactionStatus === TransactionStatus.CONFIRMED || + transactionStatus === TransactionStatus.UNCONFIRMED + ) { + return false + } + + if ( + transactionStatus === TransactionStatus.EXPIRED || + transactionStatus === TransactionStatus.PENDING + ) { + await account.deleteTransaction(transactionValue.transaction, tx) + deleted = true + } + } + }) + + return deleted + } + async getTransactionStatus( account: Account, transaction: TransactionValue, @@ -1269,6 +1359,10 @@ export class Wallet { setDefault: false, }, ): Promise { + if (!name.trim()) { + throw new Error('Account name cannot be blank') + } + if (this.getAccountByName(name)) { throw new DuplicateAccountNameError(name) } @@ -1288,6 +1382,7 @@ export class Wallet { const account = new Account({ accountValue: { + encrypted: false, version: ACCOUNT_SCHEMA_VERSION, id: uuid(), name, @@ -1304,7 +1399,20 @@ export class Wallet { }) await this.walletDb.db.transaction(async (tx) => { - await this.walletDb.setAccount(account, tx) + const accountsEncrypted = await this.walletDb.accountsEncrypted(tx) + + if (accountsEncrypted) { + Assert.isNotNull(this.masterKey) + const encryptedAccount = await this.walletDb.setEncryptedAccount( + account, + this.masterKey, + tx, + ) + this.encryptedAccountById.set(account.id, encryptedAccount) + } else { + await this.walletDb.setAccount(account, tx) + } + await account.updateHead(createdAt, tx) }) @@ -1327,23 +1435,31 @@ export class Wallet { options?: { createdAt?: number }, ): Promise { let multisigKeys = accountValue.multisigKeys + let secret: Buffer | undefined + let identity: Buffer | undefined const name = accountValue.name - if ( - accountValue.multisigKeys && - isMultisigSignerTrustedDealerImport(accountValue.multisigKeys) - ) { - const multisigSecret = await this.walletDb.getMultisigSecret( - Buffer.from(accountValue.multisigKeys.identity, 'hex'), - ) - if (!multisigSecret) { - throw new Error('Cannot import identity without a corresponding multisig secret') - } + if (accountValue.multisigKeys) { + if (isMultisigSignerTrustedDealerImport(accountValue.multisigKeys)) { + const multisigIdentity = await this.walletDb.getMultisigIdentity( + Buffer.from(accountValue.multisigKeys.identity, 'hex'), + ) + if (!multisigIdentity || !multisigIdentity.secret) { + throw new Error('Cannot import identity without a corresponding multisig secret') + } - multisigKeys = { - keyPackage: accountValue.multisigKeys.keyPackage, - publicKeyPackage: accountValue.multisigKeys.publicKeyPackage, - secret: multisigSecret.secret.toString('hex'), + multisigKeys = { + keyPackage: accountValue.multisigKeys.keyPackage, + publicKeyPackage: accountValue.multisigKeys.publicKeyPackage, + secret: multisigIdentity.secret.toString('hex'), + } + secret = multisigIdentity.secret + identity = Buffer.from(accountValue.multisigKeys.identity, 'hex') + } else if (isMultisigSignerImport(accountValue.multisigKeys)) { + secret = Buffer.from(accountValue.multisigKeys.secret, 'hex') + identity = new multisig.ParticipantSecret(secret).toIdentity().serialize() + } else if (isMultisigHardwareSignerImport(accountValue.multisigKeys)) { + identity = Buffer.from(accountValue.multisigKeys.identity, 'hex') } } @@ -1389,12 +1505,44 @@ export class Wallet { name, multisigKeys, scanningEnabled: true, + encrypted: false, }, walletDb: this.walletDb, }) await this.walletDb.db.transaction(async (tx) => { - await this.walletDb.setAccount(account, tx) + const encrypted = await this.walletDb.accountsEncrypted(tx) + + if (encrypted) { + Assert.isNotNull(this.masterKey) + await this.walletDb.setEncryptedAccount(account, this.masterKey, tx) + } else { + await this.walletDb.setAccount(account, tx) + } + + if (identity) { + const existingIdentity = await this.walletDb.getMultisigIdentity(identity, tx) + + if (!existingIdentity) { + const duplicateSecret = await this.walletDb.getMultisigSecretByName( + accountValue.name, + tx, + ) + + if (duplicateSecret) { + throw new DuplicateIdentityNameError(accountValue.name) + } + + await this.walletDb.putMultisigIdentity( + identity, + { + name: account.name, + secret, + }, + tx, + ) + } + } if (createdAt !== null) { const previousBlock = await this.chainGetBlock({ sequence: createdAt.sequence - 1 }) @@ -1419,10 +1567,26 @@ export class Wallet { return account } + async setName(account: Account, name: string, tx?: IDatabaseTransaction): Promise { + await account.setName(name, { masterKey: this.masterKey }, tx) + } + + async setScanningEnabled( + account: Account, + enabled: boolean, + tx?: IDatabaseTransaction, + ): Promise { + await account.updateScanningEnabled(enabled, { masterKey: this.masterKey }, tx) + } + get accounts(): Account[] { return Array.from(this.accountById.values()) } + get encryptedAccounts(): EncryptedAccount[] { + return Array.from(this.encryptedAccountById.values()) + } + accountExists(name: string): boolean { return this.getAccountByName(name) !== null } @@ -1441,6 +1605,7 @@ export class Wallet { createdAt: options?.resetCreatedAt ? null : account.createdAt, scanningEnabled: options?.resetScanningEnabled ? true : account.scanningEnabled, id: uuid(), + encrypted: false, }, walletDb: this.walletDb, }) @@ -1448,7 +1613,19 @@ export class Wallet { this.logger.debug(`Resetting account name: ${account.name}, id: ${account.id}`) await this.walletDb.db.withTransaction(tx, async (tx) => { - await this.walletDb.setAccount(newAccount, tx) + const encrypted = await this.walletDb.accountsEncrypted(tx) + + if (encrypted) { + Assert.isNotNull(this.masterKey) + const encryptedAccount = await this.walletDb.setEncryptedAccount( + newAccount, + this.masterKey, + tx, + ) + this.encryptedAccountById.set(newAccount.id, encryptedAccount) + } else { + await this.walletDb.setAccount(newAccount, tx) + } if (newAccount.createdAt !== null) { const previousBlock = await this.chainGetBlock({ @@ -1742,7 +1919,7 @@ export class Wallet { const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() - await this.walletDb.putMultisigSecret( + await this.walletDb.putMultisigIdentity( identity.serialize(), { name, @@ -1754,4 +1931,111 @@ export class Wallet { return identity.serialize() }) } + + async accountsEncrypted(): Promise { + return this.walletDb.accountsEncrypted() + } + + async encrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + Assert.isNull(this.masterKey) + await this.walletDb.encryptAccounts(passphrase, tx) + await this.load() + } finally { + unlock() + } + } + + async decrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + await this.walletDb.decryptAccounts(passphrase, tx) + await this.load() + } catch (e) { + this.logger.error(ErrorUtils.renderError(e, true)) + throw e + } finally { + unlock() + } + } + + async lock(tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + const encrypted = await this.walletDb.accountsEncrypted(tx) + if (!encrypted) { + return + } + + this.stopUnlockTimeout() + this.accountById.clear() + this.locked = true + + if (this.masterKey) { + await this.masterKey.lock() + } + + this.logger.info( + 'Wallet locked. Unlock the wallet to view your accounts and create transactions', + ) + } finally { + unlock() + } + } + + async unlock(passphrase: string, timeout?: number, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + const encrypted = await this.walletDb.accountsEncrypted(tx) + if (!encrypted) { + return + } + + Assert.isNotNull(this.masterKey) + const key = await this.masterKey.unlock(passphrase) + + for (const [id, account] of this.encryptedAccountById.entries()) { + this.accountById.set(id, account.decrypt(key)) + } + + this.startUnlockTimeout(timeout) + this.locked = false + } catch (e) { + this.logger.debug('Wallet unlock failed') + this.stopUnlockTimeout() + this.accountById.clear() + this.locked = true + + throw e + } finally { + unlock() + } + } + + private startUnlockTimeout(timeout?: number): void { + if (!timeout) { + timeout = DEFAULT_UNLOCK_TIMEOUT_MS + } + + this.stopUnlockTimeout() + + // Keep the wallet unlocked indefinitely + if (timeout === -1) { + return + } + + this.lockTimeout = setTimeout(() => void this.lock(), timeout) + } + + private stopUnlockTimeout(): void { + if (this.lockTimeout) { + clearTimeout(this.lockTimeout) + this.lockTimeout = null + } + } } diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index 36fc19d4c0..61c5f41085 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -790,5 +790,551 @@ "sequence": 1 } } + ], + "WalletDB encryptAccounts stores encrypted accounts": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "91edb394-fe0a-4ff9-a94b-cbcffc199b76", + "name": "A", + "spendingKey": "aef3a7e8ea8329f61ae3c54b77f02f4fc94f4dd790f3bfd3d6e745e7d68021df", + "viewKey": "8e1605538c7f0d39a292d825e699c04e93d4e883172bea8ce5cf57e6be35062e745fcba8e06b0370e963ebccb2356eed25e886209cdb4b6c6c9bc1a66652ea57", + "incomingViewKey": "3e7270177fa64cd30835284663ceccb12f800e87e3d465aee824f20a93297306", + "outgoingViewKey": "3b07da84fde489af3af658b20573426a6cd5331eb1d827a5edbf52f5aaeb9d22", + "publicAddress": "13aca6bed0937635aebecc987377729edf63d3ed9576c863d9aa7318a714d7ea", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e1b8fa4e84363090d49bee08224f8d516410650dfc6aca1e9f3fef4d1358b708" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "57c75b9c-bdb4-4705-8dc7-40847528b5bb", + "name": "B", + "spendingKey": "4377f6743472240a7adba679e5872d58b1e17695662bb384e15984b9ed7eb3ae", + "viewKey": "364d9514508d06da950874cde5cfc1fe2e4d90cd064901e4a73c0f98f82c112fb463fe0a898e3d1976a9c3c7a4e0966d41f75e9a2cbb8767ec05f36b480b80d7", + "incomingViewKey": "4d31ebeefb51dda40f1e9a844321d065794fff7788c675a76699bd6d0a63a402", + "outgoingViewKey": "8b8d94bae8d743938e7940eec2cb3489772a422f3032a3742e8b107dc7bb34ac", + "publicAddress": "61b5033c291a7221a2a35adf7ef2885f5ee3b71ad43e32a6e67be17b3e8d376e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "308703a8a801c918d4fe6168fba0ca7a4a53aadb2c6d7988031f8f9eb33cec08" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB decryptAccounts stores decrypted accounts": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "99b2f779-24b3-4280-9373-60e754b91c65", + "name": "A", + "spendingKey": "147de6b6b6054328b5409f59e38de933bbdae76d3b6b0bf42dd89b90906a19aa", + "viewKey": "37f35e558e47decd960a65e30ff310c8dc1e726a321dca21532d412c929b4e43a9a645aed8934724bde1fd8f6d1814341efb0e2f7e2edaf821ff7e0a08eee8aa", + "incomingViewKey": "48f5ae1959a4fd4117825fbf6791700c975bc93cc4423c47a77ea96362d1e904", + "outgoingViewKey": "dec94d98c3ec4a4991b7344abd6ccc24bd8857b7605dee2a97fb76d594d9a629", + "publicAddress": "fd02b46d7addd5c6f58ff6d04e560c9be3de64c5361601bb850a94ce8585f5c0", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "9f90335923ded9f0bf8fff2c9a494f5e13c9ff781db239d009e2927f1ee80908" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "16a2badd-c7a6-4bf3-ab5b-3fe79901df20", + "name": "B", + "spendingKey": "5ba5b547ce1009727b66d6e8b1d25815f88611fe04f7b30ae5b447b52b8e7910", + "viewKey": "b7646fefef3547c784a145487683400f238b2d2a97fc22fd4d45a9df1fa3e7a9c4b72f8757cc5aabb5ff8c7ea947833a6ec2368ad0cd5014bd33806c272ea993", + "incomingViewKey": "6e051e304f17ebd4a35d0b35ca9eb39a77009d0d0a92a423be4be3cf341f3d00", + "outgoingViewKey": "07cbedcdb1226d283378f2d48d173384fbd3c39abf10ce796b9bed8543b85407", + "publicAddress": "71fb617acf602a2da55fd4857978a29416903bbd240ab52886097d569b85d41b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e425c3f41ed47adb266a210760489cf8c5dc1981fa306ae55a3e3e8283aa0907" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB decryptAccounts throws an error with an invalid passphrase": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "5c0154e9-e7ba-4516-bf89-c94a6a8ee396", + "name": "A", + "spendingKey": "7a64a08fd618327398f612f00d9c53939630e19c0176826585704ed0c4e5cb87", + "viewKey": "a75051978830c2361fd2fe1275a06c5ad8ab266d9da1d608d1ccb639e92f3068f1e99b7037b83498b1250265eb2d695724e646d51f732ffeac5b36a77f982a3f", + "incomingViewKey": "5284789b53acfa30f00e81d28fc1308e7e99e2ef854d514fe6d763f81bb9b007", + "outgoingViewKey": "21bc65190cc9ad6e2c7a32d101246e2caef87a8e323d56868196e8799564d24f", + "publicAddress": "23df85a64f5a2fe410cac9be7b3426e96ce014657d82d9aefb9a92bb063ee08a", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "74fb92bf84c0c871fb9b20b8ec3b7528fc88382a84e72d0506d29cb6ac484b08" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "595ea740-96ea-43e4-8d13-a4f864f08244", + "name": "B", + "spendingKey": "8bc528f23c04739f2083d2367d6b81ba9ca2cc41d2d920dd4c417197ac3bf289", + "viewKey": "cc3c5859bc104fd104cdcfef85ffa9ff5984ea0d2b7aa4843afa0c3268166f05d51eef5f16b081b8dd252c72bf93a4446efa9f0cd6514610d9b18a215874850b", + "incomingViewKey": "a899c9f5d96de698af6ac81831c5470f839baca8d8faf76fa2c8f60b831e3502", + "outgoingViewKey": "d0a6fc11f89cfc40c72699cded70137ce8da9b2236cada30a5ce922627ae12fb", + "publicAddress": "03eb191ec40f5153bae739c162f56a80abc2e819f08b7ff2ee160d64aec24373", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1a628719c1704b0553ffcf8daab62507c2e5829d878b07b6f1573b7023d4110a" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB accountsEncrypted throws an exception if the encrypted flag is inconsistent": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "229d12aa-8d18-46ed-b831-555627e043ac", + "name": "A", + "spendingKey": "e16e752d0198841c210e2d1ef2fdc42b161224b3b44eab19a5518d9e63ab10aa", + "viewKey": "305b0e13928e722e726e31c381806a07b3d0adee97e8e3d148d5f2da1f0294c8f3d5596e76850c9c3973f0e22aec31227c027d2e1f04a8caf3215a9d6c138ac9", + "incomingViewKey": "faf451a104a6075d0537f23a38ad93717c5514218397c73e67636bbf6791bf01", + "outgoingViewKey": "01f147f4ee1bef6eb1ff55763e9d3a7412c00431061012448c1ceecbe4871dc9", + "publicAddress": "5e01cf3ca471a4e5d29257922317b276d186ad8b3d43305cbb02a63e19c847b9", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "0824b8fc93da369f7674a5cf710eb034d5044e3376bf814ad752ab4ab4f9fb07" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "8134c360-6ffa-4c16-8386-e7fdab2c4a5f", + "name": "B", + "spendingKey": "8b334ca948cc0d19c59c81b0865e123a124d852af92377f9cbd201abbc32ebfc", + "viewKey": "f293872da5f4a248af69c5fe96bfebf919b790376115caa660f90666e007f59e8c8ea059b1375a052a9e9ff7a2af1ecb027ccb894347965cca37cd4f7614506b", + "incomingViewKey": "f06b6de5dbf9bed1684973f121e6f774e81fec44dd1327a295c8e958544daf01", + "outgoingViewKey": "e0d88e8a0c02c101edf2bbd8bf1a0bd3cca2a71e66f031b8bd9f7dd6283accf0", + "publicAddress": "d74d09e7c105e1ef3e70b0db656eebb60b29e2a4d100ffad6f290e8d90ccca6b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "fee1d439c9f7a64889fd0d31adf161ace728b6c8337e161c449284be1db27905" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB accountsEncrypted returns true if the accounts are encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "f0f8c027-812e-46ab-bb88-11892712457c", + "name": "A", + "spendingKey": "e34de31cc4cdf650009b4a37a2c26cd1b31dfbbb8017c41efc6b9bce9ae78b62", + "viewKey": "808ce2bb31b0e4979d72171f7610306eb0bc42295870b7e22e9d4a02fc962cd30930c62109d9c2c64b9b0b7c98203cf80c4c590a0110053bd7ef6923cce56e3b", + "incomingViewKey": "394630d0342fca3fee576fcf403a1bd1ed7c0bf10ee6647bc0af61840ad22703", + "outgoingViewKey": "22168293332c8f42e02946c0d175a068e1f3d991f44f7317759c9117e0713b2c", + "publicAddress": "144f29e12d30a6dcedf479f9ffec2cabd61453ff32896b67981d5809a8ae775a", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "66850d6e24b4fd25e4775bb01eeab1707acc66e886bd53f04507002237cabc03" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "33e7a4ed-7f1d-49de-821a-4ed8ebcd093f", + "name": "B", + "spendingKey": "838f322feccdaa86677853da85c8be7d38e795448d78c73d272d340a62959337", + "viewKey": "c9e2a9a4837b6ccc00d7a70e84633820f46eceb841f7e152ad4733fd9742cde6774ec12c2b567ced6f122de3af446f5f766648702c8398801dce070d5ac85915", + "incomingViewKey": "3c355d3246e9b3af43865d2db052443c1f1ccecd26883d1b1f269fea7c95a307", + "outgoingViewKey": "3a2246d964b5b1686caf09b25d1db4cdbd66e24cebd230f37765fb3037560e15", + "publicAddress": "799ec271f7543e1759e369eb4c212d75e26ab33ac3af290b5f851213490ee614", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "412d70322e874e012a1f18362ed086fa33fb8748dff6e995efefbab03dbf1b01" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB accountsEncrypted returns false if the accounts are not encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "4717c1a9-11de-4a78-a141-900fbbcd5c34", + "name": "A", + "spendingKey": "c99f23c963691038d013f3adf47c067f5ad4b9b194f8049d3da13ba98c790d21", + "viewKey": "b37fe1ab65dac89e23291c18b35317f49bda3a4c158091ce09404b9ec94acbad903b1e4fec2b762e2582e59b72162f9cd804c3c7a3d2ac7408cebf4cd1919ef3", + "incomingViewKey": "aba98116d77d35b7b771961817f15d07342729abebca928a181aa2b4beacf206", + "outgoingViewKey": "6c7e7c4348ebf61d3a04499d84507a2f2ba537f84023a6e8d00d73f8f15d28cd", + "publicAddress": "a8308b45f22ae0728c0430678379a42341ae5e81c818cf254b3e172bec00a54e", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "b1658d9eacebc094b30771b0cf5d92d81fd6bc58177e9c5ed1318f118f033803" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + }, + { + "value": { + "encrypted": false, + "version": 4, + "id": "c430f181-3159-46ed-973b-2ba03312ed7b", + "name": "B", + "spendingKey": "3807adc2ce8d897ad83e6c7994d7c5923099cacc75caa1ab1987c8af642f1391", + "viewKey": "528130aa8a609ba4db35c550a1dcaa6e01318afb3df057947ab687aa26a0162e57cd3117540268fd45e5c13e7bb34194518e3c14e2e8fcb37184bb82bca9be21", + "incomingViewKey": "24f27fb4587f7ecdffa77c96477ecf7edf83a199da93bb00d59073a0fb826c01", + "outgoingViewKey": "6e9da7937ef4ba894e6c8c3213b5ec9a7464e4bd6ea0d99bc11bc3219528267d", + "publicAddress": "75bc4e93b08a721faf6385945182f6b78fda93e3a34546e6cc079d5d1afbebf3", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "b9d1122eb1f6c6c87c6a15c3d887dd20ea3debc3bad45e0c4ef62e7c530c4200" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB setAccount throws an error if existing accounts are encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "1326265d-45b5-4646-8bf1-4434c896bcd1", + "name": "A", + "spendingKey": "d71768604bc37a2cd7b48e194b58c43bee3aeb398d11b9a0ef998ef759a6e08b", + "viewKey": "9a1e4d1d5ea401cb0454e95b4681ddd6acef636f3b406f8664f9d2bcccaa979d0495d1e8372407cbb4505a7167232f1d3436d5723bff554f911d96df1d0e2821", + "incomingViewKey": "64b66b4a68d8eaba6b7bfbd55d59abe794f598750b0dc9f136d2558502c64303", + "outgoingViewKey": "5d40218d1b4da92ec42c0538380b682c179d44fbd25ef999566539dac1d70d4f", + "publicAddress": "204b3270b44e987ba022b5b8eda7f41ebf55eb19ce61b7723a031bec9ed5bfcf", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "4ebcb8cbb3bb735f9421ea40538f24ada921c9736889e2a3384ddd3707c0ad05" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB setEncryptedAccount throws an error if existing accounts are decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "0327e33a-b3f8-44de-9e31-f62c0b957ff9", + "name": "A", + "spendingKey": "4073e1efc8ca5779108f7e54033aec1d612a8423f0a2e3a4536af6a2223b08fd", + "viewKey": "e26e4085b6c0301b1cd8f0e9115e7eea63de80848b09476f304f002ba7365f5c0ab80794346f4b2185256543a5da0b9223824df459b1ccff8b03cf6ca6e4e5ac", + "incomingViewKey": "6f2ac227d5d4a5704839f4fccea70f2f37016c046e68c60d5234eebfc6eaf903", + "outgoingViewKey": "2b0364b99c1971d8a5cff5333c29b7c5b5b2164d20c6171944009bbb7cae038b", + "publicAddress": "614991cb72e5b0997f1311835aa8f15b723c234a819cf36b09121284a36c6909", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "1197fb366b1e4db881d28adb2aeac1546cb969cc0c2515c9efebfe995bc9a705" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB setEncryptedAccount saves the account": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "2d4ff08d-b895-427f-b08e-7daf670f26a9", + "name": "A", + "spendingKey": "a6977d360e93d4210c8101eee39aa84d291b615f6dbdeb7d463901e4d4604b2b", + "viewKey": "49f2ae0a4f8efb153fdaedf92067f60829ad6c3fe946a08ca54594fd145d63894997083e2e9f20780bff1bf36237d0bf6ab2b0683de8756abe89b18cced457e9", + "incomingViewKey": "cf24a9f69b4179bc336abf6827f493f64d970ddf91b2d121e7af94eedb1dfd07", + "outgoingViewKey": "2e7cd9c8c91e729c1c8f0597838bd34a2292daa3989e3194e12a131e962066ea", + "publicAddress": "9c4afe0900874d1c94955a21eb1bab1fb6e3ae310f79f407423341eaa8ea1467", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "216a960dfcc50b928b8d56cb3eb1e4aa536df07ba9d440dbb21c237e95c1cf02" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB canDecryptAccounts throws an error if the accounts are decrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "acbb2960-da97-4cf8-b66a-8bf01ddaeb67", + "name": "A", + "spendingKey": "86635081a46875b009ad5f525a3b41e66c5ffca00f2ae97864ab4d398398dc7e", + "viewKey": "483f046c9e044e9110f371b6b4b09925d77077fc9df3f5d6510fe9da4e11ce473e9c198a40335c0687a744a49cc4a7c096c32bee370caf7562a3a3c59c6cdf73", + "incomingViewKey": "0d9d2bf42ea2d2f1d656533b9fe6d9797eb646a02f4effe3dcda3bc9d955ed03", + "outgoingViewKey": "f824913fcd864447bfedbcce962ef8e3309623f518b00cd67252267fb0bb3edd", + "publicAddress": "4f0198b74577e1a81fc23aff6f180e386eeabb38c3e5376f526908488a6b1833", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "ee9140e033b9336689556554006fd016153d6bc3bbbeee736fe4a527e66c6a04" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB canDecryptAccounts returns false if the passphrase is invalid": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "6bef8a6e-d6e2-4448-bf24-21662211354d", + "name": "A", + "spendingKey": "ec168ca1316b3bf7cf0cdf0a3adecf9b7972e6ae156a729f2719ac4e157c6da4", + "viewKey": "0ce8d5f8a3c8b2073178f59a3870db54332f93b8ae0d20c773c8128021c6a93771bff1b891392e16feed8ded0f9ca05af870526f4326f6249def2675604653ae", + "incomingViewKey": "81ef82de4bd28b919f710e8782feb30d394d94137eb5a031e4c4f5d003afa404", + "outgoingViewKey": "aaea23ee6e02849596138db4623b59a2303d271c5279e7d0a834804e5a13ae97", + "publicAddress": "686d8506997e2a6ceb5a0b8311e8d4e6d9a273e151168cb0241adbf6df46f75c", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "a84451b5708e3349f33c6a9c77e3260409534e9c54f327968a464edff4a00408" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "WalletDB canDecryptAccounts returns true if the passphrase is valid": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "29a6dffb-eb57-418d-b748-2cb6b1544350", + "name": "A", + "spendingKey": "6d9175f29df2be3f8c7df0187e0b2197b31001209c22813b289e86894d054564", + "viewKey": "bbefff5ef688ac872511b1e3560dfd81af1d316b34ea177152edc2f31531bf9e50e0df53ab8f0bfde8e6b0f3cd85566cddb745c418fb8f13a10ad8f293b0e36d", + "incomingViewKey": "5d5a144161b657eea271b8d6242b9ba674938adaf10805d159cdf88bf7c18906", + "outgoingViewKey": "89e3e88af3d337bb79ac160c0358261e282bcafdc9fc6473b5cc676107f80b6d", + "publicAddress": "af2ba2cdf2fae3074d30e363e9027fa8bb36ec932cd233fd246d2a623691e612", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "01dfdc97107f322332ad1300e236f9c377cbc7b5e0c9ed327b403bd6ceb08a03" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index f9462a8c86..252f065dbd 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -1,15 +1,21 @@ /* 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 { generateKey } from '@ironfish/rust-nodejs' -import { AccountValue, AccountValueEncoding } from './accountValue' +import { generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { MasterKey } from '../masterKey' +import { + AccountValueEncoding, + DecryptedAccountValue, + EncryptedAccountValue, +} from './accountValue' describe('AccountValueEncoding', () => { it('serializes the object into a buffer and deserializes to the original object', () => { const encoder = new AccountValueEncoding() const key = generateKey() - const value: AccountValue = { + const value: DecryptedAccountValue = { + encrypted: false, id: 'id', name: 'foobar๐Ÿ‘๏ธ๐Ÿƒ๐ŸŸ', incomingViewKey: key.incomingViewKey, @@ -34,7 +40,8 @@ describe('AccountValueEncoding', () => { const encoder = new AccountValueEncoding() const key = generateKey() - const value: AccountValue = { + const value: DecryptedAccountValue = { + encrypted: false, id: 'id', name: 'foobar๐Ÿ‘๏ธ๐Ÿƒ๐ŸŸ', incomingViewKey: key.incomingViewKey, @@ -57,4 +64,47 @@ describe('AccountValueEncoding', () => { const deserializedValue = encoder.deserialize(buffer) expect(deserializedValue).toEqual(value) }) + + it('serializes an object encrypted account data into a buffer and deserializes to the original object', async () => { + const encoder = new AccountValueEncoding() + + const key = generateKey() + const value: DecryptedAccountValue = { + encrypted: false, + id: 'id', + name: 'foobar๐Ÿ‘๏ธ๐Ÿƒ๐ŸŸ', + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + publicAddress: key.publicAddress, + spendingKey: null, + viewKey: key.viewKey, + version: 1, + createdAt: null, + scanningEnabled: true, + multisigKeys: { + publicKeyPackage: 'cccc', + secret: 'deaf', + keyPackage: 'beef', + }, + proofAuthorizingKey: key.proofAuthorizingKey, + } + + const passphrase = 'foobarbaz' + const masterKey = MasterKey.generate(passphrase) + const xchacha20poly1305Key = await masterKey.unlock(passphrase) + + const data = encoder.serialize(value) + const encryptedData = xchacha20poly1305Key.encrypt(data) + + const encryptedValue: EncryptedAccountValue = { + encrypted: true, + data: encryptedData, + salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), + nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), + } + + const buffer = encoder.serialize(encryptedValue) + const deserializedValue = encoder.deserializeEncrypted(buffer) + expect(encryptedValue).toEqual(deserializedValue) + }) }) diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index 942c7357eb..50dc2be1ca 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -1,19 +1,25 @@ /* 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 { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import { KEY_LENGTH, PUBLIC_ADDRESS_LENGTH, xchacha20poly1305 } from '@ironfish/rust-nodejs' import bufio from 'bufio' import { IDatabaseEncoding } from '../../storage' -import { ACCOUNT_KEY_LENGTH } from '../account/account' import { MultisigKeys } from '../interfaces/multisigKeys' import { HeadValue, NullableHeadValueEncoding } from './headValue' import { MultisigKeysEncoding } from './multisigKeys' -export const KEY_LENGTH = ACCOUNT_KEY_LENGTH export const VIEW_KEY_LENGTH = 64 const VERSION_LENGTH = 2 -export interface AccountValue { +export type EncryptedAccountValue = { + encrypted: true + salt: Buffer + nonce: Buffer + data: Buffer +} + +export type DecryptedAccountValue = { + encrypted: false version: number id: string name: string @@ -28,8 +34,30 @@ export interface AccountValue { proofAuthorizingKey: string | null } +export type AccountValue = EncryptedAccountValue | DecryptedAccountValue export class AccountValueEncoding implements IDatabaseEncoding { serialize(value: AccountValue): Buffer { + if (value.encrypted) { + return this.serializeEncrypted(value) + } else { + return this.serializeDecrypted(value) + } + } + + serializeEncrypted(value: EncryptedAccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!value.encrypted) << 5 + bw.writeU8(flags) + bw.writeBytes(value.salt) + bw.writeBytes(value.nonce) + bw.writeVarBytes(value.data) + + return bw.render() + } + + serializeDecrypted(value: DecryptedAccountValue): Buffer { const bw = bufio.write(this.getSize(value)) let flags = 0 flags |= Number(!!value.spendingKey) << 0 @@ -37,6 +65,8 @@ export class AccountValueEncoding implements IDatabaseEncoding { flags |= Number(!!value.multisigKeys) << 2 flags |= Number(!!value.proofAuthorizingKey) << 3 flags |= Number(!!value.scanningEnabled) << 4 + flags |= Number(!!value.encrypted) << 5 + bw.writeU8(flags) bw.writeU16(value.version) bw.writeVarString(value.id, 'utf8') @@ -70,6 +100,35 @@ export class AccountValueEncoding implements IDatabaseEncoding { } deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const encrypted = Boolean(flags & (1 << 5)) + + if (encrypted) { + return this.deserializeEncrypted(buffer) + } else { + return this.deserializeDecrypted(buffer) + } + } + + deserializeEncrypted(buffer: Buffer): EncryptedAccountValue { + const reader = bufio.read(buffer, true) + + // Skip flags + reader.readU8() + + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const data = reader.readVarBytes() + return { + encrypted: true, + nonce, + salt, + data, + } + } + + deserializeDecrypted(buffer: Buffer): DecryptedAccountValue { const reader = bufio.read(buffer, true) const flags = reader.readU8() const version = reader.readU16() @@ -104,6 +163,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { : null return { + encrypted: false, version, id, name, @@ -120,6 +180,23 @@ export class AccountValueEncoding implements IDatabaseEncoding { } getSize(value: AccountValue): number { + if (value.encrypted) { + return this.getSizeEncrypted(value) + } else { + return this.getSizeDecrypted(value) + } + } + + getSizeEncrypted(value: EncryptedAccountValue): number { + let size = 0 + size += 1 // flags + size += xchacha20poly1305.XSALT_LENGTH + size += xchacha20poly1305.XNONCE_LENGTH + size += bufio.sizeVarBytes(value.data) + return size + } + + getSizeDecrypted(value: DecryptedAccountValue): number { let size = 0 size += 1 // flags size += VERSION_LENGTH diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts new file mode 100644 index 0000000000..497deaf11f --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts @@ -0,0 +1,19 @@ +/* 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 { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' + +describe('MasterKeyValueEncoding', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new MasterKeyValueEncoding() + + const value: MasterKeyValue = { + nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), + salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) +}) diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.ts b/ironfish/src/wallet/walletdb/masterKeyValue.ts new file mode 100644 index 0000000000..6a739ab0e8 --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.ts @@ -0,0 +1,32 @@ +/* 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 { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../storage' + +export type MasterKeyValue = { + nonce: Buffer + salt: Buffer +} + +export class MasterKeyValueEncoding implements IDatabaseEncoding { + serialize(value: MasterKeyValue): Buffer { + const bw = bufio.write(this.getSize()) + bw.writeBytes(value.nonce) + bw.writeBytes(value.salt) + return bw.render() + } + + deserialize(buffer: Buffer): MasterKeyValue { + const reader = bufio.read(buffer, true) + + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } + } + + getSize(): number { + return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH + } +} diff --git a/ironfish/src/wallet/walletdb/multiSigKeys.test.ts b/ironfish/src/wallet/walletdb/multiSigKeys.test.ts index 86e6f4334c..a72b2a4e16 100644 --- a/ironfish/src/wallet/walletdb/multiSigKeys.test.ts +++ b/ironfish/src/wallet/walletdb/multiSigKeys.test.ts @@ -1,7 +1,11 @@ /* 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 { MultisigCoordinator, MultisigSigner } from '../interfaces/multisigKeys' +import { + MultisigCoordinator, + MultisigHardwareSigner, + MultisigSigner, +} from '../interfaces/multisigKeys' import { MultisigKeysEncoding } from './multisigKeys' describe('multisigKeys encoder', () => { @@ -32,4 +36,18 @@ describe('multisigKeys encoder', () => { expect(deserializedValue).toEqual(value) }) }) + + describe('with a hardware multisig', () => { + it('serializes the value into a buffer and deserialized to the original value', () => { + const encoder = new MultisigKeysEncoding() + + const value: MultisigHardwareSigner = { + publicKeyPackage: 'aaaa', + identity: 'c0ffee', + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/multisigIdentityValue.ts b/ironfish/src/wallet/walletdb/multisigIdentityValue.ts new file mode 100644 index 0000000000..e362475ca4 --- /dev/null +++ b/ironfish/src/wallet/walletdb/multisigIdentityValue.ts @@ -0,0 +1,54 @@ +/* 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 { multisig } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../storage' + +export interface MultisigIdentityValue { + name: string + /** + * The secret is optional when a multisig account is generated on a Ledger device. + * The secret never leaves the Ledger device. + * + * We use a zero buffer encoding approach for the optional 'secret' field: + * - Present secret: written directly to buffer + * - Undefined secret: zero buffer of same length written + * + * This approach maintains consistent serialized size and avoids database migrations, + * while allowing distinction between undefined and actual secrets during deserialization. + */ + secret?: Buffer +} + +export class MultisigIdentityValueEncoder implements IDatabaseEncoding { + serialize(value: MultisigIdentityValue): Buffer { + const bw = bufio.write(this.getSize(value)) + bw.writeVarString(value.name, 'utf-8') + if (value.secret) { + bw.writeBytes(value.secret) + } else { + // Write a zero buffer of the same length as the secret + bw.writeBytes(Buffer.alloc(multisig.SECRET_LEN)) + } + return bw.render() + } + + deserialize(buffer: Buffer): MultisigIdentityValue { + const reader = bufio.read(buffer, true) + const name = reader.readVarString('utf-8') + const secret = reader.readBytes(multisig.SECRET_LEN) + // Check if the secret is all zeros + if (Buffer.compare(secret, Buffer.alloc(multisig.SECRET_LEN)) === 0) { + return { name, secret: undefined } + } + return { name, secret } + } + + getSize(value: MultisigIdentityValue): number { + let size = 0 + size += bufio.sizeVarString(value.name, 'utf8') + size += multisig.SECRET_LEN + return size + } +} diff --git a/ironfish/src/wallet/walletdb/multisigKeys.ts b/ironfish/src/wallet/walletdb/multisigKeys.ts index 65a00a4856..d6f8ad4c55 100644 --- a/ironfish/src/wallet/walletdb/multisigKeys.ts +++ b/ironfish/src/wallet/walletdb/multisigKeys.ts @@ -4,7 +4,11 @@ import bufio from 'bufio' import { Assert } from '../../assert' import { IDatabaseEncoding } from '../../storage' -import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' +import { + MultisigHardwareSigner, + MultisigKeys, + MultisigSigner, +} from '../interfaces/multisigKeys' export class MultisigKeysEncoding implements IDatabaseEncoding { serialize(value: MultisigKeys): Buffer { @@ -12,12 +16,15 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { let flags = 0 flags |= Number(!!isSignerMultisig(value)) << 0 + flags |= Number(!!isHardwareSignerMultisig(value)) << 1 bw.writeU8(flags) bw.writeVarBytes(Buffer.from(value.publicKeyPackage, 'hex')) if (isSignerMultisig(value)) { bw.writeVarBytes(Buffer.from(value.secret, 'hex')) bw.writeVarBytes(Buffer.from(value.keyPackage, 'hex')) + } else if (isHardwareSignerMultisig(value)) { + bw.writeVarBytes(Buffer.from(value.identity, 'hex')) } return bw.render() @@ -28,6 +35,7 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { const flags = reader.readU8() const isSigner = flags & (1 << 0) + const isHardwareSigner = flags & (1 << 1) const publicKeyPackage = reader.readVarBytes().toString('hex') if (isSigner) { @@ -38,6 +46,12 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { secret, keyPackage, } + } else if (isHardwareSigner) { + const identity = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + identity, + } } return { @@ -53,6 +67,8 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { if (isSignerMultisig(value)) { size += bufio.sizeVarString(value.secret, 'hex') size += bufio.sizeVarString(value.keyPackage, 'hex') + } else if (isHardwareSignerMultisig(value)) { + size += bufio.sizeVarString(value.identity, 'hex') } return size @@ -63,6 +79,12 @@ export function isSignerMultisig(multisigKeys: MultisigKeys): multisigKeys is Mu return 'keyPackage' in multisigKeys && 'secret' in multisigKeys } +export function isHardwareSignerMultisig( + multisigKeys: MultisigKeys, +): multisigKeys is MultisigHardwareSigner { + return 'identity' in multisigKeys +} + export function AssertIsSignerMultisig( multisigKeys: MultisigKeys, ): asserts multisigKeys is MultisigSigner { diff --git a/ironfish/src/wallet/walletdb/multisigSecretValue.ts b/ironfish/src/wallet/walletdb/multisigSecretValue.ts deleted file mode 100644 index 079d713e48..0000000000 --- a/ironfish/src/wallet/walletdb/multisigSecretValue.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* 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 { multisig } from '@ironfish/rust-nodejs' -import bufio from 'bufio' -import { IDatabaseEncoding } from '../../storage' - -export interface MultisigSecretValue { - name: string - secret: Buffer -} - -export class MultisigSecretValueEncoding implements IDatabaseEncoding { - serialize(value: MultisigSecretValue): Buffer { - const bw = bufio.write(this.getSize(value)) - bw.writeVarString(value.name, 'utf-8') - bw.writeBytes(value.secret) - return bw.render() - } - - deserialize(buffer: Buffer): MultisigSecretValue { - const reader = bufio.read(buffer, true) - const name = reader.readVarString('utf-8') - const secret = reader.readBytes(multisig.SECRET_LEN) - return { name, secret } - } - - getSize(value: MultisigSecretValue): number { - let size = 0 - size += bufio.sizeVarString(value.name, 'utf8') - size += multisig.SECRET_LEN - return size - } -} diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 90dc863fe7..e1d12e56d2 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -1,7 +1,7 @@ /* 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 { Asset, multisig } from '@ironfish/rust-nodejs' +import { Asset, generateKey, multisig } from '@ironfish/rust-nodejs' import { randomBytes } from 'crypto' import { Assert } from '../../assert' import { @@ -11,6 +11,11 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils' +import { Account } from '../account/account' +import { EncryptedAccount } from '../account/encryptedAccount' +import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' +import { DecryptedAccountValue } from './accountValue' import { DecryptedNoteValue } from './decryptedNoteValue' describe('WalletDB', () => { @@ -446,14 +451,280 @@ describe('WalletDB', () => { const secret = multisig.ParticipantSecret.random() const serializedSecret = secret.serialize() - await walletDb.putMultisigSecret(secret.toIdentity().serialize(), { + await walletDb.putMultisigIdentity(secret.toIdentity().serialize(), { secret: serializedSecret, name, }) const storedSecret = await walletDb.getMultisigSecretByName(name) Assert.isNotUndefined(storedSecret) - expect(storedSecret.secret).toEqualBuffer(serializedSecret) + expect(storedSecret).toEqualBuffer(serializedSecret) + }) + }) + + describe('encryptAccounts', () => { + it('stores encrypted accounts', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + await walletDb.encryptAccounts(passphrase) + + const encryptedAccountById = new Map() + for await (const [id, accountValue] of walletDb.loadAccounts()) { + if (!accountValue.encrypted) { + throw new Error('Unexpected behavior') + } + + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) + } + + const masterKeyValue = await walletDb.loadMasterKey() + Assert.isNotNull(masterKeyValue) + const masterKey = new MasterKey(masterKeyValue) + const key = await masterKey.unlock(passphrase) + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(key) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(key) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) + + describe('decryptAccounts', () => { + it('stores decrypted accounts', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + await walletDb.encryptAccounts(passphrase) + + const encryptedAccountById = new Map() + for await (const [id, accountValue] of walletDb.loadAccounts()) { + if (!accountValue.encrypted) { + throw new Error('Unexpected behavior') + } + + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + + await walletDb.decryptAccounts(passphrase) + + const accountById = new Map() + for await (const [id, accountValue] of walletDb.loadAccounts()) { + if (accountValue.encrypted) { + throw new Error('Unexpected behavior') + } + + accountById.set(id, new Account({ accountValue, walletDb })) + } + + const decryptedAccountA = accountById.get(accountA.id) + Assert.isNotUndefined(decryptedAccountA) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const decryptedAccountB = accountById.get(accountB.id) + Assert.isNotUndefined(decryptedAccountB) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + + it('throws an error with an invalid passphrase', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + const invalidPassphrase = 'foo' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + await walletDb.encryptAccounts(passphrase) + + const encryptedAccountById = new Map() + for await (const [id, accountValue] of walletDb.loadAccounts()) { + if (!accountValue.encrypted) { + throw new Error('Unexpected behavior') + } + + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + + await expect(walletDb.decryptAccounts(invalidPassphrase)).rejects.toThrow( + AccountDecryptionFailedError, + ) + }) + }) + + describe('accountsEncrypted', () => { + it('throws an exception if the encrypted flag is inconsistent', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + + await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + + const masterKey = MasterKey.generate(passphrase) + await masterKey.unlock(passphrase) + + await walletDb.accounts.put(accountB.id, accountB.encrypt(masterKey).serialize()) + + await expect(walletDb.accountsEncrypted()).rejects.toThrow() + }) + + it('returns true if the accounts are encrypted', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'test' + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + await walletDb.encryptAccounts(passphrase) + + expect(await walletDb.accountsEncrypted()).toBe(true) + }) + + it('returns false if the accounts are not encrypted', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + + expect(await walletDb.accountsEncrypted()).toBe(false) + }) + + it('returns false if there are no accounts', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + expect(await walletDb.accountsEncrypted()).toBe(false) + }) + }) + + describe('setAccount', () => { + it('throws an error if existing accounts are encrypted', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'foobar' + + await useAccountFixture(node.wallet, 'A') + await walletDb.encryptAccounts(passphrase) + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '0', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + const account = new Account({ accountValue, walletDb }) + + await expect(walletDb.setAccount(account)).rejects.toThrow() + }) + + it('saves the account', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '1', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + const account = new Account({ accountValue, walletDb }) + + await walletDb.setAccount(account) + + expect(await walletDb.accounts.get(account.id)).not.toBeUndefined() + expect( + await walletDb.balances.get([account.prefix, Asset.nativeId()]), + ).not.toBeUndefined() + }) + }) + + describe('setEncryptedAccount', () => { + it('throws an error if existing accounts are decrypted', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) + + await useAccountFixture(node.wallet, 'A') + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '0', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + const account = new Account({ accountValue, walletDb }) + + await expect(walletDb.setEncryptedAccount(account, masterKey)).rejects.toThrow() + }) + + it('saves the account', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'foobar' + + await useAccountFixture(node.wallet, 'A') + await walletDb.encryptAccounts(passphrase) + + const key = generateKey() + const accountValue: DecryptedAccountValue = { + encrypted: false, + id: '1', + name: 'new-account', + version: 1, + createdAt: null, + scanningEnabled: false, + ...key, + } + const account = new Account({ accountValue, walletDb }) + + const masterKeyValue = await walletDb.loadMasterKey() + Assert.isNotNull(masterKeyValue) + const masterKey = new MasterKey(masterKeyValue) + await masterKey.unlock(passphrase) + + await walletDb.setEncryptedAccount(account, masterKey) + + expect(await walletDb.accounts.get(account.id)).not.toBeUndefined() + expect( + await walletDb.balances.get([account.prefix, Asset.nativeId()]), + ).not.toBeUndefined() }) }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index d1305dd84a..2f00cac016 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -32,13 +32,16 @@ import { BufferUtils } from '../../utils' import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' +import { EncryptedAccount } from '../account/encryptedAccount' +import { MasterKey } from '../masterKey' import { AccountValue, AccountValueEncoding } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' import { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' import { AccountsDBMeta, MetaValue, MetaValueEncoding } from './metaValue' -import { MultisigSecretValue, MultisigSecretValueEncoding } from './multisigSecretValue' +import { MultisigIdentityValue, MultisigIdentityValueEncoder } from './multisigIdentityValue' import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' import { TransactionValue, TransactionValueEncoding } from './transactionValue' @@ -136,9 +139,9 @@ export class WalletDB { value: null }> - multisigSecrets: IDatabaseStore<{ + multisigIdentities: IDatabaseStore<{ key: Buffer - value: MultisigSecretValue + value: MultisigIdentityValue }> participantIdentities: IDatabaseStore<{ @@ -146,6 +149,11 @@ export class WalletDB { value: ParticipantIdentity }> + masterKey: IDatabaseStore<{ + key: 'key' + value: MasterKeyValue + }> + cacheStores: Array> nullifierBloomFilter: BloomFilter | null = null @@ -296,10 +304,10 @@ export class WalletDB { valueEncoding: NULL_ENCODING, }) - this.multisigSecrets = this.db.addStore({ + this.multisigIdentities = this.db.addStore({ name: 'ms', keyEncoding: new BufferEncoding(), - valueEncoding: new MultisigSecretValueEncoding(), + valueEncoding: new MultisigIdentityValueEncoder(), }) this.participantIdentities = this.db.addStore({ @@ -312,6 +320,12 @@ export class WalletDB { valueEncoding: new ParticipantIdentityEncoding(), }) + this.masterKey = this.db.addStore({ + name: 'mk', + keyEncoding: new StringEncoding<'key'>(), + valueEncoding: new MasterKeyValueEncoding(), + }) + // IDatabaseStores that cache and index decrypted chain data this.cacheStores = [ this.decryptedNotes, @@ -341,6 +355,10 @@ export class WalletDB { async setAccount(account: Account, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { + if (await this.accountsEncrypted(tx)) { + throw new Error('Cannot save decrypted account when accounts are encrypted') + } + await this.accounts.put(account.id, account.serialize(), tx) const nativeUnconfirmedBalance = await this.balances.get( @@ -362,6 +380,41 @@ export class WalletDB { }) } + async setEncryptedAccount( + account: Account, + masterKey: MasterKey, + tx?: IDatabaseTransaction, + ): Promise { + return this.db.withTransaction(tx, async (tx) => { + const accountsEncrypted = await this.accountsEncrypted(tx) + if (!accountsEncrypted) { + throw new Error('Cannot save encrypted account when accounts are decrypted') + } + + const encryptedAccount = account.encrypt(masterKey) + await this.accounts.put(account.id, encryptedAccount.serialize(), tx) + + const nativeUnconfirmedBalance = await this.balances.get( + [account.prefix, Asset.nativeId()], + tx, + ) + if (nativeUnconfirmedBalance === undefined) { + await this.saveUnconfirmedBalance( + account, + Asset.nativeId(), + { + unconfirmed: 0n, + blockHash: null, + sequence: null, + }, + tx, + ) + } + + return encryptedAccount + }) + } + async removeAccount(account: Account, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { await this.accounts.del(account.id, tx) @@ -391,9 +444,23 @@ export class WalletDB { return meta } - async *loadAccounts(tx?: IDatabaseTransaction): AsyncGenerator { - for await (const account of this.accounts.getAllValuesIter(tx)) { - yield account + async loadMasterKey(tx?: IDatabaseTransaction): Promise { + const record = await this.masterKey.get('key', tx) + return record ?? null + } + + async saveMasterKey(masterKey: MasterKey, tx?: IDatabaseTransaction): Promise { + const record = await this.loadMasterKey(tx) + Assert.isNull(record) + + await this.masterKey.put('key', { nonce: masterKey.nonce, salt: masterKey.salt }, tx) + } + + async *loadAccounts( + tx?: IDatabaseTransaction, + ): AsyncGenerator<[string, AccountValue], void, unknown> { + for await (const [id, account] of this.accounts.getAllIter(tx)) { + yield [id, account] } } @@ -1186,6 +1253,71 @@ export class WalletDB { } } + async encryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { + await this.db.withTransaction(tx, async (tx) => { + const record = await this.loadMasterKey(tx) + Assert.isNull(record) + + const masterKey = MasterKey.generate(passphrase) + await this.saveMasterKey(masterKey, tx) + + await masterKey.unlock(passphrase) + + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { + if (accountValue.encrypted) { + throw new Error('Wallet is already encrypted') + } + + const account = new Account({ accountValue, walletDb: this }) + const encryptedAccount = account.encrypt(masterKey) + await this.accounts.put(id, encryptedAccount.serialize(), tx) + } + + await masterKey.destroy() + }) + } + + async decryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { + await this.db.withTransaction(tx, async (tx) => { + const masterKeyValue = await this.loadMasterKey(tx) + Assert.isNotNull(masterKeyValue) + + const masterKey = new MasterKey(masterKeyValue) + const key = await masterKey.unlock(passphrase) + + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { + if (!accountValue.encrypted) { + throw new Error('Wallet is already decrypted') + } + + const encryptedAccount = new EncryptedAccount({ + accountValue, + walletDb: this, + }) + const account = encryptedAccount.decrypt(key) + await this.accounts.put(id, account.serialize(), tx) + } + + await masterKey.destroy() + await this.masterKey.del('key', tx) + }) + } + + async accountsEncrypted(tx?: IDatabaseTransaction): Promise { + const accountValues: AccountValue[] = [] + for await (const [_, account] of this.loadAccounts(tx)) { + accountValues.push(account) + } + + if (accountValues.length === 0) { + return false + } + + const allEqual = accountValues.every((a) => a.encrypted === accountValues[0].encrypted) + Assert.isTrue(allEqual) + return accountValues[0].encrypted + } + async *loadTransactionsByTime( account: Account, tx?: IDatabaseTransaction, @@ -1301,36 +1433,49 @@ export class WalletDB { await this.nullifierToTransactionHash.del([account.prefix, nullifier], tx) } - async putMultisigSecret( + async putMultisigIdentity( identity: Buffer, - value: MultisigSecretValue, + value: MultisigIdentityValue, tx?: IDatabaseTransaction, ): Promise { - await this.multisigSecrets.put(identity, value, tx) + await this.multisigIdentities.put(identity, value, tx) } - async getMultisigSecret( + async getMultisigIdentity( identity: Buffer, tx?: IDatabaseTransaction, - ): Promise { - return this.multisigSecrets.get(identity, tx) + ): Promise { + return this.multisigIdentities.get(identity, tx) } - async hasMultisigSecret(identity: Buffer, tx?: IDatabaseTransaction): Promise { - return (await this.getMultisigSecret(identity, tx)) !== undefined + async hasMultisigIdentity(identity: Buffer, tx?: IDatabaseTransaction): Promise { + return (await this.getMultisigIdentity(identity, tx)) !== undefined } - async deleteMultisigSecret(identity: Buffer, tx?: IDatabaseTransaction): Promise { - await this.multisigSecrets.del(identity, tx) + async deleteMultisigIdentity(identity: Buffer, tx?: IDatabaseTransaction): Promise { + await this.multisigIdentities.del(identity, tx) + } + + async getMultisigIdentityByName( + name: string, + tx?: IDatabaseTransaction, + ): Promise { + for await (const [identity, value] of this.multisigIdentities.getAllIter(tx)) { + if (value.name === name) { + return identity + } + } + + return undefined } async getMultisigSecretByName( name: string, tx?: IDatabaseTransaction, - ): Promise { - for await (const value of this.multisigSecrets.getAllValuesIter(tx)) { + ): Promise { + for await (const value of this.multisigIdentities.getAllValuesIter(tx)) { if (value.name === name) { - return value + return value.secret } } @@ -1338,11 +1483,13 @@ export class WalletDB { } async hasMultisigSecretName(name: string, tx?: IDatabaseTransaction): Promise { - return (await this.getMultisigSecretByName(name, tx)) !== undefined + return (await this.getMultisigIdentityByName(name, tx)) !== undefined } - async *getMultisigSecrets(tx?: IDatabaseTransaction): AsyncGenerator { - for await (const value of this.multisigSecrets.getAllValuesIter(tx)) { + async *getMultisigIdentities( + tx?: IDatabaseTransaction, + ): AsyncGenerator { + for await (const value of this.multisigIdentities.getAllValuesIter(tx)) { yield value } } diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 0ab61b20e5..9237b50784 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -1,6 +1,9 @@ # cargo-vet audits file +[audits] +reddsa = [] + [[audits.arrayvec]] who = "Andrea " criteria = "safe-to-deploy" @@ -82,13 +85,6 @@ who = "Andrea " criteria = "safe-to-deploy" delta = "0.9.95 -> 0.9.102" -[[audits.reddsa]] -who = "Andrea " -criteria = "safe-to-deploy" -delta = "0.5.1 -> 0.5.1@git:311baf8865f6e21527d1f20750d8f2cf5c9e531a" -importable = false -notes = "Unreleased changes required by ironfish-frost to support multisig wallets" - [[audits.signal-hook]] who = "andrea " criteria = "safe-to-deploy" @@ -133,3 +129,9 @@ who = "Andrea " criteria = "safe-to-deploy" delta = "0.7.1 -> 0.7.1@git:d551820030cb596eafe82226667f32b47164f91b" notes = "Fork of the official zcash_proofs owned by Iron Fish" + +[[trusted.reddsa]] +criteria = "safe-to-deploy" +user-id = 6289 # str4d +start = "2021-01-08" +end = "2025-09-12" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 52c74ea903..a9ba931a8e 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -40,7 +40,7 @@ audit-as-crates-io = true [policy."jubjub:0.9.0@git:531157cfa7b81ade207e819ef50c563843b10e30"] audit-as-crates-io = true -[policy."reddsa:0.5.1@git:311baf8865f6e21527d1f20750d8f2cf5c9e531a"] +[policy.reddsa] audit-as-crates-io = true [policy.zcash_address] @@ -70,6 +70,10 @@ criteria = "safe-to-deploy" version = "0.7.20" criteria = "safe-to-deploy" +[[exemptions.argon2]] +version = "0.5.3" +criteria = "safe-to-deploy" + [[exemptions.atomic-polyfill]] version = "0.1.11" criteria = "safe-to-deploy" @@ -98,14 +102,14 @@ criteria = "safe-to-deploy" version = "0.9.0" criteria = "safe-to-deploy" -[[exemptions.bitflags]] -version = "1.3.2" -criteria = "safe-to-deploy" - [[exemptions.bitvec]] version = "1.0.1" criteria = "safe-to-deploy" +[[exemptions.blake2]] +version = "0.10.6" +criteria = "safe-to-deploy" + [[exemptions.blake2b_simd]] version = "1.0.0" criteria = "safe-to-deploy" @@ -182,8 +186,8 @@ criteria = "safe-to-deploy" version = "0.2.4" criteria = "safe-to-deploy" -[[exemptions.const-crc32]] -version = "1.2.0" +[[exemptions.const-crc32-nostd]] +version = "1.3.1" criteria = "safe-to-deploy" [[exemptions.const-oid]] @@ -199,7 +203,7 @@ version = "0.6.0" criteria = "safe-to-deploy" [[exemptions.cpufeatures]] -version = "0.2.6" +version = "0.2.7" criteria = "safe-to-deploy" [[exemptions.criterion]] @@ -259,7 +263,7 @@ version = "0.7.1" criteria = "safe-to-deploy" [[exemptions.derive-getters]] -version = "0.3.0" +version = "0.4.0" criteria = "safe-to-deploy" [[exemptions.digest]] @@ -330,8 +334,12 @@ criteria = "safe-to-deploy" version = "0.5.1" criteria = "safe-to-deploy" +[[exemptions.frost-core]] +version = "2.0.0-rc.0" +criteria = "safe-to-deploy" + [[exemptions.frost-rerandomized]] -version = "1.0.0" +version = "2.0.0-rc.0" criteria = "safe-to-deploy" [[exemptions.funty]] @@ -382,6 +390,10 @@ criteria = "safe-to-deploy" version = "0.1.19" criteria = "safe-to-deploy" +[[exemptions.hkdf]] +version = "0.12.4" +criteria = "safe-to-deploy" + [[exemptions.hmac]] version = "0.11.0" criteria = "safe-to-deploy" @@ -431,7 +443,7 @@ version = "0.10.5" criteria = "safe-to-deploy" [[exemptions.itertools]] -version = "0.12.0" +version = "0.13.0" criteria = "safe-to-deploy" [[exemptions.itoa]] @@ -451,7 +463,7 @@ version = "0.2.150" criteria = "safe-to-deploy" [[exemptions.libloading]] -version = "0.7.4" +version = "0.8.5" criteria = "safe-to-deploy" [[exemptions.linux-raw-sys]] @@ -491,7 +503,7 @@ version = "0.8.8" criteria = "safe-to-deploy" [[exemptions.napi]] -version = "2.13.2" +version = "2.16.9" criteria = "safe-to-deploy" [[exemptions.napi-build]] @@ -499,15 +511,15 @@ version = "2.0.1" criteria = "safe-to-deploy" [[exemptions.napi-derive]] -version = "2.13.0" +version = "2.16.11" criteria = "safe-to-deploy" [[exemptions.napi-derive-backend]] -version = "1.0.52" +version = "1.0.73" criteria = "safe-to-deploy" [[exemptions.napi-sys]] -version = "2.2.3" +version = "2.4.0" criteria = "safe-to-deploy" [[exemptions.nonempty]] @@ -550,6 +562,10 @@ criteria = "safe-to-deploy" version = "0.3.2" criteria = "safe-to-deploy" +[[exemptions.password-hash]] +version = "0.5.0" +criteria = "safe-to-deploy" + [[exemptions.pasta_curves]] version = "0.4.1" criteria = "safe-to-deploy" @@ -603,7 +619,7 @@ version = "0.8.5" criteria = "safe-to-deploy" [[exemptions.reddsa]] -version = "0.3.0" +version = "0.5.1@git:ed49e9ca0699a6450f6d4a9fe62ff168f5ea1ead" criteria = "safe-to-deploy" [[exemptions.redjubjub]] @@ -719,7 +735,7 @@ version = "1.0.107" criteria = "safe-to-deploy" [[exemptions.syn]] -version = "2.0.18" +version = "2.0.77" criteria = "safe-to-deploy" [[exemptions.tempfile]] @@ -734,6 +750,14 @@ criteria = "safe-to-deploy" version = "1.0.38" criteria = "safe-to-deploy" +[[exemptions.thiserror-nostd-notrait]] +version = "1.0.57" +criteria = "safe-to-deploy" + +[[exemptions.thiserror-nostd-notrait-impl]] +version = "1.0.57" +criteria = "safe-to-deploy" + [[exemptions.threadpool]] version = "1.8.1" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index f27eb30550..cc959162f9 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -22,6 +22,12 @@ user-id = 4484 user-login = "hsivonen" user-name = "Henri Sivonen" +[[publisher.reddsa]] +version = "0.3.0" +when = "2022-05-10" +user-id = 6289 +user-login = "str4d" + [[publisher.unicode-normalization]] version = "0.1.22" when = "2022-09-16" @@ -288,16 +294,6 @@ who = "Pat Hickey " criteria = "safe-to-deploy" version = "0.1.0" -[[audits.bytecode-alliance.audits.proc-macro2]] -who = "Pat Hickey " -criteria = "safe-to-deploy" -delta = "1.0.51 -> 1.0.57" - -[[audits.bytecode-alliance.audits.quote]] -who = "Pat Hickey " -criteria = "safe-to-deploy" -delta = "1.0.23 -> 1.0.27" - [[audits.bytecode-alliance.audits.semver]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -412,6 +408,22 @@ version = "0.13.1" notes = "Skimmed the uses of `std` to ensure that nothing untoward is happening. Code uses `forbid(unsafe_code)` and, indeed, there are no uses of `unsafe`" aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.3.2" +notes = """ +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +The crate exposes a function marked as `unsafe`, but doesn't use any +`unsafe` blocks (except for tests of the single `unsafe` function). I +think this justifies marking this crate as `ub-risk-1`. + +Additional review comments can be found at https://crrev.com/c/4723145/31 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.document-features]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -484,6 +496,102 @@ version = "0.2.9" notes = "Reviewed on https://fxrev.dev/824504" aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.78" +notes = """ +Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for a benign \"fs\" hit in a doc comment) + +Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.78 -> 1.0.79" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.79 -> 1.0.80" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.80 -> 1.0.81" +notes = "Comment changes only" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.81 -> 1.0.82" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.82 -> 1.0.83" +notes = "Substantive change is replacing String with Box, saving memory." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.83 -> 1.0.84" +notes = "Only doc comment changes in `src/lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +delta = "1.0.84 -> 1.0.85" +notes = "Test-only changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.85 -> 1.0.86" +notes = """ +Comment-only changes in `build.rs`. +Reordering of `Cargo.toml` entries. +Just bumping up the version number in `lib.rs`. +Config-related changes in `test_size.rs`. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.35" +notes = """ +Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.35 -> 1.0.36" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.36 -> 1.0.37" +notes = """ +The delta just 1) inlines/expands `impl ToTokens` that used to be handled via +`primitive!` macro and 2) adds `impl ToTokens` for `CStr` and `CString`. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.unicode-xid]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -554,6 +662,21 @@ who = "David Cook " criteria = "safe-to-deploy" version = "0.12.1" +[[audits.isrg.audits.once_cell]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.17.1 -> 1.17.2" + +[[audits.isrg.audits.once_cell]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "1.17.2 -> 1.18.0" + +[[audits.isrg.audits.once_cell]] +who = "Brandon Pitman " +criteria = "safe-to-deploy" +delta = "1.18.0 -> 1.19.0" + [[audits.isrg.audits.opaque-debug]] who = "David Cook " criteria = "safe-to-deploy" @@ -697,6 +820,13 @@ criteria = "safe-to-deploy" delta = "1.0.73 -> 1.0.78" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.cpufeatures]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 0.2.8" +notes = "This release contains a single fix for an issue that affected Firefox" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.crypto-common]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -834,96 +964,6 @@ criteria = "safe-to-deploy" delta = "2.2.0 -> 2.3.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.proc-macro2]] -who = "Nika Layzell " -criteria = "safe-to-deploy" -version = "1.0.39" -notes = """ -`proc-macro2` acts as either a thin(-ish) wrapper around the std-provided -`proc_macro` crate, or as a fallback implementation of the crate, depending on -where it is used. - -If using this crate on older versions of rustc (1.56 and earlier), it will -temporarily replace the panic handler while initializing in order to detect if -it is running within a `proc_macro`, which could lead to surprising behaviour. -This should not be an issue for more recent compiler versions, which support -`proc_macro::is_available()`. - -The `proc-macro2` crate's fallback behaviour is not identical to the complex -behaviour of the rustc compiler (e.g. it does not perform unicode normalization -for identifiers), however it behaves well enough for its intended use-case -(tests and scripts processing rust code). - -`proc-macro2` does not use unsafe code, however exposes one `unsafe` API to -allow bypassing checks in the fallback implementation when constructing -`Literal` using `from_str_unchecked`. This was intended to only be used by the -`quote!` macro, however it has been removed -(https://github.com/dtolnay/quote/commit/f621fe64a8a501cae8e95ebd6848e637bbc79078), -and is likely completely unused. Even when used, this API shouldn't be able to -cause unsoundness. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.proc-macro2]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -delta = "1.0.39 -> 1.0.43" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.proc-macro2]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -delta = "1.0.43 -> 1.0.49" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.proc-macro2]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -delta = "1.0.49 -> 1.0.51" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.proc-macro2]] -who = "Jan-Erik Rediger " -criteria = "safe-to-deploy" -delta = "1.0.57 -> 1.0.59" -notes = "Enabled on Wasm" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" - -[[audits.mozilla.audits.quote]] -who = "Nika Layzell " -criteria = "safe-to-deploy" -version = "1.0.18" -notes = """ -`quote` is a utility crate used by proc-macros to generate TokenStreams -conveniently from source code. The bulk of the logic is some complex -interlocking `macro_rules!` macros which are used to parse and build the -`TokenStream` within the proc-macro. - -This crate contains no unsafe code, and the internal logic, while difficult to -read, is generally straightforward. I have audited the the quote macros, ident -formatter, and runtime logic. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.quote]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -delta = "1.0.18 -> 1.0.21" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.quote]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -delta = "1.0.21 -> 1.0.23" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.quote]] -who = "Jan-Erik Rediger " -criteria = "safe-to-deploy" -delta = "1.0.27 -> 1.0.28" -notes = "Enabled on wasm targets" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" - [[audits.mozilla.audits.rand_core]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -1095,6 +1135,29 @@ delta = "0.2.6 -> 0.3.0" notes = "Replaces some `unsafe` code by bumping MSRV to 1.66 (to access `core::hint::black_box`)." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +[[audits.zcash.audits.cpufeatures]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.8 -> 0.2.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.9 -> 0.2.11" +notes = """ +New `unsafe` block is to call `libc::getauxval(libc::AT_HWCAP)` on Linux for +LoongArch64 CPU feature detection support. This and the supporting macro code is +the same as the existing Linux code for AArch64. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.11 -> 0.2.12" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + [[audits.zcash.audits.ff]] who = "Sean Bowe " criteria = "safe-to-deploy" @@ -1138,6 +1201,16 @@ delta = "0.9.0 -> 0.10.0" notes = "I previously reviewed the crypto-sensitive portions of these changes as well." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" +[[audits.zcash.audits.once_cell]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.17.0 -> 1.17.1" +notes = """ +Small refactor that reduces the overall amount of `unsafe` code. The new strict provenance +approach looks reasonable. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + [[audits.zcash.audits.platforms]] who = "Daira Emma Hopwood " criteria = "safe-to-deploy" @@ -1156,30 +1229,12 @@ delta = "0.7.2 -> 0.8.0" notes = "Changes to unsafe (avx2) code look reasonable." aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.proc-macro2]] -who = "Jack Grigg " -criteria = "safe-to-deploy" -delta = "1.0.59 -> 1.0.60" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - [[audits.zcash.audits.rand_xorshift]] who = "Sean Bowe " criteria = "safe-to-deploy" version = "0.3.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" -[[audits.zcash.audits.reddsa]] -who = "Sean Bowe " -criteria = "safe-to-deploy" -delta = "0.3.0 -> 0.5.0" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - -[[audits.zcash.audits.reddsa]] -who = "Jack Grigg " -criteria = "safe-to-deploy" -delta = "0.5.0 -> 0.5.1" -aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" - [[audits.zcash.audits.rustc_version]] who = "Jack Grigg " criteria = "safe-to-deploy" diff --git a/yarn.lock b/yarn.lock index bf681b4abe..67a2ba2c70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2555,16 +2555,6 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" -"@oclif/plugin-autocomplete@3.1.6": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.1.6.tgz#627ab2b0d9afa95b76f4dc0ba68c0980ee877740" - integrity sha512-Eo13RHSr7c5I5miatEBGhKVkLEADzN8taUlYOs5vbRWtWlR/FoDnwSZJ72gBvvayvCHEqlBOaNBn/wufxdrDAg== - dependencies: - "@oclif/core" "^4" - ansis "^3.2.0" - debug "^4.3.5" - ejs "^3.1.10" - "@oclif/plugin-help@6.2.5", "@oclif/plugin-help@^6.2.2": version "6.2.5" resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-6.2.5.tgz#a1d8e8469b3447c055ca3ab5444388a822d06517"