From 8147d4ff60deba3f09686a9c0b8f97c5b875b3f7 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 13:41:44 -0700 Subject: [PATCH 001/114] Move JSON flags into flags module (#5199) This allows us to over-ride the built in OCLIF JSON flag group which exists at https://github.com/oclif/core/blob/213e9203fd7f0f5aaffdcc9eb28d2828f7679373/src/util/aggregate-flags.ts#L4C1-L7C3 --- ironfish-cli/src/commands/chain/status.ts | 4 ++-- ironfish-cli/src/flags.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/chain/status.ts b/ironfish-cli/src/commands/chain/status.ts index b856b1f565..c606e37d40 100644 --- a/ironfish-cli/src/commands/chain/status.ts +++ b/ironfish-cli/src/commands/chain/status.ts @@ -3,7 +3,7 @@ * 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 } from '../../flags' import * as ui from '../../ui' export default class ChainStatus extends IronfishCommand { @@ -11,7 +11,7 @@ export default class ChainStatus extends IronfishCommand { static enableJsonFlag = true static flags = { - [ColorFlagKey]: ColorFlag, + ...JsonFlags, } async start(): Promise { diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index f8ee624c88..e0fe6130c1 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({ @@ -115,6 +123,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({ From aa2bb1c8663a19ea15ceb886eb7ddbba447906e2 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 13:50:33 -0700 Subject: [PATCH 002/114] Add custom help category for RPC flags (#5200) This should make all the RPC flags show up in their own section in --help now. --- ironfish-cli/src/flags.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ironfish-cli/src/flags.ts b/ironfish-cli/src/flags.ts index e0fe6130c1..c1b0eaeab2 100644 --- a/ironfish-cli/src/flags.ts +++ b/ironfish-cli/src/flags.ts @@ -68,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', }) /** From 412e4508e54d2117efd9dc63e35cba1372ae7192 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 14:03:04 -0700 Subject: [PATCH 003/114] Added missing RemoteFlags on commands (#5201) --- ironfish-cli/src/commands/chain/blocks/info.ts | 3 ++- ironfish-cli/src/commands/chain/power.ts | 3 ++- ironfish-cli/src/commands/chain/status.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/chain/blocks/info.ts b/ironfish-cli/src/commands/chain/blocks/info.ts index b3bdfecc7e..04dbfc1402 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 { ColorFlag, ColorFlagKey, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' export default class BlockInfo extends IronfishCommand { @@ -19,6 +19,7 @@ export default class BlockInfo extends IronfishCommand { } static flags = { + ...RemoteFlags, [ColorFlagKey]: ColorFlag, } diff --git a/ironfish-cli/src/commands/chain/power.ts b/ironfish-cli/src/commands/chain/power.ts index 632448e77d..7fd0e7faee 100644 --- a/ironfish-cli/src/commands/chain/power.ts +++ b/ironfish-cli/src/commands/chain/power.ts @@ -4,13 +4,14 @@ import { FileUtils } from '@ironfish/sdk' import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey } from '../../flags' +import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../flags' export default class Power extends IronfishCommand { static description = "show the network's mining power" static enableJsonFlag = true static flags = { + ...RemoteFlags, [ColorFlagKey]: ColorFlag, history: Flags.integer({ required: false, diff --git a/ironfish-cli/src/commands/chain/status.ts b/ironfish-cli/src/commands/chain/status.ts index c606e37d40..0d8da405ba 100644 --- a/ironfish-cli/src/commands/chain/status.ts +++ b/ironfish-cli/src/commands/chain/status.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { FileUtils, renderNetworkName } from '@ironfish/sdk' import { IronfishCommand } from '../../command' -import { JsonFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' import * as ui from '../../ui' export default class ChainStatus extends IronfishCommand { @@ -12,6 +12,7 @@ export default class ChainStatus extends IronfishCommand { static flags = { ...JsonFlags, + ...RemoteFlags, } async start(): Promise { From 78a7e856b7bfcb3dce06aac8da9c3ddef29557ff Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 14:30:58 -0700 Subject: [PATCH 004/114] Upgrade commands to use JsonFlags (#5202) --- ironfish-cli/src/commands/chain/assets/info.ts | 4 ++-- ironfish-cli/src/commands/chain/blocks/info.ts | 4 ++-- ironfish-cli/src/commands/chain/power.ts | 4 ++-- ironfish-cli/src/commands/chain/transactions/info.ts | 4 ++-- ironfish-cli/src/commands/config/get.ts | 4 ++-- ironfish-cli/src/commands/config/index.ts | 4 ++-- ironfish-cli/src/commands/wallet/export.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ironfish-cli/src/commands/chain/assets/info.ts b/ironfish-cli/src/commands/chain/assets/info.ts index 354e9e40a7..d3cb6205ab 100644 --- a/ironfish-cli/src/commands/chain/assets/info.ts +++ b/ironfish-cli/src/commands/chain/assets/info.ts @@ -4,7 +4,7 @@ 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 { @@ -20,7 +20,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 04dbfc1402..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, RemoteFlags } from '../../../flags' +import { JsonFlags, RemoteFlags } from '../../../flags' import * as ui from '../../../ui' export default class BlockInfo extends IronfishCommand { @@ -20,7 +20,7 @@ export default class BlockInfo extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, } async start(): Promise { diff --git a/ironfish-cli/src/commands/chain/power.ts b/ironfish-cli/src/commands/chain/power.ts index 7fd0e7faee..0fedaeae75 100644 --- a/ironfish-cli/src/commands/chain/power.ts +++ b/ironfish-cli/src/commands/chain/power.ts @@ -4,7 +4,7 @@ import { FileUtils } from '@ironfish/sdk' import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey, RemoteFlags } from '../../flags' +import { JsonFlags, RemoteFlags } from '../../flags' export default class Power extends IronfishCommand { static description = "show the network's mining power" @@ -12,7 +12,7 @@ export default class Power extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, history: Flags.integer({ required: false, description: diff --git a/ironfish-cli/src/commands/chain/transactions/info.ts b/ironfish-cli/src/commands/chain/transactions/info.ts index 6dfecd004b..0193d9a311 100644 --- a/ironfish-cli/src/commands/chain/transactions/info.ts +++ b/ironfish-cli/src/commands/chain/transactions/info.ts @@ -5,7 +5,7 @@ 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 { @@ -14,7 +14,7 @@ export class TransactionInfo extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, } static args = { diff --git a/ironfish-cli/src/commands/config/get.ts b/ironfish-cli/src/commands/config/get.ts index 0f5a91c1a3..0f4dcbb595 100644 --- a/ironfish-cli/src/commands/config/get.ts +++ b/ironfish-cli/src/commands/config/get.ts @@ -4,7 +4,7 @@ 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 { @@ -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..6e760a3d37 100644 --- a/ironfish-cli/src/commands/config/index.ts +++ b/ironfish-cli/src/commands/config/index.ts @@ -3,7 +3,7 @@ * 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' @@ -13,7 +13,7 @@ export class ShowCommand 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/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 835e9cbacf..ee194b5fe3 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -6,7 +6,7 @@ import { Args, Flags } from '@oclif/core' import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../command' -import { ColorFlag, ColorFlagKey, EnumLanguageKeyFlag, RemoteFlags } from '../../flags' +import { EnumLanguageKeyFlag, JsonFlags, RemoteFlags } from '../../flags' import { confirmOrQuit } from '../../ui' export class ExportCommand extends IronfishCommand { @@ -15,7 +15,7 @@ export class ExportCommand extends IronfishCommand { static flags = { ...RemoteFlags, - [ColorFlagKey]: ColorFlag, + ...JsonFlags, local: Flags.boolean({ default: false, description: 'Export an account without an online node', From c89980cf11aa51ebbf0c99826de852355913b087 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 14:39:39 -0700 Subject: [PATCH 005/114] Fix start command wallet flag over-riding config (#5204) Because it defaulted to true instead of undefined. --- ironfish-cli/src/commands/start.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 09231246f8..ea0d18524f 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -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`, }), } From 0dc52d664dd5a55d7d42fd46de509b54e1d49f8b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 14:50:58 -0700 Subject: [PATCH 006/114] Add an asset info example (#5205) It will show an example to see the native currency information --- ironfish-cli/src/commands/chain/assets/info.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ironfish-cli/src/commands/chain/assets/info.ts b/ironfish-cli/src/commands/chain/assets/info.ts index d3cb6205ab..e9eaa1c4e2 100644 --- a/ironfish-cli/src/commands/chain/assets/info.ts +++ b/ironfish-cli/src/commands/chain/assets/info.ts @@ -11,6 +11,14 @@ 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, From 24c05689d61290ea64d6af5a99b72f8c3d425a7b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Fri, 2 Aug 2024 14:53:16 -0700 Subject: [PATCH 007/114] Reorder chain commands so args are before flags (#5203) This creates a standard in laying our command code. --- ironfish-cli/src/commands/chain/broadcast.ts | 8 ++++---- ironfish-cli/src/commands/chain/export.ts | 18 +++++++++--------- ironfish-cli/src/commands/chain/power.ts | 14 +++++++------- .../src/commands/chain/transactions/info.ts | 10 +++++----- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ironfish-cli/src/commands/chain/broadcast.ts b/ironfish-cli/src/commands/chain/broadcast.ts index 9d2bb42032..9d288451fa 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 diff --git a/ironfish-cli/src/commands/chain/export.ts b/ironfish-cli/src/commands/chain/export.ts index 129d25aaed..bbba0ff630 100644 --- a/ironfish-cli/src/commands/chain/export.ts +++ b/ironfish-cli/src/commands/chain/export.ts @@ -11,15 +11,6 @@ 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 args = { start: Args.integer({ default: Number(GENESIS_BLOCK_SEQUENCE), @@ -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/power.ts b/ironfish-cli/src/commands/chain/power.ts index 0fedaeae75..4c33380897 100644 --- a/ironfish-cli/src/commands/chain/power.ts +++ b/ironfish-cli/src/commands/chain/power.ts @@ -10,6 +10,13 @@ export default class Power extends IronfishCommand { static description = "show the network's mining power" static enableJsonFlag = true + static args = { + block: Args.integer({ + required: false, + description: 'The sequence of the block to estimate network speed for', + }), + } + static flags = { ...RemoteFlags, ...JsonFlags, @@ -20,13 +27,6 @@ export default class Power extends IronfishCommand { }), } - static args = { - block: Args.integer({ - required: false, - description: 'The sequence of the block to estimate network speed for', - }), - } - async start(): Promise { const { flags, args } = await this.parse(Power) diff --git a/ironfish-cli/src/commands/chain/transactions/info.ts b/ironfish-cli/src/commands/chain/transactions/info.ts index 0193d9a311..3feafe8f70 100644 --- a/ironfish-cli/src/commands/chain/transactions/info.ts +++ b/ironfish-cli/src/commands/chain/transactions/info.ts @@ -12,11 +12,6 @@ export class TransactionInfo extends IronfishCommand { static description = 'show transaction information' static enableJsonFlag = true - static flags = { - ...RemoteFlags, - ...JsonFlags, - } - 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) From 3a2ce8cddb6aec9a7199f9b166d86ae71265826a Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 5 Aug 2024 16:01:38 -0700 Subject: [PATCH 008/114] Update chain description (#5207) --- ironfish-cli/package.json | 3 +++ ironfish-cli/src/commands/chain/download.ts | 2 +- ironfish-cli/src/commands/chain/export.ts | 2 +- ironfish-cli/src/commands/chain/forks.ts | 2 +- ironfish-cli/src/commands/chain/prune.ts | 2 +- ironfish-cli/src/commands/chain/status.ts | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index a33ae829c6..73f0f090d6 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -105,6 +105,9 @@ "wallet:scanning": { "description": "Turn on or off scanning for accounts" }, + "chain": { + "description": "commands for the blockchain" + }, "chain:blocks": { "description": "commands to look at blocks" }, 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 bbba0ff630..bfdeef018f 100644 --- a/ironfish-cli/src/commands/chain/export.ts +++ b/ironfish-cli/src/commands/chain/export.ts @@ -9,7 +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 description = 'export the blockchain to a file' static args = { start: Args.integer({ 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/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 0d8da405ba..989850d8a9 100644 --- a/ironfish-cli/src/commands/chain/status.ts +++ b/ironfish-cli/src/commands/chain/status.ts @@ -7,7 +7,7 @@ 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 = { From 513bf4d7ed7c7da8cc8c42eea1e1029ab6a2fa3b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 5 Aug 2024 16:12:39 -0700 Subject: [PATCH 009/114] Delete autocomplete plugin (#5208) I don't think anyone used this, and it's confusing showing up in the list. --- ironfish-cli/package.json | 4 +--- yarn.lock | 10 ---------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 73f0f090d6..a70d8ef25c 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -63,7 +63,6 @@ "@ironfish/sdk": "2.5.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,8 +97,7 @@ "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": { 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" From 210f3c77c8ef2e944f3887db77e0e286636e3506 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 6 Aug 2024 11:46:56 -0700 Subject: [PATCH 010/114] Update command descriptions (#5209) In an attempt to make them follow the style guide --- ironfish-cli/package.json | 15 +++++++++++++++ ironfish-cli/src/commands/browse.ts | 2 +- ironfish-cli/src/commands/config/index.ts | 2 +- ironfish-cli/src/commands/faucet.ts | 2 +- ironfish-cli/src/commands/fees.ts | 2 +- ironfish-cli/src/commands/logs.ts | 7 ++----- ironfish-cli/src/commands/migrations/index.ts | 2 +- ironfish-cli/src/commands/peers/index.ts | 2 +- ironfish-cli/src/commands/repl.ts | 2 +- ironfish-cli/src/commands/reset.ts | 2 +- ironfish-cli/src/commands/rpc/status.ts | 2 +- ironfish-cli/src/commands/start.ts | 2 +- ironfish-cli/src/commands/status.ts | 2 +- ironfish-cli/src/commands/stop.ts | 2 +- ironfish-cli/src/commands/wallet/accounts.ts | 2 +- ironfish-cli/src/commands/workers/status.ts | 2 +- 16 files changed, 31 insertions(+), 19 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index a70d8ef25c..83fa0ada4b 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -111,6 +111,21 @@ }, "chain:assets": { "description": "commands to look at assets" + }, + "rpc": { + "description": "commands for the RPC server" + }, + "miners": { + "description": "commands for mining" + }, + "mempool": { + "description": "commands for the mempool" + }, + "wallet": { + "description": "commands for the wallet" + }, + "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/config/index.ts b/ironfish-cli/src/commands/config/index.ts index 6e760a3d37..60e9a64003 100644 --- a/ironfish-cli/src/commands/config/index.ts +++ b/ironfish-cli/src/commands/config/index.ts @@ -8,7 +8,7 @@ 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 = { 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..b2f58e3bbb 100644 --- a/ironfish-cli/src/commands/fees.ts +++ b/ironfish-cli/src/commands/fees.ts @@ -13,7 +13,7 @@ import { IronfishCommand } from '../command' import { RemoteFlags } from '../flags' export class FeeCommand extends IronfishCommand { - static description = `Get fee distribution for most recent blocks` + static description = 'show network transaction fees' static flags = { ...RemoteFlags, 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/migrations/index.ts b/ironfish-cli/src/commands/migrations/index.ts index 8a5b047451..661d12ff82 100644 --- a/ironfish-cli/src/commands/migrations/index.ts +++ b/ironfish-cli/src/commands/migrations/index.ts @@ -4,7 +4,7 @@ import { IronfishCommand } from '../../command' export class StatusCommand extends IronfishCommand { - static description = `List all the migration statuses` + static description = `list data migrations` async start(): Promise { await this.parse(StatusCommand) 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/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..06b0d87922 100644 --- a/ironfish-cli/src/commands/rpc/status.ts +++ b/ironfish-cli/src/commands/rpc/status.ts @@ -9,7 +9,7 @@ import { 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 information' static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index ea0d18524f..29684ccbdb 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, diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index 14e9013457..b87b20faec 100644 --- a/ironfish-cli/src/commands/status.ts +++ b/ironfish-cli/src/commands/status.ts @@ -16,7 +16,7 @@ import { 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 flags = { ...RemoteFlags, 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 index 9b14f58d28..1dec76fa36 100644 --- a/ironfish-cli/src/commands/wallet/accounts.ts +++ b/ironfish-cli/src/commands/wallet/accounts.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export class AccountsCommand extends IronfishCommand { - static description = `List all the accounts on the node` + static description = `list accounts in the wallet` static flags = { ...RemoteFlags, diff --git a/ironfish-cli/src/commands/workers/status.ts b/ironfish-cli/src/commands/workers/status.ts index 69446c663a..7278dd697b 100644 --- a/ironfish-cli/src/commands/workers/status.ts +++ b/ironfish-cli/src/commands/workers/status.ts @@ -8,7 +8,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' export default class Status extends IronfishCommand { - static description = 'Show the status of the worker pool' + static description = 'show worker pool information' static flags = { ...RemoteFlags, From 41297852c5a4ee6c2fdfebed52f817f8579f7223 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:49:31 -0400 Subject: [PATCH 011/114] feat(rust): Add xchacha20poly1305 functions (#5210) * feat(rust): Add xchacha20poly1305 functions * chore(rust): Add license header * chore(rust): Add audit for cpufeatures * chore(rust): Add audit for password-hash * chore(rust): Move crates to exemptions * refactor(rust): Use NONCE_LENGTH const --- Cargo.lock | 41 +++++++++-- ironfish-rust/Cargo.toml | 3 +- ironfish-rust/src/errors.rs | 3 + ironfish-rust/src/lib.rs | 2 + ironfish-rust/src/serializing/aead.rs | 4 +- ironfish-rust/src/xchacha20poly1305.rs | 97 ++++++++++++++++++++++++++ supply-chain/config.toml | 14 +++- supply-chain/imports.lock | 30 ++++++++ 8 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 ironfish-rust/src/xchacha20poly1305.rs diff --git a/Cargo.lock b/Cargo.lock index daf2fdc4a1..8502cec164 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" @@ -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", ] @@ -1462,13 +1483,14 @@ 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", @@ -1959,6 +1981,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 +2027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" dependencies = [ "crypto-mac", - "password-hash", + "password-hash 0.3.2", ] [[package]] diff --git a/ironfish-rust/Cargo.toml b/ironfish-rust/Cargo.toml index 1f5f52f9d1..a25a9aa253 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,7 @@ 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"] } [dev-dependencies] hex-literal = "0.4" diff --git a/ironfish-rust/src/errors.rs b/ironfish-rust/src/errors.rs index 446e1ea077..f582a44898 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -27,8 +27,11 @@ pub enum IronfishErrorKind { BellpersonSynthesis, CryptoBox, FrostLibError, + FailedArgon2Hash, FailedSignatureAggregation, FailedSignatureVerification, + FailedXChaCha20Poly1305Decryption, + FailedXChaCha20Poly1305Encryption, IllegalValue, InconsistentWitness, InvalidAssetIdentifier, 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/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs new file mode 100644 index 0000000000..b77b476c1a --- /dev/null +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -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/. */ + +use argon2::{password_hash::SaltString, Argon2}; +use chacha20poly1305::aead::Aead; +use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; +use rand::{thread_rng, RngCore}; + +use crate::errors::{IronfishError, IronfishErrorKind}; + +const KEY_LENGTH: usize = 32; +const NONCE_LENGTH: usize = 24; + +pub struct EncryptOutput { + pub salt: SaltString, + + pub nonce: [u8; NONCE_LENGTH], + + pub ciphertext: Vec, +} + +fn derive_key(passphrase: &[u8], salt: &[u8]) -> 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(Key::from(key)) +} + +pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result { + let salt = SaltString::generate(&mut thread_rng()); + let key = derive_key(passphrase, salt.to_string().as_bytes())?; + + let cipher = XChaCha20Poly1305::new(&key); + let mut nonce_bytes = [0u8; NONCE_LENGTH]; + thread_rng().fill_bytes(&mut nonce_bytes); + let nonce = XNonce::from_slice(&nonce_bytes); + + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption))?; + + Ok(EncryptOutput { + salt, + nonce: nonce_bytes, + ciphertext, + }) +} + +pub fn decrypt( + encrypted_output: EncryptOutput, + passphrase: &[u8], +) -> Result, IronfishError> { + let nonce = XNonce::from_slice(&encrypted_output.nonce); + + let key = derive_key(passphrase, encrypted_output.salt.to_string().as_bytes())?; + let cipher = XChaCha20Poly1305::new(&key); + + cipher + .decrypt(nonce, encrypted_output.ciphertext.as_ref()) + .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Decryption)) +} + +#[cfg(test)] +mod test { + use crate::xchacha20poly1305::{decrypt, encrypt}; + + #[test] + fn test_valid_passphrase() { + let plaintext = "thisissensitivedata"; + let passphrase = "supersecretpassword"; + + let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes()) + .expect("should successfully encrypt"); + let decrypted = + decrypt(encrypted_output, passphrase.as_bytes()).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 encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes()) + .expect("should successfully encrypt"); + + decrypt(encrypted_output, incorrect_passphrase.as_bytes()) + .expect_err("should fail decryption"); + } +} diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 52c74ea903..335af46a63 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -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" @@ -106,6 +110,10 @@ criteria = "safe-to-deploy" 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" @@ -199,7 +207,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]] @@ -550,6 +558,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" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index f27eb30550..cb4acf7021 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -697,6 +697,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" @@ -1095,6 +1102,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" From b5d5a7ad1e40138d4653d72e9c9bff5ae6ec4ed0 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:33:27 -0700 Subject: [PATCH 012/114] status command returns json (#5215) --- ironfish-cli/src/commands/status.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index b87b20faec..026062162f 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 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 @@ -219,7 +222,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, From e3ba090af657aacc95a937d1e9f7a41fc5594f15 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:13:41 -0700 Subject: [PATCH 013/114] use ui json element instead of custom json parsing (#5216) --- ironfish-cli/src/ui/table.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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[]) { From a3882ddece12ff7150a858e22ac21b5f7607dd7b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:48:38 -0400 Subject: [PATCH 014/114] feat(rust,rust-nodejs): Add napi `encrypt` function for xchacha20poly1305 (#5217) * feat(rust,rust-nodejs): Add napi `encrypt` function for xchacha20poly1305 * chore(rust): lint rust --- ironfish-rust-nodejs/index.d.ts | 1 + ironfish-rust-nodejs/index.js | 3 +- ironfish-rust-nodejs/src/lib.rs | 1 + ironfish-rust-nodejs/src/xchacha20poly1305.rs | 25 ++++++ ironfish-rust/src/xchacha20poly1305.rs | 80 ++++++++++++++++++- 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 ironfish-rust-nodejs/src/xchacha20poly1305.rs diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index cb4be65e4f..46736cc905 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -47,6 +47,7 @@ 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 function encrypt(plaintext: string, passphrase: string): string export const enum LanguageCode { English = 0, ChineseSimplified = 1, diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 6ca64090b1..0b5a3ffbb0 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ 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, 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, encrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding module.exports.FishHashContext = FishHashContext module.exports.KEY_LENGTH = KEY_LENGTH @@ -290,6 +290,7 @@ module.exports.TransactionPosted = TransactionPosted module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions module.exports.UnsignedTransaction = UnsignedTransaction +module.exports.encrypt = encrypt module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords 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/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs new file mode 100644 index 0000000000..f683d96349 --- /dev/null +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -0,0 +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/. */ + +use ironfish::{ + serializing::{bytes_to_hex, hex_to_vec_bytes}, + xchacha20poly1305, +}; +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use crate::to_napi_err; + +#[napi] +pub fn encrypt(plaintext: String, passphrase: String) -> Result { + let plaintext_bytes = hex_to_vec_bytes(&plaintext).map_err(to_napi_err)?; + let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?; + let result = + xchacha20poly1305::encrypt(&plaintext_bytes, &passphrase_bytes).map_err(to_napi_err)?; + + let mut vec: Vec = vec![]; + result.write(&mut vec).map_err(to_napi_err)?; + + Ok(bytes_to_hex(&vec)) +} diff --git a/ironfish-rust/src/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs index b77b476c1a..5deabd5507 100644 --- a/ironfish-rust/src/xchacha20poly1305.rs +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -2,6 +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/. */ +use std::io; + use argon2::{password_hash::SaltString, Argon2}; use chacha20poly1305::aead::Aead; use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; @@ -12,14 +14,62 @@ use crate::errors::{IronfishError, IronfishErrorKind}; const KEY_LENGTH: usize = 32; const NONCE_LENGTH: usize = 24; +#[derive(Debug)] pub struct EncryptOutput { - pub salt: SaltString, + pub salt: Vec, pub nonce: [u8; NONCE_LENGTH], pub ciphertext: Vec, } +impl EncryptOutput { + pub fn write(&self, mut writer: W) -> Result<(), IronfishError> { + let salt_len = u32::try_from(self.salt.len())?.to_le_bytes(); + writer.write_all(&salt_len)?; + writer.write_all(&self.salt)?; + + writer.write_all(&self.nonce)?; + + let ciphertext_len = u32::try_from(self.ciphertext.len())?.to_le_bytes(); + writer.write_all(&ciphertext_len)?; + writer.write_all(&self.ciphertext)?; + + Ok(()) + } + + pub fn read(mut reader: R) -> Result { + let mut salt_len = [0u8; 4]; + reader.read_exact(&mut salt_len)?; + let salt_len = u32::from_le_bytes(salt_len) as usize; + + let mut salt = vec![0u8; salt_len]; + reader.read_exact(&mut salt)?; + + let mut nonce = [0u8; NONCE_LENGTH]; + reader.read_exact(&mut nonce)?; + + let mut ciphertext_len = [0u8; 4]; + reader.read_exact(&mut ciphertext_len)?; + let ciphertext_len = u32::from_le_bytes(ciphertext_len) as usize; + + let mut ciphertext = vec![0u8; ciphertext_len]; + reader.read_exact(&mut ciphertext)?; + + Ok(EncryptOutput { + salt, + nonce, + ciphertext, + }) + } +} + +impl PartialEq for EncryptOutput { + fn eq(&self, other: &EncryptOutput) -> bool { + self.salt == other.salt && self.nonce == other.nonce && self.ciphertext == other.ciphertext + } +} + fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result { let mut key = [0u8; KEY_LENGTH]; let argon2 = Argon2::default(); @@ -33,7 +83,9 @@ fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result { pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result { let salt = SaltString::generate(&mut thread_rng()); - let key = derive_key(passphrase, salt.to_string().as_bytes())?; + let salt_str = salt.to_string(); + let salt_bytes = salt_str.as_bytes(); + let key = derive_key(passphrase, salt_bytes)?; let cipher = XChaCha20Poly1305::new(&key); let mut nonce_bytes = [0u8; NONCE_LENGTH]; @@ -45,7 +97,7 @@ pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result Result, IronfishError> { let nonce = XNonce::from_slice(&encrypted_output.nonce); - let key = derive_key(passphrase, encrypted_output.salt.to_string().as_bytes())?; + let key = derive_key(passphrase, &encrypted_output.salt[..])?; let cipher = XChaCha20Poly1305::new(&key); cipher @@ -69,6 +121,8 @@ pub fn decrypt( mod test { use crate::xchacha20poly1305::{decrypt, encrypt}; + use super::EncryptOutput; + #[test] fn test_valid_passphrase() { let plaintext = "thisissensitivedata"; @@ -94,4 +148,22 @@ mod test { decrypt(encrypted_output, incorrect_passphrase.as_bytes()) .expect_err("should fail decryption"); } + + #[test] + fn test_encrypt_output_serialization() { + let plaintext = "thisissensitivedata"; + let passphrase = "supersecretpassword"; + + let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes()) + .expect("should successfully encrypt"); + + let mut vec: Vec = vec![]; + encrypted_output + .write(&mut vec) + .expect("should serialize successfully"); + + let deserialized = EncryptOutput::read(&vec[..]).expect("should deserialize successfully"); + + assert_eq!(encrypted_output, deserialized); + } } From 601e28042c1f4942fa49f3af579f9594b2d18451 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:04:03 -0400 Subject: [PATCH 015/114] feat(rust-nodejs): Add napi `decrypt` xchacha20poly1305 function (#5218) * feat(rust,rust-nodejs): Add napi `encrypt` function for xchacha20poly1305 * chore(rust): lint rust * feat(rust-nodejs): Add napi `decrypt` xchacha20poly1305 function --- ironfish-rust-nodejs/src/xchacha20poly1305.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ironfish-rust-nodejs/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs index f683d96349..e417bb3122 100644 --- a/ironfish-rust-nodejs/src/xchacha20poly1305.rs +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -4,7 +4,7 @@ use ironfish::{ serializing::{bytes_to_hex, hex_to_vec_bytes}, - xchacha20poly1305, + xchacha20poly1305::{self, EncryptOutput}, }; use napi::bindgen_prelude::*; use napi_derive::napi; @@ -23,3 +23,15 @@ pub fn encrypt(plaintext: String, passphrase: String) -> Result { Ok(bytes_to_hex(&vec)) } + +#[napi] +pub fn decrypt(encrypted_blob: String, passphrase: String) -> Result { + let encrypted_blob_bytes = hex_to_vec_bytes(&encrypted_blob).map_err(to_napi_err)?; + let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?; + + let encrypted_output = EncryptOutput::read(&encrypted_blob_bytes[..]).map_err(to_napi_err)?; + let result = + xchacha20poly1305::decrypt(encrypted_output, &passphrase_bytes).map_err(to_napi_err)?; + + Ok(bytes_to_hex(&result[..])) +} From b2f8c0f8e6d45c487b8b4926cd0e7065b272a33e Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:27:27 -0400 Subject: [PATCH 016/114] chore(rust-nodejs): Update Napi JS index file (#5224) --- ironfish-rust-nodejs/index.d.ts | 1 + ironfish-rust-nodejs/index.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 46736cc905..814bf63fad 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -48,6 +48,7 @@ export const TRANSACTION_FEE_LENGTH: number export const LATEST_TRANSACTION_VERSION: number export function verifyTransactions(serializedTransactions: Array): boolean export function encrypt(plaintext: string, passphrase: string): string +export function decrypt(encryptedBlob: string, passphrase: string): string export const enum LanguageCode { English = 0, ChineseSimplified = 1, diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 0b5a3ffbb0..34689156e3 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ 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, encrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding +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, encrypt, decrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding module.exports.FishHashContext = FishHashContext module.exports.KEY_LENGTH = KEY_LENGTH @@ -291,6 +291,7 @@ module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions module.exports.UnsignedTransaction = UnsignedTransaction module.exports.encrypt = encrypt +module.exports.decrypt = decrypt module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords From 0a465c9afd30c96dc55f316f9c5d039994ecf0af Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:07:35 -0700 Subject: [PATCH 017/114] CLI debug command uses card output and has json support (#5212) --- ironfish-cli/src/commands/debug.ts | 85 +++++++++++++----------------- 1 file changed, 36 insertions(+), 49 deletions(-) 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}`) - }) - } } From de0bddac72dddbb9e7be97fac1d1cbe79e96cbd5 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:54:52 -0400 Subject: [PATCH 018/114] feat(ironfish,rust-nodejs): Add EncryptedAccount class (#5226) * feat(ironfish,rust-nodejs): Add EncryptedAccount class * chore(rust-nodejs): lint rust * chore(rust-nodejs): cargo clippy fix * feat(ironfish): Add test for invalid passphrase * feat(ironfish): Add error type for failed decryption --- ironfish-rust-nodejs/index.d.ts | 4 +- ironfish-rust-nodejs/src/xchacha20poly1305.rs | 29 ++++----- .../src/testUtilities/fixtures/account.ts | 7 +- .../encryptedAccount.test.ts.fixture | 64 +++++++++++++++++++ ironfish/src/wallet/account/account.ts | 13 +++- .../wallet/account/encryptedAccount.test.ts | 52 +++++++++++++++ .../src/wallet/account/encryptedAccount.ts | 37 +++++++++++ ironfish/src/wallet/errors.ts | 9 +++ ironfish/src/wallet/wallet.ts | 3 + .../src/wallet/walletdb/accountValue.test.ts | 8 ++- ironfish/src/wallet/walletdb/accountValue.ts | 19 ++++-- ironfish/src/wallet/walletdb/walletdb.ts | 8 ++- 12 files changed, 217 insertions(+), 36 deletions(-) create mode 100644 ironfish/src/wallet/account/__fixtures__/encryptedAccount.test.ts.fixture create mode 100644 ironfish/src/wallet/account/encryptedAccount.test.ts create mode 100644 ironfish/src/wallet/account/encryptedAccount.ts diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 814bf63fad..ffa853a065 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -47,8 +47,8 @@ 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 function encrypt(plaintext: string, passphrase: string): string -export function decrypt(encryptedBlob: string, passphrase: string): string +export function encrypt(plaintext: Buffer, passphrase: string): Buffer +export function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer export const enum LanguageCode { English = 0, ChineseSimplified = 1, diff --git a/ironfish-rust-nodejs/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs index e417bb3122..1135ad67b2 100644 --- a/ironfish-rust-nodejs/src/xchacha20poly1305.rs +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -2,36 +2,31 @@ * 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::{ - serializing::{bytes_to_hex, hex_to_vec_bytes}, - xchacha20poly1305::{self, EncryptOutput}, -}; -use napi::bindgen_prelude::*; +use ironfish::xchacha20poly1305::{self, EncryptOutput}; +use napi::{bindgen_prelude::*, JsBuffer}; use napi_derive::napi; use crate::to_napi_err; #[napi] -pub fn encrypt(plaintext: String, passphrase: String) -> Result { - let plaintext_bytes = hex_to_vec_bytes(&plaintext).map_err(to_napi_err)?; - let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?; - let result = - xchacha20poly1305::encrypt(&plaintext_bytes, &passphrase_bytes).map_err(to_napi_err)?; +pub fn encrypt(plaintext: JsBuffer, passphrase: String) -> Result { + let plaintext_bytes = plaintext.into_value()?; + let result = xchacha20poly1305::encrypt(plaintext_bytes.as_ref(), passphrase.as_bytes()) + .map_err(to_napi_err)?; let mut vec: Vec = vec![]; result.write(&mut vec).map_err(to_napi_err)?; - Ok(bytes_to_hex(&vec)) + Ok(Buffer::from(&vec[..])) } #[napi] -pub fn decrypt(encrypted_blob: String, passphrase: String) -> Result { - let encrypted_blob_bytes = hex_to_vec_bytes(&encrypted_blob).map_err(to_napi_err)?; - let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?; +pub fn decrypt(encrypted_blob: JsBuffer, passphrase: String) -> Result { + let encrypted_bytes = encrypted_blob.into_value()?; - let encrypted_output = EncryptOutput::read(&encrypted_blob_bytes[..]).map_err(to_napi_err)?; + let encrypted_output = EncryptOutput::read(encrypted_bytes.as_ref()).map_err(to_napi_err)?; let result = - xchacha20poly1305::decrypt(encrypted_output, &passphrase_bytes).map_err(to_napi_err)?; + xchacha20poly1305::decrypt(encrypted_output, passphrase.as_bytes()).map_err(to_napi_err)?; - Ok(bytes_to_hex(&result[..])) + Ok(Buffer::from(&result[..])) } 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/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.ts b/ironfish/src/wallet/account/account.ts index 861627fbec..ef14ecbb2f 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -15,7 +15,7 @@ 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 { DecryptedAccountValue } from '../walletdb/accountValue' import { AssetValue } from '../walletdb/assetValue' import { BalanceValue } from '../walletdb/balanceValue' import { DecryptedNoteValue } from '../walletdb/decryptedNoteValue' @@ -76,7 +76,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 +108,9 @@ export class Account { return this.spendingKey !== null } - serialize(): AccountValue { + serialize(): DecryptedAccountValue { return { + encrypted: false, version: this.version, id: this.id, name: this.name, diff --git a/ironfish/src/wallet/account/encryptedAccount.test.ts b/ironfish/src/wallet/account/encryptedAccount.test.ts new file mode 100644 index 0000000000..75c1d3dfd7 --- /dev/null +++ b/ironfish/src/wallet/account/encryptedAccount.test.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 { encrypt } from '@ironfish/rust-nodejs' +import { useAccountFixture } from '../../testUtilities/fixtures/account' +import { createNodeTest } from '../../testUtilities/nodeTest' +import { AccountDecryptionFailedError } from '../errors' +import { AccountValueEncoding } from '../walletdb/accountValue' +import { EncryptedAccount } from './encryptedAccount' + +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 encoder = new AccountValueEncoding() + const data = encoder.serialize(account.serialize()) + + const encryptedData = encrypt(data, passphrase) + const encryptedAccount = new EncryptedAccount({ + data: encryptedData, + walletDb: node.wallet.walletDb, + }) + + const decryptedAccount = encryptedAccount.decrypt(passphrase) + const decryptedData = encoder.serialize(decryptedAccount.serialize()) + expect(data.toString('hex')).toEqual(decryptedData.toString('hex')) + }) + + 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 encoder = new AccountValueEncoding() + const data = encoder.serialize(account.serialize()) + + const encryptedData = encrypt(data, passphrase) + const encryptedAccount = new EncryptedAccount({ + data: encryptedData, + walletDb: node.wallet.walletDb, + }) + + expect(() => encryptedAccount.decrypt(invalidPassphrase)).toThrow( + AccountDecryptionFailedError, + ) + }) +}) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts new file mode 100644 index 0000000000..5eb24a8588 --- /dev/null +++ b/ironfish/src/wallet/account/encryptedAccount.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 { decrypt } 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 data: Buffer + + constructor({ data, walletDb }: { data: Buffer; walletDb: WalletDB }) { + this.data = data + this.walletDb = walletDb + } + + decrypt(passphrase: string): Account { + try { + const decryptedAccountValue = decrypt(this.data, passphrase) + const encoder = new AccountValueEncoding() + const accountValue = encoder.deserialize(decryptedAccountValue) + + return new Account({ accountValue, walletDb: this.walletDb }) + } catch { + throw new AccountDecryptionFailedError() + } + } + + serialize(): EncryptedAccountValue { + return { + encrypted: true, + data: this.data, + } + } +} diff --git a/ironfish/src/wallet/errors.ts b/ironfish/src/wallet/errors.ts index 342b79c235..1b45673cdb 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -60,3 +60,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 account' + } +} diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index cb5215c514..9f117ce9ba 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1288,6 +1288,7 @@ export class Wallet { const account = new Account({ accountValue: { + encrypted: false, version: ACCOUNT_SCHEMA_VERSION, id: uuid(), name, @@ -1389,6 +1390,7 @@ export class Wallet { name, multisigKeys, scanningEnabled: true, + encrypted: false, }, walletDb: this.walletDb, }) @@ -1441,6 +1443,7 @@ export class Wallet { createdAt: options?.resetCreatedAt ? null : account.createdAt, scanningEnabled: options?.resetScanningEnabled ? true : account.scanningEnabled, id: uuid(), + encrypted: false, }, walletDb: this.walletDb, }) diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index f9462a8c86..c7d28f8feb 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -2,14 +2,15 @@ * 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 { AccountValueEncoding, DecryptedAccountValue } 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 +35,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, diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index 942c7357eb..8650832c7c 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -13,7 +13,13 @@ export const KEY_LENGTH = ACCOUNT_KEY_LENGTH export const VIEW_KEY_LENGTH = 64 const VERSION_LENGTH = 2 -export interface AccountValue { +export interface EncryptedAccountValue { + encrypted: true + data: Buffer +} + +export interface DecryptedAccountValue { + encrypted: false version: number id: string name: string @@ -28,8 +34,10 @@ export interface AccountValue { proofAuthorizingKey: string | null } -export class AccountValueEncoding implements IDatabaseEncoding { - serialize(value: AccountValue): Buffer { +export type AccountValue = EncryptedAccountValue | DecryptedAccountValue + +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: DecryptedAccountValue): Buffer { const bw = bufio.write(this.getSize(value)) let flags = 0 flags |= Number(!!value.spendingKey) << 0 @@ -69,7 +77,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { return bw.render() } - deserialize(buffer: Buffer): AccountValue { + deserialize(buffer: Buffer): DecryptedAccountValue { const reader = bufio.read(buffer, true) const flags = reader.readU8() const version = reader.readU16() @@ -104,6 +112,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { : null return { + encrypted: false, version, id, name, @@ -119,7 +128,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { } } - getSize(value: AccountValue): number { + getSize(value: DecryptedAccountValue): number { let size = 0 size += 1 // flags size += VERSION_LENGTH diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index d1305dd84a..925bf3aa54 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -32,7 +32,7 @@ import { BufferUtils } from '../../utils' import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' -import { AccountValue, AccountValueEncoding } from './accountValue' +import { AccountValueEncoding, DecryptedAccountValue } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' @@ -54,7 +54,7 @@ export class WalletDB { location: string files: FileSystem - accounts: IDatabaseStore<{ key: string; value: AccountValue }> + accounts: IDatabaseStore<{ key: string; value: DecryptedAccountValue }> meta: IDatabaseStore<{ key: keyof AccountsDBMeta @@ -391,7 +391,9 @@ export class WalletDB { return meta } - async *loadAccounts(tx?: IDatabaseTransaction): AsyncGenerator { + async *loadAccounts( + tx?: IDatabaseTransaction, + ): AsyncGenerator { for await (const account of this.accounts.getAllValuesIter(tx)) { yield account } From a1d05fb52ad61bc4fb5cfc76dc52a9edb324fb1e Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:41:10 -0700 Subject: [PATCH 019/114] CLI fees command uses card output and has json support (#5213) --- ironfish-cli/src/commands/fees.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/commands/fees.ts b/ironfish-cli/src/commands/fees.ts index b2f58e3bbb..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 = '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('') } } From 644e86bd51d06ba61b263b5b0932bb4a4cbbc88f Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:41:20 -0700 Subject: [PATCH 020/114] update config descriptions to match our new standard (#5220) --- ironfish-cli/src/commands/config/edit.ts | 2 +- ironfish-cli/src/commands/config/get.ts | 2 +- ironfish-cli/src/commands/config/set.ts | 2 +- ironfish-cli/src/commands/config/unset.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 0f4dcbb595..b21f8ccbe7 100644 --- a/ironfish-cli/src/commands/config/get.ts +++ b/ironfish-cli/src/commands/config/get.ts @@ -8,7 +8,7 @@ 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 = { 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({ From 286dc4a6872e9239b5378343a5f493f2bf4b49e5 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:59:14 -0400 Subject: [PATCH 021/114] feat(ironfish): Add serialization for encrypted accounts (#5230) --- .../src/wallet/account/encryptedAccount.ts | 2 +- .../src/wallet/walletdb/accountValue.test.ts | 46 ++++++++++++- ironfish/src/wallet/walletdb/accountValue.ts | 68 +++++++++++++++++-- ironfish/src/wallet/walletdb/walletdb.ts | 9 ++- 4 files changed, 115 insertions(+), 10 deletions(-) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts index 5eb24a8588..cd2aa1c9d9 100644 --- a/ironfish/src/wallet/account/encryptedAccount.ts +++ b/ironfish/src/wallet/account/encryptedAccount.ts @@ -20,7 +20,7 @@ export class EncryptedAccount { try { const decryptedAccountValue = decrypt(this.data, passphrase) const encoder = new AccountValueEncoding() - const accountValue = encoder.deserialize(decryptedAccountValue) + const accountValue = encoder.deserializeDecrypted(decryptedAccountValue) return new Account({ accountValue, walletDb: this.walletDb }) } catch { diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index c7d28f8feb..645e750ee3 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -1,8 +1,12 @@ /* 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 { AccountValueEncoding, DecryptedAccountValue } from './accountValue' +import { encrypt, generateKey } from '@ironfish/rust-nodejs' +import { + AccountValueEncoding, + DecryptedAccountValue, + EncryptedAccountValue, +} from './accountValue' describe('AccountValueEncoding', () => { it('serializes the object into a buffer and deserializes to the original object', () => { @@ -59,4 +63,42 @@ 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', () => { + 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 data = encoder.serialize(value) + const encryptedData = encrypt(data, passphrase) + + const encryptedValue: EncryptedAccountValue = { + encrypted: true, + data: encryptedData, + } + + 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 8650832c7c..fb5a6c935e 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -35,9 +35,27 @@ export interface DecryptedAccountValue { } 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.writeVarBytes(value.data) + + return bw.render() + } -export class AccountValueEncoding implements IDatabaseEncoding { - serialize(value: DecryptedAccountValue): Buffer { + serializeDecrypted(value: DecryptedAccountValue): Buffer { const bw = bufio.write(this.getSize(value)) let flags = 0 flags |= Number(!!value.spendingKey) << 0 @@ -45,6 +63,8 @@ export class AccountValueEncoding implements IDatabaseEncoding + accounts: IDatabaseStore<{ key: string; value: AccountValue }> meta: IDatabaseStore<{ key: keyof AccountsDBMeta @@ -395,7 +395,10 @@ export class WalletDB { tx?: IDatabaseTransaction, ): AsyncGenerator { for await (const account of this.accounts.getAllValuesIter(tx)) { - yield account + // TODO(rohanjadvani): Remove this when encrypted accounts are managed in the wallet + if (!account.encrypted) { + yield account + } } } From 468859aab4c064a63a7a22411305d981f0b50edb Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:10:30 -0400 Subject: [PATCH 022/114] feat(ironfish): Save encrypted accounts in the wallet (#5237) * feat(ironfish): Save encrypted accounts in the wallet * feat(ironfish): Revert storing id * test(ironfish): Fix account value test --- ironfish/src/wallet/wallet.ts | 16 +++++++++++++--- ironfish/src/wallet/walletdb/walletdb.ts | 11 ++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 9f117ce9ba..aded0a79d4 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -42,6 +42,7 @@ 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, @@ -94,6 +95,7 @@ export class Wallet { readonly onAccountRemoved = new Event<[account: Account]>() protected readonly accountById = new Map() + protected readonly encryptedAccounts = new Map() readonly walletDb: WalletDB private readonly logger: Logger readonly workerPool: WorkerPool @@ -208,9 +210,17 @@ 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) + for await (const [id, accountValue] of this.walletDb.loadAccounts()) { + if (accountValue.encrypted) { + const encryptedAccount = new EncryptedAccount({ + data: accountValue.data, + walletDb: this.walletDb, + }) + this.encryptedAccounts.set(id, encryptedAccount) + } else { + const account = new Account({ accountValue, walletDb: this.walletDb }) + this.accountById.set(account.id, account) + } } const meta = await this.walletDb.loadAccountsMeta() diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 48b39e1b3d..7d379922c9 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -32,7 +32,7 @@ import { BufferUtils } from '../../utils' import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' -import { AccountValue, AccountValueEncoding, DecryptedAccountValue } from './accountValue' +import { AccountValue, AccountValueEncoding } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' @@ -393,12 +393,9 @@ export class WalletDB { async *loadAccounts( tx?: IDatabaseTransaction, - ): AsyncGenerator { - for await (const account of this.accounts.getAllValuesIter(tx)) { - // TODO(rohanjadvani): Remove this when encrypted accounts are managed in the wallet - if (!account.encrypted) { - yield account - } + ): AsyncGenerator<[string, AccountValue], void, unknown> { + for await (const [id, account] of this.accounts.getAllIter(tx)) { + yield [id, account] } } From e734cc53b542cd1439c706ddcb148a38a5a409df Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:37:33 -0400 Subject: [PATCH 023/114] feat(ironfish): Add `encrypt` method for account (#5247) * feat(ironfish): Add `encrypt` method for account * fix(ironfish): Remove extra KEY_LENGTH constant * test(ironfish): Fix serialization test * test(ironfish): Update fixture * feat(ironfish): Fix encrypt test for account --- .../__fixtures__/account.test.ts.fixture | 31 +++++++++++++++++++ ironfish/src/wallet/account/account.test.ts | 13 ++++++++ ironfish/src/wallet/account/account.ts | 16 ++++++++-- .../wallet/account/encryptedAccount.test.ts | 26 +++------------- .../src/wallet/exporter/encoders/bech32.ts | 4 +-- ironfish/src/wallet/walletdb/accountValue.ts | 6 ++-- 6 files changed, 66 insertions(+), 30 deletions(-) diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index 71b3bf6140..4963fc106e 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5810,5 +5810,36 @@ } ] } + ], + "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 + } + } ] } \ 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..54385836b3 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -2596,4 +2596,17 @@ 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 encryptedAccount = account.encrypt(passphrase) + const decryptedAccount = encryptedAccount.decrypt(passphrase) + + expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index ef14ecbb2f..51302e103d 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.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 { multisig } from '@ironfish/rust-nodejs' +import { encrypt, multisig } from '@ironfish/rust-nodejs' import { Asset } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' import MurmurHash3 from 'imurmurhash' @@ -15,7 +15,7 @@ import { WithNonNull, WithRequired } from '../../utils' import { DecryptedNote } from '../../workerPool/tasks/decryptNotes' import { AssetBalances } from '../assetBalances' import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' -import { DecryptedAccountValue } from '../walletdb/accountValue' +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 @@ -1289,6 +1290,17 @@ export class Account { const publicKeyPackage = new multisig.PublicKeyPackage(this.multisigKeys.publicKeyPackage) return publicKeyPackage.identities() } + + encrypt(passphrase: string): EncryptedAccount { + const encoder = new AccountValueEncoding() + const serialized = encoder.serialize(this.serialize()) + const data = encrypt(serialized, passphrase) + + return new EncryptedAccount({ + data, + 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 index 75c1d3dfd7..9b91821a1c 100644 --- a/ironfish/src/wallet/account/encryptedAccount.test.ts +++ b/ironfish/src/wallet/account/encryptedAccount.test.ts @@ -1,12 +1,9 @@ /* 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 { encrypt } from '@ironfish/rust-nodejs' import { useAccountFixture } from '../../testUtilities/fixtures/account' import { createNodeTest } from '../../testUtilities/nodeTest' import { AccountDecryptionFailedError } from '../errors' -import { AccountValueEncoding } from '../walletdb/accountValue' -import { EncryptedAccount } from './encryptedAccount' describe('EncryptedAccount', () => { const nodeTest = createNodeTest() @@ -16,18 +13,10 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encoder = new AccountValueEncoding() - const data = encoder.serialize(account.serialize()) - - const encryptedData = encrypt(data, passphrase) - const encryptedAccount = new EncryptedAccount({ - data: encryptedData, - walletDb: node.wallet.walletDb, - }) - + const encryptedAccount = account.encrypt(passphrase) const decryptedAccount = encryptedAccount.decrypt(passphrase) - const decryptedData = encoder.serialize(decryptedAccount.serialize()) - expect(data.toString('hex')).toEqual(decryptedData.toString('hex')) + + expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) }) it('throws an error when an invalid passphrase is used', async () => { @@ -36,14 +25,7 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encoder = new AccountValueEncoding() - const data = encoder.serialize(account.serialize()) - - const encryptedData = encrypt(data, passphrase) - const encryptedAccount = new EncryptedAccount({ - data: encryptedData, - walletDb: node.wallet.walletDb, - }) + const encryptedAccount = account.encrypt(passphrase) expect(() => encryptedAccount.decrypt(invalidPassphrase)).toThrow( AccountDecryptionFailedError, 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/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index fb5a6c935e..e72104f7f5 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -1,15 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import { KEY_LENGTH, PUBLIC_ADDRESS_LENGTH } 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 @@ -18,7 +16,7 @@ export interface EncryptedAccountValue { data: Buffer } -export interface DecryptedAccountValue { +export type DecryptedAccountValue = { encrypted: false version: number id: string From f6b588647aa674f2aaf72c02eca8d742757ced45 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:51:05 -0700 Subject: [PATCH 024/114] update mempool descriptions and add json support to status (#5221) --- ironfish-cli/src/commands/mempool/status.ts | 11 +++++++---- ironfish-cli/src/commands/mempool/transactions.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) 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, From 31cbe8fefcae377e0aefd669a684c85b8cf9153e Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:51:22 -0700 Subject: [PATCH 025/114] update migrations descriptions, migrations list supports json (#5222) * update migrations descriptions, migrations list supports json * use waitForOpen instead of a try/catch for DB --- ironfish-cli/src/commands/migrations/index.ts | 30 ++++++++++++++-- .../src/commands/migrations/revert.ts | 2 +- ironfish-cli/src/commands/migrations/start.ts | 2 +- ironfish/src/migrations/migrator.ts | 36 +++++++++---------- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/ironfish-cli/src/commands/migrations/index.ts b/ironfish-cli/src/commands/migrations/index.ts index 661d12ff82..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 data migrations` + 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/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, } } } From bed1b3c46fe89474f465f610966993edefb1b595 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:51:41 -0700 Subject: [PATCH 026/114] update peers descriptions, show -> info, add json to info (#5235) --- ironfish-cli/src/commands/peers/add.ts | 2 +- ironfish-cli/src/commands/peers/banned.ts | 2 +- .../src/commands/peers/{show.ts => info.ts} | 24 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) rename ironfish-cli/src/commands/peers/{show.ts => info.ts} (78%) 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/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 { From 74239df23185786b303e1dda9a0c6ab152eb0532 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:51:57 -0700 Subject: [PATCH 027/114] update rpc descriptions, add json output (#5244) --- ironfish-cli/src/commands/rpc/status.ts | 11 +++++++---- ironfish-cli/src/commands/rpc/token.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ironfish-cli/src/commands/rpc/status.ts b/ironfish-cli/src/commands/rpc/status.ts index 06b0d87922..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 RPC server information' + 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 } } } From 430496f69ed904229d74f63c34c168a804646728 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:52:10 -0700 Subject: [PATCH 028/114] update workers status cli desc, add json (#5245) --- ironfish-cli/src/commands/workers/status.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/workers/status.ts b/ironfish-cli/src/commands/workers/status.ts index 7278dd697b..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 worker pool information' + 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 From 7966996a50283e9a2d7da29ac1eed6c1f34888aa Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:00:53 -0700 Subject: [PATCH 029/114] update miners command descriptions, add json to pool status (#5234) --- ironfish-cli/src/commands/miners/pools/start.ts | 2 +- .../src/commands/miners/pools/status.ts | 17 +++++++++++++---- ironfish-cli/src/commands/miners/start.ts | 2 +- ironfish/src/mining/stratum/clients/client.ts | 12 ++++++++++-- .../src/mining/stratum/clients/tcpClient.ts | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) 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/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() } From 9770fb70a72ef41eebc423242b1d94c91bbba80b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 15:49:32 -0700 Subject: [PATCH 030/114] Move wallet:post -> wallet:transactions:post (#5251) Making these commands consistent with our style guide --- .../src/commands/wallet/{ => transactions}/post.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename ironfish-cli/src/commands/wallet/{ => transactions}/post.ts (89%) diff --git a/ironfish-cli/src/commands/wallet/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts similarity index 89% rename from ironfish-cli/src/commands/wallet/post.ts rename to ironfish-cli/src/commands/wallet/transactions/post.ts index 8ff40c6322..d5be53bcd4 100644 --- a/ironfish-cli/src/commands/wallet/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -3,13 +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 { confirmOrQuit } from '../../../ui' +import { longPrompt } from '../../../utils/input' +import { renderRawTransactionDetails } from '../../../utils/transaction' -export class PostCommand extends IronfishCommand { +export class TransactionsPostCommand extends IronfishCommand { static summary = 'Post a raw transaction' static description = `Use this command to post a raw transaction. @@ -45,7 +45,7 @@ export class PostCommand extends IronfishCommand { } async start(): Promise { - const { flags, args } = await this.parse(PostCommand) + const { flags, args } = await this.parse(TransactionsPostCommand) let transaction = args.transaction if (!transaction) { From e53d76f387634d84e0605b9bbdbd2490b0d7e4f9 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:19:16 -0700 Subject: [PATCH 031/114] move `wallet:transaction:watch` -> `wallet:transactions:watch` (#5254) --- .../commands/wallet/{transaction => transactions}/watch.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename ironfish-cli/src/commands/wallet/{transaction => transactions}/watch.ts (88%) diff --git a/ironfish-cli/src/commands/wallet/transaction/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts similarity index 88% rename from ironfish-cli/src/commands/wallet/transaction/watch.ts rename to ironfish-cli/src/commands/wallet/transactions/watch.ts index 46b80853d2..160d2b9374 100644 --- a/ironfish-cli/src/commands/wallet/transaction/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -6,8 +6,9 @@ import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import { watchTransaction } from '../../../utils/transaction' -export class WatchTxCommand extends IronfishCommand { +export class TransactionsWatchCommand extends IronfishCommand { static description = `Wait for the status of an account transaction to confirm or expire` + static hiddenAliases = ['wallet:transaction:watch'] static flags = { ...RemoteFlags, @@ -33,7 +34,7 @@ export class WatchTxCommand extends IronfishCommand { } async start(): Promise { - const { flags, args } = await this.parse(WatchTxCommand) + const { flags, args } = await this.parse(TransactionsWatchCommand) const { hash } = args // TODO: remove account arg const account = flags.account ? flags.account : args.account From 0ac1c86dfef10d0d15150e15a7cfb5898270e365 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:19:49 -0700 Subject: [PATCH 032/114] move `wallet:transaction:import` -> `wallet:transactions:import` (#5252) --- .../commands/wallet/{transaction => transactions}/import.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename ironfish-cli/src/commands/wallet/{transaction => transactions}/import.ts (90%) diff --git a/ironfish-cli/src/commands/wallet/transaction/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts similarity index 90% rename from ironfish-cli/src/commands/wallet/transaction/import.ts rename to ironfish-cli/src/commands/wallet/transactions/import.ts index e9b1232c93..12042d83f3 100644 --- a/ironfish-cli/src/commands/wallet/transaction/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -6,10 +6,10 @@ import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import { importFile, importPipe, longPrompt } from '../../../utils/input' -export class TransactionImportCommand extends IronfishCommand { +export class TransactionsImportCommand extends IronfishCommand { static description = `Import a transaction into your wallet` - static hiddenAliases = ['wallet:transaction:add'] + static hiddenAliases = ['wallet:transaction:add', 'wallet:transaction:import'] static flags = { ...RemoteFlags, @@ -31,7 +31,7 @@ export class TransactionImportCommand extends IronfishCommand { } async start(): Promise { - const { flags, args } = await this.parse(TransactionImportCommand) + const { flags, args } = await this.parse(TransactionsImportCommand) const { transaction: txArg } = args let transaction From 400b22670649300201e241b7b9e312bed5bd7d29 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:24:48 -0700 Subject: [PATCH 033/114] move `wallet:transaction:view` -> `wallet:transactions:decode` (#5253) --- .../wallet/{transaction/view.ts => transactions/decode.ts} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename ironfish-cli/src/commands/wallet/{transaction/view.ts => transactions/decode.ts} (95%) diff --git a/ironfish-cli/src/commands/wallet/transaction/view.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts similarity index 95% rename from ironfish-cli/src/commands/wallet/transaction/view.ts rename to ironfish-cli/src/commands/wallet/transactions/decode.ts index 6ac2e81332..cecb7d46fd 100644 --- a/ironfish-cli/src/commands/wallet/transaction/view.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -20,8 +20,9 @@ import { renderUnsignedTransactionDetails, } from '../../../utils/transaction' -export class TransactionViewCommand extends IronfishCommand { +export class TransactionsDecodeCommand extends IronfishCommand { static description = `View transaction details` + static hiddenAliases = ['wallet:transaction:view'] static flags = { ...RemoteFlags, @@ -37,7 +38,7 @@ 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() From 6aff7c70dee7a96314bfbb4d4f6de53df3890c2d Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 16:34:47 -0700 Subject: [PATCH 034/114] Add hiddenAlias for wallet:transactions:post (#5255) --- ironfish-cli/src/commands/wallet/transactions/post.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index d5be53bcd4..db86c5c69a 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -19,6 +19,8 @@ export class TransactionsPostCommand extends IronfishCommand { '$ ironfish wallet:post 618c098d8d008c9f78f6155947014901a019d9ec17160dc0f0d1bb1c764b29b4...', ] + static hiddenAliases = ['wallet:post'] + static flags = { ...RemoteFlags, account: Flags.string({ From 5ef47f88cdc7faa291b92df208cf462f8bf0ac49 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 16:37:46 -0700 Subject: [PATCH 035/114] move `wallet:sign` -> `wallet:transactions:sign` (#5256) --- .../commands/wallet/{ => transactions}/sign.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) rename ironfish-cli/src/commands/wallet/{ => transactions}/sign.ts (90%) diff --git a/ironfish-cli/src/commands/wallet/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts similarity index 90% rename from ironfish-cli/src/commands/wallet/sign.ts rename to ironfish-cli/src/commands/wallet/transactions/sign.ts index 5c80ca0935..640c61bce7 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' +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 { +export class TransactionsSignCommand extends IronfishCommand { static description = `Sign an unsigned transaction` + + static hiddenAliases = ['wallet:sign'] + static flags = { ...RemoteFlags, unsignedTransaction: Flags.string({ @@ -34,7 +37,7 @@ 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() if (!flags.broadcast && flags.watch) { From 014d66b46c0b5440b5ad8b024b54d30ec665862f Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 16:45:16 -0700 Subject: [PATCH 036/114] Move `wallet:transaction` to `wallet:transactions:info` (#5257) --- .../wallet/{transaction/index.ts => transactions/info.ts} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename ironfish-cli/src/commands/wallet/{transaction/index.ts => transactions/info.ts} (97%) diff --git a/ironfish-cli/src/commands/wallet/transaction/index.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts similarity index 97% rename from ironfish-cli/src/commands/wallet/transaction/index.ts rename to ironfish-cli/src/commands/wallet/transactions/info.ts index a55b5c6ef4..5c6ab2550c 100644 --- a/ironfish-cli/src/commands/wallet/transaction/index.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -21,9 +21,11 @@ import { } from '../../../utils' import { getExplorer } from '../../../utils/explorer' -export class TransactionCommand extends IronfishCommand { +export class TransactionInfoCommand extends IronfishCommand { static description = `Display an account transaction` + static hiddenAliases = ['wallet:transaction'] + static flags = { ...RemoteFlags, account: Flags.string({ @@ -44,7 +46,7 @@ export class TransactionCommand extends IronfishCommand { } async start(): Promise { - const { flags, args } = await this.parse(TransactionCommand) + const { flags, args } = await this.parse(TransactionInfoCommand) const { hash } = args // TODO: remove account arg const account = flags.account ? flags.account : args.account From bbf8a1e3655a2a49a370a16b22a2859ca1ccd943 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 16:48:18 -0700 Subject: [PATCH 037/114] Move `wallet:accounts` -> `wallet` (#5258) --- ironfish-cli/src/commands/wallet/{accounts.ts => index.ts} | 2 ++ 1 file changed, 2 insertions(+) rename ironfish-cli/src/commands/wallet/{accounts.ts => index.ts} (95%) diff --git a/ironfish-cli/src/commands/wallet/accounts.ts b/ironfish-cli/src/commands/wallet/index.ts similarity index 95% rename from ironfish-cli/src/commands/wallet/accounts.ts rename to ironfish-cli/src/commands/wallet/index.ts index 1dec76fa36..b54f416b46 100644 --- a/ironfish-cli/src/commands/wallet/accounts.ts +++ b/ironfish-cli/src/commands/wallet/index.ts @@ -8,6 +8,8 @@ import { RemoteFlags } from '../../flags' export class AccountsCommand extends IronfishCommand { static description = `list accounts in the wallet` + static hiddenAliases = ['wallet:accounts'] + static flags = { ...RemoteFlags, displayName: Flags.boolean({ From 4f39cbc4556bfe50e7baef3e1814b12fd78adb2d Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:11:15 -0700 Subject: [PATCH 038/114] `wallet:transactions:decode` prompts for transaction before account (#5261) --- ironfish-cli/src/commands/wallet/transactions/decode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/transactions/decode.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts index cecb7d46fd..baafd0f8b2 100644 --- a/ironfish-cli/src/commands/wallet/transactions/decode.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -42,8 +42,6 @@ export class TransactionsDecodeCommand extends IronfishCommand { const client = await this.connectRpc() - const account = flags.account ?? (await this.selectAccount(client)) - let transactionString = flags.transaction if (!transactionString) { transactionString = await longPrompt( @@ -54,6 +52,8 @@ export class TransactionsDecodeCommand extends IronfishCommand { ) } + const account = flags.account ?? (await this.selectAccount(client)) + const rawTransaction = this.tryDeserializeRawTransaction(transactionString) if (rawTransaction) { return await renderRawTransactionDetails(client, rawTransaction, account, this.logger) From abbf3f9e9d47023822d8e051e3549118b849b774 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 13 Aug 2024 22:05:43 -0700 Subject: [PATCH 039/114] Add table output to wallet command (#5265) This adds optional new table output to the wallet command --- ironfish-cli/src/commands/wallet/index.ts | 62 +++++++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/index.ts b/ironfish-cli/src/commands/wallet/index.ts index b54f416b46..ece1e6da28 100644 --- a/ironfish-cli/src/commands/wallet/index.ts +++ b/ironfish-cli/src/commands/wallet/index.ts @@ -1,36 +1,76 @@ /* 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 chalk from 'chalk' import { IronfishCommand } from '../../command' -import { RemoteFlags } from '../../flags' +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, - displayName: Flags.boolean({ - default: false, - description: `Display a hash of the account's read-only keys along with the account name`, - }), + ...JsonFlags, + ...ui.TableFlags, } - async start(): Promise { + async start(): Promise { const { flags } = await this.parse(AccountsCommand) const client = await this.connectRpc() - const response = await client.wallet.getAccounts({ displayName: flags.displayName }) + const response = await client.wallet.getAccountsStatus() if (response.content.accounts.length === 0) { this.log('you have no accounts') + return [] } - for (const name of response.content.accounts) { - this.log(name) - } + 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 } } From ab0847bed0f8c379e99e347dd31d8d9042977ed0 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:05:57 -0700 Subject: [PATCH 040/114] move args to be above flags in wallet command definitions (#5259) --- ironfish-cli/src/commands/wallet/address.ts | 8 +++---- ironfish-cli/src/commands/wallet/assets.ts | 14 ++++++------ ironfish-cli/src/commands/wallet/balance.ts | 14 ++++++------ ironfish-cli/src/commands/wallet/balances.ts | 14 ++++++------ ironfish-cli/src/commands/wallet/export.ts | 14 ++++++------ ironfish-cli/src/commands/wallet/import.ts | 14 ++++++------ .../src/commands/wallet/notes/index.ts | 14 ++++++------ ironfish-cli/src/commands/wallet/reset.ts | 14 ++++++------ .../src/commands/wallet/scanning/off.ts | 8 +++---- .../src/commands/wallet/scanning/on.ts | 8 +++---- .../src/commands/wallet/transactions.ts | 14 ++++++------ .../commands/wallet/transactions/import.ts | 14 ++++++------ .../src/commands/wallet/transactions/info.ts | 16 +++++++------- .../src/commands/wallet/transactions/post.ts | 12 +++++----- .../src/commands/wallet/transactions/watch.ts | 22 +++++++++---------- 15 files changed, 100 insertions(+), 100 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index e555c689e5..a1d760c7ea 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -10,10 +10,6 @@ export class AddressCommand extends IronfishCommand { The address for an account is the accounts public key, see more here: https://ironfish.network/docs/whitepaper/5_account` - static flags = { - ...RemoteFlags, - } - static args = { account: Args.string({ required: false, @@ -21,6 +17,10 @@ export class AddressCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(AddressCommand) const { account } = args diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index b21fcfb779..557829ca25 100644 --- a/ironfish-cli/src/commands/wallet/assets.ts +++ b/ironfish-cli/src/commands/wallet/assets.ts @@ -25,6 +25,13 @@ const MIN_ASSET_NAME_COLUMN_WIDTH = ASSET_NAME_LENGTH / 2 + 1 export class AssetsCommand extends IronfishCommand { static description = `Display the wallet's assets` + static args = { + account: Args.string({ + required: false, + description: 'Name of the account. DEPRECATED: use --account flag', + }), + } + static flags = { ...RemoteFlags, ...TableFlags, @@ -34,13 +41,6 @@ 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 diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index 4e74cf5800..f2bac956d7 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -15,6 +15,13 @@ export class BalanceCommand extends IronfishCommand { 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 args = { + account: Args.string({ + required: false, + description: 'Name of the account to get balance for. DEPRECATED: use --account flag', + }), + } + static flags = { ...RemoteFlags, account: Flags.string({ @@ -39,13 +46,6 @@ export class BalanceCommand extends IronfishCommand { }), } - 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 diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index ca96906f1f..8f11881e51 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -13,6 +13,13 @@ type AssetBalancePairs = { asset: RpcAsset; balance: GetBalancesResponse['balanc export class BalancesCommand extends IronfishCommand { static description = `Display the account's balances for all assets` + static args = { + account: Args.string({ + required: false, + description: 'Name of the account to get balances for. DEPRECATED: use --account flag', + }), + } + static flags = { ...RemoteFlags, ...TableFlags, @@ -30,13 +37,6 @@ export class BalancesCommand extends IronfishCommand { }), } - 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 client = await this.connectRpc() diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index ee194b5fe3..33f64ced8e 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -13,6 +13,13 @@ export class ExportCommand extends IronfishCommand { static description = `Export an account` static enableJsonFlag = true + static args = { + account: Args.string({ + required: false, + description: 'Name of the account to export', + }), + } + static flags = { ...RemoteFlags, ...JsonFlags, @@ -39,13 +46,6 @@ 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 { local, path: exportPath, viewonly: viewOnly } = flags diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 535e5eaa11..9a2e7ba478 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -17,6 +17,13 @@ import { Ledger } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { 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, rescan: Flags.boolean({ @@ -40,13 +47,6 @@ export class ImportCommand extends IronfishCommand { }), } - 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 diff --git a/ironfish-cli/src/commands/wallet/notes/index.ts b/ironfish-cli/src/commands/wallet/notes/index.ts index a00d62efce..ebc30d8a49 100644 --- a/ironfish-cli/src/commands/wallet/notes/index.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -12,6 +12,13 @@ const { sort: _, ...tableFlags } = TableFlags export class NotesCommand extends IronfishCommand { static description = `Display the account notes` + static args = { + account: Args.string({ + required: false, + description: 'Name of the account to get notes for. DEPRECATED: use --account flag', + }), + } + static flags = { ...RemoteFlags, ...tableFlags, @@ -21,13 +28,6 @@ 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 diff --git a/ironfish-cli/src/commands/wallet/reset.ts b/ironfish-cli/src/commands/wallet/reset.ts index b7e57e3e7c..6cf4cda02e 100644 --- a/ironfish-cli/src/commands/wallet/reset.ts +++ b/ironfish-cli/src/commands/wallet/reset.ts @@ -10,6 +10,13 @@ import { confirmOrQuit } from '../../ui' export class ResetCommand extends IronfishCommand { static description = `Resets the transaction of an account but keeps all keys.` + static args = { + account: Args.string({ + required: true, + description: 'Name of the account to reset', + }), + } + static flags = { ...RemoteFlags, resetCreated: Flags.boolean({ @@ -26,13 +33,6 @@ 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 diff --git a/ironfish-cli/src/commands/wallet/scanning/off.ts b/ironfish-cli/src/commands/wallet/scanning/off.ts index 7dc843fd31..bfe9680d8f 100644 --- a/ironfish-cli/src/commands/wallet/scanning/off.ts +++ b/ironfish-cli/src/commands/wallet/scanning/off.ts @@ -8,10 +8,6 @@ import { RemoteFlags } from '../../../flags' 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 flags = { - ...RemoteFlags, - } - static args = { account: Args.string({ required: true, @@ -19,6 +15,10 @@ export class ScanningOffCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(ScanningOffCommand) const { account } = args diff --git a/ironfish-cli/src/commands/wallet/scanning/on.ts b/ironfish-cli/src/commands/wallet/scanning/on.ts index 58339bdd66..bb08d3de74 100644 --- a/ironfish-cli/src/commands/wallet/scanning/on.ts +++ b/ironfish-cli/src/commands/wallet/scanning/on.ts @@ -8,10 +8,6 @@ import { RemoteFlags } from '../../../flags' 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 flags = { - ...RemoteFlags, - } - static args = { account: Args.string({ required: true, @@ -19,6 +15,10 @@ export class ScanningOnCommand extends IronfishCommand { }), } + static flags = { + ...RemoteFlags, + } + async start(): Promise { const { args } = await this.parse(ScanningOnCommand) const { account } = args diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 49cff44e0d..95fecf79db 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -22,6 +22,13 @@ const { sort: _, ...tableFlags } = TableFlags export class TransactionsCommand extends IronfishCommand { static description = `Display the account transactions` + static args = { + account: Args.string({ + required: false, + description: 'Name of the account. DEPRECATED: use --account flag', + }), + } + static flags = { ...RemoteFlags, ...tableFlags, @@ -52,13 +59,6 @@ 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 diff --git a/ironfish-cli/src/commands/wallet/transactions/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts index 12042d83f3..0b83a4797e 100644 --- a/ironfish-cli/src/commands/wallet/transactions/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -11,6 +11,13 @@ export class TransactionsImportCommand extends IronfishCommand { static hiddenAliases = ['wallet:transaction:add', 'wallet:transaction:import'] + static args = { + transaction: Args.string({ + required: false, + description: 'The transaction in hex encoding', + }), + } + static flags = { ...RemoteFlags, path: Flags.string({ @@ -23,13 +30,6 @@ export class TransactionsImportCommand extends IronfishCommand { }), } - static args = { - transaction: Args.string({ - required: false, - description: 'The transaction in hex encoding', - }), - } - async start(): Promise { const { flags, args } = await this.parse(TransactionsImportCommand) const { transaction: txArg } = args diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 5c6ab2550c..58295989d1 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -26,14 +26,6 @@ export class TransactionInfoCommand extends IronfishCommand { static hiddenAliases = ['wallet:transaction'] - static flags = { - ...RemoteFlags, - account: Flags.string({ - char: 'a', - description: 'Name of the account to get transaction details for', - }), - } - static args = { hash: Args.string({ required: true, @@ -45,6 +37,14 @@ export class TransactionInfoCommand extends IronfishCommand { }), } + 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(TransactionInfoCommand) const { hash } = args diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index db86c5c69a..4f5c184891 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -21,6 +21,12 @@ export class TransactionsPostCommand extends IronfishCommand { static hiddenAliases = ['wallet:post'] + static args = { + transaction: Args.string({ + description: 'The raw transaction in hex encoding', + }), + } + static flags = { ...RemoteFlags, account: Flags.string({ @@ -40,12 +46,6 @@ export class TransactionsPostCommand extends IronfishCommand { }), } - static args = { - transaction: Args.string({ - description: 'The raw transaction in hex encoding', - }), - } - async start(): Promise { const { flags, args } = await this.parse(TransactionsPostCommand) let transaction = args.transaction diff --git a/ironfish-cli/src/commands/wallet/transactions/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts index 160d2b9374..9f4e567301 100644 --- a/ironfish-cli/src/commands/wallet/transactions/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -10,6 +10,17 @@ export class TransactionsWatchCommand extends IronfishCommand { static description = `Wait for the status of an account transaction to confirm or expire` static hiddenAliases = ['wallet:transaction:watch'] + 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', + }), + } + static flags = { ...RemoteFlags, account: Flags.string({ @@ -22,17 +33,6 @@ export class TransactionsWatchCommand extends IronfishCommand { }), } - 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(TransactionsWatchCommand) const { hash } = args From 3a452dae1009114d0158320318d5c6eacbe63f2b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:30:37 -0400 Subject: [PATCH 041/114] feat(ironfish): Add encrypt method to wallet (#5248) --- .../__fixtures__/wallet.test.ts.fixture | 60 +++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 28 +++++++++ ironfish/src/wallet/wallet.ts | 22 ++++++- .../__fixtures__/walletdb.test.ts.fixture | 60 +++++++++++++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 36 +++++++++++ ironfish/src/wallet/walletdb/walletdb.ts | 13 ++++ 6 files changed, 217 insertions(+), 2 deletions(-) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 53339cb8d4..5e6c1465ab 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -6898,5 +6898,65 @@ } ] } + ], + "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 71e3357ad8..ca148b0983 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -2374,4 +2374,32 @@ 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 encryptedAccountA = node.wallet.encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = node.wallet.encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index aded0a79d4..88a08fd27f 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -95,7 +95,7 @@ export class Wallet { readonly onAccountRemoved = new Event<[account: Account]>() protected readonly accountById = new Map() - protected readonly encryptedAccounts = new Map() + readonly encryptedAccountById = new Map() readonly walletDb: WalletDB private readonly logger: Logger readonly workerPool: WorkerPool @@ -210,13 +210,16 @@ export class Wallet { } private async load(): Promise { + this.encryptedAccountById.clear() + this.accountById.clear() + for await (const [id, accountValue] of this.walletDb.loadAccounts()) { if (accountValue.encrypted) { const encryptedAccount = new EncryptedAccount({ data: accountValue.data, walletDb: this.walletDb, }) - this.encryptedAccounts.set(id, encryptedAccount) + this.encryptedAccountById.set(id, encryptedAccount) } else { const account = new Account({ accountValue, walletDb: this.walletDb }) this.accountById.set(account.id, account) @@ -1435,6 +1438,10 @@ export class Wallet { return Array.from(this.accountById.values()) } + get encryptedAccounts(): EncryptedAccount[] { + return Array.from(this.encryptedAccountById.values()) + } + accountExists(name: string): boolean { return this.getAccountByName(name) !== null } @@ -1767,4 +1774,15 @@ export class Wallet { return identity.serialize() }) } + + async encrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + await this.walletDb.encryptAccounts(this.accounts, passphrase, tx) + await this.load() + } finally { + unlock() + } + } } diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index 36fc19d4c0..bd287fc1ba 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -790,5 +790,65 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 90dc863fe7..00632d7b40 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -11,6 +11,7 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils' +import { EncryptedAccount } from '../account/encryptedAccount' import { DecryptedNoteValue } from './decryptedNoteValue' describe('WalletDB', () => { @@ -456,4 +457,39 @@ describe('WalletDB', () => { expect(storedSecret.secret).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([accountA, accountB], 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({ data: accountValue.data, walletDb }), + ) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) + + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 7d379922c9..54a95904f3 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1188,6 +1188,19 @@ export class WalletDB { } } + async encryptAccounts( + accounts: Account[], + passphrase: string, + tx?: IDatabaseTransaction, + ): Promise { + await this.db.withTransaction(tx, async (tx) => { + for (const account of accounts) { + const encryptedAccount = account.encrypt(passphrase) + await this.accounts.put(account.id, encryptedAccount.serialize(), tx) + } + }) + } + async *loadTransactionsByTime( account: Account, tx?: IDatabaseTransaction, From a33e3eb49469b6b5429a013c5bf97f0460525dce Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:56:51 -0400 Subject: [PATCH 042/114] feat(ironfish): Add decrypt method to wallet (#5266) --- .../__fixtures__/wallet.test.ts.fixture | 120 ++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 47 +++++++ ironfish/src/wallet/wallet.ts | 13 +- .../__fixtures__/walletdb.test.ts.fixture | 120 ++++++++++++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 83 ++++++++++++ ironfish/src/wallet/walletdb/walletdb.ts | 14 ++ 6 files changed, 396 insertions(+), 1 deletion(-) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 5e6c1465ab..b764b39ee8 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -6958,5 +6958,125 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index ca148b0983..7e64b23c29 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -23,6 +23,7 @@ import { import { AsyncUtils, BufferUtils, ORE_TO_IRON } from '../utils' import { Account, TransactionStatus, TransactionType } from '../wallet' import { + AccountDecryptionFailedError, DuplicateAccountNameError, DuplicateSpendingKeyError, MaxMemoLengthError, @@ -2402,4 +2403,50 @@ describe('Wallet', () => { 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) + }) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 88a08fd27f..5aaaf25bc7 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -94,7 +94,7 @@ 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 @@ -1785,4 +1785,15 @@ export class Wallet { unlock() } } + + async decrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + await this.walletDb.decryptAccounts(this.encryptedAccounts, passphrase, tx) + await this.load() + } finally { + unlock() + } + } } diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index bd287fc1ba..b73d3d92ae 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -850,5 +850,125 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 00632d7b40..38adb0412f 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -11,7 +11,9 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils' +import { Account } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' +import { AccountDecryptionFailedError } from '../errors' import { DecryptedNoteValue } from './decryptedNoteValue' describe('WalletDB', () => { @@ -492,4 +494,85 @@ describe('WalletDB', () => { 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([accountA, accountB], 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({ data: accountValue.data, walletDb }), + ) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + + await walletDb.decryptAccounts([encryptedAccountA, encryptedAccountB], 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([accountA, accountB], 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({ data: accountValue.data, walletDb }), + ) + } + + const encryptedAccountA = encryptedAccountById.get(accountA.id) + Assert.isNotUndefined(encryptedAccountA) + const encryptedAccountB = encryptedAccountById.get(accountB.id) + Assert.isNotUndefined(encryptedAccountB) + + await expect( + walletDb.decryptAccounts([encryptedAccountA, encryptedAccountB], invalidPassphrase), + ).rejects.toThrow(AccountDecryptionFailedError) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 54a95904f3..dedc30852b 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -32,6 +32,7 @@ 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 { AccountValue, AccountValueEncoding } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' @@ -1201,6 +1202,19 @@ export class WalletDB { }) } + async decryptAccounts( + encryptedAccounts: EncryptedAccount[], + passphrase: string, + tx?: IDatabaseTransaction, + ): Promise { + await this.db.withTransaction(tx, async (tx) => { + for (const encryptedAccount of encryptedAccounts) { + const account = encryptedAccount.decrypt(passphrase) + await this.accounts.put(account.id, account.serialize(), tx) + } + }) + } + async *loadTransactionsByTime( account: Account, tx?: IDatabaseTransaction, From 0aab27ee5b743b77bc816035caca2c8eb22029a3 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:09:17 -0700 Subject: [PATCH 043/114] Add the ability to unset default account in CLI (#5263) You can now use `wallet:use --unset` to unset the default account --- ironfish-cli/src/commands/wallet/use.ts | 19 +++++++++++++++---- ironfish/src/rpc/routes/wallet/useAccount.ts | 12 ++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/use.ts b/ironfish-cli/src/commands/wallet/use.ts index 165f8d6c60..de1b4498f6 100644 --- a/ironfish-cli/src/commands/wallet/use.ts +++ b/ironfish-cli/src/commands/wallet/use.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 { Args } from '@oclif/core' +import { Args, Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' @@ -10,21 +10,32 @@ export class UseCommand extends IronfishCommand { 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 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/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() }, ) From df09affad2f102472dca47993f64923f5bc635b1 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:32:57 -0700 Subject: [PATCH 044/114] fix `wallet:which --displayName` output (#5272) Previously, the RPC wasn't receiving this parameter properly due to it being left out of the yup schema. Add it to the schema so that the flag is properly passed through. --- ironfish/src/rpc/routes/wallet/getAccounts.ts | 1 + 1 file changed, 1 insertion(+) 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({}) From bed90c6b36ce24e77e85bf27fb4579e387cd93d6 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:33:14 -0700 Subject: [PATCH 045/114] Update wallet command flag description for account (#5273) This makes it more clear that it expects a name of an account --- ironfish-cli/src/commands/wallet/burn.ts | 2 +- ironfish-cli/src/commands/wallet/chainport/send.ts | 2 +- ironfish-cli/src/commands/wallet/import.ts | 8 ++++---- ironfish-cli/src/commands/wallet/mint.ts | 2 +- .../src/commands/wallet/multisig/account/participants.ts | 2 +- .../src/commands/wallet/multisig/commitment/aggregate.ts | 2 +- .../src/commands/wallet/multisig/commitment/create.ts | 2 +- .../src/commands/wallet/multisig/signature/aggregate.ts | 2 +- .../src/commands/wallet/multisig/signature/create.ts | 2 +- ironfish-cli/src/commands/wallet/notes/combine.ts | 2 +- ironfish-cli/src/commands/wallet/send.ts | 2 +- ironfish-cli/src/commands/wallet/transactions/decode.ts | 2 +- ironfish-cli/src/commands/wallet/transactions/post.ts | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 5827df6420..f6e326a42c 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -34,7 +34,7 @@ export class Burn extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to burn from', + description: 'Name of the account to burn from', }), fee: IronFlag({ char: 'o', diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index b613651778..11e6d65b77 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -45,7 +45,7 @@ export class BridgeCommand extends IronfishCommand { }), account: Flags.string({ char: 'f', - description: 'The account to send the asset from', + description: 'Name of the account to send the asset from', }), to: Flags.string({ char: 't', diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 9a2e7ba478..3105057b9d 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -32,16 +32,16 @@ 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'], }), @@ -61,7 +61,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`, diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 0557493128..2c40ea30fe 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -40,7 +40,7 @@ export class Mint extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to mint from', + description: 'Name of the account to mint from', }), fee: IronFlag({ char: 'o', diff --git a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts index eca3361fc9..70d856010f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -12,7 +12,7 @@ export class MultisigAccountParticipants extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to list group identities for', + description: 'Name of the account to list group identities for', }), } diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index f09021af7c..0ec4cb5b18 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -15,7 +15,7 @@ export class CreateSigningPackage extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to use when creating the signing package', + description: 'Name of the account to use when creating the signing package', required: false, }), unsignedTransaction: Flags.string({ diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index fff272cd81..2cbbc76e65 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -18,7 +18,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { account: Flags.string({ char: 'f', description: - 'The account to use for generating the commitment, must be a multisig participant account', + 'Name of the account to use for generating the commitment, must be a multisig participant account', required: false, }), unsignedTransaction: Flags.string({ diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 7d22da5913..18f23b058e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -16,7 +16,7 @@ export class MultisigSign extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'Account to use when aggregating signature shares', + description: 'Name of the account to use when aggregating signature shares', required: false, }), signingPackage: Flags.string({ diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 39812cec25..820f842ba6 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -18,7 +18,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account from which the signature share will be created', + description: 'Name of the account from which the signature share will be created', required: false, }), signingPackage: Flags.string({ diff --git a/ironfish-cli/src/commands/wallet/notes/combine.ts b/ironfish-cli/src/commands/wallet/notes/combine.ts index c48a31dd61..ccbda96c7b 100644 --- a/ironfish-cli/src/commands/wallet/notes/combine.ts +++ b/ironfish-cli/src/commands/wallet/notes/combine.ts @@ -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, diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index fb442012be..00068f2247 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -42,7 +42,7 @@ export class Send extends IronfishCommand { ...RemoteFlags, account: Flags.string({ char: 'f', - description: 'The account to send money from', + description: 'Name of the account to send money from', }), amount: ValueFlag({ char: 'a', diff --git a/ironfish-cli/src/commands/wallet/transactions/decode.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts index baafd0f8b2..f6b31775e3 100644 --- a/ironfish-cli/src/commands/wallet/transactions/decode.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -28,7 +28,7 @@ export class TransactionsDecodeCommand extends IronfishCommand { ...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', diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index 4f5c184891..b956ad64e4 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -30,7 +30,7 @@ export class TransactionsPostCommand extends IronfishCommand { 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, From 67b47b20518e0375464c740ba9b987fb3df9ce76 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Wed, 14 Aug 2024 16:46:49 -0700 Subject: [PATCH 046/114] Change wallet:status to output aggregation (#5275) --- ironfish-cli/src/commands/wallet/status.ts | 95 +++++++++++----------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index ad53d7d637..48d649bacb 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -1,63 +1,66 @@ /* 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 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() - 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, + } } } From 500abe5b067d88915a66590bf76a62164a5c395f Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:56:31 -0400 Subject: [PATCH 047/114] feat(ironfish): Add lock to wallet (#5270) * feat(ironfish): Add lock to wallet * test(ironfish): Add test for no accounts --- .../__fixtures__/wallet.test.ts.fixture | 120 ++++++++++++ ironfish/src/wallet/wallet.test.ts | 45 +++++ ironfish/src/wallet/wallet.ts | 23 +++ .../__fixtures__/walletdb.test.ts.fixture | 180 ++++++++++++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 44 +++++ ironfish/src/wallet/walletdb/walletdb.ts | 15 ++ 6 files changed, 427 insertions(+) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index b764b39ee8..82463c6aa4 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -7078,5 +7078,125 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 7e64b23c29..f6205a52a7 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -2449,4 +2449,49 @@ describe('Wallet', () => { 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') + + // TODO(rohanjadvani) + // This is temporary for a unit test to keep PRs small. + // This will be refactored once unlock comes in a subsequent change. + // The goal is to mock an unlocked state by copying and setting + // decrypted accounts within the wallet. + const accountById = new Map(node.wallet.accountById.entries()) + + await node.wallet.encrypt(passphrase) + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.encryptedAccounts).toHaveLength(2) + + // Mock unlock until the method is implemented + node.wallet.locked = false + for (const [k, v] of accountById.entries()) { + node.wallet.accountById.set(k, v) + } + expect(node.wallet.accounts).toHaveLength(2) + + await node.wallet.lock() + expect(node.wallet.accounts).toHaveLength(0) + expect(node.wallet.locked).toBe(true) + }) + }) }) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 5aaaf25bc7..256c2581bd 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -110,6 +110,7 @@ export class Wallet { protected isStarted = false protected isOpen = false protected isSyncingTransactionGossip = false + locked: boolean protected eventLoopTimeout: SetTimeoutToken | null = null private readonly createTransactionMutex: Mutex private readonly eventLoopAbortController: AbortController @@ -144,6 +145,7 @@ export class Wallet { this.networkId = networkId this.nodeClient = nodeClient || null this.rebroadcastAfter = rebroadcastAfter ?? 10 + this.locked = false this.createTransactionMutex = new Mutex() this.eventLoopAbortController = new AbortController() @@ -220,9 +222,13 @@ export class Wallet { walletDb: this.walletDb, }) 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 } } @@ -231,6 +237,7 @@ export class Wallet { } private unload(): void { + this.encryptedAccountById.clear() this.accountById.clear() this.defaultAccount = null @@ -1796,4 +1803,20 @@ export class Wallet { unlock() } } + + async lock(tx?: IDatabaseTransaction): Promise { + const unlock = await this.createTransactionMutex.lock() + + try { + const encrypted = await this.walletDb.accountsEncrypted(tx) + if (!encrypted) { + return + } + + this.accountById.clear() + this.locked = true + } finally { + unlock() + } + } } diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index b73d3d92ae..ba3e16aea8 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -970,5 +970,185 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 38adb0412f..9d0526bedd 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -575,4 +575,48 @@ describe('WalletDB', () => { ).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') + + await walletDb.accounts.put(accountB.id, accountB.encrypt(passphrase).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' + + const accountA = await useAccountFixture(node.wallet, 'A') + const accountB = await useAccountFixture(node.wallet, 'B') + await walletDb.encryptAccounts([accountA, accountB], 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) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index dedc30852b..d9d432ea26 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1215,6 +1215,21 @@ export class WalletDB { }) } + 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, From 3f1243d49f703bf045759d0bf12ca58baa6811cf Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:55:02 -0700 Subject: [PATCH 048/114] clarify arg/flag names to better indicate what resource it is acting on (#5274) --- ironfish-cli/src/commands/wallet/create.ts | 4 ++-- ironfish-cli/src/commands/wallet/transactions.ts | 5 +++-- ironfish-cli/src/commands/wallet/transactions/info.ts | 4 ++-- ironfish-cli/src/commands/wallet/transactions/post.ts | 4 ++-- ironfish-cli/src/commands/wallet/transactions/watch.ts | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index fcb9758c6c..2ca08b8a94 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -11,7 +11,7 @@ export class CreateCommand extends IronfishCommand { static description = `Create a new account for sending and receiving coins` static args = { - account: Args.string({ + name: Args.string({ required: false, description: 'Name of the account', }), @@ -23,7 +23,7 @@ export class CreateCommand extends IronfishCommand { async start(): Promise { const { args } = await this.parse(CreateCommand) - let name = args.account + let name = args.name if (!name) { name = await inputPrompt('Enter the name of the account', true) diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 95fecf79db..1f8f3521df 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -36,8 +36,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({ @@ -77,7 +78,7 @@ export class TransactionsCommand extends IronfishCommand { 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/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 58295989d1..1d7fc42238 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -27,7 +27,7 @@ export class TransactionInfoCommand extends IronfishCommand { static hiddenAliases = ['wallet:transaction'] static args = { - hash: Args.string({ + transaction: Args.string({ required: true, description: 'Hash of the transaction', }), @@ -47,7 +47,7 @@ export class TransactionInfoCommand extends IronfishCommand { async start(): Promise { const { flags, args } = await this.parse(TransactionInfoCommand) - const { hash } = args + const { transaction: hash } = args // TODO: remove account arg const account = flags.account ? flags.account : args.account diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index b956ad64e4..ce636d9da9 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -22,7 +22,7 @@ export class TransactionsPostCommand extends IronfishCommand { static hiddenAliases = ['wallet:post'] static args = { - transaction: Args.string({ + raw_transaction: Args.string({ description: 'The raw transaction in hex encoding', }), } @@ -48,7 +48,7 @@ export class TransactionsPostCommand extends IronfishCommand { async start(): Promise { const { flags, args } = await this.parse(TransactionsPostCommand) - let transaction = args.transaction + let transaction = args.raw_transaction if (!transaction) { transaction = await longPrompt('Enter the raw transaction in hex encoding', { diff --git a/ironfish-cli/src/commands/wallet/transactions/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts index 9f4e567301..29438c4f64 100644 --- a/ironfish-cli/src/commands/wallet/transactions/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -11,7 +11,7 @@ export class TransactionsWatchCommand extends IronfishCommand { static hiddenAliases = ['wallet:transaction:watch'] static args = { - hash: Args.string({ + transaction: Args.string({ required: true, description: 'Hash of the transaction', }), @@ -35,7 +35,7 @@ export class TransactionsWatchCommand extends IronfishCommand { async start(): Promise { const { flags, args } = await this.parse(TransactionsWatchCommand) - const { hash } = args + const { transaction: hash } = args // TODO: remove account arg const account = flags.account ? flags.account : args.account From b92d6c150b3b77f7484a97c5ffee113aaca31d89 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 15 Aug 2024 12:07:03 -0700 Subject: [PATCH 049/114] Update wallet command descriptions (#5277) --- ironfish-cli/package.json | 18 ++++++++++++--- ironfish-cli/src/commands/wallet/address.ts | 2 +- ironfish-cli/src/commands/wallet/assets.ts | 2 +- ironfish-cli/src/commands/wallet/balance.ts | 22 ++++++++++++++----- ironfish-cli/src/commands/wallet/balances.ts | 2 +- ironfish-cli/src/commands/wallet/burn.ts | 4 +++- ironfish-cli/src/commands/wallet/create.ts | 2 +- ironfish-cli/src/commands/wallet/delete.ts | 2 +- ironfish-cli/src/commands/wallet/export.ts | 2 +- ironfish-cli/src/commands/wallet/import.ts | 2 +- ironfish-cli/src/commands/wallet/mint.ts | 4 +++- .../src/commands/wallet/notes/index.ts | 2 +- ironfish-cli/src/commands/wallet/prune.ts | 2 +- ironfish-cli/src/commands/wallet/rename.ts | 2 +- ironfish-cli/src/commands/wallet/rescan.ts | 2 +- ironfish-cli/src/commands/wallet/reset.ts | 2 +- .../src/commands/wallet/scanning/off.ts | 4 +++- .../src/commands/wallet/scanning/on.ts | 4 +++- ironfish-cli/src/commands/wallet/send.ts | 2 +- ironfish-cli/src/commands/wallet/status.ts | 2 +- .../src/commands/wallet/transactions.ts | 2 +- .../commands/wallet/transactions/decode.ts | 2 +- .../commands/wallet/transactions/import.ts | 2 +- .../src/commands/wallet/transactions/info.ts | 2 +- .../src/commands/wallet/transactions/post.ts | 2 +- .../src/commands/wallet/transactions/sign.ts | 2 +- .../src/commands/wallet/transactions/watch.ts | 2 +- ironfish-cli/src/commands/wallet/use.ts | 2 +- ironfish-cli/src/commands/wallet/which.ts | 2 +- 29 files changed, 67 insertions(+), 35 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 83fa0ada4b..b7fed0ff61 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -100,9 +100,6 @@ "@oclif/plugin-warn-if-update-available" ], "topics": { - "wallet:scanning": { - "description": "Turn on or off scanning for accounts" - }, "chain": { "description": "commands for the blockchain" }, @@ -112,6 +109,9 @@ "chain:assets": { "description": "commands to look at assets" }, + "chain:transactions": { + "description": "commands to look at transactions" + }, "rpc": { "description": "commands for the RPC server" }, @@ -124,6 +124,18 @@ "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/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index a1d760c7ea..8815793fea 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' 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` diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index 557829ca25..96f935d5b1 100644 --- a/ironfish-cli/src/commands/wallet/assets.ts +++ b/ironfish-cli/src/commands/wallet/assets.ts @@ -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 args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index f2bac956d7..213ba1343c 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -9,11 +9,23 @@ import * as ui from '../../ui' import { renderAssetWithVerificationStatus } 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 args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index 8f11881e51..b5eeba3e65 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -11,7 +11,7 @@ import { compareAssets, renderAssetWithVerificationStatus } 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 args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index f6e326a42c..60fee2ae71 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -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', diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index 2ca08b8a94..003811ec63 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { 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 = { name: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/delete.ts b/ironfish-cli/src/commands/wallet/delete.ts index 7aa7569a54..ca67f077fe 100644 --- a/ironfish-cli/src/commands/wallet/delete.ts +++ b/ironfish-cli/src/commands/wallet/delete.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { inputPrompt } from '../../ui' export class DeleteCommand extends IronfishCommand { - static description = `Permanently delete an account` + static description = `delete an account` static args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 33f64ced8e..4a47898fd7 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -10,7 +10,7 @@ import { EnumLanguageKeyFlag, JsonFlags, RemoteFlags } from '../../flags' import { confirmOrQuit } from '../../ui' export class ExportCommand extends IronfishCommand { - static description = `Export an account` + static description = `export an account` static enableJsonFlag = true static args = { diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 3105057b9d..4d40d125d1 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -15,7 +15,7 @@ import { importFile, importPipe, longPrompt } from '../../utils/input' 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({ diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 2c40ea30fe..f753823560 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -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', diff --git a/ironfish-cli/src/commands/wallet/notes/index.ts b/ironfish-cli/src/commands/wallet/notes/index.ts index ebc30d8a49..09ac835d01 100644 --- a/ironfish-cli/src/commands/wallet/notes/index.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -10,7 +10,7 @@ import { TableCols } from '../../../utils/table' const { sort: _, ...tableFlags } = TableFlags export class NotesCommand extends IronfishCommand { - static description = `Display the account notes` + static description = `list the account's notes` static args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/prune.ts b/ironfish-cli/src/commands/wallet/prune.ts index 9347b935a1..113b16e977 100644 --- a/ironfish-cli/src/commands/wallet/prune.ts +++ b/ironfish-cli/src/commands/wallet/prune.ts @@ -6,7 +6,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' 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({ diff --git a/ironfish-cli/src/commands/wallet/rename.ts b/ironfish-cli/src/commands/wallet/rename.ts index 224913fe3c..cdb2d53b32 100644 --- a/ironfish-cli/src/commands/wallet/rename.ts +++ b/ironfish-cli/src/commands/wallet/rename.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' 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({ diff --git a/ironfish-cli/src/commands/wallet/rescan.ts b/ironfish-cli/src/commands/wallet/rescan.ts index 3761dbb22c..f8cc223a9a 100644 --- a/ironfish-cli/src/commands/wallet/rescan.ts +++ b/ironfish-cli/src/commands/wallet/rescan.ts @@ -10,7 +10,7 @@ import { 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, diff --git a/ironfish-cli/src/commands/wallet/reset.ts b/ironfish-cli/src/commands/wallet/reset.ts index 6cf4cda02e..addbf11f93 100644 --- a/ironfish-cli/src/commands/wallet/reset.ts +++ b/ironfish-cli/src/commands/wallet/reset.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { confirmOrQuit } from '../../ui' 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 args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/scanning/off.ts b/ironfish-cli/src/commands/wallet/scanning/off.ts index bfe9680d8f..63825620eb 100644 --- a/ironfish-cli/src/commands/wallet/scanning/off.ts +++ b/ironfish-cli/src/commands/wallet/scanning/off.ts @@ -6,7 +6,9 @@ import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' 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 + +The wallet will no longer scan the blockchain for new account transactions.` static args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/scanning/on.ts b/ironfish-cli/src/commands/wallet/scanning/on.ts index bb08d3de74..028be4495a 100644 --- a/ironfish-cli/src/commands/wallet/scanning/on.ts +++ b/ironfish-cli/src/commands/wallet/scanning/on.ts @@ -6,7 +6,9 @@ import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' 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 + +Scanning is on by default. The wallet will scan the blockchain for new account transactions.` static args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 00068f2247..334d3793ef 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -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', diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index 48d649bacb..9a9bebaad0 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -7,7 +7,7 @@ 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 = { diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 1f8f3521df..1cfc85eb15 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -20,7 +20,7 @@ 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 args = { account: Args.string({ diff --git a/ironfish-cli/src/commands/wallet/transactions/decode.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts index f6b31775e3..1186d5d018 100644 --- a/ironfish-cli/src/commands/wallet/transactions/decode.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -21,7 +21,7 @@ import { } from '../../../utils/transaction' export class TransactionsDecodeCommand extends IronfishCommand { - static description = `View transaction details` + static description = `show an encoded transaction's details` static hiddenAliases = ['wallet:transaction:view'] static flags = { diff --git a/ironfish-cli/src/commands/wallet/transactions/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts index 0b83a4797e..cf8e34e663 100644 --- a/ironfish-cli/src/commands/wallet/transactions/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -7,7 +7,7 @@ import { RemoteFlags } from '../../../flags' import { importFile, importPipe, longPrompt } from '../../../utils/input' export class TransactionsImportCommand extends IronfishCommand { - static description = `Import a transaction into your wallet` + static description = `import a transaction into the wallet` static hiddenAliases = ['wallet:transaction:add', 'wallet:transaction:import'] diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 1d7fc42238..8b45b65595 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -22,7 +22,7 @@ import { import { getExplorer } from '../../../utils/explorer' export class TransactionInfoCommand extends IronfishCommand { - static description = `Display an account transaction` + static description = `show an account transaction's info` static hiddenAliases = ['wallet:transaction'] diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index ce636d9da9..1a800717e2 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -10,7 +10,7 @@ import { longPrompt } from '../../../utils/input' import { renderRawTransactionDetails } from '../../../utils/transaction' export class TransactionsPostCommand extends IronfishCommand { - static summary = 'Post a raw transaction' + static summary = 'post a raw transaction' static description = `Use this command to post a raw transaction. The output is a finalized posted transaction.` diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 640c61bce7..1d52dcfa41 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -11,7 +11,7 @@ import { Ledger } from '../../../utils/ledger' import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' export class TransactionsSignCommand extends IronfishCommand { - static description = `Sign an unsigned transaction` + static description = `sign an unsigned transaction` static hiddenAliases = ['wallet:sign'] diff --git a/ironfish-cli/src/commands/wallet/transactions/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts index 29438c4f64..e874f216b7 100644 --- a/ironfish-cli/src/commands/wallet/transactions/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -7,7 +7,7 @@ import { RemoteFlags } from '../../../flags' import { watchTransaction } from '../../../utils/transaction' export class TransactionsWatchCommand extends IronfishCommand { - static description = `Wait for the status of an account transaction to confirm or expire` + static description = `wait for an account transaction to confirm` static hiddenAliases = ['wallet:transaction:watch'] static args = { diff --git a/ironfish-cli/src/commands/wallet/use.ts b/ironfish-cli/src/commands/wallet/use.ts index de1b4498f6..ea00f39d8c 100644 --- a/ironfish-cli/src/commands/wallet/use.ts +++ b/ironfish-cli/src/commands/wallet/use.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' 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({ diff --git a/ironfish-cli/src/commands/wallet/which.ts b/ironfish-cli/src/commands/wallet/which.ts index 3317328aec..e96eb855b3 100644 --- a/ironfish-cli/src/commands/wallet/which.ts +++ b/ironfish-cli/src/commands/wallet/which.ts @@ -6,7 +6,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' 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 From c0cf9af879ac2abc7569ed87ec25a68218723203 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:12:40 -0700 Subject: [PATCH 050/114] Add `wallet:transactions:delete` command (#5276) * Add `wallet:transactions:delete` command * remove remoteflags --- .../commands/wallet/transactions/delete.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/transactions/delete.ts 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..f26e54417a --- /dev/null +++ b/ironfish-cli/src/commands/wallet/transactions/delete.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 { NodeUtils, TransactionStatus } from '@ironfish/sdk' +import { Args, ux } from '@oclif/core' +import { IronfishCommand } from '../../../command' + +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 + + ux.action.start('Opening node') + const node = await this.sdk.node() + await NodeUtils.waitForOpen(node) + ux.action.stop('Done.') + + const accounts = node.wallet.accounts + const transactionHash = Buffer.from(transaction, 'hex') + let deleted = false + + for (const account of accounts) { + const transactionValue = await account.getTransaction(transactionHash) + + if (transactionValue == null) { + continue + } + + const transactionStatus = await node.wallet.getTransactionStatus( + account, + transactionValue, + ) + + if ( + transactionStatus === TransactionStatus.CONFIRMED || + transactionStatus === TransactionStatus.UNCONFIRMED + ) { + this.error(`Transaction ${transaction} is already on a block, so it cannot be deleted`) + } + + if ( + transactionStatus === TransactionStatus.EXPIRED || + transactionStatus === TransactionStatus.PENDING + ) { + await account.deleteTransaction(transactionValue.transaction) + deleted = true + } + } + + if (deleted) { + this.log(`Transaction ${transaction} deleted from wallet`) + } else { + this.log(`No transaction with hash ${transaction} found in wallet`) + } + } +} From cf812ef0c32487e1dfc9e65d3cce9b8ab4f1d867 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:41:31 -0400 Subject: [PATCH 051/114] feat(ironfish): Add unlock to wallet (#5281) * feat(ironfish): Add unlock to wallet * feat(ironfish): Relock if unlock throws an error --- .../__fixtures__/wallet.test.ts.fixture | 180 ++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 75 ++++++-- ironfish/src/wallet/wallet.ts | 56 ++++++ ironfish/src/wallet/walletdb/accountValue.ts | 2 +- 4 files changed, 300 insertions(+), 13 deletions(-) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 82463c6aa4..e6f96e3967 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -7198,5 +7198,185 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index f6205a52a7..3ec5a3949e 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -2471,22 +2471,11 @@ describe('Wallet', () => { await useAccountFixture(node.wallet, 'A') await useAccountFixture(node.wallet, 'B') - // TODO(rohanjadvani) - // This is temporary for a unit test to keep PRs small. - // This will be refactored once unlock comes in a subsequent change. - // The goal is to mock an unlocked state by copying and setting - // decrypted accounts within the wallet. - const accountById = new Map(node.wallet.accountById.entries()) - await node.wallet.encrypt(passphrase) expect(node.wallet.accounts).toHaveLength(0) expect(node.wallet.encryptedAccounts).toHaveLength(2) - // Mock unlock until the method is implemented - node.wallet.locked = false - for (const [k, v] of accountById.entries()) { - node.wallet.accountById.set(k, v) - } + await node.wallet.unlock(passphrase) expect(node.wallet.accounts).toHaveLength(2) await node.wallet.lock() @@ -2494,4 +2483,66 @@ describe('Wallet', () => { 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) + + for (const [id, account] of node.wallet.accountById.entries()) { + const encryptedAccount = node.wallet.encryptedAccountById.get(id) + Assert.isNotUndefined(encryptedAccount) + const decryptedAccount = encryptedAccount.decrypt(passphrase) + + 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 256c2581bd..a978edb6ef 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -90,6 +90,8 @@ export type TransactionOutput = { assetId: Buffer } +export const DEFAULT_UNLOCK_TIMEOUT_MS = 5 * 60 * 1000 + export class Wallet { readonly onAccountImported = new Event<[account: Account]>() readonly onAccountRemoved = new Event<[account: Account]>() @@ -112,6 +114,7 @@ export class Wallet { 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 @@ -146,6 +149,7 @@ export class Wallet { this.nodeClient = nodeClient || null this.rebroadcastAfter = rebroadcastAfter ?? 10 this.locked = false + this.lockTimeout = null this.createTransactionMutex = new Mutex() this.eventLoopAbortController = new AbortController() @@ -271,6 +275,8 @@ export class Wallet { clearTimeout(this.eventLoopTimeout) } + this.stopUnlockTimeout() + await this.scanner.abort() this.eventLoopAbortController.abort() await this.eventLoopPromise @@ -1813,10 +1819,60 @@ export class Wallet { return } + this.stopUnlockTimeout() this.accountById.clear() this.locked = true } 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 + } + + for (const [id, account] of this.encryptedAccountById.entries()) { + this.accountById.set(id, account.decrypt(passphrase)) + } + + 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/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index e72104f7f5..b19cecc17c 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -11,7 +11,7 @@ import { MultisigKeysEncoding } from './multisigKeys' export const VIEW_KEY_LENGTH = 64 const VERSION_LENGTH = 2 -export interface EncryptedAccountValue { +export type EncryptedAccountValue = { encrypted: true data: Buffer } From a9050724ee2f5a6b6e22ff31001240b6b1e39e86 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 15 Aug 2024 14:35:07 -0700 Subject: [PATCH 052/114] Move common UI components into UI (#5287) This moves various UI components I've seen into the CLI's UI folder. It moves select prompts and other things from utils into the UI if they are related to a UI component. --- ironfish-cli/src/commands/wallet/burn.ts | 3 +- .../src/commands/wallet/chainport/send.ts | 9 +- ironfish-cli/src/commands/wallet/import.ts | 2 +- ironfish-cli/src/commands/wallet/mint.ts | 3 +- .../wallet/multisig/commitment/aggregate.ts | 6 +- .../wallet/multisig/commitment/create.ts | 9 +- .../commands/wallet/multisig/dealer/create.ts | 9 +- .../commands/wallet/multisig/dkg/round1.ts | 10 +- .../commands/wallet/multisig/dkg/round2.ts | 10 +- .../commands/wallet/multisig/dkg/round3.ts | 12 +- .../wallet/multisig/signature/aggregate.ts | 6 +- .../wallet/multisig/signature/create.ts | 7 +- .../src/commands/wallet/notes/combine.ts | 14 +- ironfish-cli/src/commands/wallet/send.ts | 3 +- .../commands/wallet/transactions/decode.ts | 37 +---- .../commands/wallet/transactions/import.ts | 2 +- .../src/commands/wallet/transactions/post.ts | 7 +- .../src/commands/wallet/transactions/sign.ts | 4 +- ironfish-cli/src/ui/index.ts | 2 + .../src/{utils/input.ts => ui/longPrompt.ts} | 0 ironfish-cli/src/ui/prompt.ts | 30 ++++ ironfish-cli/src/ui/prompts.ts | 139 ++++++++++++++++++ ironfish-cli/src/utils/asset.ts | 117 --------------- ironfish-cli/src/utils/multisig.ts | 30 +--- 24 files changed, 227 insertions(+), 244 deletions(-) rename ironfish-cli/src/{utils/input.ts => ui/longPrompt.ts} (100%) create mode 100644 ironfish-cli/src/ui/prompts.ts diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 60fee2ae71..a0f44c5467 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -14,7 +14,6 @@ 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 { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -126,7 +125,7 @@ This will destroy tokens and decrease supply for a given asset.` 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 11e6d65b77..3e09610055 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, @@ -118,7 +117,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 +176,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 +190,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/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 4d40d125d1..1d2fe158c0 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -11,7 +11,7 @@ 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 { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { Ledger } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index f753823560..f48e28362d 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -18,7 +18,6 @@ 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 { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -170,7 +169,7 @@ This will create tokens and increase supply for a given asset.` 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/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index 0ec4cb5b18..b5d5a32ceb 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 { @@ -41,14 +41,14 @@ export class CreateSigningPackage extends IronfishCommand { 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(',') diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index 2cbbc76e65..e118d4c611 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' @@ -48,7 +47,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { 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,7 +63,7 @@ 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, }) } @@ -81,7 +80,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..dd35237a8d 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` @@ -39,7 +38,7 @@ export class MultisigCreateDealer extends IronfishCommand { 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,7 +54,7 @@ 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') @@ -130,7 +129,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..723608eded 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -4,9 +4,7 @@ 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' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -37,12 +35,12 @@ export class DkgRound1Command extends IronfishCommand { 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,7 +56,7 @@ 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') diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index ebc0f409c6..0f18e31ec3 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -4,9 +4,7 @@ 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' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -37,12 +35,12 @@ export class DkgRound2Command extends IronfishCommand { 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 +48,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 }, ) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 7cb163bcd3..9cdba205e0 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -4,9 +4,7 @@ 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' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -47,12 +45,12 @@ export class DkgRound3Command extends IronfishCommand { 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 +58,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 +76,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, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 18f23b058e..946be5a9ea 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' @@ -50,12 +50,12 @@ export class MultisigSign extends IronfishCommand { 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(',') diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 820f842ba6..42f446b877 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' @@ -43,7 +42,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { 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() @@ -63,7 +62,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 ccbda96c7b..0bcf50cc3e 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' @@ -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) @@ -246,7 +246,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 +286,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 +356,7 @@ export class CombineNotesCommand extends IronfishCommand { })}`, ) - await confirmOrQuit('', flags.confirm) + await ui.confirmOrQuit('', flags.confirm) transactionTimer.start() @@ -449,7 +449,7 @@ export class CombineNotesCommand extends IronfishCommand { if (resultingNotes) { this.log('') - table( + ui.table( resultingNotes, { hash: { diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 334d3793ef..b9a0b2abb9 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -16,7 +16,6 @@ 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 { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -137,7 +136,7 @@ export class Send extends IronfishCommand { } 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/transactions/decode.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts index 1186d5d018..8aa1f00512 100644 --- a/ironfish-cli/src/commands/wallet/transactions/decode.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -5,15 +5,13 @@ 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, @@ -42,9 +40,11 @@ export class TransactionsDecodeCommand extends IronfishCommand { const client = await this.connectRpc() + 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, @@ -52,8 +52,6 @@ export class TransactionsDecodeCommand extends IronfishCommand { ) } - const account = flags.account ?? (await this.selectAccount(client)) - const rawTransaction = this.tryDeserializeRawTransaction(transactionString) if (rawTransaction) { return await renderRawTransactionDetails(client, rawTransaction, account, this.logger) @@ -77,33 +75,6 @@ export class TransactionsDecodeCommand 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/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts index cf8e34e663..413b497096 100644 --- a/ironfish-cli/src/commands/wallet/transactions/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -4,7 +4,7 @@ import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' -import { importFile, importPipe, longPrompt } from '../../../utils/input' +import { importFile, importPipe, longPrompt } from '../../../ui/longPrompt' export class TransactionsImportCommand extends IronfishCommand { static description = `import a transaction into the wallet` diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index 1a800717e2..42f4fa3546 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -5,8 +5,7 @@ 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 * as ui from '../../../ui' import { renderRawTransactionDetails } from '../../../utils/transaction' export class TransactionsPostCommand extends IronfishCommand { @@ -51,7 +50,7 @@ export class TransactionsPostCommand extends IronfishCommand { let transaction = args.raw_transaction 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, }) } @@ -75,7 +74,7 @@ export class TransactionsPostCommand 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/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 1d52dcfa41..0e67d59735 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -6,7 +6,7 @@ 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 * as ui from '../../../ui' import { Ledger } from '../../../utils/ledger' import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' @@ -46,7 +46,7 @@ export class TransactionsSignCommand 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/ui/index.ts b/ironfish-cli/src/ui/index.ts index 1b524bebe7..56d39b3e1f 100644 --- a/ironfish-cli/src/ui/index.ts +++ b/ironfish-cli/src/ui/index.ts @@ -4,6 +4,8 @@ export * from './card' export * from './json' +export * from './longPrompt' export * from './progressBar' export * from './prompt' +export * from './prompts' export * from './table' 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..9a4750eb43 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -52,3 +52,33 @@ 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<{ + name: string + value: T + }>([ + { + name: 'prompt', + message: message, + type: 'list', + choices: values, + }, + ]) + + return selection.value +} diff --git a/ironfish-cli/src/ui/prompts.ts b/ironfish-cli/src/ui/prompts.ts new file mode 100644 index 0000000000..55a15a25a0 --- /dev/null +++ b/ironfish-cli/src/ui/prompts.ts @@ -0,0 +1,139 @@ +/* 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): Promise { + const accountsResponse = await client.wallet.getAccounts() + return listPrompt('Select account', 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/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/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 -} From caf266ac548f3a2460728b2aa3ad1b111bce8f85 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Thu, 15 Aug 2024 14:53:52 -0700 Subject: [PATCH 053/114] Make wallet:address support JSON output (#5290) Also convert the output to a card --- ironfish-cli/src/commands/wallet/address.ts | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index 8815793fea..4a42444f5f 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -3,13 +3,16 @@ * 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 = `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 enableJsonFlag = true + static args = { account: Args.string({ required: false, @@ -19,22 +22,25 @@ export class AddressCommand extends IronfishCommand { static flags = { ...RemoteFlags, + ...JsonFlags, } - async start(): Promise { + async start(): Promise { const { args } = await this.parse(AddressCommand) - const { account } = args const client = await this.connectRpc() 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 } } From 077961e790165b5c7bb4366bd41bfd7745be6dd8 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:23:51 -0700 Subject: [PATCH 054/114] supports deploying branch-named Docker images to AWS (#5250) adds a workflow input for `AWS:{BRANCH}` and adds a deploy step for deploying the branch-named image to our AWS ECR repository --- .github/workflows/deploy-node-docker-image.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) 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: | From f9fbde1771ed94c7765dfe9981d0e9c23528870b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:28:20 -0400 Subject: [PATCH 055/114] feat(ironfish): Remove cached accounts when encrypting/decrypting (#5299) --- ironfish/src/wallet/wallet.ts | 4 +-- ironfish/src/wallet/walletdb/walletdb.test.ts | 20 +++++------ ironfish/src/wallet/walletdb/walletdb.ts | 33 +++++++++++-------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index a978edb6ef..a2b9d0af72 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1792,7 +1792,7 @@ export class Wallet { const unlock = await this.createTransactionMutex.lock() try { - await this.walletDb.encryptAccounts(this.accounts, passphrase, tx) + await this.walletDb.encryptAccounts(passphrase, tx) await this.load() } finally { unlock() @@ -1803,7 +1803,7 @@ export class Wallet { const unlock = await this.createTransactionMutex.lock() try { - await this.walletDb.decryptAccounts(this.encryptedAccounts, passphrase, tx) + await this.walletDb.decryptAccounts(passphrase, tx) await this.load() } finally { unlock() diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 9d0526bedd..ae19cab8e7 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -469,7 +469,7 @@ describe('WalletDB', () => { const accountA = await useAccountFixture(node.wallet, 'A') const accountB = await useAccountFixture(node.wallet, 'B') - await walletDb.encryptAccounts([accountA, accountB], passphrase) + await walletDb.encryptAccounts(passphrase) const encryptedAccountById = new Map() for await (const [id, accountValue] of walletDb.loadAccounts()) { @@ -503,7 +503,7 @@ describe('WalletDB', () => { const accountA = await useAccountFixture(node.wallet, 'A') const accountB = await useAccountFixture(node.wallet, 'B') - await walletDb.encryptAccounts([accountA, accountB], passphrase) + await walletDb.encryptAccounts(passphrase) const encryptedAccountById = new Map() for await (const [id, accountValue] of walletDb.loadAccounts()) { @@ -522,7 +522,7 @@ describe('WalletDB', () => { const encryptedAccountB = encryptedAccountById.get(accountB.id) Assert.isNotUndefined(encryptedAccountB) - await walletDb.decryptAccounts([encryptedAccountA, encryptedAccountB], passphrase) + await walletDb.decryptAccounts(passphrase) const accountById = new Map() for await (const [id, accountValue] of walletDb.loadAccounts()) { @@ -551,7 +551,7 @@ describe('WalletDB', () => { const accountA = await useAccountFixture(node.wallet, 'A') const accountB = await useAccountFixture(node.wallet, 'B') - await walletDb.encryptAccounts([accountA, accountB], passphrase) + await walletDb.encryptAccounts(passphrase) const encryptedAccountById = new Map() for await (const [id, accountValue] of walletDb.loadAccounts()) { @@ -570,9 +570,9 @@ describe('WalletDB', () => { const encryptedAccountB = encryptedAccountById.get(accountB.id) Assert.isNotUndefined(encryptedAccountB) - await expect( - walletDb.decryptAccounts([encryptedAccountA, encryptedAccountB], invalidPassphrase), - ).rejects.toThrow(AccountDecryptionFailedError) + await expect(walletDb.decryptAccounts(invalidPassphrase)).rejects.toThrow( + AccountDecryptionFailedError, + ) }) }) @@ -595,9 +595,9 @@ describe('WalletDB', () => { 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([accountA, accountB], passphrase) + await useAccountFixture(node.wallet, 'A') + await useAccountFixture(node.wallet, 'B') + await walletDb.encryptAccounts(passphrase) expect(await walletDb.accountsEncrypted()).toBe(true) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index d9d432ea26..b89f050f97 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1189,28 +1189,33 @@ export class WalletDB { } } - async encryptAccounts( - accounts: Account[], - passphrase: string, - tx?: IDatabaseTransaction, - ): Promise { + async encryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { - for (const account of accounts) { + for await (const [id, accountValue] of this.accounts.getAllIter()) { + if (accountValue.encrypted) { + throw new Error('Account is already encrypted') + } + + const account = new Account({ accountValue, walletDb: this }) const encryptedAccount = account.encrypt(passphrase) - await this.accounts.put(account.id, encryptedAccount.serialize(), tx) + await this.accounts.put(id, encryptedAccount.serialize(), tx) } }) } - async decryptAccounts( - encryptedAccounts: EncryptedAccount[], - passphrase: string, - tx?: IDatabaseTransaction, - ): Promise { + async decryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { - for (const encryptedAccount of encryptedAccounts) { + for await (const [id, accountValue] of this.accounts.getAllIter()) { + if (!accountValue.encrypted) { + throw new Error('Account is already decrypted') + } + + const encryptedAccount = new EncryptedAccount({ + data: accountValue.data, + walletDb: this, + }) const account = encryptedAccount.decrypt(passphrase) - await this.accounts.put(account.id, account.serialize(), tx) + await this.accounts.put(id, account.serialize(), tx) } }) } From 93908a9037e6159b0c4a82f35012bf0053e2da7b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:50:48 -0400 Subject: [PATCH 056/114] feat(ironfish): Check if wallet is encrypted when calling setAccount (#5300) * feat(ironfish): Remove cached accounts when encrypting/decrypting * feat(ironfish): Check if wallet is encrypted when calling setAccount --- .../__fixtures__/walletdb.test.ts.fixture | 31 +++++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 52 ++++++++++++++++++- ironfish/src/wallet/walletdb/walletdb.ts | 4 ++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index ba3e16aea8..3b6b2975ef 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -1150,5 +1150,36 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index ae19cab8e7..2b98d9c4b9 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 { @@ -14,6 +14,7 @@ import { AsyncUtils } from '../../utils' import { Account } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' import { AccountDecryptionFailedError } from '../errors' +import { DecryptedAccountValue } from './accountValue' import { DecryptedNoteValue } from './decryptedNoteValue' describe('WalletDB', () => { @@ -619,4 +620,53 @@ describe('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() + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index b89f050f97..23d6d4b7c3 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -342,6 +342,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( From a3477de4431056493a171cc837bc5a456298692f Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:51:11 -0400 Subject: [PATCH 057/114] feat(ironfish): Return if the wallet is locked in getStatus (#5307) --- ironfish/src/rpc/routes/node/getStatus.test.ts | 3 +++ ironfish/src/rpc/routes/node/getStatus.ts | 3 +++ 2 files changed, 6 insertions(+) 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, From 0567590457df7ff154cabd433d90dfc43606c5f5 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 19 Aug 2024 12:36:55 -0700 Subject: [PATCH 058/114] Always yield native balance in balances (#5308) --- ironfish/src/wallet/account/account.ts | 80 +++++++++++++++++--------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 51302e103d..48e392bee4 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -942,40 +942,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, } } } From 00e7750e9a93c3cc9c244d103d591232c197d46e Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 19 Aug 2024 12:57:10 -0700 Subject: [PATCH 059/114] Make balance output consistent (#5309) Also remove things that are not visible in `wallet:status` --- ironfish-cli/src/commands/wallet/balance.ts | 6 ++-- ironfish-cli/src/commands/wallet/balances.ts | 30 +++++++------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index 213ba1343c..c2a7c117d6 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -102,9 +102,7 @@ Balance is your coins from all of your transactions, even if they are on forks o 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, @@ -116,7 +114,7 @@ Balance is your coins from all of your transactions, even if they are on forks o 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 b5eeba3e65..b5073e09f7 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -66,7 +66,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 +76,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 +86,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, }, } } From f41ef7cd1da3919fc676ef7801ee0441679561b6 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 19 Aug 2024 14:31:41 -0700 Subject: [PATCH 060/114] Fix account prompt returning undefined (#5310) --- ironfish-cli/src/ui/prompt.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index 9a4750eb43..b4fcf6a76a 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -68,10 +68,7 @@ export async function listPrompt( values.sort((a, b) => a.name.localeCompare(b.name)) } - const selection = await inquirer.prompt<{ - name: string - value: T - }>([ + const selection = await inquirer.prompt<{ prompt: T }>([ { name: 'prompt', message: message, @@ -80,5 +77,5 @@ export async function listPrompt( }, ]) - return selection.value + return selection.prompt } From 8a213b705741a936350c281bc677dd87ed44578e Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:28:17 -0700 Subject: [PATCH 061/114] Disallow creation of accounts with no names (#5312) --- .../__fixtures__/account.test.ts.fixture | 62 +++++++++++++++++++ ironfish/src/wallet/account/account.test.ts | 21 +++++++ ironfish/src/wallet/account/account.ts | 4 ++ ironfish/src/wallet/wallet.test.ts | 12 ++++ ironfish/src/wallet/wallet.ts | 4 ++ 5 files changed, 103 insertions(+) diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index 4963fc106e..dd43f15737 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5841,5 +5841,67 @@ "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 + } + } ] } \ 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 54385836b3..695ee3c8ad 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -167,6 +167,27 @@ 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') + }) + }) + describe('loadPendingTransactions', () => { it('should load pending transactions', async () => { const { node } = nodeTest diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 48e392bee4..d894187205 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -128,6 +128,10 @@ export class Account { } async setName(name: string, tx?: IDatabaseTransaction): Promise { + if (!name.trim()) { + throw new Error('Account name cannot be blank') + } + this.name = name await this.walletDb.setAccount(this, tx) diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 3ec5a3949e..33c25083db 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -809,6 +809,18 @@ 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', + ) + }) }) describe('removeAccount', () => { diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index a2b9d0af72..95121913ac 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1295,6 +1295,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) } From e4548fab900f1c161035663db182d917423cf8ba Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Mon, 19 Aug 2024 17:04:21 -0700 Subject: [PATCH 062/114] Add new useAccount helper an upgrade commands (#5311) * Add new helper to select a user account * Upgrade commands to use useAccount --- ironfish-cli/src/commands/wallet/assets.ts | 18 +++++--------- ironfish-cli/src/commands/wallet/balance.ts | 17 ++++--------- ironfish-cli/src/commands/wallet/balances.ts | 17 ++++--------- ironfish-cli/src/commands/wallet/burn.ts | 15 ++---------- ironfish-cli/src/commands/wallet/export.ts | 20 ++++++++-------- ironfish-cli/src/commands/wallet/mint.ts | 15 ++---------- .../src/commands/wallet/notes/index.ts | 16 ++++--------- ironfish-cli/src/commands/wallet/reset.ts | 23 +++++++++--------- ironfish-cli/src/commands/wallet/send.ts | 17 +++---------- .../src/commands/wallet/transactions.ts | 17 ++++--------- .../src/commands/wallet/transactions/info.ts | 10 ++++---- ironfish-cli/src/ui/prompts.ts | 7 ++++-- ironfish-cli/src/utils/account.ts | 24 +++++++++++++++++++ ironfish-cli/src/utils/index.ts | 1 + 14 files changed, 88 insertions(+), 129 deletions(-) create mode 100644 ironfish-cli/src/utils/account.ts diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index 96f935d5b1..3f52d25a54 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 { renderAssetWithVerificationStatus, useAccount } from '../../utils' import { TableCols } from '../../utils/table' const MAX_ASSET_METADATA_COLUMN_WIDTH = ASSET_METADATA_LENGTH + 1 @@ -25,13 +25,6 @@ const MIN_ASSET_NAME_COLUMN_WIDTH = ASSET_NAME_LENGTH / 2 + 1 export class AssetsCommand extends IronfishCommand { static description = `list the account's assets` - static args = { - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), - } - static flags = { ...RemoteFlags, ...TableFlags, @@ -42,11 +35,12 @@ export class AssetsCommand extends IronfishCommand { } 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() + + 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 c2a7c117d6..8a86d54862 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -2,11 +2,11 @@ * 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 = `show the account's balance for an asset @@ -27,13 +27,6 @@ Balance is your coins from all of your transactions, even if they are on forks o }, ] - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get balance for. DEPRECATED: use --account flag', - }), - } - static flags = { ...RemoteFlags, account: Flags.string({ @@ -59,12 +52,12 @@ Balance is your coins from all of your transactions, even if they are on forks o } 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() + const account = await useAccount(client, flags.account) + const response = await client.wallet.getAccountBalance({ account, assetId: flags.assetId, diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index b5073e09f7..f6d644c160 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -2,24 +2,17 @@ * 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 { compareAssets, renderAssetWithVerificationStatus, useAccount } from '../../utils' type AssetBalancePairs = { asset: RpcAsset; balance: GetBalancesResponse['balances'][number] } export class BalancesCommand extends IronfishCommand { static description = `show the account's balance for all assets` - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get balances for. DEPRECATED: use --account flag', - }), - } - static flags = { ...RemoteFlags, ...TableFlags, @@ -38,11 +31,11 @@ export class BalancesCommand extends IronfishCommand { } async start(): Promise { - const { flags, args } = await this.parse(BalancesCommand) + const { flags } = await this.parse(BalancesCommand) const client = await this.connectRpc() - // TODO: remove account arg - const account = flags.account ? flags.account : args.account + const account = await useAccount(client, flags.account) + const response = await client.wallet.getAccountBalances({ account, confirmations: flags.confirmations, diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index a0f44c5467..7eb6386593 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -14,6 +14,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -108,19 +109,7 @@ This will destroy tokens and decrease supply for a given asset.` } } - 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 diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 4a47898fd7..00a1a619be 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -2,27 +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 { EnumLanguageKeyFlag, JsonFlags, RemoteFlags } from '../../flags' import { confirmOrQuit } from '../../ui' +import { useAccount } from '../../utils' export class ExportCommand extends IronfishCommand { static description = `export an account` static enableJsonFlag = true - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to export', - }), - } - static flags = { ...RemoteFlags, ...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', @@ -47,9 +45,8 @@ export class ExportCommand extends IronfishCommand { } 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 +59,9 @@ export class ExportCommand extends IronfishCommand { : AccountFormat.Base64Json const client = await this.connectRpc(local) + + const account = await useAccount(client, flags.account) + const response = await client.wallet.exportAccount({ account, viewOnly, diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index f48e28362d..7f11c3b033 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -18,6 +18,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -129,19 +130,7 @@ This will create tokens and increase supply for a given asset.` } } - 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 diff --git a/ironfish-cli/src/commands/wallet/notes/index.ts b/ironfish-cli/src/commands/wallet/notes/index.ts index 09ac835d01..13d014f8f6 100644 --- a/ironfish-cli/src/commands/wallet/notes/index.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -2,23 +2,17 @@ * 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 { useAccount } from '../../../utils' import { TableCols } from '../../../utils/table' const { sort: _, ...tableFlags } = TableFlags export class NotesCommand extends IronfishCommand { static description = `list the account's notes` - static args = { - account: Args.string({ - required: false, - description: 'Name of the account to get notes for. DEPRECATED: use --account flag', - }), - } - static flags = { ...RemoteFlags, ...tableFlags, @@ -29,14 +23,14 @@ export class NotesCommand extends IronfishCommand { } 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() + const account = await useAccount(client, flags.account) + const response = client.wallet.getAccountNotesStream({ account }) let showHeader = !flags['no-header'] diff --git a/ironfish-cli/src/commands/wallet/reset.ts b/ironfish-cli/src/commands/wallet/reset.ts index addbf11f93..09f95a61a9 100644 --- a/ironfish-cli/src/commands/wallet/reset.ts +++ b/ironfish-cli/src/commands/wallet/reset.ts @@ -2,23 +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 { useAccount } from '../../utils' export class ResetCommand extends IronfishCommand { static description = `resets an account's balance and rescans` - static args = { - account: Args.string({ - required: true, - description: 'Name of the account to reset', - }), - } - 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', @@ -34,8 +32,11 @@ export class ResetCommand extends IronfishCommand { } async start(): Promise { - const { args, flags } = await this.parse(ResetCommand) - const { account } = args + const { flags } = await this.parse(ResetCommand) + + const client = await this.connectRpc() + + const account = await useAccount(client, flags.account) await confirmOrQuit( `Are you sure you want to reset the account '${account}'?` + @@ -45,8 +46,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/send.ts b/ironfish-cli/src/commands/wallet/send.ts index b9a0b2abb9..9dc6f07f80 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -16,6 +16,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { HexFlag, IronFlag, RemoteFlags, ValueFlag } from '../../flags' import * as ui from '../../ui' +import { useAccount } from '../../utils' import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' @@ -121,7 +122,6 @@ 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() @@ -135,6 +135,8 @@ export class Send extends IronfishCommand { } } + const from = await useAccount(client, flags.account, 'Select an account to send from') + if (assetId == null) { const asset = await ui.assetPrompt(client, from, { action: 'send', @@ -190,19 +192,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/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index 1cfc85eb15..f9e062d42a 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -10,11 +10,11 @@ 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 { getAssetsByIDs, useAccount } from '../../utils' import { extractChainportDataFromTransaction } from '../../utils/chainport' import { Format, TableCols } from '../../utils/table' @@ -22,13 +22,6 @@ const { sort: _, ...tableFlags } = TableFlags export class TransactionsCommand extends IronfishCommand { static description = `list the account's transactions` - static args = { - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), - } - static flags = { ...RemoteFlags, ...tableFlags, @@ -61,9 +54,7 @@ export class TransactionsCommand extends IronfishCommand { } 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' @@ -74,6 +65,8 @@ export class TransactionsCommand extends IronfishCommand { const client = await this.connectRpc() + const account = await useAccount(client, flags.account) + const networkId = (await client.chain.getNetworkInfo()).content.networkId const response = client.wallet.getAccountTransactionsStream({ diff --git a/ironfish-cli/src/commands/wallet/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 8b45b65595..74467b8e33 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -18,6 +18,7 @@ import { extractChainportDataFromTransaction, fetchChainportNetworkMap, getAssetsByIDs, + useAccount, } from '../../../utils' import { getExplorer } from '../../../utils/explorer' @@ -31,10 +32,6 @@ export class TransactionInfoCommand extends IronfishCommand { required: true, description: 'Hash of the transaction', }), - account: Args.string({ - required: false, - description: 'Name of the account. DEPRECATED: use --account flag', - }), } static flags = { @@ -48,10 +45,11 @@ export class TransactionInfoCommand extends IronfishCommand { async start(): Promise { const { flags, args } = await this.parse(TransactionInfoCommand) const { transaction: hash } = args - // TODO: remove account arg - const account = flags.account ? flags.account : args.account const client = await this.connectRpc() + + 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/ui/prompts.ts b/ironfish-cli/src/ui/prompts.ts index 55a15a25a0..8c2fead622 100644 --- a/ironfish-cli/src/ui/prompts.ts +++ b/ironfish-cli/src/ui/prompts.ts @@ -8,9 +8,12 @@ import inquirer from 'inquirer' import { getAssetsByIDs, renderAssetWithVerificationStatus } from '../utils' import { listPrompt } from './prompt' -export async function accountPrompt(client: Pick): Promise { +export async function accountPrompt( + client: Pick, + message: string = 'Select account', +): Promise { const accountsResponse = await client.wallet.getAccounts() - return listPrompt('Select account', accountsResponse.content.accounts, (a) => a) + return listPrompt(message, accountsResponse.content.accounts, (a) => a) } export async function multisigSecretPrompt(client: Pick): Promise { diff --git a/ironfish-cli/src/utils/account.ts b/ironfish-cli/src/utils/account.ts new file mode 100644 index 0000000000..9747ca4f86 --- /dev/null +++ b/ironfish-cli/src/utils/account.ts @@ -0,0 +1,24 @@ +/* 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 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/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' From 698e819e47d3db7c55bba63f9c5deeff5ded41fa Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:03:10 -0700 Subject: [PATCH 063/114] Add the ability to delete a transaction to the wallet and RPC (#5286) * Add the ability to delete a transaction to the wallet and RPC * add unit tests for wallet and rpc --- .../commands/wallet/transactions/delete.ts | 46 +- ironfish/src/rpc/clients/client.ts | 13 + .../deleteTransaction.test.ts.fixture | 154 ++++++ .../routes/wallet/deleteTransaction.test.ts | 65 +++ .../rpc/routes/wallet/deleteTransaction.ts | 42 ++ ironfish/src/rpc/routes/wallet/index.ts | 1 + .../__fixtures__/wallet.test.ts.fixture | 446 ++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 160 +++++++ ironfish/src/wallet/wallet.ts | 42 ++ 9 files changed, 930 insertions(+), 39 deletions(-) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/deleteTransaction.test.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/deleteTransaction.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/deleteTransaction.ts diff --git a/ironfish-cli/src/commands/wallet/transactions/delete.ts b/ironfish-cli/src/commands/wallet/transactions/delete.ts index f26e54417a..ab448ca688 100644 --- a/ironfish-cli/src/commands/wallet/transactions/delete.ts +++ b/ironfish-cli/src/commands/wallet/transactions/delete.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 { NodeUtils, TransactionStatus } from '@ironfish/sdk' -import { Args, ux } from '@oclif/core' +import { Args } from '@oclif/core' import { IronfishCommand } from '../../../command' export default class TransactionsDelete extends IronfishCommand { @@ -19,47 +18,16 @@ export default class TransactionsDelete extends IronfishCommand { const { args } = await this.parse(TransactionsDelete) const { transaction } = args - ux.action.start('Opening node') - const node = await this.sdk.node() - await NodeUtils.waitForOpen(node) - ux.action.stop('Done.') + const client = await this.connectRpc() - const accounts = node.wallet.accounts - const transactionHash = Buffer.from(transaction, 'hex') - let deleted = false + const response = await client.wallet.deleteTransaction({ hash: transaction }) - for (const account of accounts) { - const transactionValue = await account.getTransaction(transactionHash) - - if (transactionValue == null) { - continue - } - - const transactionStatus = await node.wallet.getTransactionStatus( - account, - transactionValue, - ) - - if ( - transactionStatus === TransactionStatus.CONFIRMED || - transactionStatus === TransactionStatus.UNCONFIRMED - ) { - this.error(`Transaction ${transaction} is already on a block, so it cannot be deleted`) - } - - if ( - transactionStatus === TransactionStatus.EXPIRED || - transactionStatus === TransactionStatus.PENDING - ) { - await account.deleteTransaction(transactionValue.transaction) - deleted = true - } - } - - if (deleted) { + if (response.content.deleted) { this.log(`Transaction ${transaction} deleted from wallet`) } else { - this.log(`No transaction with hash ${transaction} found in wallet`) + this.error( + `Transaction ${transaction} was not deleted. Either it is on a block already or does not exist`, + ) } } } diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index f8b70ac435..e1c1f859eb 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -174,6 +174,10 @@ import type { UseAccountResponse, } from '../routes' import { ApiNamespace } from '../routes/namespaces' +import { + DeleteTransactionRequest, + DeleteTransactionResponse, +} from '../routes/wallet/deleteTransaction' export abstract class RpcClient { abstract close(): void @@ -586,6 +590,15 @@ export abstract class RpcClient { ).waitForEnd() }, + deleteTransaction: ( + params: DeleteTransactionRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/deleteTransaction`, + params, + ).waitForEnd() + }, + estimateFeeRates: ( params?: EstimateFeeRatesRequest, ): Promise> => { 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/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/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index 775f5ebbe5..7794c13cb3 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -9,6 +9,7 @@ export * from './burnAsset' export * from './create' export * from './createAccount' export * from './createTransaction' +export * from './deleteTransaction' export * from './estimateFeeRates' export * from './exportAccount' export * from './getAccountNotesStream' diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index e6f96e3967..5a812a0cb9 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -7378,5 +7378,451 @@ "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==" + } + ] + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 33c25083db..4447827aea 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -784,6 +784,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 diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 95121913ac..8c57fa3e46 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1195,6 +1195,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, From 4e59deaae12365d06e33ef75d5a92cbb9e88d1fc Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:21:54 -0400 Subject: [PATCH 064/114] feat(ironfish): Add `wallet/encrypt` (#5318) --- ironfish/src/rpc/adapters/errors.ts | 1 + ironfish/src/rpc/clients/client.ts | 10 ++ .../__fixtures__/encrypt.test.ts.fixture | 122 ++++++++++++++++++ .../src/rpc/routes/wallet/encrypt.test.ts | 37 ++++++ ironfish/src/rpc/routes/wallet/encrypt.ts | 41 ++++++ .../rpc/routes/wallet/getAccountsStatus.ts | 10 +- ironfish/src/rpc/routes/wallet/index.ts | 1 + ironfish/src/wallet/wallet.ts | 4 + 8 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/encrypt.test.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/encrypt.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/encrypt.ts diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index de84887961..58b079da9f 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -14,6 +14,7 @@ export enum RPC_ERROR_CODES { DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name', IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required', MULTISIG_SECRET_NOT_FOUND = 'multisig-secret-not-found', + WALLET_ALREADY_ENCRYPTED = 'wallet-already-encrypted', } /** diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index e1c1f859eb..c464b7b38b 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -178,6 +178,7 @@ import { DeleteTransactionRequest, DeleteTransactionResponse, } from '../routes/wallet/deleteTransaction' +import { EncryptWalletRequest, EncryptWalletResponse } from '../routes/wallet/encrypt' export abstract class RpcClient { abstract close(): void @@ -640,6 +641,15 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + encrypt: ( + params: EncryptWalletRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/encrypt`, + params, + ).waitForEnd() + }, } mempool = { 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/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/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/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index 7794c13cb3..2ed36fe1f1 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -11,6 +11,7 @@ export * from './createAccount' export * from './createTransaction' export * from './deleteTransaction' export * from './estimateFeeRates' +export * from './encrypt' export * from './exportAccount' export * from './getAccountNotesStream' export * from './getAccountStatus' diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 8c57fa3e46..7c470e5dca 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1834,6 +1834,10 @@ export class Wallet { }) } + async accountsEncrypted(): Promise { + return this.walletDb.accountsEncrypted() + } + async encrypt(passphrase: string, tx?: IDatabaseTransaction): Promise { const unlock = await this.createTransactionMutex.lock() From e3f7083b347a25662b2ab410dcc0d9eb99905e39 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 20 Aug 2024 13:08:23 -0700 Subject: [PATCH 065/114] Standardizxe account flags to char 'a' (#5315) And rename or delete command specific competing flags. These command flags had chars that were not consistent across commands like "confirmations". It's best not to have command specific single letter char shortcuts. --- ironfish-cli/src/commands/wallet/burn.ts | 7 +------ ironfish-cli/src/commands/wallet/chainport/send.ts | 8 +------- ironfish-cli/src/commands/wallet/mint.ts | 9 +-------- .../src/commands/wallet/multisig/account/participants.ts | 2 +- .../src/commands/wallet/multisig/commitment/aggregate.ts | 2 +- .../src/commands/wallet/multisig/commitment/create.ts | 2 +- .../src/commands/wallet/multisig/signature/aggregate.ts | 2 +- .../src/commands/wallet/multisig/signature/create.ts | 2 +- ironfish-cli/src/commands/wallet/send.ts | 8 +------- 9 files changed, 9 insertions(+), 33 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index 7eb6386593..d7c04a72cb 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -35,28 +35,24 @@ This will destroy tokens and decrease supply for a given asset.` static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + 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({ @@ -64,7 +60,6 @@ This will destroy tokens and decrease supply for a given asset.` 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, diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 3e09610055..9a0b086d60 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -43,36 +43,30 @@ export class BridgeCommand extends IronfishCommand { description: 'Wait for the transaction to be confirmed on Ironfish', }), account: Flags.string({ - char: 'f', + 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.', }), diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 7f11c3b033..e390114779 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -41,38 +41,32 @@ This will create tokens and increase supply for a given asset.` static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + 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, }), @@ -81,7 +75,6 @@ This will create tokens and increase supply for a given asset.` 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, diff --git a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts index 70d856010f..ffc13e194c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -11,7 +11,7 @@ export class MultisigAccountParticipants extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: 'Name of the account to list group identities for', }), } diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index b5d5a32ceb..9494cd1cf7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -14,7 +14,7 @@ export class CreateSigningPackage extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: 'Name of the account to use when creating the signing package', required: false, }), diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index e118d4c611..6ad0af3c52 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -15,7 +15,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: 'Name of the account to use for generating the commitment, must be a multisig participant account', required: false, diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index 946be5a9ea..df89fcd993 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -15,7 +15,7 @@ export class MultisigSign extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: 'Name of the account to use when aggregating signature shares', required: false, }), diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 42f446b877..984aa2e645 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -16,7 +16,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + char: 'a', description: 'Name of the account from which the signature share will be created', required: false, }), diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 9dc6f07f80..dbb425d7f2 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -41,32 +41,27 @@ export class Send extends IronfishCommand { static flags = { ...RemoteFlags, account: Flags.string({ - char: 'f', + 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,7 +78,6 @@ 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, From be4afa56ef9b6c244ec3b3c692299202dac4556b Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 20 Aug 2024 13:08:44 -0700 Subject: [PATCH 066/114] Delete required:false in flags which is default (#5316) This does nothing as all flags are by default required: false. --- ironfish-cli/src/commands/wallet/balance.ts | 2 -- ironfish-cli/src/commands/wallet/balances.ts | 1 - ironfish-cli/src/commands/wallet/burn.ts | 1 - ironfish-cli/src/commands/wallet/export.ts | 2 -- ironfish-cli/src/commands/wallet/mint.ts | 5 ----- .../src/commands/wallet/multisig/commitment/aggregate.ts | 1 - .../src/commands/wallet/multisig/commitment/create.ts | 1 - .../src/commands/wallet/multisig/signature/aggregate.ts | 1 - .../src/commands/wallet/multisig/signature/create.ts | 2 -- ironfish-cli/src/commands/wallet/send.ts | 1 - ironfish-cli/src/commands/wallet/transactions/post.ts | 1 - ironfish-cli/src/commands/wallet/transactions/watch.ts | 1 - 12 files changed, 19 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index 8a86d54862..28c28945ed 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -42,11 +42,9 @@ Balance is your coins from all of your transactions, even if they are on forks o 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', }), } diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index f6d644c160..adbcee3e24 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -25,7 +25,6 @@ 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', }), } diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index d7c04a72cb..b3993342f4 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -62,7 +62,6 @@ This will destroy tokens and decrease supply for a given asset.` confirmations: Flags.integer({ 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, diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 00a1a619be..5dc1033600 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -31,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, diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index e390114779..4d4b1c4490 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -60,15 +60,12 @@ This will create tokens and increase supply for a given asset.` }), assetId: Flags.string({ description: 'Identifier for the asset', - required: false, }), metadata: Flags.string({ description: 'Metadata for the asset', - required: false, }), name: Flags.string({ description: 'Name for the asset', - required: false, }), confirm: Flags.boolean({ default: false, @@ -77,7 +74,6 @@ This will create tokens and increase supply for a given asset.` confirmations: Flags.integer({ 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, @@ -99,7 +95,6 @@ This will create tokens and increase supply for a given asset.` }), transferOwnershipTo: Flags.string({ description: 'The public address of the account to transfer ownership of this asset to.', - required: false, }), unsignedTransaction: Flags.boolean({ default: false, diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts index 9494cd1cf7..e57f49d0be 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -16,7 +16,6 @@ export class CreateSigningPackage extends IronfishCommand { account: Flags.string({ char: 'a', description: 'Name of the account to use when creating the signing package', - required: false, }), unsignedTransaction: Flags.string({ char: 'u', diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index 6ad0af3c52..b455efc3ba 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -18,7 +18,6 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { char: 'a', description: 'Name of the account to use for generating the commitment, must be a multisig participant account', - required: false, }), unsignedTransaction: Flags.string({ char: 'u', diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index df89fcd993..d41cf1454d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -17,7 +17,6 @@ export class MultisigSign extends IronfishCommand { account: Flags.string({ char: 'a', description: 'Name of the account to use when aggregating signature shares', - required: false, }), signingPackage: Flags.string({ char: 'p', diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 984aa2e645..176dcb0eac 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -18,12 +18,10 @@ export class CreateSignatureShareCommand extends IronfishCommand { account: Flags.string({ char: 'a', description: 'Name of the account from which the signature share will be created', - required: false, }), signingPackage: Flags.string({ char: 's', description: 'The signing package for which the signature share will be created', - required: false, }), confirm: Flags.boolean({ default: false, diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index dbb425d7f2..8cdf302f85 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -80,7 +80,6 @@ export class Send extends IronfishCommand { confirmations: Flags.integer({ description: 'Minimum number of block confirmations needed to include a note. Set to 0 to include all blocks.', - required: false, }), assetId: HexFlag({ char: 'i', diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index 42f4fa3546..bf00fceb2d 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -31,7 +31,6 @@ export class TransactionsPostCommand extends IronfishCommand { account: Flags.string({ description: 'Name of the account that created the raw transaction', char: 'f', - required: false, deprecated: true, }), confirm: Flags.boolean({ diff --git a/ironfish-cli/src/commands/wallet/transactions/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts index e874f216b7..d64826eb10 100644 --- a/ironfish-cli/src/commands/wallet/transactions/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -28,7 +28,6 @@ export class TransactionsWatchCommand 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', }), } From dd8f936cec7f8857b58b2692db9e23bb4062535a Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:25:00 -0400 Subject: [PATCH 067/114] feat(ironfish): Add `wallet/decrypt` (#5319) --- ironfish/src/rpc/adapters/errors.ts | 1 + ironfish/src/rpc/clients/client.ts | 10 + .../__fixtures__/decrypt.test.ts.fixture | 182 ++++++++++++++++++ .../src/rpc/routes/wallet/decrypt.test.ts | 64 ++++++ ironfish/src/rpc/routes/wallet/decrypt.ts | 41 ++++ ironfish/src/rpc/routes/wallet/index.ts | 1 + ironfish/src/wallet/wallet.ts | 3 + 7 files changed, 302 insertions(+) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/decrypt.test.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/decrypt.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/decrypt.ts diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index 58b079da9f..cc5ac36fda 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -14,6 +14,7 @@ export enum RPC_ERROR_CODES { DUPLICATE_ACCOUNT_NAME = 'duplicate-account-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 c464b7b38b..99c5a0f980 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -174,6 +174,7 @@ import type { UseAccountResponse, } from '../routes' import { ApiNamespace } from '../routes/namespaces' +import { DecryptWalletRequest, DecryptWalletResponse } from '../routes/wallet/decrypt' import { DeleteTransactionRequest, DeleteTransactionResponse, @@ -650,6 +651,15 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + decrypt: ( + params: DecryptWalletRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/decrypt`, + params, + ).waitForEnd() + }, } mempool = { 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/decrypt.test.ts b/ironfish/src/rpc/routes/wallet/decrypt.test.ts new file mode 100644 index 0000000000..17628ef3a5 --- /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 account') + + 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/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index 2ed36fe1f1..09e9a38d62 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -9,6 +9,7 @@ export * from './burnAsset' export * from './create' export * from './createAccount' export * from './createTransaction' +export * from './decrypt' export * from './deleteTransaction' export * from './estimateFeeRates' export * from './encrypt' diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 7c470e5dca..34d141e663 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1855,6 +1855,9 @@ export class Wallet { try { await this.walletDb.decryptAccounts(passphrase, tx) await this.load() + } catch (e) { + this.logger.error(ErrorUtils.renderError(e, true)) + throw e } finally { unlock() } From 704d0ec1f5f0791c52008c5249a92b598a1d7478 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 20 Aug 2024 13:31:47 -0700 Subject: [PATCH 068/114] Change wallet:delete to use new confirmInput (#5320) This makes the user type in the account name they are trying to delete --- ironfish-cli/src/commands/wallet/delete.ts | 13 ++++++------- ironfish-cli/src/ui/prompt.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/delete.ts b/ironfish-cli/src/commands/wallet/delete.ts index ca67f077fe..b2da47e60e 100644 --- a/ironfish-cli/src/commands/wallet/delete.ts +++ b/ironfish-cli/src/commands/wallet/delete.ts @@ -5,7 +5,7 @@ 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 = `delete an account` @@ -40,12 +40,11 @@ export class DeleteCommand extends IronfishCommand { 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/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index b4fcf6a76a..6f9f9aa94c 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -28,6 +28,27 @@ export async function inputPrompt(message: string, required: boolean = false): P 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', From 2ab7c462d2ce851da4aa1ff4227b864a1f532cc2 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 20 Aug 2024 13:32:22 -0700 Subject: [PATCH 069/114] Change wallet:rename arg names (#5314) --- ironfish-cli/src/commands/wallet/rename.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/rename.ts b/ironfish-cli/src/commands/wallet/rename.ts index cdb2d53b32..bd0e4760ac 100644 --- a/ironfish-cli/src/commands/wallet/rename.ts +++ b/ironfish-cli/src/commands/wallet/rename.ts @@ -9,13 +9,13 @@ export class RenameCommand extends IronfishCommand { 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 +25,10 @@ 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 client.wallet.renameAccount({ account: args.old_name, newName: args.new_name }) + this.log(`Account ${args.old_name} renamed to ${args.new_name}`) } } From 9780475e9923409e878de601249c2cee072fe32b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:18:39 -0400 Subject: [PATCH 070/114] feat(ironfish): Add `wallet/unlock` (#5321) --- ironfish/src/rpc/clients/client.ts | 8 + .../__fixtures__/unlock.test.ts.fixture | 182 ++++++++++++++++++ ironfish/src/rpc/routes/wallet/index.ts | 1 + ironfish/src/rpc/routes/wallet/unlock.test.ts | 76 ++++++++ ironfish/src/rpc/routes/wallet/unlock.ts | 41 ++++ 5 files changed, 308 insertions(+) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/unlock.test.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/unlock.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/unlock.ts diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 99c5a0f980..944f360d45 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -180,6 +180,7 @@ import { DeleteTransactionResponse, } from '../routes/wallet/deleteTransaction' import { EncryptWalletRequest, EncryptWalletResponse } from '../routes/wallet/encrypt' +import { UnlockWalletRequest, UnlockWalletResponse } from '../routes/wallet/unlock' export abstract class RpcClient { abstract close(): void @@ -660,6 +661,13 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + unlock: (params: UnlockWalletRequest): Promise> => { + return this.request( + `${ApiNamespace.wallet}/unlock`, + params, + ).waitForEnd() + }, } mempool = { 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/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index 09e9a38d62..cba8f4f5d8 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -45,5 +45,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/unlock.test.ts b/ironfish/src/rpc/routes/wallet/unlock.test.ts new file mode 100644 index 0000000000..ccff1c86c4 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/unlock.test.ts @@ -0,0 +1,76 @@ +/* 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 account') + + 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]) + + // Temporary until the lock RPC is added + await routeTest.node.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() + }, +) From a54784990e59dcef6799d8b25dd9e37ec6b55542 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:15:33 -0400 Subject: [PATCH 071/114] feat(ironfish): Add `wallet/lock` (#5322) --- ironfish/src/rpc/clients/client.ts | 24 +++- .../wallet/__fixtures__/lock.test.ts.fixture | 122 ++++++++++++++++++ ironfish/src/rpc/routes/wallet/index.ts | 1 + ironfish/src/rpc/routes/wallet/lock.test.ts | 51 ++++++++ ironfish/src/rpc/routes/wallet/lock.ts | 28 ++++ ironfish/src/rpc/routes/wallet/unlock.test.ts | 3 +- 6 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 ironfish/src/rpc/routes/wallet/__fixtures__/lock.test.ts.fixture create mode 100644 ironfish/src/rpc/routes/wallet/lock.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/lock.ts diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 944f360d45..28114d969a 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, @@ -135,6 +141,8 @@ import type { ImportResponse, IsValidPublicAddressRequest, IsValidPublicAddressResponse, + LockWalletRequest, + LockWalletResponse, MintAssetRequest, MintAssetResponse, OnGossipRequest, @@ -166,6 +174,8 @@ import type { StopNodeResponse, SubmitBlockRequest, SubmitBlockResponse, + UnlockWalletRequest, + UnlockWalletResponse, UnsetConfigRequest, UnsetConfigResponse, UploadConfigRequest, @@ -174,13 +184,6 @@ import type { UseAccountResponse, } from '../routes' import { ApiNamespace } from '../routes/namespaces' -import { DecryptWalletRequest, DecryptWalletResponse } from '../routes/wallet/decrypt' -import { - DeleteTransactionRequest, - DeleteTransactionResponse, -} from '../routes/wallet/deleteTransaction' -import { EncryptWalletRequest, EncryptWalletResponse } from '../routes/wallet/encrypt' -import { UnlockWalletRequest, UnlockWalletResponse } from '../routes/wallet/unlock' export abstract class RpcClient { abstract close(): void @@ -668,6 +671,13 @@ export abstract class RpcClient { params, ).waitForEnd() }, + + lock: (params?: LockWalletRequest): Promise> => { + return this.request( + `${ApiNamespace.wallet}/lock`, + params, + ).waitForEnd() + }, } mempool = { 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/index.ts b/ironfish/src/rpc/routes/wallet/index.ts index cba8f4f5d8..7ab200afc7 100644 --- a/ironfish/src/rpc/routes/wallet/index.ts +++ b/ironfish/src/rpc/routes/wallet/index.ts @@ -31,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' 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/unlock.test.ts b/ironfish/src/rpc/routes/wallet/unlock.test.ts index ccff1c86c4..33c2f14145 100644 --- a/ironfish/src/rpc/routes/wallet/unlock.test.ts +++ b/ironfish/src/rpc/routes/wallet/unlock.test.ts @@ -70,7 +70,6 @@ describe('Route wallet/unlock', () => { const decryptedAccounts = await routeTest.client.wallet.getAccounts() expect(decryptedAccounts.content.accounts.sort()).toEqual([accountA.name, accountB.name]) - // Temporary until the lock RPC is added - await routeTest.node.wallet.lock() + await routeTest.client.wallet.lock() }) }) From 7088bfdaa46d718d92ae276b1589313c5f3964e1 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 22 Aug 2024 16:55:42 -0700 Subject: [PATCH 072/114] chore: adds test that included dkg key generation and signing all in one test (#5324) --- ironfish-rust/src/transaction/tests.rs | 197 ++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index fa3e05ae9a..b68bf4e500 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,195 @@ 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); + 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"); +} From 70b736d8ecbbd5a3f84e6170c41f2a21cdf4dd2b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:27:46 -0400 Subject: [PATCH 073/114] feat(cli): Add `wallet:encrypt` (#5327) * feat(cli): Add `wallet:encrypt` * Update ironfish-cli/src/commands/wallet/encrypt.ts Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --------- Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --- ironfish-cli/src/commands/wallet/encrypt.ts | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/encrypt.ts diff --git a/ironfish-cli/src/commands/wallet/encrypt.ts b/ironfish-cli/src/commands/wallet/encrypt.ts new file mode 100644 index 0000000000..b840208a1e --- /dev/null +++ b/ironfish-cli/src/commands/wallet/encrypt.ts @@ -0,0 +1,53 @@ +/* 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', + }), + } + + 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) + } + + 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') + } +} From 487fbeec222a332d1778a29716a5212630c7de25 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:27:53 -0400 Subject: [PATCH 074/114] feat(cli): Add `wallet:decrypt` (#5328) * feat(cli): Add `wallet:decrypt` * Update ironfish-cli/src/commands/wallet/decrypt.ts Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --------- Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --- ironfish-cli/src/commands/wallet/decrypt.ts | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/decrypt.ts diff --git a/ironfish-cli/src/commands/wallet/decrypt.ts b/ironfish-cli/src/commands/wallet/decrypt.ts new file mode 100644 index 0000000000..3362da2b95 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/decrypt.ts @@ -0,0 +1,53 @@ +/* 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 a passphrase to decrypt the wallet', 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') + } +} From b463f6075a20770991f247c6996268c71affd5fd Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:28:00 -0400 Subject: [PATCH 075/114] feat(cli): Add `wallet:unlock` (#5329) * feat(cli): Add `wallet:unlock` * Update ironfish-cli/src/commands/wallet/unlock.ts Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --------- Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --- ironfish-cli/src/commands/wallet/unlock.ts | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/unlock.ts diff --git a/ironfish-cli/src/commands/wallet/unlock.ts b/ironfish-cli/src/commands/wallet/unlock.ts new file mode 100644 index 0000000000..83819307bc --- /dev/null +++ b/ironfish-cli/src/commands/wallet/unlock.ts @@ -0,0 +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 { 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 a passphrase to unlock the wallet', 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) + } +} From a1c7df27f56eee95ba661bd6dfa6fe7827ab6021 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:43:29 -0400 Subject: [PATCH 076/114] feat(cli): Add `wallet:lock` (#5330) --- ironfish-cli/src/commands/wallet/lock.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 ironfish-cli/src/commands/wallet/lock.ts 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) + } +} From d1abc670731e875b313d368ee19230de09b49c0a Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:45:46 -0400 Subject: [PATCH 077/114] feat(cli,ironfish): Check if the wallet is locked before fetching accounts (#5333) --- ironfish-cli/src/utils/account.ts | 5 +++++ ironfish/src/rpc/routes/wallet/utils.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/ironfish-cli/src/utils/account.ts b/ironfish-cli/src/utils/account.ts index 9747ca4f86..a867d86707 100644 --- a/ironfish-cli/src/utils/account.ts +++ b/ironfish-cli/src/utils/account.ts @@ -14,6 +14,11 @@ export async function useAccount( 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) { 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) { From d5868c858db9ca60ca6f81e9fbf59fabef14cdce Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:01:54 -0400 Subject: [PATCH 078/114] feat(ironfish): Notify via log when the wallet locks (#5334) --- ironfish/src/wallet/wallet.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 34d141e663..e2d16d793a 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1875,6 +1875,10 @@ export class Wallet { this.stopUnlockTimeout() this.accountById.clear() this.locked = true + + this.logger.info( + 'Wallet locked. Unlock the wallet to view your accounts and create transactions', + ) } finally { unlock() } From d456d62c793f68221bfbaa376fe64057145e996c Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:24:51 -0400 Subject: [PATCH 079/114] feat(cli): Hide passphrases in encryption commands (#5343) --- ironfish-cli/src/commands/wallet/decrypt.ts | 4 +++- ironfish-cli/src/commands/wallet/encrypt.ts | 18 +++++++++++++++++- ironfish-cli/src/commands/wallet/unlock.ts | 4 +++- ironfish-cli/src/ui/prompt.ts | 14 +++++++++----- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/decrypt.ts b/ironfish-cli/src/commands/wallet/decrypt.ts index 3362da2b95..4eedbcba31 100644 --- a/ironfish-cli/src/commands/wallet/decrypt.ts +++ b/ironfish-cli/src/commands/wallet/decrypt.ts @@ -32,7 +32,9 @@ export class DecryptCommand extends IronfishCommand { let passphrase = flags.passphrase if (!passphrase) { - passphrase = await inputPrompt('Enter a passphrase to decrypt the wallet', true) + passphrase = await inputPrompt('Enter a passphrase to decrypt the wallet', true, { + password: true, + }) } try { diff --git a/ironfish-cli/src/commands/wallet/encrypt.ts b/ironfish-cli/src/commands/wallet/encrypt.ts index b840208a1e..2573ceb8c1 100644 --- a/ironfish-cli/src/commands/wallet/encrypt.ts +++ b/ironfish-cli/src/commands/wallet/encrypt.ts @@ -17,6 +17,9 @@ export class EncryptCommand extends IronfishCommand { passphrase: Flags.string({ description: 'Passphrase to encrypt the wallet with', }), + confirm: Flags.boolean({ + description: 'Suppress the passphrase confirmation prompt', + }), } async start(): Promise { @@ -32,7 +35,20 @@ export class EncryptCommand extends IronfishCommand { let passphrase = flags.passphrase if (!passphrase) { - passphrase = await inputPrompt('Enter a passphrase to encrypt the wallet', true) + 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 { diff --git a/ironfish-cli/src/commands/wallet/unlock.ts b/ironfish-cli/src/commands/wallet/unlock.ts index 83819307bc..784f93fb85 100644 --- a/ironfish-cli/src/commands/wallet/unlock.ts +++ b/ironfish-cli/src/commands/wallet/unlock.ts @@ -36,7 +36,9 @@ export class UnlockCommand extends IronfishCommand { let passphrase = flags.passphrase if (!passphrase) { - passphrase = await inputPrompt('Enter a passphrase to unlock the wallet', true) + passphrase = await inputPrompt('Enter a passphrase to unlock the wallet', true, { + password: true, + }) } try { diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index 6f9f9aa94c..f670e28044 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -5,24 +5,28 @@ 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 From f737dee6b6549d83f3850dbdb7fbff9176ce0133 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:38:17 -0400 Subject: [PATCH 080/114] feat(cli): Return locked warning in wallet:accounts and wallet:which (#5344) --- ironfish-cli/src/commands/wallet/index.ts | 5 +++++ ironfish-cli/src/commands/wallet/which.ts | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/index.ts b/ironfish-cli/src/commands/wallet/index.ts index ece1e6da28..62d5f2bfa2 100644 --- a/ironfish-cli/src/commands/wallet/index.ts +++ b/ironfish-cli/src/commands/wallet/index.ts @@ -25,6 +25,11 @@ export class AccountsCommand extends IronfishCommand { 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 [] diff --git a/ironfish-cli/src/commands/wallet/which.ts b/ironfish-cli/src/commands/wallet/which.ts index e96eb855b3..e69e05ed62 100644 --- a/ironfish-cli/src/commands/wallet/which.ts +++ b/ironfish-cli/src/commands/wallet/which.ts @@ -25,6 +25,12 @@ export class WhichCommand extends IronfishCommand { const client = await this.connectRpc() + 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: { accounts: [accountName], From 67a8f6eb545f319d29ca29849ab35842eb5baf5a Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:45:35 -0400 Subject: [PATCH 081/114] feat(cli): Update status to show when the wallet is locked (#5345) --- ironfish-cli/src/commands/status.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/status.ts b/ironfish-cli/src/commands/status.ts index 026062162f..b6dbf169cd 100644 --- a/ironfish-cli/src/commands/status.ts +++ b/ironfish-cli/src/commands/status.ts @@ -187,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` From 08c10663f981b717e46f683ba5939ce3089d3cb4 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:24:39 -0400 Subject: [PATCH 082/114] feat(ironfish): Add passphrase to import account for encrypted dbs (#5352) * feat(ironfish): Add passphrase to import account for encrypted dbs * feat(ironfish): Move valid passphrase check to db --- .../__fixtures__/wallet.test.ts.fixture | 93 +++++++++++ ironfish/src/wallet/wallet.test.ts | 72 ++++++++ ironfish/src/wallet/wallet.ts | 11 +- .../__fixtures__/walletdb.test.ts.fixture | 155 ++++++++++++++++++ ironfish/src/wallet/walletdb/walletdb.test.ts | 85 ++++++++++ ironfish/src/wallet/walletdb/walletdb.ts | 72 +++++++- 6 files changed, 482 insertions(+), 6 deletions(-) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 5a812a0cb9..dccff9684d 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -7824,5 +7824,98 @@ } ] } + ], + "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 4447827aea..d466dec5aa 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -30,6 +30,7 @@ import { } from './errors' import { toAccountImport } from './exporter' import { AssetStatus, Wallet } from './wallet' +import { DecryptedAccountValue } from './walletdb/accountValue' describe('Wallet', () => { const nodeTest = createNodeTest() @@ -651,6 +652,77 @@ 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 throw an error when the wallet is encrypted and the passphrase is incorrect', 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, { passphrase: 'incorrect' }), + ).rejects.toThrow('Your passphrase is incorrect') + }) + + 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, + } + + const account = await node.wallet.importAccount(accountValue, { passphrase }) + 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', () => { diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index e2d16d793a..e5935b0b07 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1397,7 +1397,7 @@ export class Wallet { async importAccount( accountValue: AccountImport, - options?: { createdAt?: number }, + options?: { createdAt?: number; passphrase?: string }, ): Promise { let multisigKeys = accountValue.multisigKeys const name = accountValue.name @@ -1468,7 +1468,14 @@ export class Wallet { }) await this.walletDb.db.transaction(async (tx) => { - await this.walletDb.setAccount(account, tx) + const encrypted = await this.walletDb.accountsEncrypted(tx) + + if (encrypted) { + Assert.isNotUndefined(options?.passphrase) + await this.walletDb.setEncryptedAccount(account, options.passphrase, tx) + } else { + await this.walletDb.setAccount(account, tx) + } if (createdAt !== null) { const previousBlock = await this.chainGetBlock({ sequence: createdAt.sequence - 1 }) diff --git a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture index 3b6b2975ef..61c5f41085 100644 --- a/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture +++ b/ironfish/src/wallet/walletdb/__fixtures__/walletdb.test.ts.fixture @@ -1181,5 +1181,160 @@ "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/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 2b98d9c4b9..d1bff95abc 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -669,4 +669,89 @@ describe('WalletDB', () => { ).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' + + 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, passphrase)).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 }) + + await walletDb.setEncryptedAccount(account, passphrase) + + expect(await walletDb.accounts.get(account.id)).not.toBeUndefined() + expect( + await walletDb.balances.get([account.prefix, Asset.nativeId()]), + ).not.toBeUndefined() + }) + }) + + describe('canDecryptAccounts', () => { + it('throws an error if the accounts are decrypted', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + + await useAccountFixture(node.wallet, 'A') + + await expect(walletDb.canDecryptAccounts('invalid')).rejects.toThrow() + }) + + it('returns false if the passphrase is invalid', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'foobar' + + await useAccountFixture(node.wallet, 'A') + await walletDb.encryptAccounts(passphrase) + + expect(await walletDb.canDecryptAccounts('invalid')).toBe(false) + }) + + it('returns true if the passphrase is valid', async () => { + const node = (await nodeTest.createSetup()).node + const walletDb = node.wallet.walletDb + const passphrase = 'foobar' + + await useAccountFixture(node.wallet, 'A') + await walletDb.encryptAccounts(passphrase) + + expect(await walletDb.canDecryptAccounts(passphrase)).toBe(true) + }) + }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 23d6d4b7c3..ed9a591d5d 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -33,6 +33,7 @@ import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' +import { AccountDecryptionFailedError } from '../errors' import { AccountValue, AccountValueEncoding } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' @@ -367,6 +368,69 @@ export class WalletDB { }) } + async setEncryptedAccount( + account: Account, + passphrase: string, + tx?: IDatabaseTransaction, + ): Promise { + await 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 validPassphrase = await this.canDecryptAccounts(passphrase, tx) + Assert.isTrue(validPassphrase, 'Your passphrase is incorrect') + + const encryptedAccount = account.encrypt(passphrase) + 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, + ) + } + }) + } + + async canDecryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { + return this.db.withTransaction(tx, async (tx) => { + for await (const [_, accountValue] of this.accounts.getAllIter(tx)) { + if (!accountValue.encrypted) { + throw new Error('Wallet is already decrypted') + } + + const encryptedAccount = new EncryptedAccount({ + data: accountValue.data, + walletDb: this, + }) + + try { + encryptedAccount.decrypt(passphrase) + } catch (e) { + if (e instanceof AccountDecryptionFailedError) { + return false + } + + throw e + } + } + + return true + }) + } + async removeAccount(account: Account, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { await this.accounts.del(account.id, tx) @@ -1195,9 +1259,9 @@ export class WalletDB { async encryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { - for await (const [id, accountValue] of this.accounts.getAllIter()) { + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { if (accountValue.encrypted) { - throw new Error('Account is already encrypted') + throw new Error('Wallet is already encrypted') } const account = new Account({ accountValue, walletDb: this }) @@ -1209,9 +1273,9 @@ export class WalletDB { async decryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { - for await (const [id, accountValue] of this.accounts.getAllIter()) { + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { if (!accountValue.encrypted) { - throw new Error('Account is already decrypted') + throw new Error('Wallet is already decrypted') } const encryptedAccount = new EncryptedAccount({ From 8a949b310749ad15cef2568a88a06dbaa39b88d6 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:07:10 -0400 Subject: [PATCH 083/114] feat(cli): Log message in start when the wallet is locked (#5336) --- ironfish-cli/src/commands/start.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 29684ccbdb..847951f645 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -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 From 3e3b650f2d73e14f36d0f3f795845b8140b97d0f Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 4 Sep 2024 16:35:21 -0400 Subject: [PATCH 084/114] Improve RPC client types for broadcastTransaction and isValidPublicAddress (#5356) Also logs if the transaction is not accepted or broadcast when calling the CLI chain:broadcast command. --- ironfish-cli/src/commands/chain/broadcast.ts | 14 +++++++++++++- ironfish/src/rpc/clients/client.ts | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/chain/broadcast.ts b/ironfish-cli/src/commands/chain/broadcast.ts index 9d288451fa..e10d32e18c 100644 --- a/ironfish-cli/src/commands/chain/broadcast.ts +++ b/ironfish-cli/src/commands/chain/broadcast.ts @@ -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/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 28114d969a..79742e1d00 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -1013,7 +1013,7 @@ export abstract class RpcClient { isValidPublicAddress: ( params: IsValidPublicAddressRequest, - ): Promise> => { + ): Promise> => { return this.request( `${ApiNamespace.chain}/isValidPublicAddress`, params, @@ -1022,7 +1022,7 @@ export abstract class RpcClient { broadcastTransaction: ( params: BroadcastTransactionRequest, - ): Promise> => { + ): Promise> => { return this.request( `${ApiNamespace.chain}/broadcastTransaction`, params, From f6b340a4473057263e5ca552cfe3c74b348fb58c Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:00:46 -0400 Subject: [PATCH 085/114] feat(ironfish): Add passphrase to reset account methods (#5354) * feat(ironfish): Add passphrase to reset account methods * feat(ironfish): Fix validation for passphrase * feat(ironfish): Update return type --- .../src/rpc/routes/wallet/resetAccount.ts | 3 + .../__fixtures__/wallet.test.ts.fixture | 93 +++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 42 +++++++++ ironfish/src/wallet/wallet.ts | 19 +++- ironfish/src/wallet/walletdb/walletdb.ts | 6 +- 5 files changed, 159 insertions(+), 4 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/resetAccount.ts b/ironfish/src/rpc/routes/wallet/resetAccount.ts index 22e6d86467..56ec609bb4 100644 --- a/ironfish/src/rpc/routes/wallet/resetAccount.ts +++ b/ironfish/src/rpc/routes/wallet/resetAccount.ts @@ -11,6 +11,7 @@ export type ResetAccountRequest = { account: string resetCreatedAt?: boolean resetScanningEnabled?: boolean + passphrase?: string } export type ResetAccountResponse = undefined @@ -19,6 +20,7 @@ export const ResetAccountRequestSchema: yup.ObjectSchema = account: yup.string().defined(), resetCreatedAt: yup.boolean(), resetScanningEnabled: yup.boolean(), + passphrase: yup.string().optional(), }) .defined() @@ -37,6 +39,7 @@ routes.register( await context.wallet.resetAccount(account, { resetCreatedAt: request.data.resetCreatedAt, resetScanningEnabled: request.data.resetScanningEnabled, + passphrase: request.data.passphrase, }) request.end() diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index dccff9684d..4314a13677 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -7917,5 +7917,98 @@ "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 + } + } ] } \ No newline at end of file diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index d466dec5aa..a1dd9ed8c2 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -2421,6 +2421,48 @@ 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('should throw an error if the wallet is encrypted and the passphrase is incorrect', 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, { passphrase: 'incorrect' }), + ).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.resetAccount(account, { passphrase }) + + const newAccount = node.wallet.getAccountByName(account.name) + Assert.isNotNull(newAccount) + + const encryptedAccount = node.wallet.encryptedAccountById.get(newAccount.id) + Assert.isNotUndefined(encryptedAccount) + + const decryptedAccount = encryptedAccount.decrypt(passphrase) + expect(decryptedAccount.name).toEqual(account.name) + expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) + }) }) describe('getTransactionType', () => { diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index e5935b0b07..20d5e82896 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -352,16 +352,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 { @@ -1517,6 +1519,7 @@ export class Wallet { options?: { resetCreatedAt?: boolean resetScanningEnabled?: boolean + passphrase?: string }, tx?: IDatabaseTransaction, ): Promise { @@ -1534,7 +1537,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.isNotUndefined(options?.passphrase) + const encryptedAccount = await this.walletDb.setEncryptedAccount( + newAccount, + options.passphrase, + tx, + ) + this.encryptedAccountById.set(newAccount.id, encryptedAccount) + } else { + await this.walletDb.setAccount(newAccount, tx) + } if (newAccount.createdAt !== null) { const previousBlock = await this.chainGetBlock({ diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index ed9a591d5d..c7a623b85e 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -372,8 +372,8 @@ export class WalletDB { account: Account, passphrase: string, tx?: IDatabaseTransaction, - ): Promise { - await this.db.withTransaction(tx, async (tx) => { + ): 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') @@ -401,6 +401,8 @@ export class WalletDB { tx, ) } + + return encryptedAccount }) } From df7be6f1eb932be03d19589505d64a031b20b804 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:31:31 -0400 Subject: [PATCH 086/114] feat(ironfish): Require passphrase when creating encrypted account (#5357) --- ironfish/src/rpc/routes/wallet/create.ts | 4 +- .../src/rpc/routes/wallet/createAccount.ts | 7 +- .../__fixtures__/wallet.test.ts.fixture | 93 +++++++++++++++++++ ironfish/src/wallet/wallet.test.ts | 46 +++++++++ ironfish/src/wallet/wallet.ts | 17 +++- 5 files changed, 162 insertions(+), 5 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index 18738ebd4f..bba982d93b 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -30,7 +30,9 @@ routes.register( ) } - const account = await context.wallet.createAccount(name) + const account = await context.wallet.createAccount(name, { + passphrase: request.data.passphrase, + }) if (context.wallet.nodeClient) { void context.wallet.scan() } diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index 4a9fb5e32a..8e1566d066 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.ts @@ -18,7 +18,7 @@ import { AssertHasRpcContext } from '../rpcContext' * Hence, we're adding a new createAccount endpoint and will eventually sunset the create endpoint. */ -export type CreateAccountRequest = { name: string; default?: boolean } +export type CreateAccountRequest = { name: string; default?: boolean; passphrase?: string } export type CreateAccountResponse = { name: string publicAddress: string @@ -29,6 +29,7 @@ export const CreateAccountRequestSchema: yup.ObjectSchema .object({ name: yup.string().defined(), default: yup.boolean().optional(), + passphrase: yup.string().optional(), }) .defined() @@ -48,7 +49,9 @@ routes.register( let account try { - account = await context.wallet.createAccount(request.data.name) + account = await context.wallet.createAccount(request.data.name, { + passphrase: request.data.passphrase, + }) } catch (e) { if (e instanceof DuplicateAccountNameError) { throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) diff --git a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture index 4314a13677..ce480f7ba5 100644 --- a/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture +++ b/ironfish/src/wallet/__fixtures__/wallet.test.ts.fixture @@ -8010,5 +8010,98 @@ "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/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index a1dd9ed8c2..44ebfcd7ca 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -22,6 +22,7 @@ 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, @@ -1053,6 +1054,51 @@ describe('Wallet', () => { '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 throw an error if the wallet is encrypted and an incorrect 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', { passphrase: 'incorrect ' }), + ).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) + + const account = await node.wallet.createAccount('B', { passphrase }) + + const accountValue = await node.wallet.walletDb.accounts.get(account.id) + Assert.isNotUndefined(accountValue) + Assert.isTrue(accountValue.encrypted) + + const encryptedAccount = new EncryptedAccount({ + data: accountValue.data, + walletDb: node.wallet.walletDb, + }) + const decryptedAccount = encryptedAccount.decrypt(passphrase) + + expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) + expect(decryptedAccount.name).toEqual(account.name) + }) }) describe('removeAccount', () => { diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 20d5e82896..6047b23add 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1335,7 +1335,7 @@ export class Wallet { async createAccount( name: string, - options: { createdAt?: HeadValue | null; setDefault?: boolean } = { + options: { createdAt?: HeadValue | null; setDefault?: boolean; passphrase?: string } = { setDefault: false, }, ): Promise { @@ -1379,7 +1379,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.isNotUndefined(options.passphrase) + const encryptedAccount = await this.walletDb.setEncryptedAccount( + account, + options.passphrase, + tx, + ) + this.encryptedAccountById.set(account.id, encryptedAccount) + } else { + await this.walletDb.setAccount(account, tx) + } + await account.updateHead(createdAt, tx) }) From 22db8b35cea6e2af06b3f3c312327a3ebc4d56e1 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:32:23 -0400 Subject: [PATCH 087/114] feat(ironfish): Require passphrase when renaming encrypted accounts (#5355) --- ironfish/src/rpc/routes/wallet/rename.ts | 2 +- .../src/rpc/routes/wallet/renameAccount.ts | 5 +- .../__fixtures__/account.test.ts.fixture | 93 +++++++++++++++++++ ironfish/src/wallet/account/account.test.ts | 44 +++++++++ ironfish/src/wallet/account/account.ts | 15 ++- 5 files changed, 154 insertions(+), 5 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/rename.ts b/ironfish/src/rpc/routes/wallet/rename.ts index 760c00124f..f0461f4bef 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 account.setName(request.data.newName, { passphrase: request.data.passphrase }) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/renameAccount.ts b/ironfish/src/rpc/routes/wallet/renameAccount.ts index 5268ea4430..05f4761020 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.ts @@ -7,13 +7,14 @@ import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' -export type RenameAccountRequest = { account: string; newName: string } +export type RenameAccountRequest = { account: string; newName: string; passphrase?: string } export type RenameAccountResponse = undefined export const RenameAccountRequestSchema: yup.ObjectSchema = yup .object({ account: yup.string().defined(), newName: yup.string().defined(), + passphrase: yup.string().optional(), }) .defined() @@ -28,7 +29,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName) + await account.setName(request.data.newName, { passphrase: request.data.passphrase }) request.end() }, ) diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index dd43f15737..351defc450 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5903,5 +5903,98 @@ "sequence": 1 } } + ], + "Accounts setName should throw an error if the passphrase is missing and the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "6f0698b4-a99c-46aa-9391-59f0c55cd755", + "name": "accountA", + "spendingKey": "89001fcdef6bff7e9fd76d4ae6275bf6786afc2797eded9df094ae4a6894782d", + "viewKey": "389bc77ae499f3edc0dc445a732add6f36c275b260efe2c791cc515f6c2c0cd75f5b31e9b1f82edb0ee57b9304ece90d48f43c36d78660b04960b9692485d058", + "incomingViewKey": "fdd70ff012b4e48576bdd71207ebe1e3747811c77fa1a25862c3b869123ce007", + "outgoingViewKey": "9664b763a0418a476d072c715fcbbba58bcaf60cc951ba017195d75453131f11", + "publicAddress": "14a9bfb247dcf632f85ff79ebef222cc9ccf364f9b3e3e0ee39b75d68f80782a", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "8c11ae2523136f4b11bada56d9bcab2b6591cc99cf7aede8a238c5fefd7fce0d" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ], + "Accounts setName should throw an error if the passphrase is incorrect and the wallet is encrypted": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "92b122e5-9f14-453b-a364-e20a7d107305", + "name": "accountA", + "spendingKey": "3d68ebfd3d600792fc94d583bbab97ab4d02b9f41aa2a6655e151e41d4a33d8d", + "viewKey": "51e699920432cf221351568ade21fb8af4110c7ad57ad90cc2664340d50d6f207a19eb846feb76588f467e4dce99d0da074ca680aca11d3d8c91bd47ae5d9081", + "incomingViewKey": "15f8cb20e3a7b494474f4c4737bc0403dbb5de6e532e2c83a4469cd7f95f5b02", + "outgoingViewKey": "fc1b2e0e85cca6ddaf657baf2c69c76b403afe1adadd73b794c5cb81724d240d", + "publicAddress": "899ea1e9be7202aedab0a41f6b9cf661ce20e41ba11c1ee9d15ac64d8c96a391", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "d0d08a05953a50a0ce8b35e0c410b7dde77e3bf6099aaa4fc413de2096f7ef0a" + }, + "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": "1dd7e196-ffed-4fe0-8dbd-c49f82bf44b7", + "name": "accountA", + "spendingKey": "71ec323628c95bd56df353ec444b8ceb3d463603e82a89d4ac3336bdf630993a", + "viewKey": "5219abc719ecb8954e77d89e60f4fc82588e8f619ba4da38b53ed0471aeccb200cc7cec29ca533c769c96213417f78ccfaf2ba3f12a4b948a0da242805eb044c", + "incomingViewKey": "d9d70e59490b44c078300a23376e4fc516b47a31231c77538d17740f399e3d00", + "outgoingViewKey": "e3eca4b31e0d4b48e27458db2eff35b4d713d84b0b07505fdc8d49b2e40ab69d", + "publicAddress": "fbc47fe75ef9534b28b95fd17288488b89a4b752191a323a3703520095e8c24b", + "createdAt": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + }, + "scanningEnabled": true, + "proofAuthorizingKey": "f335356ed8f77c4facebc2ad9377ab05e6d71ec83aa772df0c23e2733458a10c" + }, + "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 695ee3c8ad..9701a0b410 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -19,6 +19,7 @@ import { import { AsyncUtils } from '../../utils/async' import { BalanceValue } from '../walletdb/balanceValue' import { Account } from './account' +import { EncryptedAccount } from './encryptedAccount' describe('Accounts', () => { const nodeTest = createNodeTest() @@ -186,6 +187,49 @@ describe('Accounts', () => { 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 the passphrase is incorrect 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', { passphrase: 'incorrect ' })).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 account.setName(newName, { passphrase }) + + const accountValue = await node.wallet.walletDb.accounts.get(account.id) + Assert.isNotUndefined(accountValue) + Assert.isTrue(accountValue.encrypted) + + const encryptedAccount = new EncryptedAccount({ + data: accountValue.data, + walletDb: node.wallet.walletDb, + }) + const decryptedAccount = encryptedAccount.decrypt(passphrase) + + expect(decryptedAccount.name).toEqual(newName) + }) }) describe('loadPendingTransactions', () => { diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index d894187205..0c47c5160b 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -127,14 +127,25 @@ export class Account { } } - async setName(name: string, tx?: IDatabaseTransaction): Promise { + async setName( + name: string, + options?: { passphrase?: string }, + 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?.passphrase) + await this.walletDb.setEncryptedAccount(this, options.passphrase, tx) + } else { + await this.walletDb.setAccount(this, tx) + } } async *getNotes( From a2740dcf99971234bbdd299ce6e12a9d08ca5c13 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:39:40 -0400 Subject: [PATCH 088/114] feat(cli): Check if the wallet is unlocked in wallet CLI commands (#5358) --- ironfish-cli/src/commands/wallet/address.ts | 1 + ironfish-cli/src/commands/wallet/assets.ts | 3 ++- ironfish-cli/src/commands/wallet/balance.ts | 1 + ironfish-cli/src/commands/wallet/balances.ts | 3 ++- ironfish-cli/src/commands/wallet/burn.ts | 1 + .../src/commands/wallet/chainport/send.ts | 1 + ironfish-cli/src/commands/wallet/create.ts | 3 ++- ironfish-cli/src/commands/wallet/decrypt.ts | 2 +- ironfish-cli/src/commands/wallet/delete.ts | 1 + ironfish-cli/src/commands/wallet/export.ts | 3 ++- ironfish-cli/src/commands/wallet/import.ts | 3 ++- ironfish-cli/src/commands/wallet/index.ts | 1 + ironfish-cli/src/commands/wallet/mint.ts | 1 + .../wallet/multisig/account/participants.ts | 2 ++ .../wallet/multisig/commitment/aggregate.ts | 5 +++-- .../wallet/multisig/commitment/create.ts | 4 +++- .../commands/wallet/multisig/dealer/create.ts | 5 +++-- .../src/commands/wallet/multisig/dkg/round1.ts | 1 + .../src/commands/wallet/multisig/dkg/round2.ts | 1 + .../src/commands/wallet/multisig/dkg/round3.ts | 1 + .../wallet/multisig/participant/create.ts | 11 +++++++---- .../wallet/multisig/participant/index.ts | 2 ++ .../wallet/multisig/participants/index.ts | 6 ++++-- .../wallet/multisig/signature/aggregate.ts | 5 +++-- .../wallet/multisig/signature/create.ts | 5 +++-- .../src/commands/wallet/notes/combine.ts | 1 + .../src/commands/wallet/notes/index.ts | 7 ++++--- ironfish-cli/src/commands/wallet/prune.ts | 9 +++++++++ ironfish-cli/src/commands/wallet/rename.ts | 2 ++ ironfish-cli/src/commands/wallet/rescan.ts | 3 ++- ironfish-cli/src/commands/wallet/reset.ts | 3 ++- .../src/commands/wallet/scanning/off.ts | 2 ++ .../src/commands/wallet/scanning/on.ts | 2 ++ ironfish-cli/src/commands/wallet/send.ts | 1 + ironfish-cli/src/commands/wallet/status.ts | 1 + .../src/commands/wallet/transactions.ts | 3 ++- .../src/commands/wallet/transactions/decode.ts | 1 + .../src/commands/wallet/transactions/delete.ts | 2 ++ .../src/commands/wallet/transactions/import.ts | 5 ++++- .../src/commands/wallet/transactions/info.ts | 1 + .../src/commands/wallet/transactions/post.ts | 5 +++-- .../src/commands/wallet/transactions/sign.ts | 1 + .../src/commands/wallet/transactions/watch.ts | 2 ++ ironfish-cli/src/commands/wallet/unlock.ts | 2 +- ironfish-cli/src/commands/wallet/use.ts | 3 +++ ironfish-cli/src/commands/wallet/which.ts | 2 ++ ironfish-cli/src/ui/index.ts | 1 + ironfish-cli/src/ui/wallet.ts | 18 ++++++++++++++++++ 48 files changed, 118 insertions(+), 31 deletions(-) create mode 100644 ironfish-cli/src/ui/wallet.ts diff --git a/ironfish-cli/src/commands/wallet/address.ts b/ironfish-cli/src/commands/wallet/address.ts index 4a42444f5f..349d5932ae 100644 --- a/ironfish-cli/src/commands/wallet/address.ts +++ b/ironfish-cli/src/commands/wallet/address.ts @@ -29,6 +29,7 @@ export class AddressCommand extends IronfishCommand { const { args } = await this.parse(AddressCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const response = await client.wallet.getAccountPublicKey({ account: args.account, diff --git a/ironfish-cli/src/commands/wallet/assets.ts b/ironfish-cli/src/commands/wallet/assets.ts index 3f52d25a54..32adc0de6a 100644 --- a/ironfish-cli/src/commands/wallet/assets.ts +++ b/ironfish-cli/src/commands/wallet/assets.ts @@ -12,7 +12,7 @@ import { BufferUtils } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableFlags } from '../../ui' +import { checkWalletUnlocked, table, TableFlags } from '../../ui' import { renderAssetWithVerificationStatus, useAccount } from '../../utils' import { TableCols } from '../../utils/table' @@ -38,6 +38,7 @@ export class AssetsCommand extends IronfishCommand { const { flags } = await this.parse(AssetsCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/balance.ts b/ironfish-cli/src/commands/wallet/balance.ts index 28c28945ed..5a0062e311 100644 --- a/ironfish-cli/src/commands/wallet/balance.ts +++ b/ironfish-cli/src/commands/wallet/balance.ts @@ -53,6 +53,7 @@ Balance is your coins from all of your transactions, even if they are on forks o const { flags } = await this.parse(BalanceCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/balances.ts b/ironfish-cli/src/commands/wallet/balances.ts index adbcee3e24..74abc0971b 100644 --- a/ironfish-cli/src/commands/wallet/balances.ts +++ b/ironfish-cli/src/commands/wallet/balances.ts @@ -5,7 +5,7 @@ import { BufferUtils, CurrencyUtils, GetBalancesResponse, RpcAsset } from '@iron import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableColumns, TableFlags } from '../../ui' +import { checkWalletUnlocked, table, TableColumns, TableFlags } from '../../ui' import { compareAssets, renderAssetWithVerificationStatus, useAccount } from '../../utils' type AssetBalancePairs = { asset: RpcAsset; balance: GetBalancesResponse['balances'][number] } @@ -32,6 +32,7 @@ export class BalancesCommand extends IronfishCommand { async start(): Promise { const { flags } = await this.parse(BalancesCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/burn.ts b/ironfish-cli/src/commands/wallet/burn.ts index b3993342f4..1ee14c9777 100644 --- a/ironfish-cli/src/commands/wallet/burn.ts +++ b/ironfish-cli/src/commands/wallet/burn.ts @@ -92,6 +92,7 @@ This will destroy tokens and decrease supply for a given asset.` 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() diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 9a0b086d60..879839afc2 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -80,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 diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index 003811ec63..db1c1a40c0 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -5,7 +5,7 @@ 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` @@ -30,6 +30,7 @@ export class CreateCommand extends IronfishCommand { } const client = await this.connectRpc() + await checkWalletUnlocked(client) this.log(`Creating account ${name}`) const result = await client.wallet.createAccount({ name }) diff --git a/ironfish-cli/src/commands/wallet/decrypt.ts b/ironfish-cli/src/commands/wallet/decrypt.ts index 4eedbcba31..177fc07ef6 100644 --- a/ironfish-cli/src/commands/wallet/decrypt.ts +++ b/ironfish-cli/src/commands/wallet/decrypt.ts @@ -32,7 +32,7 @@ export class DecryptCommand extends IronfishCommand { let passphrase = flags.passphrase if (!passphrase) { - passphrase = await inputPrompt('Enter a passphrase to decrypt the wallet', true, { + passphrase = await inputPrompt('Enter your passphrase to decrypt the wallet', true, { password: true, }) } diff --git a/ironfish-cli/src/commands/wallet/delete.ts b/ironfish-cli/src/commands/wallet/delete.ts index b2da47e60e..1dd2083347 100644 --- a/ironfish-cli/src/commands/wallet/delete.ts +++ b/ironfish-cli/src/commands/wallet/delete.ts @@ -34,6 +34,7 @@ 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 }) diff --git a/ironfish-cli/src/commands/wallet/export.ts b/ironfish-cli/src/commands/wallet/export.ts index 5dc1033600..5a891d9c3a 100644 --- a/ironfish-cli/src/commands/wallet/export.ts +++ b/ironfish-cli/src/commands/wallet/export.ts @@ -7,7 +7,7 @@ import fs from 'fs' import path from 'path' import { IronfishCommand } from '../../command' import { EnumLanguageKeyFlag, JsonFlags, RemoteFlags } from '../../flags' -import { confirmOrQuit } from '../../ui' +import { checkWalletUnlocked, confirmOrQuit } from '../../ui' import { useAccount } from '../../utils' export class ExportCommand extends IronfishCommand { @@ -57,6 +57,7 @@ export class ExportCommand extends IronfishCommand { : AccountFormat.Base64Json const client = await this.connectRpc(local) + await checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 1d2fe158c0..862c0684ce 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -10,7 +10,7 @@ import { import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { inputPrompt } from '../../ui' +import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { Ledger } from '../../utils/ledger' @@ -52,6 +52,7 @@ export class ImportCommand extends IronfishCommand { const { blob } = args const client = await this.connectRpc() + await checkWalletUnlocked(client) let account: string diff --git a/ironfish-cli/src/commands/wallet/index.ts b/ironfish-cli/src/commands/wallet/index.ts index 62d5f2bfa2..92ad0dc869 100644 --- a/ironfish-cli/src/commands/wallet/index.ts +++ b/ironfish-cli/src/commands/wallet/index.ts @@ -22,6 +22,7 @@ export class AccountsCommand extends IronfishCommand { const { flags } = await this.parse(AccountsCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const response = await client.wallet.getAccountsStatus() diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 4d4b1c4490..9981524248 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -107,6 +107,7 @@ This will create tokens and increase supply for a given asset.` 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() diff --git a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts index ffc13e194c..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` @@ -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 e57f49d0be..5ed54ff9e2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/aggregate.ts @@ -38,6 +38,9 @@ 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 ui.longPrompt('Enter the unsigned transaction', { @@ -54,8 +57,6 @@ export class CreateSigningPackage extends IronfishCommand { } 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 b455efc3ba..8f3a3614c1 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -44,6 +44,9 @@ 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 ui.longPrompt( @@ -67,7 +70,6 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { }) } - const client = await this.connectRpc() const unsignedTransaction = new UnsignedTransaction( Buffer.from(unsignedTransactionInput, 'hex'), ) diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index dd35237a8d..095ebc0ed7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts @@ -36,6 +36,9 @@ 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 ui.longPrompt( @@ -61,8 +64,6 @@ export class MultisigCreateDealer extends IronfishCommand { } } - const client = await this.connectRpc() - const name = await this.getCoordinatorName(client, flags.name?.trim()) const response = await client.wallet.multisig.createTrustedDealerKeyPackage({ diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 723608eded..01767a45bc 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -32,6 +32,7 @@ export class DkgRound1Command extends IronfishCommand { const { flags } = await this.parse(DkgRound1Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index 0f18e31ec3..fcb9a59f17 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -32,6 +32,7 @@ export class DkgRound2Command extends IronfishCommand { const { flags } = await this.parse(DkgRound2Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 9cdba205e0..53a0458b48 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -42,6 +42,7 @@ export class DkgRound3Command extends IronfishCommand { const { flags } = await this.parse(DkgRound3Command) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) let participantName = flags.participantName if (!participantName) { 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..d86ccb02cd 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,6 +14,8 @@ 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 = [] @@ -27,7 +29,7 @@ export class MultisigParticipants extends IronfishCommand { // sort identities by name participants.sort((a, b) => a.name.localeCompare(b.name)) - table( + ui.table( participants, { name: { diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts index d41cf1454d..463f63267b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/aggregate.ts @@ -47,6 +47,9 @@ 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 ui.longPrompt('Enter the signing package', { required: true }) @@ -63,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 176dcb0eac..a156964794 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -38,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 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(), diff --git a/ironfish-cli/src/commands/wallet/notes/combine.ts b/ironfish-cli/src/commands/wallet/notes/combine.ts index 0bcf50cc3e..e11fb33e32 100644 --- a/ironfish-cli/src/commands/wallet/notes/combine.ts +++ b/ironfish-cli/src/commands/wallet/notes/combine.ts @@ -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 diff --git a/ironfish-cli/src/commands/wallet/notes/index.ts b/ironfish-cli/src/commands/wallet/notes/index.ts index 13d014f8f6..e1ac874f94 100644 --- a/ironfish-cli/src/commands/wallet/notes/index.ts +++ b/ironfish-cli/src/commands/wallet/notes/index.ts @@ -5,11 +5,11 @@ import { CurrencyUtils, RpcAsset } from '@ironfish/sdk' 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 = `list the account's notes` @@ -28,6 +28,7 @@ export class NotesCommand extends IronfishCommand { const assetLookup: Map = new Map() const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const account = await useAccount(client, flags.account) @@ -43,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 113b16e977..6a0b16a1b8 100644 --- a/ironfish-cli/src/commands/wallet/prune.ts +++ b/ironfish-cli/src/commands/wallet/prune.ts @@ -4,6 +4,7 @@ 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 = `deletes expired transactions from the wallet` @@ -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 bd0e4760ac..84055283f1 100644 --- a/ironfish-cli/src/commands/wallet/rename.ts +++ b/ironfish-cli/src/commands/wallet/rename.ts @@ -4,6 +4,7 @@ import { Args } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' +import { checkWalletUnlocked } from '../../ui' export class RenameCommand extends IronfishCommand { static description = 'rename the name of an account' @@ -27,6 +28,7 @@ export class RenameCommand extends IronfishCommand { const { args } = await this.parse(RenameCommand) const client = await this.connectRpc() + 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 f8cc223a9a..e90470129b 100644 --- a/ironfish-cli/src/commands/wallet/rescan.ts +++ b/ironfish-cli/src/commands/wallet/rescan.ts @@ -6,7 +6,7 @@ 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 { @@ -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 09f95a61a9..3936e1d01c 100644 --- a/ironfish-cli/src/commands/wallet/reset.ts +++ b/ironfish-cli/src/commands/wallet/reset.ts @@ -5,7 +5,7 @@ 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 { @@ -35,6 +35,7 @@ export class ResetCommand extends IronfishCommand { const { flags } = await this.parse(ResetCommand) const client = await this.connectRpc() + await checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/scanning/off.ts b/ironfish-cli/src/commands/wallet/scanning/off.ts index 63825620eb..2d0ddee970 100644 --- a/ironfish-cli/src/commands/wallet/scanning/off.ts +++ b/ironfish-cli/src/commands/wallet/scanning/off.ts @@ -4,6 +4,7 @@ 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 @@ -26,6 +27,7 @@ The wallet will no longer scan the blockchain for new account transactions.` 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 028be4495a..8d350d28a9 100644 --- a/ironfish-cli/src/commands/wallet/scanning/on.ts +++ b/ironfish-cli/src/commands/wallet/scanning/on.ts @@ -4,6 +4,7 @@ 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 @@ -26,6 +27,7 @@ Scanning is on by default. The wallet will scan the blockchain for new account t 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 8cdf302f85..66107f8355 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -117,6 +117,7 @@ export class Send extends IronfishCommand { let to = flags.to const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) if (!flags.offline) { const status = await client.wallet.getNodeStatus() diff --git a/ironfish-cli/src/commands/wallet/status.ts b/ironfish-cli/src/commands/wallet/status.ts index 9a9bebaad0..7582216e71 100644 --- a/ironfish-cli/src/commands/wallet/status.ts +++ b/ironfish-cli/src/commands/wallet/status.ts @@ -19,6 +19,7 @@ export class StatusCommand extends IronfishCommand { await this.parse(StatusCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const [nodeStatus, walletStatus] = await Promise.all([ client.node.getStatus(), diff --git a/ironfish-cli/src/commands/wallet/transactions.ts b/ironfish-cli/src/commands/wallet/transactions.ts index f9e062d42a..07fdedbdb7 100644 --- a/ironfish-cli/src/commands/wallet/transactions.ts +++ b/ironfish-cli/src/commands/wallet/transactions.ts @@ -13,7 +13,7 @@ import { import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' -import { table, TableColumns, TableFlags } from '../../ui' +import { checkWalletUnlocked, table, TableColumns, TableFlags } from '../../ui' import { getAssetsByIDs, useAccount } from '../../utils' import { extractChainportDataFromTransaction } from '../../utils/chainport' import { Format, TableCols } from '../../utils/table' @@ -64,6 +64,7 @@ export class TransactionsCommand extends IronfishCommand { : Format.cli const client = await this.connectRpc() + await checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/transactions/decode.ts b/ironfish-cli/src/commands/wallet/transactions/decode.ts index 8aa1f00512..6708890474 100644 --- a/ironfish-cli/src/commands/wallet/transactions/decode.ts +++ b/ironfish-cli/src/commands/wallet/transactions/decode.ts @@ -39,6 +39,7 @@ export class TransactionsDecodeCommand extends IronfishCommand { const { flags } = await this.parse(TransactionsDecodeCommand) const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const account = flags.account ?? (await ui.accountPrompt(client)) diff --git a/ironfish-cli/src/commands/wallet/transactions/delete.ts b/ironfish-cli/src/commands/wallet/transactions/delete.ts index ab448ca688..f236579172 100644 --- a/ironfish-cli/src/commands/wallet/transactions/delete.ts +++ b/ironfish-cli/src/commands/wallet/transactions/delete.ts @@ -3,6 +3,7 @@ * 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' @@ -19,6 +20,7 @@ export default class TransactionsDelete extends IronfishCommand { const { transaction } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const response = await client.wallet.deleteTransaction({ hash: transaction }) diff --git a/ironfish-cli/src/commands/wallet/transactions/import.ts b/ironfish-cli/src/commands/wallet/transactions/import.ts index 413b497096..a98ef438fe 100644 --- a/ironfish-cli/src/commands/wallet/transactions/import.ts +++ b/ironfish-cli/src/commands/wallet/transactions/import.ts @@ -4,6 +4,7 @@ import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' import { importFile, importPipe, longPrompt } from '../../../ui/longPrompt' export class TransactionsImportCommand extends IronfishCommand { @@ -42,6 +43,9 @@ export class TransactionsImportCommand 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 TransactionsImportCommand 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/transactions/info.ts b/ironfish-cli/src/commands/wallet/transactions/info.ts index 74467b8e33..ee816378ef 100644 --- a/ironfish-cli/src/commands/wallet/transactions/info.ts +++ b/ironfish-cli/src/commands/wallet/transactions/info.ts @@ -47,6 +47,7 @@ export class TransactionInfoCommand extends IronfishCommand { const { transaction: hash } = args const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) const account = await useAccount(client, flags.account) diff --git a/ironfish-cli/src/commands/wallet/transactions/post.ts b/ironfish-cli/src/commands/wallet/transactions/post.ts index bf00fceb2d..e5a0412a95 100644 --- a/ironfish-cli/src/commands/wallet/transactions/post.ts +++ b/ironfish-cli/src/commands/wallet/transactions/post.ts @@ -48,6 +48,9 @@ export class TransactionsPostCommand extends IronfishCommand { 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 ui.longPrompt('Enter the raw transaction in hex encoding', { required: true, @@ -57,8 +60,6 @@ export class TransactionsPostCommand 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') diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 0e67d59735..87b57ca0b8 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.ts @@ -39,6 +39,7 @@ export class TransactionsSignCommand extends IronfishCommand { async start(): Promise { 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') diff --git a/ironfish-cli/src/commands/wallet/transactions/watch.ts b/ironfish-cli/src/commands/wallet/transactions/watch.ts index d64826eb10..fda93437c5 100644 --- a/ironfish-cli/src/commands/wallet/transactions/watch.ts +++ b/ironfish-cli/src/commands/wallet/transactions/watch.ts @@ -4,6 +4,7 @@ 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 TransactionsWatchCommand extends IronfishCommand { @@ -39,6 +40,7 @@ export class TransactionsWatchCommand extends IronfishCommand { 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 index 784f93fb85..bf874f11c6 100644 --- a/ironfish-cli/src/commands/wallet/unlock.ts +++ b/ironfish-cli/src/commands/wallet/unlock.ts @@ -36,7 +36,7 @@ export class UnlockCommand extends IronfishCommand { let passphrase = flags.passphrase if (!passphrase) { - passphrase = await inputPrompt('Enter a passphrase to unlock the wallet', true, { + passphrase = await inputPrompt('Enter your passphrase to unlock the wallet', true, { password: true, }) } diff --git a/ironfish-cli/src/commands/wallet/use.ts b/ironfish-cli/src/commands/wallet/use.ts index ea00f39d8c..be31581e1b 100644 --- a/ironfish-cli/src/commands/wallet/use.ts +++ b/ironfish-cli/src/commands/wallet/use.ts @@ -4,6 +4,7 @@ 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 wallet account' @@ -31,6 +32,8 @@ export class UseCommand extends IronfishCommand { } const client = await this.connectRpc() + await checkWalletUnlocked(client) + await client.wallet.useAccount({ account }) if (account == null) { this.log('The default account has been unset') diff --git a/ironfish-cli/src/commands/wallet/which.ts b/ironfish-cli/src/commands/wallet/which.ts index e69e05ed62..c00b459549 100644 --- a/ironfish-cli/src/commands/wallet/which.ts +++ b/ironfish-cli/src/commands/wallet/which.ts @@ -4,6 +4,7 @@ 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 default wallet account @@ -24,6 +25,7 @@ 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) { diff --git a/ironfish-cli/src/ui/index.ts b/ironfish-cli/src/ui/index.ts index 56d39b3e1f..9ae9ad0931 100644 --- a/ironfish-cli/src/ui/index.ts +++ b/ironfish-cli/src/ui/index.ts @@ -9,3 +9,4 @@ export * from './progressBar' export * from './prompt' export * from './prompts' export * from './table' +export * from './wallet' 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 }) +} From 18f1beae597949f595d12070130ebb77ced69aeb Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:24:51 -0400 Subject: [PATCH 089/114] feat(rust): Add struct for XChaCha20Poly1305 key (#5365) --- Cargo.lock | 10 ++ ironfish-rust/Cargo.toml | 2 + ironfish-rust/src/errors.rs | 1 + ironfish-rust/src/xchacha20poly1305.rs | 175 +++++++++++++++++++++---- supply-chain/config.toml | 4 + 5 files changed, 168 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8502cec164..5db7af30e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1335,6 +1335,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" @@ -1497,6 +1506,7 @@ dependencies = [ "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)", diff --git a/ironfish-rust/Cargo.toml b/ironfish-rust/Cargo.toml index a25a9aa253..2ecfa87512 100644 --- a/ironfish-rust/Cargo.toml +++ b/ironfish-rust/Cargo.toml @@ -53,6 +53,8 @@ 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 f582a44898..0de86e44fc 100644 --- a/ironfish-rust/src/errors.rs +++ b/ironfish-rust/src/errors.rs @@ -32,6 +32,7 @@ pub enum IronfishErrorKind { FailedSignatureVerification, FailedXChaCha20Poly1305Decryption, FailedXChaCha20Poly1305Encryption, + FailedHkdfExpansion, IllegalValue, InconsistentWitness, InvalidAssetIdentifier, diff --git a/ironfish-rust/src/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs index 5deabd5507..e8af9077ab 100644 --- a/ironfish-rust/src/xchacha20poly1305.rs +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -4,21 +4,140 @@ use std::io; +use argon2::RECOMMENDED_SALT_LEN; use argon2::{password_hash::SaltString, Argon2}; 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}; -const KEY_LENGTH: usize = 32; -const NONCE_LENGTH: usize = 24; +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)) + } +} #[derive(Debug)] pub struct EncryptOutput { pub salt: Vec, - pub nonce: [u8; NONCE_LENGTH], + pub nonce: [u8; XNONCE_LENGTH], pub ciphertext: Vec, } @@ -46,7 +165,7 @@ impl EncryptOutput { let mut salt = vec![0u8; salt_len]; reader.read_exact(&mut salt)?; - let mut nonce = [0u8; NONCE_LENGTH]; + let mut nonce = [0u8; XNONCE_LENGTH]; reader.read_exact(&mut nonce)?; let mut ciphertext_len = [0u8; 4]; @@ -88,7 +207,7 @@ pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result = vec![]; - encrypted_output - .write(&mut vec) - .expect("should serialize successfully"); + let encryption_key = XChaCha20Poly1305Key::generate(passphrase.as_bytes()) + .expect("should successfully generate key"); - let deserialized = EncryptOutput::read(&vec[..]).expect("should deserialize successfully"); + 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!(encrypted_output, deserialized); + assert_eq!(key.key, derived_key); } } diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 335af46a63..95775df76f 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -390,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" From 1b640a20ab0ee5cfbb06c205c46bb7296ad7fb52 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:36:50 -0700 Subject: [PATCH 090/114] defines MultisigHardwareSigner for Ledger multisig keys (#5366) a multisig account generated using a Ledger device will have a access to the participant identity, but not the secret or keyPackage uses a separate interface for MultisigHardwareSigner to cover this case and expands the MultisigKeys type to cover this interface the distinct interface allows us to store multisig keys for the Ledger case without a database migration for existing multisig keys --- .../src/wallet/interfaces/multisigKeys.ts | 7 +++++- .../src/wallet/walletdb/multiSigKeys.test.ts | 20 +++++++++++++++- ironfish/src/wallet/walletdb/multisigKeys.ts | 24 ++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) 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/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/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 { From 1b777bf9c08abd954bac77f85149bbd9acb85e4f Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:16:22 -0700 Subject: [PATCH 091/114] dont parse this diagram as a doctest (#5368) --- ironfish-rust/src/frost_utils/account_keys.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-rust/src/frost_utils/account_keys.rs b/ironfish-rust/src/frost_utils/account_keys.rs index bb5380ce39..84124647cb 100644 --- a/ironfish-rust/src/frost_utils/account_keys.rs +++ b/ironfish-rust/src/frost_utils/account_keys.rs @@ -23,7 +23,7 @@ pub struct MultisigAccountKeys { /// Derives the account keys for a multisig account, realizing the following key hierarchy: /// -/// ``` +/// ```ignore /// ak ─┐ /// ├─ ivk ── pk /// gsk ── nsk ── nk ─┘ From b3d1e72d243be3478cb90f33546f11b42a90583c Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 12 Sep 2024 14:03:33 -0700 Subject: [PATCH 092/114] make secret optional in multisig secrets We need to make the secret field optional to accommodate the ledger device usecase. The secret never leaves the device. We can write a migration where we add a flag that indicates if the secret exists or not. But we thought of an alternative that doesn't require a migration - store an empty buffer if the secret doesn't exist. When deserializing, if the buffer is all zeros, then you know the secret is undefined. --- .../rpc/routes/wallet/exportAccount.test.ts | 13 +++-- .../rpc/routes/wallet/importAccount.test.ts | 2 +- .../wallet/multisig/createParticipant.test.ts | 10 ++-- .../routes/wallet/multisig/dkg/round1.test.ts | 2 +- .../rpc/routes/wallet/multisig/dkg/round1.ts | 2 +- .../rpc/routes/wallet/multisig/dkg/round2.ts | 2 +- .../rpc/routes/wallet/multisig/dkg/round3.ts | 2 +- .../routes/wallet/multisig/getIdentities.ts | 2 +- .../rpc/routes/wallet/multisig/getIdentity.ts | 7 ++- ironfish/src/wallet/exporter/encryption.ts | 6 ++- ironfish/src/wallet/wallet.test.slow.ts | 4 +- ironfish/src/wallet/wallet.ts | 8 +-- .../wallet/walletdb/multisigIdentityValue.ts | 54 +++++++++++++++++++ .../wallet/walletdb/multisigSecretValue.ts | 34 ------------ ironfish/src/wallet/walletdb/walletdb.test.ts | 4 +- ironfish/src/wallet/walletdb/walletdb.ts | 42 ++++++++------- 16 files changed, 112 insertions(+), 82 deletions(-) create mode 100644 ironfish/src/wallet/walletdb/multisigIdentityValue.ts delete mode 100644 ironfish/src/wallet/walletdb/multisigSecretValue.ts 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/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index 1948e81d07..8c9a8276a2 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -370,7 +370,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, }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts index 1390a84c0e..1c6cddc716 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts @@ -45,10 +45,12 @@ 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( - Buffer.from(response.content.identity, 'hex'), - ) + const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(name) Assert.isNotUndefined(secretValue) - expect(secretValue.name).toEqual(name) + + const identity = await routeTest.node.wallet.walletDb.getMultisigIdentity(name) + Assert.isNotUndefined(identity) + + expect(identity.name).toEqual(name) }) }) 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.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..79a12af078 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts @@ -45,7 +45,7 @@ routes.register( for await (const [ identity, { name }, - ] of context.wallet.walletDb.multisigSecrets.getAllIter()) { + ] of context.wallet.walletDb.multisigIdentities.getAllIter()) { identities.push({ name, identity: identity.toString('hex'), diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts index 59c0d16325..bed99978fb 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts @@ -36,13 +36,12 @@ routes.register( const { name } = request.data - const record = await context.wallet.walletDb.getMultisigSecretByName(name) - if (record === undefined) { + const secret = await context.wallet.walletDb.getMultisigSecretByName(name) + if (secret === undefined) { throw new RpcValidationError(`No identity found with name ${name}`, 404) } - const secret = new multisig.ParticipantSecret(record.secret) - const identity = secret.toIdentity() + const identity = new multisig.ParticipantSecret(secret).toIdentity() request.end({ identity: identity.serialize().toString('hex') }) }, diff --git a/ironfish/src/wallet/exporter/encryption.ts b/ironfish/src/wallet/exporter/encryption.ts index 9e1a293117..eb6f0430d5 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) 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.ts b/ironfish/src/wallet/wallet.ts index 6047b23add..019aa81415 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1421,17 +1421,17 @@ export class Wallet { accountValue.multisigKeys && isMultisigSignerTrustedDealerImport(accountValue.multisigKeys) ) { - const multisigSecret = await this.walletDb.getMultisigSecret( + const multisigIdentity = await this.walletDb.getMultisigIdentity( Buffer.from(accountValue.multisigKeys.identity, 'hex'), ) - if (!multisigSecret) { + 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'), + secret: multisigIdentity.secret.toString('hex'), } } @@ -1856,7 +1856,7 @@ export class Wallet { const secret = multisig.ParticipantSecret.random() const identity = secret.toIdentity() - await this.walletDb.putMultisigSecret( + await this.walletDb.putMultisigIdentity( identity.serialize(), { name, 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/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 d1bff95abc..03d4e36e75 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -450,14 +450,14 @@ 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) }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index c7a623b85e..70b7a913ac 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -40,7 +40,7 @@ import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' import { HeadValue, NullableHeadValueEncoding } from './headValue' 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' @@ -138,9 +138,9 @@ export class WalletDB { value: null }> - multisigSecrets: IDatabaseStore<{ + multisigIdentities: IDatabaseStore<{ key: Buffer - value: MultisigSecretValue + value: MultisigIdentityValue }> participantIdentities: IDatabaseStore<{ @@ -298,10 +298,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({ @@ -1420,36 +1420,36 @@ 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 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 } } @@ -1460,8 +1460,10 @@ export class WalletDB { return (await this.getMultisigSecretByName(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 } } From 76748880ba7eef065db56ece18de7722e8bd4e3b Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 12 Sep 2024 17:10:11 -0700 Subject: [PATCH 093/114] fix test --- .../routes/wallet/multisig/createParticipant.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts index 1c6cddc716..546ba09e0d 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/createParticipant.test.ts @@ -45,12 +45,10 @@ describe('Route wallet/multisig/createParticipant', () => { const name = 'identity' const response = await routeTest.client.wallet.multisig.createParticipant({ name }) - const secretValue = await routeTest.node.wallet.walletDb.getMultisigSecretByName(name) + const secretValue = await routeTest.node.wallet.walletDb.getMultisigIdentity( + Buffer.from(response.content.identity, 'hex'), + ) Assert.isNotUndefined(secretValue) - - const identity = await routeTest.node.wallet.walletDb.getMultisigIdentity(name) - Assert.isNotUndefined(identity) - - expect(identity.name).toEqual(name) + expect(secretValue.name).toEqual(name) }) }) From d8cfd5b46b9fb063ac72c1412c51a36c7f83d5b6 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:00:46 -0700 Subject: [PATCH 094/114] supports importing multisig Ledger accounts (#5367) handles import of accounts where multisigKeys contains an identity, but no keyPackage adds json test case --- .../__importTestCases__/2p6p0_json_multisig.txt | 1 + ironfish/src/rpc/routes/wallet/serializers.ts | 8 ++++++++ ironfish/src/wallet/exporter/accountImport.ts | 3 ++- ironfish/src/wallet/exporter/multisig.ts | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 ironfish/src/rpc/routes/wallet/__importTestCases__/2p6p0_json_multisig.txt 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/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/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/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 } From 5ea1113d9a4cc17cc35dad3557a9de4e2df47c66 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:20:20 -0700 Subject: [PATCH 095/114] updates ironfish to use latest ironfish-frost changes, etc. (#5369) * wip * adds helper functions to deserialize round2 public packages deserialize_round2_combined_public_package takes a serialized 'CombinedPublicPackage' from dkg round2 and returns an object containing an array of round2 public packages with all fields available as strings * dirty upgrade to frost no-std * fixes errors and warnings from error formatting throws FrostLibErrors using new_with_source to give error messages updates decryption test for new decryption error message * uses decrypt_legacy to decrypt legacy account exports we've updated encryption/decryption in the ironfish-frost crate and changed the structure of encrypted data older account exports cannot be decrypted with the current 'decrypt' method and must use 'decrypt_legacy' instead defines 'decrypt_legacy_data' on ParticipantSecret and updates account decryption to try decrypting with that method if the first decryption attempt fails * updates ironfish-frost dependency to latest commit on main * fixes rust lint in multisig.rs, removes commented-out code * broadens cargo vet audit policy for reddsa * updates cargo vet with exemptions and audits for new dependencies * updates ironfish-frost in Cargo.lock for allocation fix * uses default features from ironfish-frost * bubbles up errors as napi errors in multisig.rs instead of unwrapping * refactors derive_account_keys to return a result * removes unwrap uses * avoids mapping FrostLibErrors unnecessarily --------- Co-authored-by: Mat --- Cargo.lock | 110 +++++--- ironfish-rust-nodejs/Cargo.toml | 4 +- ironfish-rust-nodejs/index.d.ts | 52 ++-- ironfish-rust-nodejs/index.js | 4 +- ironfish-rust-nodejs/src/multisig.rs | 103 ++++++- ironfish-rust-nodejs/src/structs/note.rs | 3 +- .../src/structs/transaction.rs | 2 +- ironfish-rust/src/frost_utils/account_keys.rs | 21 +- ironfish-rust/src/frost_utils/mod.rs | 2 +- ironfish-rust/src/frost_utils/split_secret.rs | 4 +- .../src/frost_utils/split_spender_key.rs | 6 +- ironfish-rust/src/transaction/tests.rs | 3 +- ironfish-rust/src/transaction/unsigned.rs | 6 +- .../routes/wallet/multisig/dkg/round3.test.ts | 2 +- ironfish/src/wallet/exporter/encryption.ts | 6 +- supply-chain/audits.toml | 16 +- supply-chain/config.toml | 42 +-- supply-chain/imports.lock | 261 ++++++++++-------- 18 files changed, 405 insertions(+), 242 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5db7af30e1..7524426c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,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" @@ -725,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]] @@ -753,7 +753,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -774,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]] @@ -964,7 +964,7 @@ checksum = "55a9a55d1dab3b07854648d48e366f684aefe2ac78ae28cec3bf65e3cd53d9a3" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -1079,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", ] @@ -1522,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#06f16dd1684b7741f3bd6ba3e490343671626129" dependencies = [ "blake3", "chacha20 0.9.1", @@ -1580,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", ] @@ -1659,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]] @@ -1756,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", @@ -1775,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", @@ -1799,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", ] @@ -1877,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" @@ -1916,7 +1918,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -2158,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", ] @@ -2261,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", @@ -2494,7 +2496,7 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -2654,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", @@ -2720,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" @@ -2968,7 +2990,7 @@ checksum = "b3fd98999db9227cf28e59d83e1f120f42bc233d4b152e8fab9bc87d5bb1e0f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.77", ] [[package]] @@ -3264,8 +3286,6 @@ checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ "curve25519-dalek", "rand_core", - "serde", - "zeroize", ] [[package]] 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 ffa853a065..ff4a1438c2 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,9 +63,9 @@ 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 function encrypt(plaintext: Buffer, passphrase: string): Buffer -export function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer +export declare function verifyTransactions(serializedTransactions: Array): boolean +export declare function encrypt(plaintext: Buffer, passphrase: string): Buffer +export declare function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer export const enum LanguageCode { English = 0, ChineseSimplified = 1, @@ -67,13 +84,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. * @@ -87,8 +104,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 @@ -322,6 +339,7 @@ export namespace multisig { static random(): ParticipantSecret toIdentity(): ParticipantIdentity decryptData(jsBytes: Buffer): Buffer + decryptLegacyData(jsBytes: Buffer): Buffer } export class ParticipantIdentity { constructor(jsBytes: Buffer) diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index 34689156e3..7521666684 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, encrypt, decrypt, 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, encrypt, decrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = 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 diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 957676b23a..8fae0761b1 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -14,13 +14,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 +32,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 +80,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[..])) @@ -165,10 +170,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 +210,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(), + ))) } } @@ -429,7 +442,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 +465,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 +479,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/src/frost_utils/account_keys.rs b/ironfish-rust/src/frost_utils/account_keys.rs index 84124647cb..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; @@ -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/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index b68bf4e500..5190481263 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -999,7 +999,8 @@ fn test_dkg_signing() { ) .expect("round 3 failed"); - 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) + .expect("account key derivation failed"); let public_address = account_keys.public_address; // create raw/proposed 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/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/wallet/exporter/encryption.ts b/ironfish/src/wallet/exporter/encryption.ts index eb6f0430d5..4f1e6d3715 100644 --- a/ironfish/src/wallet/exporter/encryption.ts +++ b/ironfish/src/wallet/exporter/encryption.ts @@ -63,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/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 95775df76f..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] @@ -102,10 +102,6 @@ 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" @@ -190,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]] @@ -267,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]] @@ -338,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]] @@ -443,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]] @@ -463,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]] @@ -503,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]] @@ -511,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]] @@ -619,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]] @@ -735,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]] @@ -750,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 cb4acf7021..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" @@ -841,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" @@ -1168,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" @@ -1186,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" From 5ef336819be7afc78af1253fde149e9f4aea1158 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:52:38 -0400 Subject: [PATCH 096/114] feat(rust-nodejs): Create napi struct for xchacha20poly1305 key (#5370) * feat(rust-nodejs): Create napi struct for xchacha20poly1305 key * feat(ironfish): Add xchacha namespace * feat(ironfish): Add X --- ironfish-rust-nodejs/index.d.ts | 19 +++ ironfish-rust-nodejs/index.js | 3 +- ironfish-rust-nodejs/src/xchacha20poly1305.rs | 134 +++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index ff4a1438c2..e7eb933dc8 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -365,3 +365,22 @@ export namespace multisig { signers(): Array } } +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 7521666684..e746cf16e7 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -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, encrypt, decrypt, 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, encrypt, decrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig, xchacha20poly1305 } = nativeBinding module.exports.FishHashContext = FishHashContext module.exports.deserializePublicPackage = deserializePublicPackage @@ -308,3 +308,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/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs index 1135ad67b2..ec23677c65 100644 --- a/ironfish-rust-nodejs/src/xchacha20poly1305.rs +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -2,12 +2,144 @@ * 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::{self, EncryptOutput}; +use ironfish::xchacha20poly1305::{ + self, EncryptOutput, 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[..])) + } +} + #[napi] pub fn encrypt(plaintext: JsBuffer, passphrase: String) -> Result { let plaintext_bytes = plaintext.into_value()?; From e0d08af78e0e482a05743efc6c438eb3ceeb8969 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Fri, 13 Sep 2024 13:17:26 -0700 Subject: [PATCH 097/114] feat: Import multisig identity during account import Edge cases solved - cannot import an account with a name that is the same as an existing multisig identity of a different secret. - if the secret of this account is the same as of an identity in the wallet, then we can import this account. --- ironfish-cli/src/commands/wallet/import.ts | 8 +- ironfish/src/rpc/adapters/errors.ts | 1 + .../importAccount.test.ts.fixture | 8 +- .../rpc/routes/wallet/importAccount.test.ts | 119 +++++++++++++++++- .../src/rpc/routes/wallet/importAccount.ts | 5 +- ironfish/src/testUtilities/keys.ts | 23 +++- ironfish/src/wallet/errors.ts | 9 ++ ironfish/src/wallet/wallet.ts | 34 +++++ 8 files changed, 194 insertions(+), 13 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 862c0684ce..4ff92863f9 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -116,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' @@ -125,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/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index cc5ac36fda..5835d90c0d 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -12,6 +12,7 @@ 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', 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..c67a509e84 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:0KDCZq74l7x/xBpk6pN4GKueDfxqDpBfVVbEBle3piE=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:o67WawihP0SziWZCyKRf8E9IJLHEAKJRxtHegUciYYU=" + "data": "base64:V+JntG6Mh/CV2y6OUBo0XHM1SsQd2BuBUpVQjfd8UTY=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1718920385337, + "timestamp": 1726270914810, "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////8AAAAAyk5bfvKdSTdwi+EqeXx2tuNHJaRs6/OyyTqqtfXiOpeCwrIXEHclFCfWxdYf26U+WL6RmJa2EXCOEU6DWMvSdRv9DjGNEdEs3psBNEoBsPyCxX3g2+2YZOL4q/khIPoKWnEYwtr4Y0LK4CZ2fXcBrwD6RGx/rx6XQI8KCln/DXoY8XogZd5TzPBkEgje6l7AnKSMYJSaj4NxsNHZImLJOcvJsJDKPYaupuA57kTbb9+2mXV4Ww2wX0AzPXgdbtML2WmWSDPhMqNTQ4fSEf+wej8Bdu19SfPHAVcnGM5IlmMnWxLmLpoWJSMqyLHyuIm7U+awFCB2Rnp/ctiDv5Pvw0zXsHnTD1m8FGTtDBXsCLSrcnkAScYq2k9TKSPoebA0pNNqXuK2yCCcuFwGMgno/kepNOjgIQC2NxnzhjxnVdJiw9tTgMgXqSAlXqWpSVP/px2gtax7dFibMtdv8ytLigKMfy9fAAl1L8IVrk2QclO4WHGelqhidGNNRCofwiL43lWwTeX3xFhwLetRp4coJ8xk/zu/IOiLxeL+Pgj3gCsMSgHzwWiLTjWVB0j7zm75Idrgbm+sgS8A6rEnPdlkHvW7aNLYrTHxigi7q6LdHC1m0tzkMPLTqUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwPTtFNObdUhDGdWBuYySM8KCwmFmBaUol9uIxYe/w3Ip9yh0GHfuULUqZHBBCfl/McTVbJl4h86Tb0qRJSoJABQ==" } ] } diff --git a/ironfish/src/rpc/routes/wallet/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index 8c9a8276a2..b5d8913675 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -7,12 +7,14 @@ import path from 'path' import { createTrustedDealerKeyPackages, useMinerBlockFixture } from '../../../testUtilities' import { createRouteTest } from '../../../testUtilities/routeTest' import { JsonEncoder } from '../../../wallet' +import { 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 +53,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, @@ -412,4 +414,117 @@ 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 duplicate 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) + }) }) 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/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/errors.ts b/ironfish/src/wallet/errors.ts index 1b45673cdb..abb1a16961 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -43,6 +43,15 @@ 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 DuplicateSpendingKeyError extends Error { name = this.constructor.name diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 019aa81415..a3a7439224 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -46,11 +46,13 @@ 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 { MintAssetOptions } from './interfaces/mintAssetOptions' @@ -1415,6 +1417,7 @@ export class Wallet { options?: { createdAt?: number; passphrase?: string }, ): Promise { let multisigKeys = accountValue.multisigKeys + let secret: Buffer | undefined const name = accountValue.name if ( @@ -1433,6 +1436,11 @@ export class Wallet { publicKeyPackage: accountValue.multisigKeys.publicKeyPackage, secret: multisigIdentity.secret.toString('hex'), } + secret = multisigIdentity.secret + } + + if (accountValue.multisigKeys && isMultisigSignerImport(accountValue.multisigKeys)) { + secret = Buffer.from(accountValue.multisigKeys.secret, 'hex') } if (name && this.getAccountByName(name)) { @@ -1492,6 +1500,32 @@ export class Wallet { await this.walletDb.setAccount(account, tx) } + if (secret) { + const identitySerialized = new multisig.ParticipantSecret(secret) + .toIdentity() + .serialize() + const multisigIdentity = await this.walletDb.getMultisigIdentity(identitySerialized, tx) + + if (!multisigIdentity) { + const duplicateSecret = await this.walletDb.getMultisigSecretByName( + accountValue.name, + tx, + ) + if (duplicateSecret) { + throw new DuplicateIdentityNameError(accountValue.name) + } + + await this.walletDb.putMultisigIdentity( + identitySerialized, + { + name: account.name, + secret, + }, + tx, + ) + } + } + if (createdAt !== null) { const previousBlock = await this.chainGetBlock({ sequence: createdAt.sequence - 1 }) From 83e49dcdf6e8cb9dfe5492526eb60312803db000 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:06:19 -0400 Subject: [PATCH 098/114] feat(ironfish): Store salt and nonce on encrypted account (#5371) * feat(rust-nodejs): Create napi struct for xchacha20poly1305 key * feat(ironfish): Store salt and nonce on encrypted account * cargo fmt * Fix imports * Fix import --- ironfish/src/wallet/account/encryptedAccount.ts | 8 +++++++- ironfish/src/wallet/walletdb/accountValue.test.ts | 4 +++- ironfish/src/wallet/walletdb/accountValue.ts | 12 +++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts index cd2aa1c9d9..580cceca98 100644 --- a/ironfish/src/wallet/account/encryptedAccount.ts +++ b/ironfish/src/wallet/account/encryptedAccount.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 { decrypt } from '@ironfish/rust-nodejs' +import { decrypt, xchacha20poly1305 } from '@ironfish/rust-nodejs' import { AccountDecryptionFailedError } from '../errors' import { AccountValueEncoding, EncryptedAccountValue } from '../walletdb/accountValue' import { WalletDB } from '../walletdb/walletdb' @@ -9,9 +9,13 @@ import { Account } from './account' export class EncryptedAccount { private readonly walletDb: WalletDB + readonly salt: Buffer + readonly nonce: Buffer readonly data: Buffer constructor({ data, walletDb }: { data: Buffer; walletDb: WalletDB }) { + this.salt = Buffer.alloc(xchacha20poly1305.XSALT_LENGTH) + this.nonce = Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH) this.data = data this.walletDb = walletDb } @@ -31,6 +35,8 @@ export class EncryptedAccount { serialize(): EncryptedAccountValue { return { encrypted: true, + salt: this.salt, + nonce: this.nonce, data: this.data, } } diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index 645e750ee3..6260f5e60e 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.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 { encrypt, generateKey } from '@ironfish/rust-nodejs' +import { encrypt, generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' import { AccountValueEncoding, DecryptedAccountValue, @@ -95,6 +95,8 @@ describe('AccountValueEncoding', () => { const encryptedValue: EncryptedAccountValue = { encrypted: true, data: encryptedData, + salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), + nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), } const buffer = encoder.serialize(encryptedValue) diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index b19cecc17c..50dc2be1ca 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.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 { KEY_LENGTH, 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 { MultisigKeys } from '../interfaces/multisigKeys' @@ -13,6 +13,8 @@ const VERSION_LENGTH = 2 export type EncryptedAccountValue = { encrypted: true + salt: Buffer + nonce: Buffer data: Buffer } @@ -48,6 +50,8 @@ export class AccountValueEncoding implements IDatabaseEncoding { 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() @@ -113,9 +117,13 @@ export class AccountValueEncoding implements IDatabaseEncoding { // 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, } } @@ -182,6 +190,8 @@ export class AccountValueEncoding implements IDatabaseEncoding { 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 } From b192ec31455d3f6d40e99354c1eaa52351e814b1 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Fri, 13 Sep 2024 13:17:26 -0700 Subject: [PATCH 099/114] feat: Import multisig hw identity merging identity and secret logic --- .../importAccount.test.ts.fixture | 8 +- .../rpc/routes/wallet/importAccount.test.ts | 119 +++++++++++++++++- ironfish/src/wallet/wallet.ts | 57 +++++---- 3 files changed, 151 insertions(+), 33 deletions(-) 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 c67a509e84..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:0KDCZq74l7x/xBpk6pN4GKueDfxqDpBfVVbEBle3piE=" + "data": "base64:ZXWDsqog2eRQq6WZ70lqE9cVSUINpf1J8m6T/NS5UTQ=" }, "transactionCommitment": { "type": "Buffer", - "data": "base64:V+JntG6Mh/CV2y6OUBo0XHM1SsQd2BuBUpVQjfd8UTY=" + "data": "base64:iIFwx36ZjBXyt1DsFBf5Av/sNYw4+Z5ZMXV1m0bNLm4=" }, "target": "9282972777491357380673661573939192202192629606981189395159182914949423", "randomness": "0", - "timestamp": 1726270914810, + "timestamp": 1726274775730, "graffiti": "0000000000000000000000000000000000000000000000000000000000000000", "noteSize": 4, "work": "0" @@ -22,7 +22,7 @@ "transactions": [ { "type": "Buffer", - "data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAyk5bfvKdSTdwi+EqeXx2tuNHJaRs6/OyyTqqtfXiOpeCwrIXEHclFCfWxdYf26U+WL6RmJa2EXCOEU6DWMvSdRv9DjGNEdEs3psBNEoBsPyCxX3g2+2YZOL4q/khIPoKWnEYwtr4Y0LK4CZ2fXcBrwD6RGx/rx6XQI8KCln/DXoY8XogZd5TzPBkEgje6l7AnKSMYJSaj4NxsNHZImLJOcvJsJDKPYaupuA57kTbb9+2mXV4Ww2wX0AzPXgdbtML2WmWSDPhMqNTQ4fSEf+wej8Bdu19SfPHAVcnGM5IlmMnWxLmLpoWJSMqyLHyuIm7U+awFCB2Rnp/ctiDv5Pvw0zXsHnTD1m8FGTtDBXsCLSrcnkAScYq2k9TKSPoebA0pNNqXuK2yCCcuFwGMgno/kepNOjgIQC2NxnzhjxnVdJiw9tTgMgXqSAlXqWpSVP/px2gtax7dFibMtdv8ytLigKMfy9fAAl1L8IVrk2QclO4WHGelqhidGNNRCofwiL43lWwTeX3xFhwLetRp4coJ8xk/zu/IOiLxeL+Pgj3gCsMSgHzwWiLTjWVB0j7zm75Idrgbm+sgS8A6rEnPdlkHvW7aNLYrTHxigi7q6LdHC1m0tzkMPLTqUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwPTtFNObdUhDGdWBuYySM8KCwmFmBaUol9uIxYe/w3Ip9yh0GHfuULUqZHBBCfl/McTVbJl4h86Tb0qRJSoJABQ==" + "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/importAccount.test.ts b/ironfish/src/rpc/routes/wallet/importAccount.test.ts index b5d8913675..549fbf4817 100644 --- a/ironfish/src/rpc/routes/wallet/importAccount.test.ts +++ b/ironfish/src/rpc/routes/wallet/importAccount.test.ts @@ -4,10 +4,15 @@ 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 { isMultisigSignerImport } from '../../../wallet/exporter' +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' @@ -322,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(), + ) + } } }) @@ -442,7 +465,7 @@ describe('Route wallet/importAccount', () => { expect.assertions(2) }) - it('should not import multisig account with duplicate identity name', async () => { + it('should not import multisig account with secret with the same identity name', async () => { const name = 'duplicateIdentityNameTest' const { @@ -527,4 +550,96 @@ describe('Route wallet/importAccount', () => { 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/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index a3a7439224..94585bd740 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -54,7 +54,10 @@ import { } 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 { ScanState } from './scanner/scanState' import { WalletScanner } from './scanner/walletScanner' @@ -1418,29 +1421,31 @@ export class Wallet { ): Promise { let multisigKeys = accountValue.multisigKeys let secret: Buffer | undefined + let identity: Buffer | undefined const name = accountValue.name - if ( - accountValue.multisigKeys && - 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') - } + 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: multisigIdentity.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') } - secret = multisigIdentity.secret - } - - if (accountValue.multisigKeys && isMultisigSignerImport(accountValue.multisigKeys)) { - secret = Buffer.from(accountValue.multisigKeys.secret, 'hex') } if (name && this.getAccountByName(name)) { @@ -1500,23 +1505,21 @@ export class Wallet { await this.walletDb.setAccount(account, tx) } - if (secret) { - const identitySerialized = new multisig.ParticipantSecret(secret) - .toIdentity() - .serialize() - const multisigIdentity = await this.walletDb.getMultisigIdentity(identitySerialized, tx) + if (identity) { + const existingIdentity = await this.walletDb.getMultisigIdentity(identity, tx) - if (!multisigIdentity) { + if (!existingIdentity) { const duplicateSecret = await this.walletDb.getMultisigSecretByName( accountValue.name, tx, ) + if (duplicateSecret) { throw new DuplicateIdentityNameError(accountValue.name) } await this.walletDb.putMultisigIdentity( - identitySerialized, + identity, { name: account.name, secret, From cf2941f3c49919ecddac82cda327da477a001720 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:25:47 -0400 Subject: [PATCH 100/114] feat(ironfish): Create master key (#5375) --- ironfish/src/wallet/masterKey.test.ts | 37 ++++++++ ironfish/src/wallet/masterKey.ts | 93 +++++++++++++++++++ .../wallet/walletdb/masterKeyValue.test.ts | 32 +++++++ .../src/wallet/walletdb/masterKeyValue.ts | 46 +++++++++ 4 files changed, 208 insertions(+) create mode 100644 ironfish/src/wallet/masterKey.test.ts create mode 100644 ironfish/src/wallet/masterKey.ts create mode 100644 ironfish/src/wallet/walletdb/masterKeyValue.test.ts create mode 100644 ironfish/src/wallet/walletdb/masterKeyValue.ts 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..b2dab80726 --- /dev/null +++ b/ironfish/src/wallet/masterKey.ts @@ -0,0 +1,93 @@ +/* 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) + } +} diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts new file mode 100644 index 0000000000..351f643200 --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.test.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 { MasterKeyValue, NullableMasterKeyValueEncoding } from './masterKeyValue' + +describe('MasterKeyValueEncoding', () => { + describe('with a defined value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new NullableMasterKeyValueEncoding() + + 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) + }) + }) + + describe('with a null value', () => { + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new NullableMasterKeyValueEncoding() + + const value = null + 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..a706dcfafa --- /dev/null +++ b/ironfish/src/wallet/walletdb/masterKeyValue.ts @@ -0,0 +1,46 @@ +/* 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 NullableMasterKeyValueEncoding + implements IDatabaseEncoding +{ + serialize(value: MasterKeyValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeBytes(value.nonce) + bw.writeBytes(value.salt) + } + + return bw.render() + } + + deserialize(buffer: Buffer): MasterKeyValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } + } + + return null + } + + getSize(value: MasterKeyValue | null): number { + if (!value) { + return 0 + } + + return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH + } +} From e3a51ce07928a1201e26daee4adb3714e512362e Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:01:12 -0700 Subject: [PATCH 101/114] adds napi methods to support Ledger multisig (#5376) adds typescript version of test_dkg_signing example adds multisig.test.slow.ts that replicates the logic of test_dkg_signing from ironfish-rust - adds method to retrieve frost signing package from deserialized signing package - adds signingPackageFromRaw method - allows construction of signing package from identities and raw commitments (from frost, not ironfish) - adds method to NativeSigningCommitment to get raw_commitments - defines NativeSignatureShare to support deserializing ironfish SignatureShares and accessing the underlying identity and frost signature share - adds from_frost factor method to reconstruct SignatureShare from parts --- ironfish-rust-nodejs/index.d.ts | 10 ++ ironfish-rust-nodejs/src/multisig.rs | 75 +++++++- .../src/structs/transaction.rs | 32 ++++ ironfish/src/multisig.test.slow.ts | 163 ++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 ironfish/src/multisig.test.slow.ts diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index e7eb933dc8..5eac35fc49 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -251,6 +251,7 @@ export class UnsignedTransaction { publicKeyRandomness(): string hash(): Buffer signingPackage(nativeIdentiferCommitments: Array): string + signingPackageFromRaw(identities: Array, rawCommitments: Array): string sign(spenderHexKey: string): Buffer addSignature(signature: Buffer): Buffer } @@ -333,6 +334,13 @@ 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 @@ -356,6 +364,7 @@ export namespace multisig { export class SigningCommitment { constructor(jsBytes: Buffer) identity(): Buffer + rawCommitments(): Buffer verifyChecksum(transactionHash: Buffer, signerIdentities: Array): boolean } export type NativeSigningPackage = SigningPackage @@ -363,6 +372,7 @@ export namespace multisig { constructor(jsBytes: Buffer) unsignedTransaction(): NativeUnsignedTransaction signers(): Array + frostSigningPackage(): Buffer } } export namespace xchacha20poly1305 { diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 8fae0761b1..0291d9bfde 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -4,7 +4,9 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ - frost::{keys::KeyPackage, round2, Randomizer}, + frost::{ + frost::round2::SignatureShare as FrostSignatureShare, keys::KeyPackage, round2, Randomizer, + }, frost_utils::{ account_keys::derive_account_keys, signing_package::SigningPackage, split_spender_key::split_spender_key, @@ -136,6 +138,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, @@ -333,6 +384,17 @@ impl NativeSigningCommitment { 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, @@ -378,6 +440,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")] diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 17070949cd..3b597bb932 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -12,6 +12,7 @@ use ironfish::frost::round1::SigningCommitments; use ironfish::frost::round2::SignatureShare as FrostSignatureShare; use ironfish::frost::Identifier; use ironfish::frost_utils::signing_package::SigningPackage; +use ironfish::participant::Identity; use ironfish::serializing::bytes_to_hex; use ironfish::serializing::fr::FrSerializable; use ironfish::serializing::hex_to_vec_bytes; @@ -455,6 +456,37 @@ impl NativeUnsignedTransaction { Ok(bytes_to_hex(&vec)) } + #[napi] + pub fn signing_package_from_raw( + &self, + identities: Vec, + raw_commitments: Vec, + ) -> Result { + let mut commitments = Vec::new(); + + for (index, identity) in identities.iter().enumerate() { + let identity_bytes = hex_to_vec_bytes(identity).map_err(to_napi_err)?; + let identity = Identity::deserialize_from(&identity_bytes[..]).map_err(to_napi_err)?; + + let raw_commitment = &raw_commitments[index]; + let commitment_bytes = hex_to_vec_bytes(raw_commitment).map_err(to_napi_err)?; + let commitment = + SigningCommitments::deserialize(&commitment_bytes[..]).map_err(to_napi_err)?; + + commitments.push((identity, commitment)); + } + + let signing_package = self + .transaction + .signing_package(commitments) + .map_err(to_napi_err)?; + + let mut vec: Vec = vec![]; + signing_package.write(&mut vec).map_err(to_napi_err)?; + + Ok(bytes_to_hex(&vec)) + } + #[napi] pub fn sign(&mut self, spender_hex_key: String) -> Result { let spender_key = SaplingKey::from_hex(&spender_hex_key).map_err(to_napi_err)?; diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts new file mode 100644 index 0000000000..8af50cad91 --- /dev/null +++ b/ironfish/src/multisig.test.slow.ts @@ -0,0 +1,163 @@ +/* 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 + + 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 commitmentIdentities: string[] = [] + const rawCommitments: string[] = [] + for (const commitment of commitments) { + const signingCommitment = new multisig.SigningCommitment(Buffer.from(commitment, 'hex')) + commitmentIdentities.push(signingCommitment.identity().toString('hex')) + rawCommitments.push(signingCommitment.rawCommitments().toString('hex')) + } + + const signingPackage = unsignedTransaction.signingPackageFromRaw( + commitmentIdentities, + rawCommitments, + ) + + // 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() + }) + }) +}) From 1249c3cbcdbc6f4ae38e62cdf6e5ccd1a72e7bbc Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:30:03 -0400 Subject: [PATCH 102/114] feat(rust,rust-nodejs,ironfish): Remove old encrypt/decrypt methods (#5377) * feat(ironfish): Create master key * feat(rust,rust-nodejs,ironfish): Remove old encrypt/decrypt methods * chore(rust): lint * test(ironfish): Fix tests * Fix test * fixtures --- ironfish-rust-nodejs/index.d.ts | 2 - ironfish-rust-nodejs/index.js | 4 +- ironfish-rust-nodejs/src/xchacha20poly1305.rs | 25 +---- ironfish-rust/src/xchacha20poly1305.rs | 105 +----------------- ironfish/src/rpc/routes/wallet/create.ts | 4 +- .../src/rpc/routes/wallet/createAccount.ts | 7 +- ironfish/src/rpc/routes/wallet/rename.ts | 2 +- .../src/rpc/routes/wallet/renameAccount.ts | 5 +- .../src/rpc/routes/wallet/resetAccount.ts | 3 - .../__fixtures__/account.test.ts.fixture | 44 ++++---- ironfish/src/wallet/account/account.test.ts | 24 ++-- ironfish/src/wallet/account/account.ts | 23 ++-- .../wallet/account/encryptedAccount.test.ts | 19 +++- .../src/wallet/account/encryptedAccount.ts | 23 ++-- ironfish/src/wallet/masterKey.ts | 6 + ironfish/src/wallet/wallet.test.ts | 88 ++++++--------- ironfish/src/wallet/wallet.ts | 48 ++++++-- .../src/wallet/walletdb/accountValue.test.ts | 10 +- .../wallet/walletdb/masterKeyValue.test.ts | 33 ++---- .../src/wallet/walletdb/masterKeyValue.ts | 34 ++---- ironfish/src/wallet/walletdb/walletdb.test.ts | 73 ++++-------- ironfish/src/wallet/walletdb/walletdb.ts | 85 ++++++++------ 22 files changed, 265 insertions(+), 402 deletions(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 5eac35fc49..fdf0e6c3a4 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -64,8 +64,6 @@ export const TRANSACTION_EXPIRATION_LENGTH: number export const TRANSACTION_FEE_LENGTH: number export const LATEST_TRANSACTION_VERSION: number export declare function verifyTransactions(serializedTransactions: Array): boolean -export declare function encrypt(plaintext: Buffer, passphrase: string): Buffer -export declare function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer export const enum LanguageCode { English = 0, ChineseSimplified = 1, diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index e746cf16e7..e4d1006a92 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -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, encrypt, decrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig, xchacha20poly1305 } = 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 @@ -292,8 +292,6 @@ module.exports.TransactionPosted = TransactionPosted module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions module.exports.UnsignedTransaction = UnsignedTransaction -module.exports.encrypt = encrypt -module.exports.decrypt = decrypt module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords diff --git a/ironfish-rust-nodejs/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs index ec23677c65..da3a5f6820 100644 --- a/ironfish-rust-nodejs/src/xchacha20poly1305.rs +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use ironfish::xchacha20poly1305::{ - self, EncryptOutput, XChaCha20Poly1305Key, KEY_LENGTH as KEY_SIZE, SALT_LENGTH as SALT_SIZE, + XChaCha20Poly1305Key, KEY_LENGTH as KEY_SIZE, SALT_LENGTH as SALT_SIZE, XNONCE_LENGTH as XNONCE_SIZE, }; use napi::{bindgen_prelude::*, JsBuffer}; @@ -139,26 +139,3 @@ impl NativeXChaCha20Poly1305Key { Ok(Buffer::from(&result[..])) } } - -#[napi] -pub fn encrypt(plaintext: JsBuffer, passphrase: String) -> Result { - let plaintext_bytes = plaintext.into_value()?; - let result = xchacha20poly1305::encrypt(plaintext_bytes.as_ref(), passphrase.as_bytes()) - .map_err(to_napi_err)?; - - let mut vec: Vec = vec![]; - result.write(&mut vec).map_err(to_napi_err)?; - - Ok(Buffer::from(&vec[..])) -} - -#[napi] -pub fn decrypt(encrypted_blob: JsBuffer, passphrase: String) -> Result { - let encrypted_bytes = encrypted_blob.into_value()?; - - let encrypted_output = EncryptOutput::read(encrypted_bytes.as_ref()).map_err(to_napi_err)?; - let result = - xchacha20poly1305::decrypt(encrypted_output, passphrase.as_bytes()).map_err(to_napi_err)?; - - Ok(Buffer::from(&result[..])) -} diff --git a/ironfish-rust/src/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs index e8af9077ab..cefb0fb2c3 100644 --- a/ironfish-rust/src/xchacha20poly1305.rs +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -4,8 +4,8 @@ use std::io; +use argon2::Argon2; use argon2::RECOMMENDED_SALT_LEN; -use argon2::{password_hash::SaltString, Argon2}; use chacha20poly1305::aead::Aead; use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; use hkdf::Hkdf; @@ -133,109 +133,6 @@ impl XChaCha20Poly1305Key { } } -#[derive(Debug)] -pub struct EncryptOutput { - pub salt: Vec, - - pub nonce: [u8; XNONCE_LENGTH], - - pub ciphertext: Vec, -} - -impl EncryptOutput { - pub fn write(&self, mut writer: W) -> Result<(), IronfishError> { - let salt_len = u32::try_from(self.salt.len())?.to_le_bytes(); - writer.write_all(&salt_len)?; - writer.write_all(&self.salt)?; - - writer.write_all(&self.nonce)?; - - let ciphertext_len = u32::try_from(self.ciphertext.len())?.to_le_bytes(); - writer.write_all(&ciphertext_len)?; - writer.write_all(&self.ciphertext)?; - - Ok(()) - } - - pub fn read(mut reader: R) -> Result { - let mut salt_len = [0u8; 4]; - reader.read_exact(&mut salt_len)?; - let salt_len = u32::from_le_bytes(salt_len) as usize; - - let mut salt = vec![0u8; salt_len]; - reader.read_exact(&mut salt)?; - - let mut nonce = [0u8; XNONCE_LENGTH]; - reader.read_exact(&mut nonce)?; - - let mut ciphertext_len = [0u8; 4]; - reader.read_exact(&mut ciphertext_len)?; - let ciphertext_len = u32::from_le_bytes(ciphertext_len) as usize; - - let mut ciphertext = vec![0u8; ciphertext_len]; - reader.read_exact(&mut ciphertext)?; - - Ok(EncryptOutput { - salt, - nonce, - ciphertext, - }) - } -} - -impl PartialEq for EncryptOutput { - fn eq(&self, other: &EncryptOutput) -> bool { - self.salt == other.salt && self.nonce == other.nonce && self.ciphertext == other.ciphertext - } -} - -fn derive_key(passphrase: &[u8], salt: &[u8]) -> 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(Key::from(key)) -} - -pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result { - let salt = SaltString::generate(&mut thread_rng()); - let salt_str = salt.to_string(); - let salt_bytes = salt_str.as_bytes(); - let key = derive_key(passphrase, salt_bytes)?; - - let cipher = XChaCha20Poly1305::new(&key); - let mut nonce_bytes = [0u8; XNONCE_LENGTH]; - thread_rng().fill_bytes(&mut nonce_bytes); - let nonce = XNonce::from_slice(&nonce_bytes); - - let ciphertext = cipher - .encrypt(nonce, plaintext) - .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption))?; - - Ok(EncryptOutput { - salt: salt_bytes.to_vec(), - nonce: nonce_bytes, - ciphertext, - }) -} - -pub fn decrypt( - encrypted_output: EncryptOutput, - passphrase: &[u8], -) -> Result, IronfishError> { - let nonce = XNonce::from_slice(&encrypted_output.nonce); - - let key = derive_key(passphrase, &encrypted_output.salt[..])?; - let cipher = XChaCha20Poly1305::new(&key); - - cipher - .decrypt(nonce, encrypted_output.ciphertext.as_ref()) - .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Decryption)) -} - #[cfg(test)] mod test { use crate::xchacha20poly1305::XChaCha20Poly1305Key; diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index bba982d93b..18738ebd4f 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -30,9 +30,7 @@ routes.register( ) } - const account = await context.wallet.createAccount(name, { - passphrase: request.data.passphrase, - }) + const account = await context.wallet.createAccount(name) if (context.wallet.nodeClient) { void context.wallet.scan() } diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index 8e1566d066..4a9fb5e32a 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.ts @@ -18,7 +18,7 @@ import { AssertHasRpcContext } from '../rpcContext' * Hence, we're adding a new createAccount endpoint and will eventually sunset the create endpoint. */ -export type CreateAccountRequest = { name: string; default?: boolean; passphrase?: string } +export type CreateAccountRequest = { name: string; default?: boolean } export type CreateAccountResponse = { name: string publicAddress: string @@ -29,7 +29,6 @@ export const CreateAccountRequestSchema: yup.ObjectSchema .object({ name: yup.string().defined(), default: yup.boolean().optional(), - passphrase: yup.string().optional(), }) .defined() @@ -49,9 +48,7 @@ routes.register( let account try { - account = await context.wallet.createAccount(request.data.name, { - passphrase: request.data.passphrase, - }) + account = await context.wallet.createAccount(request.data.name) } catch (e) { if (e instanceof DuplicateAccountNameError) { throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) diff --git a/ironfish/src/rpc/routes/wallet/rename.ts b/ironfish/src/rpc/routes/wallet/rename.ts index f0461f4bef..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, { passphrase: request.data.passphrase }) + 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 05f4761020..6c5de737ef 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.ts @@ -7,14 +7,13 @@ import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' -export type RenameAccountRequest = { account: string; newName: string; passphrase?: string } +export type RenameAccountRequest = { account: string; newName: string } export type RenameAccountResponse = undefined export const RenameAccountRequestSchema: yup.ObjectSchema = yup .object({ account: yup.string().defined(), newName: yup.string().defined(), - passphrase: yup.string().optional(), }) .defined() @@ -29,7 +28,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName, { passphrase: request.data.passphrase }) + await context.wallet.setName(account, request.data.newName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/resetAccount.ts b/ironfish/src/rpc/routes/wallet/resetAccount.ts index 56ec609bb4..22e6d86467 100644 --- a/ironfish/src/rpc/routes/wallet/resetAccount.ts +++ b/ironfish/src/rpc/routes/wallet/resetAccount.ts @@ -11,7 +11,6 @@ export type ResetAccountRequest = { account: string resetCreatedAt?: boolean resetScanningEnabled?: boolean - passphrase?: string } export type ResetAccountResponse = undefined @@ -20,7 +19,6 @@ export const ResetAccountRequestSchema: yup.ObjectSchema = account: yup.string().defined(), resetCreatedAt: yup.boolean(), resetScanningEnabled: yup.boolean(), - passphrase: yup.string().optional(), }) .defined() @@ -39,7 +37,6 @@ routes.register( await context.wallet.resetAccount(account, { resetCreatedAt: request.data.resetCreatedAt, resetScanningEnabled: request.data.resetScanningEnabled, - passphrase: request.data.passphrase, }) request.end() diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index 351defc450..b0e82282cf 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5909,13 +5909,13 @@ "value": { "encrypted": false, "version": 4, - "id": "6f0698b4-a99c-46aa-9391-59f0c55cd755", + "id": "aea047be-8c73-448e-8e8d-22844f49736f", "name": "accountA", - "spendingKey": "89001fcdef6bff7e9fd76d4ae6275bf6786afc2797eded9df094ae4a6894782d", - "viewKey": "389bc77ae499f3edc0dc445a732add6f36c275b260efe2c791cc515f6c2c0cd75f5b31e9b1f82edb0ee57b9304ece90d48f43c36d78660b04960b9692485d058", - "incomingViewKey": "fdd70ff012b4e48576bdd71207ebe1e3747811c77fa1a25862c3b869123ce007", - "outgoingViewKey": "9664b763a0418a476d072c715fcbbba58bcaf60cc951ba017195d75453131f11", - "publicAddress": "14a9bfb247dcf632f85ff79ebef222cc9ccf364f9b3e3e0ee39b75d68f80782a", + "spendingKey": "431a4e45c614fd41d7cee2de809e4464e92a125752d6f6839c0af7c706a01f67", + "viewKey": "4be28bbdca174c4e7499d913ea02642d74136eea9058f4e6eb7586dd5c864e905128a5aa5d25356934e5f4cf36575445b8dc60bfa672e1c135521cb5610e9b4c", + "incomingViewKey": "78b6e7887d55853b84d5d0b3b80d787379750b8839ddbc738882124453a5c004", + "outgoingViewKey": "3ce3ee26d7ab6c76838b6920eb6b5cf8fa3aa1ca54940d3b3cbf20532be34e41", + "publicAddress": "09d7f58ee5ae406b19e714b7fab882b83334e5b566a64c9d9707fcc513426163", "createdAt": { "hash": { "type": "Buffer", @@ -5924,7 +5924,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "8c11ae2523136f4b11bada56d9bcab2b6591cc99cf7aede8a238c5fefd7fce0d" + "proofAuthorizingKey": "c95606e98895f390f247f0c9e4beb22a039adfdab5cd11dac28263870dfe4802" }, "head": { "hash": { @@ -5935,18 +5935,18 @@ } } ], - "Accounts setName should throw an error if the passphrase is incorrect and the wallet is encrypted": [ + "Accounts setName should throw an error if there is no master key and the wallet is encrypted": [ { "value": { "encrypted": false, "version": 4, - "id": "92b122e5-9f14-453b-a364-e20a7d107305", + "id": "df4836f9-2889-471c-b9c3-c1aaa38fc595", "name": "accountA", - "spendingKey": "3d68ebfd3d600792fc94d583bbab97ab4d02b9f41aa2a6655e151e41d4a33d8d", - "viewKey": "51e699920432cf221351568ade21fb8af4110c7ad57ad90cc2664340d50d6f207a19eb846feb76588f467e4dce99d0da074ca680aca11d3d8c91bd47ae5d9081", - "incomingViewKey": "15f8cb20e3a7b494474f4c4737bc0403dbb5de6e532e2c83a4469cd7f95f5b02", - "outgoingViewKey": "fc1b2e0e85cca6ddaf657baf2c69c76b403afe1adadd73b794c5cb81724d240d", - "publicAddress": "899ea1e9be7202aedab0a41f6b9cf661ce20e41ba11c1ee9d15ac64d8c96a391", + "spendingKey": "433f92654fe57940d18e87481c52588fec36fa552f64dca434b1726e022722cd", + "viewKey": "1d6e8a7faea61bdfd77d6e3c3b63951b3e613a8907bd6786fe0466e3490a8558eec35c9e08b3abbbaa0b596f427801bfc306297d0c3bf1b50ad02aa62cae702f", + "incomingViewKey": "88328774271cf9b9437e0b76ffc05c5697742d2004b9430a257590cceccbb000", + "outgoingViewKey": "5cb8a083108e4f3aa7d5db9a6ddce584d9b3a941cf707ee691b016ddf6c0fc87", + "publicAddress": "534ba54bbf1721cb16e4c8a702f25d96a0c99c2f02c38431be8870a28fc04914", "createdAt": { "hash": { "type": "Buffer", @@ -5955,7 +5955,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "d0d08a05953a50a0ce8b35e0c410b7dde77e3bf6099aaa4fc413de2096f7ef0a" + "proofAuthorizingKey": "25079aaa8548a644fc43ba77584bb0cb53adb60e3b2a033865df05569ee44302" }, "head": { "hash": { @@ -5971,13 +5971,13 @@ "value": { "encrypted": false, "version": 4, - "id": "1dd7e196-ffed-4fe0-8dbd-c49f82bf44b7", + "id": "de02e4f1-ad01-464d-9e5b-9b4e83c57f0c", "name": "accountA", - "spendingKey": "71ec323628c95bd56df353ec444b8ceb3d463603e82a89d4ac3336bdf630993a", - "viewKey": "5219abc719ecb8954e77d89e60f4fc82588e8f619ba4da38b53ed0471aeccb200cc7cec29ca533c769c96213417f78ccfaf2ba3f12a4b948a0da242805eb044c", - "incomingViewKey": "d9d70e59490b44c078300a23376e4fc516b47a31231c77538d17740f399e3d00", - "outgoingViewKey": "e3eca4b31e0d4b48e27458db2eff35b4d713d84b0b07505fdc8d49b2e40ab69d", - "publicAddress": "fbc47fe75ef9534b28b95fd17288488b89a4b752191a323a3703520095e8c24b", + "spendingKey": "4c946561f929bfc55faf90cda2f3c8bc6881b0cf96f0a422acb4c2240d5b995b", + "viewKey": "3120c5db86dd17d6b11f14df5f652878306aba202c74db71aad778e45ae4888843eaa4e8fa18ee78a762c2ed3b024c29d0ccd4163b0e49dba1e9c0b2055fd5b5", + "incomingViewKey": "2b1bcb2a5821e8f0485740aaec3157d43af552e853c97fa09dedb86442095003", + "outgoingViewKey": "dff7ee410247d8d2b63cd0da40509bd8fc3e96a4d26c04ec9fdb016da2fbb2ea", + "publicAddress": "9a3f5703d2cf40fc2899bf25793249b44e187acbd5c1f6f4f20ad4143408e42e", "createdAt": { "hash": { "type": "Buffer", @@ -5986,7 +5986,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "f335356ed8f77c4facebc2ad9377ab05e6d71ec83aa772df0c23e2733458a10c" + "proofAuthorizingKey": "8726a9a636670be8e1fd12a0b4b42a1d0e30a526b28cae014afc7b241ad1fb0d" }, "head": { "hash": { diff --git a/ironfish/src/wallet/account/account.test.ts b/ironfish/src/wallet/account/account.test.ts index 9701a0b410..47cb5b61e0 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -17,6 +17,7 @@ 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' @@ -198,14 +199,14 @@ describe('Accounts', () => { await expect(account.setName('B')).rejects.toThrow() }) - it('should throw an error if the passphrase is incorrect and the wallet is encrypted', async () => { + 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', { passphrase: 'incorrect ' })).rejects.toThrow() + await expect(account.setName('B')).rejects.toThrow() }) it('should save the encrypted account if the passphrase is correct and the wallet is encrypted', async () => { @@ -216,17 +217,23 @@ describe('Accounts', () => { const account = await useAccountFixture(node.wallet, 'accountA') await node.wallet.encrypt(passphrase) - await account.setName(newName, { 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({ - data: accountValue.data, + accountValue, walletDb: node.wallet.walletDb, }) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) expect(decryptedAccount.name).toEqual(newName) }) @@ -2668,8 +2675,11 @@ describe('Accounts', () => { const account = await useAccountFixture(node.wallet) const passphrase = 'foo' - const encryptedAccount = account.encrypt(passphrase) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + 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 0c47c5160b..9f880e0558 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 { encrypt, 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,6 +14,7 @@ import { WithNonNull, WithRequired } from '../../utils' import { DecryptedNote } from '../../workerPool/tasks/decryptNotes' import { AssetBalances } from '../assetBalances' import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' +import { MasterKey } from '../masterKey' import { AccountValueEncoding, DecryptedAccountValue } from '../walletdb/accountValue' import { AssetValue } from '../walletdb/assetValue' import { BalanceValue } from '../walletdb/balanceValue' @@ -129,7 +129,7 @@ export class Account { async setName( name: string, - options?: { passphrase?: string }, + options?: { masterKey: MasterKey | null }, tx?: IDatabaseTransaction, ): Promise { if (!name.trim()) { @@ -141,8 +141,9 @@ export class Account { this.name = name if (walletEncrypted) { - Assert.isNotUndefined(options?.passphrase) - await this.walletDb.setEncryptedAccount(this, options.passphrase, tx) + Assert.isNotUndefined(options) + Assert.isNotNull(options?.masterKey) + await this.walletDb.setEncryptedAccount(this, options.masterKey, tx) } else { await this.walletDb.setAccount(this, tx) } @@ -1330,13 +1331,19 @@ export class Account { return publicKeyPackage.identities() } - encrypt(passphrase: string): EncryptedAccount { + encrypt(masterKey: MasterKey): EncryptedAccount { const encoder = new AccountValueEncoding() const serialized = encoder.serialize(this.serialize()) - const data = encrypt(serialized, passphrase) + const derivedKey = masterKey.deriveNewKey() + const data = derivedKey.encrypt(serialized) return new EncryptedAccount({ - data, + accountValue: { + encrypted: true, + data, + salt: derivedKey.salt(), + nonce: derivedKey.nonce(), + }, walletDb: this.walletDb, }) } diff --git a/ironfish/src/wallet/account/encryptedAccount.test.ts b/ironfish/src/wallet/account/encryptedAccount.test.ts index 9b91821a1c..6dac099c3b 100644 --- a/ironfish/src/wallet/account/encryptedAccount.test.ts +++ b/ironfish/src/wallet/account/encryptedAccount.test.ts @@ -4,6 +4,7 @@ import { useAccountFixture } from '../../testUtilities/fixtures/account' import { createNodeTest } from '../../testUtilities/nodeTest' import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' describe('EncryptedAccount', () => { const nodeTest = createNodeTest() @@ -13,8 +14,11 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encryptedAccount = account.encrypt(passphrase) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + 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()) }) @@ -25,10 +29,13 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encryptedAccount = account.encrypt(passphrase) + 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(invalidPassphrase)).toThrow( - AccountDecryptionFailedError, - ) + expect(() => encryptedAccount.decrypt(invalidKey)).toThrow(AccountDecryptionFailedError) }) }) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts index 580cceca98..44f321d4d7 100644 --- a/ironfish/src/wallet/account/encryptedAccount.ts +++ b/ironfish/src/wallet/account/encryptedAccount.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 { decrypt, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' import { AccountDecryptionFailedError } from '../errors' import { AccountValueEncoding, EncryptedAccountValue } from '../walletdb/accountValue' import { WalletDB } from '../walletdb/walletdb' @@ -13,16 +13,25 @@ export class EncryptedAccount { readonly nonce: Buffer readonly data: Buffer - constructor({ data, walletDb }: { data: Buffer; walletDb: WalletDB }) { - this.salt = Buffer.alloc(xchacha20poly1305.XSALT_LENGTH) - this.nonce = Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH) - this.data = data + constructor({ + accountValue, + walletDb, + }: { + accountValue: EncryptedAccountValue + walletDb: WalletDB + }) { + this.salt = accountValue.salt + this.nonce = accountValue.nonce + this.data = accountValue.data this.walletDb = walletDb } - decrypt(passphrase: string): Account { + decrypt(masterKey: xchacha20poly1305.XChaCha20Poly1305Key): Account { try { - const decryptedAccountValue = decrypt(this.data, passphrase) + 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) diff --git a/ironfish/src/wallet/masterKey.ts b/ironfish/src/wallet/masterKey.ts index b2dab80726..4b3ba40cc8 100644 --- a/ironfish/src/wallet/masterKey.ts +++ b/ironfish/src/wallet/masterKey.ts @@ -90,4 +90,10 @@ export class MasterKey { return this.masterKey.deriveKey(salt, nonce) } + + async destroy(): Promise { + await this.lock() + this.nonce.fill(0) + this.salt.fill(0) + } } diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 44ebfcd7ca..f6b1213e59 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -675,29 +675,6 @@ describe('Wallet', () => { await expect(node.wallet.importAccount(accountValue)).rejects.toThrow() }) - it('should throw an error when the wallet is encrypted and the passphrase is incorrect', 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, { passphrase: 'incorrect' }), - ).rejects.toThrow('Your passphrase is incorrect') - }) - it('should encrypt and store the account if the wallet is encrypted', async () => { const { node } = await nodeTest.createSetup() const passphrase = 'foo' @@ -716,7 +693,10 @@ describe('Wallet', () => { ...key, } - const account = await node.wallet.importAccount(accountValue, { passphrase }) + 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) @@ -1065,18 +1045,6 @@ describe('Wallet', () => { await expect(node.wallet.createAccount('B')).rejects.toThrow() }) - it('should throw an error if the wallet is encrypted and an incorrect 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', { passphrase: 'incorrect ' }), - ).rejects.toThrow() - }) - it('should save a new encrypted account with the correct passphrase', async () => { const { node } = await nodeTest.createSetup() const passphrase = 'foo' @@ -1084,17 +1052,23 @@ describe('Wallet', () => { await useAccountFixture(node.wallet, 'A') await node.wallet.encrypt(passphrase) - const account = await node.wallet.createAccount('B', { 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({ - data: accountValue.data, + accountValue, walletDb: node.wallet.walletDb, }) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + + 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) @@ -2478,18 +2452,6 @@ describe('Wallet', () => { await expect(node.wallet.resetAccount(account)).rejects.toThrow() }) - it('should throw an error if the wallet is encrypted and the passphrase is incorrect', 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, { passphrase: 'incorrect' }), - ).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' @@ -2497,7 +2459,8 @@ describe('Wallet', () => { const account = await useAccountFixture(node.wallet, 'A') await node.wallet.encrypt(passphrase) - await node.wallet.resetAccount(account, { passphrase }) + await node.wallet.unlock(passphrase) + await node.wallet.resetAccount(account) const newAccount = node.wallet.getAccountByName(account.name) Assert.isNotNull(newAccount) @@ -2505,7 +2468,12 @@ describe('Wallet', () => { const encryptedAccount = node.wallet.encryptedAccountById.get(newAccount.id) Assert.isNotUndefined(encryptedAccount) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + 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) }) @@ -2724,14 +2692,18 @@ describe('Wallet', () => { 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(passphrase) + 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(passphrase) + const decryptedAccountB = encryptedAccountB.decrypt(key) expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) }) }) @@ -2866,10 +2838,14 @@ describe('Wallet', () => { 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(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 94585bd740..bf7d738667 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -59,6 +59,7 @@ import { 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' @@ -95,7 +96,7 @@ export type TransactionOutput = { assetId: Buffer } -export const DEFAULT_UNLOCK_TIMEOUT_MS = 5 * 60 * 1000 +export const DEFAULT_UNLOCK_TIMEOUT_MS = 24 * 60 * 60 * 1000 export class Wallet { readonly onAccountImported = new Event<[account: Account]>() @@ -111,6 +112,7 @@ 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 @@ -155,6 +157,7 @@ export class Wallet { this.rebroadcastAfter = rebroadcastAfter ?? 10 this.locked = false this.lockTimeout = null + this.masterKey = null this.createTransactionMutex = new Mutex() this.eventLoopAbortController = new AbortController() @@ -223,12 +226,18 @@ export class Wallet { private async load(): Promise { 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({ - data: accountValue.data, walletDb: this.walletDb, + accountValue, }) this.encryptedAccountById.set(id, encryptedAccount) @@ -280,6 +289,10 @@ export class Wallet { clearTimeout(this.eventLoopTimeout) } + if (this.masterKey) { + await this.masterKey.destroy() + } + this.stopUnlockTimeout() await this.scanner.abort() @@ -1340,7 +1353,7 @@ export class Wallet { async createAccount( name: string, - options: { createdAt?: HeadValue | null; setDefault?: boolean; passphrase?: string } = { + options: { createdAt?: HeadValue | null; setDefault?: boolean } = { setDefault: false, }, ): Promise { @@ -1387,10 +1400,10 @@ export class Wallet { const accountsEncrypted = await this.walletDb.accountsEncrypted(tx) if (accountsEncrypted) { - Assert.isNotUndefined(options.passphrase) + Assert.isNotNull(this.masterKey) const encryptedAccount = await this.walletDb.setEncryptedAccount( account, - options.passphrase, + this.masterKey, tx, ) this.encryptedAccountById.set(account.id, encryptedAccount) @@ -1417,7 +1430,7 @@ export class Wallet { async importAccount( accountValue: AccountImport, - options?: { createdAt?: number; passphrase?: string }, + options?: { createdAt?: number }, ): Promise { let multisigKeys = accountValue.multisigKeys let secret: Buffer | undefined @@ -1499,8 +1512,8 @@ export class Wallet { const encrypted = await this.walletDb.accountsEncrypted(tx) if (encrypted) { - Assert.isNotUndefined(options?.passphrase) - await this.walletDb.setEncryptedAccount(account, options.passphrase, tx) + Assert.isNotNull(this.masterKey) + await this.walletDb.setEncryptedAccount(account, this.masterKey, tx) } else { await this.walletDb.setAccount(account, tx) } @@ -1552,6 +1565,10 @@ export class Wallet { return account } + async setName(account: Account, name: string, tx?: IDatabaseTransaction): Promise { + await account.setName(name, { masterKey: this.masterKey }, tx) + } + get accounts(): Account[] { return Array.from(this.accountById.values()) } @@ -1569,7 +1586,6 @@ export class Wallet { options?: { resetCreatedAt?: boolean resetScanningEnabled?: boolean - passphrase?: string }, tx?: IDatabaseTransaction, ): Promise { @@ -1590,10 +1606,10 @@ export class Wallet { const encrypted = await this.walletDb.accountsEncrypted(tx) if (encrypted) { - Assert.isNotUndefined(options?.passphrase) + Assert.isNotNull(this.masterKey) const encryptedAccount = await this.walletDb.setEncryptedAccount( newAccount, - options.passphrase, + this.masterKey, tx, ) this.encryptedAccountById.set(newAccount.id, encryptedAccount) @@ -1914,6 +1930,7 @@ export class Wallet { const unlock = await this.createTransactionMutex.lock() try { + Assert.isNull(this.masterKey) await this.walletDb.encryptAccounts(passphrase, tx) await this.load() } finally { @@ -1948,6 +1965,10 @@ export class Wallet { 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', ) @@ -1965,8 +1986,11 @@ export class Wallet { 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(passphrase)) + this.accountById.set(id, account.decrypt(key)) } this.startUnlockTimeout(timeout) diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index 6260f5e60e..252f065dbd 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -1,7 +1,8 @@ /* 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 { encrypt, generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { MasterKey } from '../masterKey' import { AccountValueEncoding, DecryptedAccountValue, @@ -64,7 +65,7 @@ describe('AccountValueEncoding', () => { expect(deserializedValue).toEqual(value) }) - it('serializes an object encrypted account data into a buffer and deserializes to the original object', () => { + it('serializes an object encrypted account data into a buffer and deserializes to the original object', async () => { const encoder = new AccountValueEncoding() const key = generateKey() @@ -89,8 +90,11 @@ describe('AccountValueEncoding', () => { } const passphrase = 'foobarbaz' + const masterKey = MasterKey.generate(passphrase) + const xchacha20poly1305Key = await masterKey.unlock(passphrase) + const data = encoder.serialize(value) - const encryptedData = encrypt(data, passphrase) + const encryptedData = xchacha20poly1305Key.encrypt(data) const encryptedValue: EncryptedAccountValue = { encrypted: true, diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts index 351f643200..497deaf11f 100644 --- a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts +++ b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts @@ -2,31 +2,18 @@ * 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, NullableMasterKeyValueEncoding } from './masterKeyValue' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' describe('MasterKeyValueEncoding', () => { - describe('with a defined value', () => { - it('serializes the value into a buffer and deserializes to the original value', () => { - const encoder = new NullableMasterKeyValueEncoding() + 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) - }) - }) - - describe('with a null value', () => { - it('serializes the value into a buffer and deserializes to the original value', () => { - const encoder = new NullableMasterKeyValueEncoding() - - const value = null - const buffer = encoder.serialize(value) - const deserializedValue = encoder.deserialize(buffer) - expect(deserializedValue).toEqual(value) - }) + 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 index a706dcfafa..6a739ab0e8 100644 --- a/ironfish/src/wallet/walletdb/masterKeyValue.ts +++ b/ironfish/src/wallet/walletdb/masterKeyValue.ts @@ -10,37 +10,23 @@ export type MasterKeyValue = { salt: Buffer } -export class NullableMasterKeyValueEncoding - implements IDatabaseEncoding -{ - serialize(value: MasterKeyValue | null): Buffer { - const bw = bufio.write(this.getSize(value)) - - if (value) { - bw.writeBytes(value.nonce) - bw.writeBytes(value.salt) - } - +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 | null { + deserialize(buffer: Buffer): MasterKeyValue { const reader = bufio.read(buffer, true) - if (reader.left()) { - const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) - const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) - return { nonce, salt } - } - - return null + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } } - getSize(value: MasterKeyValue | null): number { - if (!value) { - return 0 - } - + getSize(): number { return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH } } diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 03d4e36e75..e1d12e56d2 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -14,6 +14,7 @@ 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' @@ -478,20 +479,22 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + 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(passphrase) + const decryptedAccountA = encryptedAccountA.decrypt(key) expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) const encryptedAccountB = encryptedAccountById.get(accountB.id) Assert.isNotUndefined(encryptedAccountB) - const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + const decryptedAccountB = encryptedAccountB.decrypt(key) expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) }) }) @@ -512,10 +515,7 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) } const encryptedAccountA = encryptedAccountById.get(accountA.id) @@ -560,10 +560,7 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) } const encryptedAccountA = encryptedAccountById.get(accountA.id) @@ -586,7 +583,10 @@ describe('WalletDB', () => { await useAccountFixture(node.wallet, 'A') const accountB = await useAccountFixture(node.wallet, 'B') - await walletDb.accounts.put(accountB.id, accountB.encrypt(passphrase).serialize()) + 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() }) @@ -675,6 +675,7 @@ describe('WalletDB', () => { const node = (await nodeTest.createSetup()).node const walletDb = node.wallet.walletDb const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) await useAccountFixture(node.wallet, 'A') @@ -690,7 +691,7 @@ describe('WalletDB', () => { } const account = new Account({ accountValue, walletDb }) - await expect(walletDb.setEncryptedAccount(account, passphrase)).rejects.toThrow() + await expect(walletDb.setEncryptedAccount(account, masterKey)).rejects.toThrow() }) it('saves the account', async () => { @@ -713,7 +714,12 @@ describe('WalletDB', () => { } const account = new Account({ accountValue, walletDb }) - await walletDb.setEncryptedAccount(account, passphrase) + 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( @@ -721,37 +727,4 @@ describe('WalletDB', () => { ).not.toBeUndefined() }) }) - - describe('canDecryptAccounts', () => { - it('throws an error if the accounts are decrypted', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - - await useAccountFixture(node.wallet, 'A') - - await expect(walletDb.canDecryptAccounts('invalid')).rejects.toThrow() - }) - - it('returns false if the passphrase is invalid', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - const passphrase = 'foobar' - - await useAccountFixture(node.wallet, 'A') - await walletDb.encryptAccounts(passphrase) - - expect(await walletDb.canDecryptAccounts('invalid')).toBe(false) - }) - - it('returns true if the passphrase is valid', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - const passphrase = 'foobar' - - await useAccountFixture(node.wallet, 'A') - await walletDb.encryptAccounts(passphrase) - - expect(await walletDb.canDecryptAccounts(passphrase)).toBe(true) - }) - }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 70b7a913ac..b9477924a3 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -33,12 +33,13 @@ import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' -import { AccountDecryptionFailedError } from '../errors' +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 { MultisigIdentityValue, MultisigIdentityValueEncoder } from './multisigIdentityValue' import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' @@ -148,6 +149,11 @@ export class WalletDB { value: ParticipantIdentity }> + masterKey: IDatabaseStore<{ + key: 'key' + value: MasterKeyValue + }> + cacheStores: Array> nullifierBloomFilter: BloomFilter | null = null @@ -314,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, @@ -370,7 +382,7 @@ export class WalletDB { async setEncryptedAccount( account: Account, - passphrase: string, + masterKey: MasterKey, tx?: IDatabaseTransaction, ): Promise { return this.db.withTransaction(tx, async (tx) => { @@ -379,10 +391,7 @@ export class WalletDB { throw new Error('Cannot save encrypted account when accounts are decrypted') } - const validPassphrase = await this.canDecryptAccounts(passphrase, tx) - Assert.isTrue(validPassphrase, 'Your passphrase is incorrect') - - const encryptedAccount = account.encrypt(passphrase) + const encryptedAccount = account.encrypt(masterKey) await this.accounts.put(account.id, encryptedAccount.serialize(), tx) const nativeUnconfirmedBalance = await this.balances.get( @@ -406,33 +415,6 @@ export class WalletDB { }) } - async canDecryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { - return this.db.withTransaction(tx, async (tx) => { - for await (const [_, accountValue] of this.accounts.getAllIter(tx)) { - if (!accountValue.encrypted) { - throw new Error('Wallet is already decrypted') - } - - const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, - walletDb: this, - }) - - try { - encryptedAccount.decrypt(passphrase) - } catch (e) { - if (e instanceof AccountDecryptionFailedError) { - return false - } - - throw e - } - } - - return true - }) - } - async removeAccount(account: Account, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { await this.accounts.del(account.id, tx) @@ -462,6 +444,18 @@ export class WalletDB { return meta } + 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> { @@ -1261,32 +1255,51 @@ 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(passphrase) + 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({ - data: accountValue.data, + accountValue, walletDb: this, }) - const account = encryptedAccount.decrypt(passphrase) + const account = encryptedAccount.decrypt(key) await this.accounts.put(id, account.serialize(), tx) } + + await masterKey.destroy() + await this.masterKey.del('key', tx) }) } From 4e383efb94bb9f35afb517665f9a29b670781a21 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 17 Sep 2024 14:58:23 -0700 Subject: [PATCH 103/114] Displays whether a participant has a secret or not This is going to be a useful feature when we start to integrate ledger because all identities from ledger will not export their secret. --- .../commands/wallet/multisig/participants/index.ts | 13 +++++++++++-- .../src/rpc/routes/wallet/multisig/getIdentities.ts | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts index d86ccb02cd..bbaf0cd33e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participants/index.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participants/index.ts @@ -18,10 +18,15 @@ export class MultisigParticipants extends IronfishCommand { 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, }) } @@ -36,6 +41,10 @@ export class MultisigParticipants extends IronfishCommand { 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/src/rpc/routes/wallet/multisig/getIdentities.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentities.ts index 79a12af078..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 }, + { name, secret }, ] of context.wallet.walletDb.multisigIdentities.getAllIter()) { identities.push({ name, identity: identity.toString('hex'), + hasSecret: secret ? true : false, }) } From baf314e7467eb3484eb078bbb49321678a8b459c Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:58:25 -0700 Subject: [PATCH 104/114] allows constructing NativeSigningCommitment from raw (#5378) updates ironfish-frost dependency to latest commit adds napi binding for SigningCommitment::from_raw to support constructing a signing commitment from its raw parts (the signer identity, the raw commitments, and the transaction hash and list of signers for computing a checksum) removes signing_package_from_raw in favor of constructing SingingCommitments and using existing signing_package method --- Cargo.lock | 2 +- ironfish-rust-nodejs/index.d.ts | 3 +- ironfish-rust-nodejs/src/multisig.rs | 37 ++++++++++++++++++- .../src/structs/transaction.rs | 32 ---------------- ironfish/src/multisig.test.slow.ts | 23 +++++++----- 5 files changed, 53 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7524426c83..cfb4a048eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1524,7 +1524,7 @@ dependencies = [ [[package]] name = "ironfish-frost" version = "0.1.0" -source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#06f16dd1684b7741f3bd6ba3e490343671626129" +source = "git+https://github.com/iron-fish/ironfish-frost.git?branch=main#d4681df8a5a613fd9e716212aae3be8602acd227" dependencies = [ "blake3", "chacha20 0.9.1", diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index fdf0e6c3a4..4e4a05af4e 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -249,7 +249,6 @@ export class UnsignedTransaction { publicKeyRandomness(): string hash(): Buffer signingPackage(nativeIdentiferCommitments: Array): string - signingPackageFromRaw(identities: Array, rawCommitments: Array): string sign(spenderHexKey: string): Buffer addSignature(signature: Buffer): Buffer } @@ -361,6 +360,8 @@ export namespace multisig { 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 diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 0291d9bfde..bb5f4b2a3d 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -5,7 +5,9 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ frost::{ - frost::round2::SignatureShare as FrostSignatureShare, keys::KeyPackage, round2, Randomizer, + frost::{round1::SigningCommitments, round2::SignatureShare as FrostSignatureShare}, + keys::KeyPackage, + round2, Randomizer, }, frost_utils::{ account_keys::derive_account_keys, signing_package::SigningPackage, @@ -379,6 +381,39 @@ 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()) diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 3b597bb932..17070949cd 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -12,7 +12,6 @@ use ironfish::frost::round1::SigningCommitments; use ironfish::frost::round2::SignatureShare as FrostSignatureShare; use ironfish::frost::Identifier; use ironfish::frost_utils::signing_package::SigningPackage; -use ironfish::participant::Identity; use ironfish::serializing::bytes_to_hex; use ironfish::serializing::fr::FrSerializable; use ironfish::serializing::hex_to_vec_bytes; @@ -456,37 +455,6 @@ impl NativeUnsignedTransaction { Ok(bytes_to_hex(&vec)) } - #[napi] - pub fn signing_package_from_raw( - &self, - identities: Vec, - raw_commitments: Vec, - ) -> Result { - let mut commitments = Vec::new(); - - for (index, identity) in identities.iter().enumerate() { - let identity_bytes = hex_to_vec_bytes(identity).map_err(to_napi_err)?; - let identity = Identity::deserialize_from(&identity_bytes[..]).map_err(to_napi_err)?; - - let raw_commitment = &raw_commitments[index]; - let commitment_bytes = hex_to_vec_bytes(raw_commitment).map_err(to_napi_err)?; - let commitment = - SigningCommitments::deserialize(&commitment_bytes[..]).map_err(to_napi_err)?; - - commitments.push((identity, commitment)); - } - - let signing_package = self - .transaction - .signing_package(commitments) - .map_err(to_napi_err)?; - - let mut vec: Vec = vec![]; - signing_package.write(&mut vec).map_err(to_napi_err)?; - - Ok(bytes_to_hex(&vec)) - } - #[napi] pub fn sign(&mut self, spender_hex_key: String) -> Result { let spender_key = SaplingKey::from_hex(&spender_hex_key).map_err(to_napi_err)?; diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts index 8af50cad91..a6a3e01955 100644 --- a/ironfish/src/multisig.test.slow.ts +++ b/ironfish/src/multisig.test.slow.ts @@ -110,18 +110,23 @@ describe('multisig', () => { // Simulates receiving raw commitments from Ledger // Ledger app generates raw commitments, not wrapped SigningCommitment - const commitmentIdentities: string[] = [] - const rawCommitments: string[] = [] + const signingCommitments: string[] = [] for (const commitment of commitments) { - const signingCommitment = new multisig.SigningCommitment(Buffer.from(commitment, 'hex')) - commitmentIdentities.push(signingCommitment.identity().toString('hex')) - rawCommitments.push(signingCommitment.rawCommitments().toString('hex')) + 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.signingPackageFromRaw( - commitmentIdentities, - rawCommitments, - ) + 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 From 9263726906e9677a0d98a85f3b85ba530e934e49 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 17 Sep 2024 15:57:21 -0700 Subject: [PATCH 105/114] Add ledger flag to dkg rounds (#5380) In preparation for adding ledger support to the DKG rounds, this commit adds a `--ledger` flag to the `dkg round1`, `dkg round2`, and `dkg round3` commands. This flag will be used to specify that the user wants to use a Ledger device to perform DKG operations --- .../commands/wallet/multisig/dkg/round1.ts | 24 +++++++++++++++++++ .../commands/wallet/multisig/dkg/round2.ts | 24 +++++++++++++++++++ .../commands/wallet/multisig/dkg/round3.ts | 24 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index 01767a45bc..b26085cf0c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -5,6 +5,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' 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' @@ -26,6 +27,11 @@ 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 { @@ -64,6 +70,11 @@ export class DkgRound1Command extends IronfishCommand { } } + if (flags.ledger) { + await this.performRound1WithLedger() + return + } + const response = await client.wallet.multisig.dkg.round1({ participantName, participants: identities.map((identity) => ({ identity })), @@ -81,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 fcb9a59f17..d2b5b4027b 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -5,6 +5,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' 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' @@ -26,6 +27,11 @@ 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 { @@ -63,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, @@ -81,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 53a0458b48..12034755fd 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -5,6 +5,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' 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' @@ -36,6 +37,11 @@ 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 { @@ -100,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, @@ -113,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 + } + } + } } From 7413250a63ef568369c01776b1433e9d1f061596 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:54:22 -0700 Subject: [PATCH 106/114] defines from_frost factory method on NativePublicKeyPackage (#5381) allows us to construct a PublicKeyPackage from the raw parts: the frost public key package, the list of signer identities, and the minimum number of signers following the round3_min changes to ironfish-frost the Ledger app will produce a raw frost public key package at the end of DKG round3, so we will need to construct the ironfish-frost PublicKeyPackage from its parts --- ironfish-rust-nodejs/index.d.ts | 3 +++ ironfish-rust-nodejs/src/multisig.rs | 38 +++++++++++++++++++++++++++- ironfish/src/multisig.test.slow.ts | 18 +++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 4e4a05af4e..eb4ed16352 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -354,7 +354,10 @@ 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 diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index bb5f4b2a3d..dde0171e5f 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -5,7 +5,10 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ frost::{ - frost::{round1::SigningCommitments, round2::SignatureShare as FrostSignatureShare}, + frost::{ + keys::PublicKeyPackage as FrostPublicKeyPackage, round1::SigningCommitments, + round2::SignatureShare as FrostSignatureShare, + }, keys::KeyPackage, round2, Randomizer, }, @@ -351,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 @@ -360,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() diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts index a6a3e01955..f07eaa92c6 100644 --- a/ironfish/src/multisig.test.slow.ts +++ b/ironfish/src/multisig.test.slow.ts @@ -54,6 +54,24 @@ describe('multisig', () => { 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( From 336bc06dba23c90a3e9a473719f71388c0b2ca7d Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 18 Sep 2024 08:46:36 -0700 Subject: [PATCH 107/114] adds wallet/multisig/importParticipant RPC (#5383) * adds wallet/multisig/importParticipant RPC provides an RPC route for importing an existing identity and optional secret into the walletdb useful for creating a record of the identity retrieved from a Ledger device participating in DKG to generate a multisig account throws errors if the identity already exists in the walletdb or if the name is already in use for another identity or account * fixes importParticipant test --- ironfish/src/rpc/clients/client.ts | 11 ++ .../wallet/multisig/importParticipant.test.ts | 108 ++++++++++++++++++ .../wallet/multisig/importParticipant.ts | 70 ++++++++++++ .../src/rpc/routes/wallet/multisig/index.ts | 1 + ironfish/src/wallet/errors.ts | 9 ++ ironfish/src/wallet/walletdb/walletdb.ts | 15 ++- 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts create mode 100644 ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 79742e1d00..06e3760519 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -138,6 +138,8 @@ import type { GetWorkersStatusRequest, GetWorkersStatusResponse, ImportAccountRequest, + ImportParticipantRequest, + ImportParticipantResponse, ImportResponse, IsValidPublicAddressRequest, IsValidPublicAddressResponse, @@ -275,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> => { 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..e004606d9e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts @@ -0,0 +1,108 @@ +/* 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 participant already exists for the identity ${identity + .serialize() + .toString('hex')}`, + ), + 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..db07f7dd5e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts @@ -0,0 +1,70 @@ +/* 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 { + DuplicateAccountNameError, + DuplicateIdentityError, + DuplicateIdentityNameError, +} from '../../../../wallet/errors' +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 DuplicateIdentityNameError(request.data.name) + } + + if ( + await context.wallet.walletDb.getMultisigIdentity( + Buffer.from(request.data.identity, 'hex'), + ) + ) { + throw new DuplicateIdentityError(request.data.identity) + } + + if (context.wallet.getAccountByName(request.data.name)) { + throw new DuplicateAccountNameError(request.data.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/wallet/errors.ts b/ironfish/src/wallet/errors.ts index abb1a16961..dd1e4f9d1d 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -52,6 +52,15 @@ export class DuplicateIdentityNameError extends Error { } } +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 diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index b9477924a3..2f00cac016 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -1456,6 +1456,19 @@ export class WalletDB { 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, @@ -1470,7 +1483,7 @@ 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 *getMultisigIdentities( From a57ca307c18fa9b62d732914af68b893fe9df608 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:53:11 -0700 Subject: [PATCH 108/114] fixes wallet/multisig/getIdentity for undefined secrets (#5388) uses walletDb.getMultisigIdentityByName instead of looking up secret by name since some identities may not have secrets (i.e., identities created using Ledger) these identities won't be found when looking identities up using walletDb.getMultisigSecretByName --- ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts index bed99978fb..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,13 +35,11 @@ routes.register( const { name } = request.data - const secret = await context.wallet.walletDb.getMultisigSecretByName(name) - if (secret === undefined) { + const identity = await context.wallet.walletDb.getMultisigIdentityByName(name) + if (identity === undefined) { throw new RpcValidationError(`No identity found with name ${name}`, 404) } - const identity = new multisig.ParticipantSecret(secret).toIdentity() - - request.end({ identity: identity.serialize().toString('hex') }) + request.end({ identity: identity.toString('hex') }) }, ) From 36deabb22dab6b18f4de255b1673ab20f9a31424 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:53:20 -0700 Subject: [PATCH 109/114] fixes errors in wallet/multisig/importParticipant (#5385) throws RPC errors with recognizable error codes allows CLI to hadnle RPC errors more easily --- .../wallet/multisig/importParticipant.test.ts | 4 +--- .../wallet/multisig/importParticipant.ts | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts index e004606d9e..6d7e782aec 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.test.ts @@ -28,9 +28,7 @@ describe('Route wallet/multisig/importParticipant', () => { ).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( - `Multisig participant already exists for the identity ${identity - .serialize() - .toString('hex')}`, + `Multisig identity ${identity.serialize().toString('hex')} already exists`, ), status: 400, }), diff --git a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts index db07f7dd5e..0e1de10590 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts @@ -2,11 +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 * as yup from 'yup' -import { - DuplicateAccountNameError, - DuplicateIdentityError, - DuplicateIdentityNameError, -} from '../../../../wallet/errors' +import { RPC_ERROR_CODES, RpcValidationError } from '../../../adapters' import { ApiNamespace } from '../../namespaces' import { routes } from '../../router' import { AssertHasRpcContext } from '../../rpcContext' @@ -42,7 +38,11 @@ routes.register Date: Thu, 19 Sep 2024 15:35:08 -0400 Subject: [PATCH 110/114] fix(ironfish): Fix unlock hanging in memory client CLI (#5392) --- ironfish/src/rpc/clients/memoryClient.ts | 4 +++- ironfish/src/rpc/routes/node/stopNode.ts | 2 +- ironfish/src/wallet/masterKey.ts | 2 -- ironfish/src/wallet/wallet.ts | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) 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/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/wallet/masterKey.ts b/ironfish/src/wallet/masterKey.ts index 4b3ba40cc8..c75eb1c3d5 100644 --- a/ironfish/src/wallet/masterKey.ts +++ b/ironfish/src/wallet/masterKey.ts @@ -93,7 +93,5 @@ export class MasterKey { async destroy(): Promise { await this.lock() - this.nonce.fill(0) - this.salt.fill(0) } } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index bf7d738667..d3ff8da34c 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -280,6 +280,12 @@ export class Wallet { } async stop(): Promise { + if (this.masterKey) { + await this.masterKey.destroy() + } + + this.stopUnlockTimeout() + if (!this.isStarted) { return } @@ -289,12 +295,6 @@ export class Wallet { clearTimeout(this.eventLoopTimeout) } - if (this.masterKey) { - await this.masterKey.destroy() - } - - this.stopUnlockTimeout() - await this.scanner.abort() this.eventLoopAbortController.abort() await this.eventLoopPromise From c5ff945504aecc8c8bc5e283e74447170132a31c Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:10:34 -0400 Subject: [PATCH 111/114] fix(ironfish): Guard event loop when the wallet is locked (#5393) --- ironfish/src/wallet/wallet.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index d3ff8da34c..b961bf8a02 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -308,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) { From 9319ca0294d198fafdd33a20e302d5a85c461750 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:10:49 -0400 Subject: [PATCH 112/114] feat(ironfish): Write encrypted accounts when toggling scanning (#5394) --- ironfish/src/rpc/routes/wallet/setScanning.ts | 2 +- ironfish/src/wallet/account/account.ts | 12 +++++++++++- ironfish/src/wallet/wallet.ts | 8 ++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) 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/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 9f880e0558..fc21d97f2d 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -1299,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( diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index b961bf8a02..b37c278120 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1571,6 +1571,14 @@ export class Wallet { 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()) } From b27366f058fb8b662cdbd2705257f13e19a4328c Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:11:03 -0400 Subject: [PATCH 113/114] feat(cli): Check the wallet is unlocked before prompting for name (#5395) * feat(cli): Check the wallet is unlocked before prompting for name * feat(cli): Add this.exit(0) * update message * update tests --- ironfish-cli/src/commands/wallet/create.ts | 9 +++++---- ironfish/src/rpc/routes/wallet/decrypt.test.ts | 2 +- ironfish/src/rpc/routes/wallet/unlock.test.ts | 2 +- ironfish/src/wallet/errors.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/create.ts b/ironfish-cli/src/commands/wallet/create.ts index db1c1a40c0..56238c933e 100644 --- a/ironfish-cli/src/commands/wallet/create.ts +++ b/ironfish-cli/src/commands/wallet/create.ts @@ -23,15 +23,14 @@ export class CreateCommand extends IronfishCommand { async start(): Promise { const { args } = await this.parse(CreateCommand) - let name = args.name + 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() - await checkWalletUnlocked(client) - this.log(`Creating account ${name}`) const result = await client.wallet.createAccount({ name }) @@ -44,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/src/rpc/routes/wallet/decrypt.test.ts b/ironfish/src/rpc/routes/wallet/decrypt.test.ts index 17628ef3a5..e347ebecb9 100644 --- a/ironfish/src/rpc/routes/wallet/decrypt.test.ts +++ b/ironfish/src/rpc/routes/wallet/decrypt.test.ts @@ -55,7 +55,7 @@ describe('Route wallet/encrypt', () => { await expect( routeTest.client.wallet.decrypt({ passphrase: invalidPassphrase }), - ).rejects.toThrow('Request failed (400) error: Failed to decrypt account') + ).rejects.toThrow('Request failed (400) error: Failed to decrypt wallet') status = await routeTest.client.wallet.getAccountsStatus() expect(status.content.encrypted).toBe(true) diff --git a/ironfish/src/rpc/routes/wallet/unlock.test.ts b/ironfish/src/rpc/routes/wallet/unlock.test.ts index 33c2f14145..9e2ddf9e6d 100644 --- a/ironfish/src/rpc/routes/wallet/unlock.test.ts +++ b/ironfish/src/rpc/routes/wallet/unlock.test.ts @@ -42,7 +42,7 @@ describe('Route wallet/unlock', () => { await expect( routeTest.client.wallet.unlock({ passphrase: invalidPassphrase }), - ).rejects.toThrow('Request failed (400) error: Failed to decrypt account') + ).rejects.toThrow('Request failed (400) error: Failed to decrypt wallet') status = await routeTest.client.wallet.getAccountsStatus() expect(status.content.encrypted).toBe(true) diff --git a/ironfish/src/wallet/errors.ts b/ironfish/src/wallet/errors.ts index dd1e4f9d1d..e0d9c0c16e 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -84,6 +84,6 @@ export class AccountDecryptionFailedError extends Error { constructor() { super() - this.message = 'Failed to decrypt account' + this.message = 'Failed to decrypt wallet' } } From 6c16fc686e413f8fccb674661dcb2c99f4a2ff5b Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:00:48 -0400 Subject: [PATCH 114/114] v2.6.0 (#5404) --- ironfish-cli/package.json | 6 +++--- ironfish-rust-nodejs/npm/darwin-arm64/package.json | 2 +- ironfish-rust-nodejs/npm/darwin-x64/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-arm64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-gnu/package.json | 2 +- ironfish-rust-nodejs/npm/linux-x64-musl/package.json | 2 +- ironfish-rust-nodejs/npm/win32-x64-msvc/package.json | 2 +- ironfish-rust-nodejs/package.json | 2 +- ironfish/package.json | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index b7fed0ff61..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,8 +59,8 @@ "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-help": "6.2.5", 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/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",