From a790e39102d243cf74f93313a9c17d9b29305d17 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:24:01 -0400 Subject: [PATCH 01/37] feat(cli): Unhide encryption CLI commands (#5396) --- ironfish-cli/src/commands/wallet/decrypt.ts | 2 -- ironfish-cli/src/commands/wallet/encrypt.ts | 2 -- ironfish-cli/src/commands/wallet/lock.ts | 2 -- ironfish-cli/src/commands/wallet/unlock.ts | 2 -- 4 files changed, 8 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/decrypt.ts b/ironfish-cli/src/commands/wallet/decrypt.ts index 177fc07ef6..8e512cecc7 100644 --- a/ironfish-cli/src/commands/wallet/decrypt.ts +++ b/ironfish-cli/src/commands/wallet/decrypt.ts @@ -8,8 +8,6 @@ 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 = { diff --git a/ironfish-cli/src/commands/wallet/encrypt.ts b/ironfish-cli/src/commands/wallet/encrypt.ts index 2573ceb8c1..018febb7ba 100644 --- a/ironfish-cli/src/commands/wallet/encrypt.ts +++ b/ironfish-cli/src/commands/wallet/encrypt.ts @@ -8,8 +8,6 @@ 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 = { diff --git a/ironfish-cli/src/commands/wallet/lock.ts b/ironfish-cli/src/commands/wallet/lock.ts index 3fc5ce43a1..763f8c8f0c 100644 --- a/ironfish-cli/src/commands/wallet/lock.ts +++ b/ironfish-cli/src/commands/wallet/lock.ts @@ -6,8 +6,6 @@ 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 = { diff --git a/ironfish-cli/src/commands/wallet/unlock.ts b/ironfish-cli/src/commands/wallet/unlock.ts index bf874f11c6..1326df62ff 100644 --- a/ironfish-cli/src/commands/wallet/unlock.ts +++ b/ironfish-cli/src/commands/wallet/unlock.ts @@ -8,8 +8,6 @@ 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 = { From 2d73d0f20c6a7495b0077827edff5b8b1623383f Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:34:36 -0400 Subject: [PATCH 02/37] feat(cli,ironfish): Add descriptions to primitives index (#5412) --- ironfish-cli/src/utils/transaction.ts | 4 ++-- ironfish/src/primitives/index.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index b60e4ddccf..66a20de65e 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -5,11 +5,13 @@ import { Asset } from '@ironfish/rust-nodejs' import { assetMetadataWithDefaults, + BurnDescription, createRootLogger, CurrencyUtils, GetTransactionNotesResponse, GetUnsignedTransactionNotesResponse, Logger, + MintDescription, PromiseUtils, RawTransaction, RpcAsset, @@ -19,8 +21,6 @@ import { TransactionStatus, UnsignedTransaction, } from '@ironfish/sdk' -import { BurnDescription } from '@ironfish/sdk/src/primitives/burnDescription' -import { MintDescription } from '@ironfish/sdk/src/primitives/mintDescription' import { ux } from '@oclif/core' import { ProgressBar, ProgressBarPresets } from '../ui' import { getAssetsByIDs, getAssetVerificationByIds } from './asset' diff --git a/ironfish/src/primitives/index.ts b/ironfish/src/primitives/index.ts index 658cf72313..50e79ca522 100644 --- a/ironfish/src/primitives/index.ts +++ b/ironfish/src/primitives/index.ts @@ -10,3 +10,5 @@ export { Target } from './target' export { Transaction } from './transaction' export { RawTransaction, RawTransactionSerde } from './rawTransaction' export { UnsignedTransaction } from './unsignedTransaction' +export { MintDescription } from './mintDescription' +export { BurnDescription } from './burnDescription' From e3f84d53c8200e195719f4ceb25b66a08d6d7775 Mon Sep 17 00:00:00 2001 From: Rohan Jadvani <5459049+rohanjadvani@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:34:45 -0400 Subject: [PATCH 03/37] feat(cli,ironfish): Improve export for AccountImport (#5414) --- ironfish-cli/src/commands/wallet/multisig/dealer/create.ts | 3 +-- ironfish/src/wallet/index.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts b/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts index 095ebc0ed7..e56e8eeda0 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dealer/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dealer/create.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 { ACCOUNT_SCHEMA_VERSION, JsonEncoder, RpcClient } from '@ironfish/sdk' -import { AccountImport } from '@ironfish/sdk/src/wallet/exporter' +import { ACCOUNT_SCHEMA_VERSION, AccountImport, JsonEncoder, RpcClient } from '@ironfish/sdk' import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' diff --git a/ironfish/src/wallet/index.ts b/ironfish/src/wallet/index.ts index eab15210d6..e9a61f7f56 100644 --- a/ironfish/src/wallet/index.ts +++ b/ironfish/src/wallet/index.ts @@ -3,8 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ export * from './account/account' export * from './wallet' -export * from './exporter/encoder' -export * from './exporter/account' +export * from './exporter' export { AccountValue } from './walletdb/accountValue' export { Base64JsonEncoder } from './exporter/encoders/base64json' export { JsonEncoder } from './exporter/encoders/json' From a26259bbffa667b03077a7e0307153a1f98c41b0 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:11:35 -0700 Subject: [PATCH 04/37] Add the ability to use a ledger device with the mint command (#5417) --- ironfish-cli/src/commands/wallet/mint.ts | 17 +++++ ironfish-cli/src/commands/wallet/send.ts | 91 +++-------------------- ironfish-cli/src/utils/ledger.ts | 93 +++++++++++++++++++++++- 3 files changed, 118 insertions(+), 83 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index 9981524248..dea10ca21a 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -23,6 +23,7 @@ import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' import { selectFee } from '../../utils/fees' +import { sendTransactionWithLedger } from '../../utils/ledger' import { watchTransaction } from '../../utils/transaction' export class Mint extends IronfishCommand { @@ -102,6 +103,10 @@ This will create tokens and increase supply for a given asset.` 'Return a serialized UnsignedTransaction. Use it to create a transaction and build proofs but not post to the network', exclusive: ['rawTransaction'], }), + ledger: Flags.boolean({ + default: false, + description: 'Mint a transaction using a Ledger device', + }), } async start(): Promise { @@ -286,6 +291,18 @@ This will create tokens and increase supply for a given asset.` assetData, ) + if (flags.ledger) { + await sendTransactionWithLedger( + client, + raw, + account, + flags.watch, + flags.confirm, + this.logger, + ) + this.exit(0) + } + ux.action.start('Sending the transaction') const response = await client.wallet.postTransaction({ diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 66107f8355..4c7efe5231 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -8,7 +8,6 @@ import { isValidPublicAddress, RawTransaction, RawTransactionSerde, - RpcClient, TimeUtils, Transaction, } from '@ironfish/sdk' @@ -21,7 +20,7 @@ import { promptCurrency } from '../../utils/currency' import { promptExpiration } from '../../utils/expiration' import { getExplorer } from '../../utils/explorer' import { selectFee } from '../../utils/fees' -import { Ledger } from '../../utils/ledger' +import { sendTransactionWithLedger } from '../../utils/ledger' import { getSpendPostTimeInMs, updateSpendPostTimeInMs } from '../../utils/spendPostTime' import { displayTransactionSummary, @@ -259,7 +258,14 @@ export class Send extends IronfishCommand { } if (flags.ledger) { - await this.sendTransactionWithLedger(client, raw, from, flags.watch, flags.confirm) + await sendTransactionWithLedger( + client, + raw, + from, + flags.watch, + flags.confirm, + this.logger, + ) this.exit(0) } @@ -351,83 +357,4 @@ export class Send extends IronfishCommand { }) } } - - private async sendTransactionWithLedger( - client: RpcClient, - raw: RawTransaction, - from: string | undefined, - watch: boolean, - confirm: boolean, - ): Promise { - const ledger = new Ledger(this.logger) - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } - - const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content - .publicKey - - const ledgerPublicKey = await ledger.getPublicAddress() - - if (publicKey !== ledgerPublicKey) { - this.error( - `The public key on the ledger device does not match the public key of the account '${from}'`, - ) - } - - const buildTransactionResponse = await client.wallet.buildTransaction({ - account: from, - rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), - }) - - const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction - - const signature = (await ledger.sign(unsignedTransaction)).toString('hex') - - this.log(`\nSignature: ${signature}`) - - const addSignatureResponse = await client.wallet.addSignature({ - unsignedTransaction, - signature, - }) - - const signedTransaction = addSignatureResponse.content.transaction - const bytes = Buffer.from(signedTransaction, 'hex') - - const transaction = new Transaction(bytes) - - this.log(`\nSigned Transaction: ${signedTransaction}`) - this.log(`\nHash: ${transaction.hash().toString('hex')}`) - this.log(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) - - await ui.confirmOrQuit('', confirm) - - const addTransactionResponse = await client.wallet.addTransaction({ - transaction: signedTransaction, - broadcast: true, - }) - - if (addTransactionResponse.content.accepted === false) { - this.error( - `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, - ) - } - - if (watch) { - this.log('') - - await watchTransaction({ - client, - logger: this.logger, - account: from, - hash: transaction.hash().toString('hex'), - }) - } - } } diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index fe472383ec..9a560e5ce5 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -1,9 +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 { createRootLogger, Logger } from '@ironfish/sdk' +import { + createRootLogger, + CurrencyUtils, + Logger, + RawTransaction, + RawTransactionSerde, + RpcClient, + Transaction, +} from '@ironfish/sdk' import { AccountImport } from '@ironfish/sdk/src/wallet/exporter' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import { Errors, ux } from '@oclif/core' import IronfishApp, { IronfishKeys, ResponseAddress, @@ -11,6 +20,8 @@ import IronfishApp, { ResponseSign, ResponseViewKey, } from '@zondax/ledger-ironfish' +import * as ui from '../ui' +import { watchTransaction } from './transaction' export class Ledger { app: IronfishApp | undefined @@ -160,3 +171,83 @@ export class Ledger { return response.signature } } + +export async function sendTransactionWithLedger( + client: RpcClient, + raw: RawTransaction, + from: string | undefined, + watch: boolean, + confirm: boolean, + logger?: Logger, +): Promise { + const ledger = new Ledger(logger) + try { + await ledger.connect() + } catch (e) { + if (e instanceof Error) { + Errors.error(e.message) + } else { + throw e + } + } + + const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content + .publicKey + + const ledgerPublicKey = await ledger.getPublicAddress() + + if (publicKey !== ledgerPublicKey) { + Errors.error( + `The public key on the ledger device does not match the public key of the account '${from}'`, + ) + } + + const buildTransactionResponse = await client.wallet.buildTransaction({ + account: from, + rawTransaction: RawTransactionSerde.serialize(raw).toString('hex'), + }) + + const unsignedTransaction = buildTransactionResponse.content.unsignedTransaction + + const signature = (await ledger.sign(unsignedTransaction)).toString('hex') + + ux.stdout(`\nSignature: ${signature}`) + + const addSignatureResponse = await client.wallet.addSignature({ + unsignedTransaction, + signature, + }) + + const signedTransaction = addSignatureResponse.content.transaction + const bytes = Buffer.from(signedTransaction, 'hex') + + const transaction = new Transaction(bytes) + + ux.stdout(`\nSigned Transaction: ${signedTransaction}`) + ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) + ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) + + await ui.confirmOrQuit('', confirm) + + const addTransactionResponse = await client.wallet.addTransaction({ + transaction: signedTransaction, + broadcast: true, + }) + + if (addTransactionResponse.content.accepted === false) { + Errors.error( + `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, + ) + } + + if (watch) { + ux.stdout('') + + await watchTransaction({ + client, + logger, + account: from, + hash: transaction.hash().toString('hex'), + }) + } +} From dcaabd851816e9f994a6f81739d6d090f6e6fad9 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:47:54 -0700 Subject: [PATCH 05/37] exports makeFakeWitness test util from ironfish sdk (#5415) * exports makeFakeWitness test util from ironfish sdk allows external developers to test transactions without a blockchain * exports makeFakeWitness as part of testUtilities export creates the same effect as a namespace --- ironfish/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ironfish/src/index.ts b/ironfish/src/index.ts index 1ee9e55d5b..8e8a483d92 100644 --- a/ironfish/src/index.ts +++ b/ironfish/src/index.ts @@ -28,3 +28,6 @@ export * from './package' export * from './platform' export * from './primitives' export { getFeeRate } from './memPool' + +import { makeFakeWitness } from './testUtilities' +export const testUtilities = { makeFakeWitness } From b6789839183770416737365ddc9a933b5276d386 Mon Sep 17 00:00:00 2001 From: Jason Spafford Date: Tue, 24 Sep 2024 08:53:55 -0700 Subject: [PATCH 06/37] Enable transfering in a mint in the CLI (#5418) --- ironfish-cli/src/commands/wallet/mint.ts | 55 +++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index dea10ca21a..c3f92df229 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -97,6 +97,12 @@ 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.', }), + transferTo: Flags.string({ + description: 'transfer all newly minted coins to this public address', + }), + transferToMemo: Flags.string({ + description: 'The memo of transfer when using transferTo', + }), unsignedTransaction: Flags.boolean({ default: false, description: @@ -218,6 +224,12 @@ This will create tokens and increase supply for a given asset.` } } + if (flags.transferTo) { + if (!isValidPublicAddress(flags.transferTo)) { + this.error('transferTo must be a valid public address') + } + } + let expiration = flags.expiration if ((flags.rawTransaction || flags.unsignedTransaction) && expiration === undefined) { expiration = await promptExpiration({ logger: this.logger, client: client }) @@ -228,25 +240,34 @@ This will create tokens and increase supply for a given asset.` this.exit(1) } + const mint = { + // Only provide the asset id if we are not minting an asset for the first time + ...(assetData != null ? { assetId } : {}), + name: name, + metadata: metadata, + value: CurrencyUtils.encode(amount), + transferOwnershipTo: flags.transferOwnershipTo, + } + const params: CreateTransactionRequest = { account, outputs: [], - mints: [ - { - // Only provide the asset id if we are not minting an asset for the first time - ...(assetData != null ? { assetId } : {}), - name, - metadata, - value: CurrencyUtils.encode(amount), - transferOwnershipTo: flags.transferOwnershipTo, - }, - ], + mints: [mint], fee: flags.fee ? CurrencyUtils.encode(flags.fee) : null, feeRate: flags.feeRate ? CurrencyUtils.encode(flags.feeRate) : null, expiration: expiration, confirmations: flags.confirmations, } + if (flags.transferTo) { + params.outputs.push({ + publicAddress: flags.transferTo, + amount: mint.value, + assetId: assetId, + memo: flags.transferToMemo, + }) + } + let raw: RawTransaction if (params.fee === null && params.feeRate === null) { raw = await selectFee({ @@ -287,6 +308,7 @@ This will create tokens and increase supply for a given asset.` name, metadata, flags.transferOwnershipTo, + flags.transferTo, flags.confirm, assetData, ) @@ -341,6 +363,7 @@ This will create tokens and increase supply for a given asset.` Value: renderedValue, Fee: renderedFee, Hash: transaction.hash().toString('hex'), + 'Transfered To': flags.transferTo ? flags.transferTo : undefined, }), ) @@ -373,6 +396,7 @@ This will create tokens and increase supply for a given asset.` name?: string, metadata?: string, transferOwnershipTo?: string, + transferTo?: string, confirm?: boolean, assetData?: RpcAsset, ): Promise { @@ -393,6 +417,17 @@ This will create tokens and increase supply for a given asset.` `Fee: ${renderedFee}`, ] + if (transferTo) { + confirmMessage.push( + `\nAll ${CurrencyUtils.render( + amount, + false, + assetId, + assetData?.verification, + )} of this asset will be transferred to ${transferTo}.`, + ) + } + if (transferOwnershipTo) { confirmMessage.push( `Ownership of this asset will be transferred to ${transferOwnershipTo}. The current account will no longer have any permission to mint or modify this asset. This cannot be undone.`, From 9339d42d65c9d1e9842b75addee05cd4004c2602 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:16:43 -0700 Subject: [PATCH 07/37] use the testnet api url for asset verification when on testnet network (#5384) --- .../src/assets/assetsVerificationApi.test.ts | 28 ++++++++++++++++++- ironfish/src/assets/assetsVerificationApi.ts | 12 ++++++-- ironfish/src/assets/assetsVerifier.test.ts | 6 ++-- ironfish/src/assets/assetsVerifier.ts | 8 +++--- ironfish/src/node.ts | 23 +++++++-------- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/ironfish/src/assets/assetsVerificationApi.test.ts b/ironfish/src/assets/assetsVerificationApi.test.ts index bf177144bc..ffc87745f2 100644 --- a/ironfish/src/assets/assetsVerificationApi.test.ts +++ b/ironfish/src/assets/assetsVerificationApi.test.ts @@ -3,7 +3,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import nock from 'nock' import { NodeFileProvider } from '../fileSystems' -import { AssetsVerificationApi } from './assetsVerificationApi' +import { + AssetsVerificationApi, + getDefaultAssetVerificationEndpoint, +} from './assetsVerificationApi' const assetData1 = { identifier: '0123', @@ -239,3 +242,26 @@ describe('Assets Verification API Client', () => { }) }) }) + +describe('getDefaultAssetVerificationEndpoint', () => { + it('returns the testnet url with the testnet id', () => { + expect(getDefaultAssetVerificationEndpoint(0)).toEqual( + 'https://testnet.api.ironfish.network/assets/verified_metadata', + ) + }) + + it('returns the regular url with any other id', () => { + expect(getDefaultAssetVerificationEndpoint(1)).toEqual( + 'https://api.ironfish.network/assets/verified_metadata', + ) + expect(getDefaultAssetVerificationEndpoint(10)).toEqual( + 'https://api.ironfish.network/assets/verified_metadata', + ) + }) + + it('returns the regular url with no id', () => { + expect(getDefaultAssetVerificationEndpoint()).toEqual( + 'https://api.ironfish.network/assets/verified_metadata', + ) + }) +}) diff --git a/ironfish/src/assets/assetsVerificationApi.ts b/ironfish/src/assets/assetsVerificationApi.ts index 491e980ea7..32835f8b35 100644 --- a/ironfish/src/assets/assetsVerificationApi.ts +++ b/ironfish/src/assets/assetsVerificationApi.ts @@ -79,8 +79,8 @@ export class AssetsVerificationApi { readonly url: string - constructor(options: { files: FileSystem; url?: string; timeout?: number }) { - this.url = options?.url || 'https://api.ironfish.network/assets/verified_metadata' + constructor(options: { files: FileSystem; url: string; timeout?: number }) { + this.url = options.url this.timeout = options?.timeout ?? 30 * 1000 // 30 seconds this.adapter = isFileUrl(this.url) ? axiosFileAdapter(options.files) @@ -130,6 +130,14 @@ export class AssetsVerificationApi { } } +export function getDefaultAssetVerificationEndpoint(networkId?: number): string { + if (networkId === 0) { + return 'https://testnet.api.ironfish.network/assets/verified_metadata' + } + + return 'https://api.ironfish.network/assets/verified_metadata' +} + const isFileUrl = (url: string): boolean => { const parsedUrl = new URL(url) return parsedUrl.protocol === 'file:' diff --git a/ironfish/src/assets/assetsVerifier.test.ts b/ironfish/src/assets/assetsVerifier.test.ts index 83b182d16a..8eb23e6363 100644 --- a/ironfish/src/assets/assetsVerifier.test.ts +++ b/ironfish/src/assets/assetsVerifier.test.ts @@ -7,6 +7,8 @@ import { VerifiedAssetsCacheStore } from '../fileStores/verifiedAssets' import { NodeFileProvider } from '../fileSystems' import { AssetsVerifier } from './assetsVerifier' +const apiUrl = 'https://example.com/endpoint' + /* eslint-disable jest/no-standalone-expect */ /* eslint-disable @typescript-eslint/no-explicit-any */ const assetData1 = { @@ -58,7 +60,7 @@ describe('AssetsVerifier', () => { }) it('does not refresh when not started', () => { - const assetsVerifier = new AssetsVerifier({ files }) + const assetsVerifier = new AssetsVerifier({ files, apiUrl }) const refresh = jest.spyOn(assetsVerifier as any, 'refresh') jest.runOnlyPendingTimers() @@ -191,7 +193,7 @@ describe('AssetsVerifier', () => { describe('verify', () => { it("returns 'unknown' when not started", () => { - const assetsVerifier = new AssetsVerifier({ files }) + const assetsVerifier = new AssetsVerifier({ files, apiUrl }) expect(assetsVerifier.verify('0123')).toStrictEqual({ status: 'unknown' }) expect(assetsVerifier.verify('4567')).toStrictEqual({ status: 'unknown' }) diff --git a/ironfish/src/assets/assetsVerifier.ts b/ironfish/src/assets/assetsVerifier.ts index 9f40b6f9a0..d7e08c4735 100644 --- a/ironfish/src/assets/assetsVerifier.ts +++ b/ironfish/src/assets/assetsVerifier.ts @@ -35,13 +35,13 @@ export class AssetsVerifier { constructor(options: { files: FileSystem - apiUrl?: string + apiUrl: string cache?: VerifiedAssetsCacheStore logger?: Logger }) { - this.logger = options?.logger ?? createRootLogger() - this.api = new AssetsVerificationApi({ url: options?.apiUrl, files: options.files }) - this.cache = options?.cache + this.logger = options.logger ?? createRootLogger() + this.api = new AssetsVerificationApi({ url: options.apiUrl, files: options.files }) + this.cache = options.cache this.started = false if (this.cache?.config?.apiUrl === this.api.url) { diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index bdd70abd05..34e411e9a9 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { BoxKeyPair, FishHashContext } from '@ironfish/rust-nodejs' import { v4 as uuid } from 'uuid' -import { AssetsVerifier } from './assets' +import { AssetsVerifier, getDefaultAssetVerificationEndpoint } from './assets' import { Blockchain } from './blockchain' import { BlockHasher } from './blockHasher' import { @@ -223,16 +223,6 @@ export class FullNode { const peerStore = new PeerStore(files, dataDir) await peerStore.load() - const verifiedAssetsCache = new VerifiedAssetsCacheStore(files, dataDir) - await verifiedAssetsCache.load() - - const assetsVerifier = new AssetsVerifier({ - files, - apiUrl: config.get('assetVerificationApi'), - cache: verifiedAssetsCache, - logger, - }) - const numWorkers = calculateWorkers(config.get('nodeWorkers'), config.get('nodeWorkersMax')) const workerPool = new WorkerPool({ logger, metrics, numWorkers }) @@ -249,6 +239,17 @@ export class FullNode { const network = new Network(networkDefinition) + const verifiedAssetsCache = new VerifiedAssetsCacheStore(files, dataDir) + await verifiedAssetsCache.load() + + const assetsVerifier = new AssetsVerifier({ + files, + apiUrl: + config.get('assetVerificationApi') || getDefaultAssetVerificationEndpoint(network.id), + cache: verifiedAssetsCache, + logger, + }) + if (!config.isSet('bootstrapNodes')) { config.setOverride('bootstrapNodes', network.bootstrapNodes) } From a2f5a8b3d500e6b0291135280a42e54cfdf4a827 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:31:44 -0700 Subject: [PATCH 08/37] Revert "exports makeFakeWitness test util from ironfish sdk (#5415)" (#5419) This reverts commit dcaabd851816e9f994a6f81739d6d090f6e6fad9. --- ironfish/src/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/ironfish/src/index.ts b/ironfish/src/index.ts index 8e8a483d92..1ee9e55d5b 100644 --- a/ironfish/src/index.ts +++ b/ironfish/src/index.ts @@ -28,6 +28,3 @@ export * from './package' export * from './platform' export * from './primitives' export { getFeeRate } from './memPool' - -import { makeFakeWitness } from './testUtilities' -export const testUtilities = { makeFakeWitness } From 57325d42ccaaea02a7fbdb509282917c79cfeeea Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Tue, 24 Sep 2024 11:28:00 -0700 Subject: [PATCH 09/37] Feat/ledger dkg cli (#5421) * updates zondax/ledger-ironfish dependency to dev branch * updates zondax/ledger-ironfish-js to local dependency * updates zondax/ledger-js to dev branch on iron-fish fork * updates ledger.ts for upgrade to @zondax/ledger-js (#5386) * updates ledger.ts for upgrade to @zondax/ledger-js removes response handling for error codes, which are no longer included in responses implements tryInstruction to handle errors from any app instructions adds methods to help discriminate between KeyResponse types and checks type of KeyResponse for each type of key adds method to recognize Ledger response errors defines classes for common recoverable Ledger errors: locked device and app not open. the error code for the app not being open is identified in the docs as a 'technical error', but this is the error code we get when the app isn't open * adds ledger flag to participant creation (#5387) * adds ledger flag to participant creation adds dkgGetIDentity method to ledger util module adds '--ledger' flag to 'wallet:multisig:participant:create' command uses 'wallet/multisig/importParticipant' RPC to add the participant identity to the walletDb * implements ledger dkg round1 (#5389) * implements ledger dkg round1 adds dkgRound1 method to Ledger util fills in logic of performRound1WithLedger reads identity from node and adds it to list of signer identities if it's missing * implements ledger dkg round2 (#5390) * implements ledger dkg round2 adds dkgRound2 method to Ledger util implements performRound2WithLedger in round2 CLI command * implements ledger dkg round3 (#5391) * implements ledger dkg round3 implements dkgRound3 method on Ledger util class implements dkgRetrieveKeys on Ledger util class to retrieve all shared multisig keys implements dkgGetPublicPackage on Ledger util class to retrieve public key package updates round3 command to run dkg round3, retrieve keys, get public key package, and import account with ledger flag * includes participant's own gsk share in round3 input * renames createParticipantWithLedger to getIdentityFromLedger * fixes Ledger approval output in round3 (#5397) ask to approve round3 on Ledger, but don't ask for approval/confirmation when retrieving keys or public package * adds ledger option for creating signing commitment (#5398) * adds ledger option for creating signing commitment implements dkgGetCommitments on Ledger util class adds '--ledger' flag to 'wallet:multisig:commitment:create' adds multisig account selector to 'commitment:create', fetches the identity for the selected account adds 'hash' method to UnsignedTransaction primitive * adds ledger flag for creating signature shares (#5399) implements dkgSign on Ledger util class to obtain a signature share from the device adds '--ledger' flag to 'wallet:multisig:signature:create' command adds selector to command to choose multisig account if one is not provided adds 'publicKeyRandomness' method to UnsignedTransaction primitive to extract the randomness from the transaction (needed for signing) adds 'serialize' napi method to NativeSignatureShare to support constructing the signature share from its raw parts and then printing it * improves error message from importParticipant RPC (#5402) includes name of conflicting identity in error message if the request tries to import an identity that already exists in the database * implements ledger multisig backup command (#5400) * implements ledger multisig backup command adds a CLI command, 'wallet:multisig:ledger:backup', to create an encrypted backup of multisig keys from the ironfish dkg ledger app users can restore the keys to their ledger app if they reinstall the app on their device or overwrite the multisig keys in the app * adds cli command to restore ledger multisig backup (#5401) * adds cli command to restore ledger multisig backup if the ironfish dkg ledger app is uninstalled, or if it is used to create a second set of keys, then the multisig keys on the device may be lost the 'wallet:multisig:ledger:backup' command creates an encrypted key backup that can be restored to the device the 'wallet:multisig:ledger:restore' command restores the backed up keys to the ledger * Update ironfish-cli/src/commands/wallet/multisig/ledger/restore.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> * updates command description adds additional log output asking the user to save the backup and explaining how to restore the keys * fixes lint --------- Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> * feat: DKG Create Command (#5403) * adds ledger backup to the end of round3 (#5406) creates an encrypted backup and prints it to the terminal at the end of round3 of dkg * prompts for new name and retries import in round3 (#5407) extracts the import/name prompt loop logic from the 'wallet:import' command into an 'importAccount' util uses the 'importAccount' util to import the account at the end of round3 so that import is retried in the case of name collisions * adds cli command to import multisig account from ledger (#5409) reads identity, shared multisig keys, and public key package from Ledger device imports multisig account to wallet and prompts for new name if the provided name is taken removes hardcoded account version number from other ledger imports (these imports aren't from serialized accounts, so the schema version should always be the current version) * adds clear signing to ledger signing flows (#5413) uses 'reviewTransaction' method from '@zondax/ledger-ironfish' to force user to review decrypted transaction outputs on the ledger device updates our '@zondax/ledger-ironfish' dependency to use the latest release, which now includes dkg methods note: for interactive signing we may only need to review once before creating the signing commitments * always display asset id with notes in transaction details when approving a transaction using a ledger device users are shown the asset id of each output note on the ledger device the transaction details they see in the cli will not include the asset id if the asset is verified (the symbol will be displayed instead). this makes it more difficult to verify that the transaction you're seeing on the ledger is correct based on what the cli is showing you if the asset has a verified symbol, also displays the asset id for the note on the line below the amount * Revert "always display asset id with notes in transaction details" This reverts commit e7f3e46ff68460f57c9f39a417812782a0e9fec7. * always display asset id with notes in transaction details (#5416) when approving a transaction using a ledger device users are shown the asset id of each output note on the ledger device the transaction details they see in the cli will not include the asset id if the asset is verified (the symbol will be displayed instead). this makes it more difficult to verify that the transaction you're seeing on the ledger is correct based on what the cli is showing you if the asset has a verified symbol, also displays the asset id for the note on the line below the amount * Adding ledger support for dkg:create (#5410) Also added the ability to reuse an existing identity for the DKG process. The user can specify the account name separately from the participant name. * multisig:sign command (#5411) * fix typo --------- Co-authored-by: Hugh Cunningham Co-authored-by: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Co-authored-by: mat-if <97762857+mat-if@users.noreply.github.com> --- ironfish-cli/package.json | 4 +- ironfish-cli/src/commands/wallet/import.ts | 58 +- .../wallet/multisig/commitment/create.ts | 68 ++- .../commands/wallet/multisig/dkg/create.ts | 530 ++++++++++++++++++ .../commands/wallet/multisig/dkg/round1.ts | 33 +- .../commands/wallet/multisig/dkg/round2.ts | 28 +- .../commands/wallet/multisig/dkg/round3.ts | 115 +++- .../commands/wallet/multisig/ledger/backup.ts | 33 ++ .../commands/wallet/multisig/ledger/import.ts | 67 +++ .../wallet/multisig/ledger/restore.ts | 45 ++ .../wallet/multisig/participant/create.ts | 39 +- .../src/commands/wallet/multisig/sign.ts | 372 ++++++++++++ .../wallet/multisig/signature/create.ts | 72 ++- ironfish-cli/src/ui/index.ts | 1 + ironfish-cli/src/ui/prompt.ts | 31 + ironfish-cli/src/ui/retry.ts | 32 ++ ironfish-cli/src/utils/account.ts | 63 ++- ironfish-cli/src/utils/ledger.ts | 340 ++++++++--- ironfish-cli/src/utils/transaction.ts | 18 +- ironfish-rust-nodejs/index.d.ts | 1 + ironfish-rust-nodejs/src/multisig.rs | 5 + .../src/primitives/unsignedTransaction.ts | 12 + ironfish/src/rpc/adapters/errors.ts | 1 + .../rpc/routes/wallet/multisig/getIdentity.ts | 8 +- .../wallet/multisig/importParticipant.ts | 11 +- yarn.lock | 59 +- 26 files changed, 1886 insertions(+), 160 deletions(-) create mode 100644 ironfish-cli/src/commands/wallet/multisig/dkg/create.ts create mode 100644 ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts create mode 100644 ironfish-cli/src/commands/wallet/multisig/ledger/import.ts create mode 100644 ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts create mode 100644 ironfish-cli/src/commands/wallet/multisig/sign.ts create mode 100644 ironfish-cli/src/ui/retry.ts diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 0e143eec4c..1ff2ff7ce2 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -68,7 +68,7 @@ "@oclif/plugin-warn-if-update-available": "3.1.8", "@types/keccak": "3.0.4", "@types/tar": "6.1.1", - "@zondax/ledger-ironfish": "0.1.2", + "@zondax/ledger-ironfish": "0.4.0", "axios": "1.7.2", "bech32": "2.0.0", "blessed": "0.1.81", @@ -148,4 +148,4 @@ "url": "https://github.com/iron-fish/ironfish/issues" }, "homepage": "https://ironfish.network" -} +} \ No newline at end of file diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 4ff92863f9..f80fed408d 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -1,17 +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 { - AccountFormat, - encodeAccountImport, - RPC_ERROR_CODES, - RpcRequestError, -} from '@ironfish/sdk' +import { AccountFormat, encodeAccountImport } from '@ironfish/sdk' import { Args, Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' +import { importAccount } from '../../utils' import { Ledger } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { @@ -102,49 +98,15 @@ export class ImportCommand extends IronfishCommand { flags.name = name } - let result - - while (!result) { - try { - result = await client.wallet.importAccount({ - account, - rescan: flags.rescan, - name: flags.name, - createdAt: flags.createdAt, - }) - } catch (e) { - 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.DUPLICATE_IDENTITY_NAME.toString()) - ) { - const message = 'Enter a name for the account' - - if (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString()) { - this.log() - 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}'`) - } - - flags.name = name - continue - } - - throw e - } - } + const { name, isDefaultAccount } = await importAccount( + client, + account, + this.logger, + flags.name, + flags.createdAt, + flags.rescan, + ) - const { name, isDefaultAccount } = result.content this.log(`Account ${name} imported.`) if (isDefaultAccount) { diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index 8f3a3614c1..f94271f962 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -1,11 +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 { UnsignedTransaction } from '@ironfish/sdk' +import { multisig } from '@ironfish/rust-nodejs' +import { RpcClient, UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -36,6 +38,10 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { path: Flags.string({ description: 'Path to a JSON file containing multisig transaction data', }), + ledger: Flags.boolean({ + default: false, + description: 'Create signing commitment using a Ledger device', + }), } async start(): Promise { @@ -47,6 +53,11 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) + let participantName = flags.account + if (!participantName) { + participantName = await ui.multisigSecretPrompt(client) + } + let identities = options.identity if (!identities || identities.length < 2) { const input = await ui.longPrompt( @@ -77,14 +88,24 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { await renderUnsignedTransactionDetails( client, unsignedTransaction, - flags.account, + participantName, this.logger, ) await ui.confirmOrQuit('Confirm signing commitment creation', flags.confirm) + if (flags.ledger) { + await this.createSigningCommitmentWithLedger( + client, + participantName, + unsignedTransaction, + identities, + ) + return + } + const response = await client.wallet.multisig.createSigningCommitment({ - account: flags.account, + account: participantName, unsignedTransaction: unsignedTransactionInput, signers: identities.map((identity) => ({ identity })), }) @@ -96,4 +117,45 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { this.log('Next step:') this.log('Send the commitment to the multisig account coordinator.') } + + async createSigningCommitmentWithLedger( + client: RpcClient, + participantName: string, + unsignedTransaction: UnsignedTransaction, + signers: string[], + ): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + const transactionHash = await ledger.reviewTransaction( + unsignedTransaction.serialize().toString('hex'), + ) + + const rawCommitments = await ledger.dkgGetCommitments(transactionHash.toString('hex')) + + const signingCommitment = multisig.SigningCommitment.fromRaw( + identity, + rawCommitments, + transactionHash, + signers, + ) + + this.log('\nCommitment:\n') + this.log(signingCommitment.serialize().toString('hex')) + + this.log() + this.log('Next step:') + this.log('Send the commitment to the multisig account coordinator.') + } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts new file mode 100644 index 0000000000..775512b042 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -0,0 +1,530 @@ +/* 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 { + deserializePublicPackage, + deserializeRound2CombinedPublicPackage, +} from '@ironfish/rust-nodejs' +import { AccountFormat, Assert, encodeAccountImport, RpcClient } from '@ironfish/sdk' +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 DkgCreateCommand extends IronfishCommand { + static description = 'Interactive command to create a multisignature account using DKG' + + static flags = { + ...RemoteFlags, + participant: Flags.string({ + char: 'n', + description: 'The name of the secret to use for encryption during DKG', + }), + newAccount: Flags.string({ + char: 'a', + description: 'The name to set for multisig account to be created', + }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + }), + } + + async start(): Promise { + const { flags } = await this.parse(DkgCreateCommand) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + let ledger: Ledger | undefined = undefined + + if (flags.ledger) { + ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + } + + const accountName = await this.getAccountName(client, flags.newAccount) + + const { name: participantName, identity } = ledger + ? await ui.retryStep(() => { + Assert.isNotUndefined(ledger) + return this.getIdentityFromLedger(ledger, client, flags.participant) + }, this.logger) + : await this.getParticipant(client, flags.participant) + + this.log(`Identity for ${participantName}: \n${identity} \n`) + + const { round1, totalParticipants } = await ui.retryStep( + async () => { + return this.performRound1(client, participantName, identity, ledger) + }, + this.logger, + true, + ) + + this.log('\n============================================') + this.log('\nRound 1 Encrypted Secret Package:') + this.log(round1.secretPackage) + + this.log('\nRound 1 Public Package:') + this.log(round1.publicPackage) + this.log('\n============================================') + + this.log('\nShare your Round 1 Public Package with other participants.') + + const { round2: round2Result, round1PublicPackages } = await ui.retryStep( + async () => { + return this.performRound2(client, participantName, round1, totalParticipants, ledger) + }, + this.logger, + true, + ) + + this.log('\n============================================') + this.log('\nRound 2 Encrypted Secret Package:') + this.log(round2Result.secretPackage) + + this.log('\nRound 2 Public Package:') + this.log(round2Result.publicPackage) + this.log('\n============================================') + this.log('\nShare your Round 2 Public Package with other participants.') + + await ui.retryStep( + async () => { + return this.performRound3( + client, + accountName, + participantName, + round2Result, + round1PublicPackages, + totalParticipants, + ledger, + ) + }, + this.logger, + true, + ) + + this.log('Multisig account created successfully using DKG!') + } + + private async getParticipant(client: RpcClient, participantName?: string) { + const identities = (await client.wallet.multisig.getIdentities()).content.identities + + if (participantName) { + const foundIdentity = identities.find((i) => i.name === participantName) + if (!foundIdentity) { + throw new Error(`Participant with name ${participantName} not found`) + } + + return { + name: foundIdentity.name, + identity: foundIdentity.identity, + } + } + + const name = await ui.inputPrompt('Enter the name of the participant', true) + const foundIdentity = identities.find((i) => i.name === name) + + if (foundIdentity) { + this.log('Found an identity with the same name') + + return { + ...foundIdentity, + } + } + + const identity = (await client.wallet.multisig.createParticipant({ name })).content.identity + + return { + name, + identity, + } + } + + private async getAccountName(client: RpcClient, accountName?: string) { + let name: string + if (accountName) { + name = accountName + } else { + name = await ui.inputPrompt('Enter a name for the new multisig account', true) + } + + const accounts = (await client.wallet.getAccounts()).content.accounts + + if (accounts.find((a) => a === name)) { + this.log('An account with the same name already exists') + name = await ui.inputPrompt('Enter a new name for the account', true) + } + + return name + } + + async getIdentityFromLedger( + ledger: Ledger, + client: RpcClient, + name?: string, + ): Promise<{ + name: string + identity: string + }> { + // TODO(hughy): support multiple identities using index + const identity = await ledger.dkgGetIdentity(0) + + const allIdentities = (await client.wallet.multisig.getIdentities()).content.identities + + const foundIdentity = allIdentities.find((i) => i.identity === identity.toString('hex')) + + if (foundIdentity) { + this.log(`Identity already exists with name: ${foundIdentity.name}`) + + return { + name: foundIdentity.name, + identity: identity.toString('hex'), + } + } + + name = await ui.inputPrompt('Enter a name for the identity', true) + + while (allIdentities.find((i) => i.name === name)) { + this.log('An identity with the same name already exists') + name = await ui.inputPrompt('Enter a new name for the identity', true) + } + + await client.wallet.multisig.importParticipant({ + name, + identity: identity.toString('hex'), + }) + + return { + name, + identity: identity.toString('hex'), + } + } + + async createParticipant( + client: RpcClient, + name: string, + ): Promise<{ + name: string + identity: string + }> { + const identity = (await client.wallet.multisig.createParticipant({ name })).content.identity + return { + name, + identity, + } + } + + async performRound1WithLedger( + ledger: Ledger, + client: RpcClient, + participantName: string, + identities: string[], + minSigners: number, + ): Promise<{ + round1: { secretPackage: string; publicPackage: string } + }> { + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + if (!identities.includes(identity)) { + identities.push(identity) + } + + // TODO(hughy): determine how to handle multiple identities using index + const { publicPackage, secretPackage } = await ledger.dkgRound1(0, identities, minSigners) + + return { + round1: { + secretPackage: secretPackage.toString('hex'), + publicPackage: publicPackage.toString('hex'), + }, + } + } + + async performRound1( + client: RpcClient, + participantName: string, + currentIdentity: string, + ledger: Ledger | undefined, + ): Promise<{ + round1: { secretPackage: string; publicPackage: string } + totalParticipants: number + }> { + this.log('\nCollecting Participant Info and Performing Round 1...') + + let input = await ui.inputPrompt('Enter the total number of participants', true) + const totalParticipants = parseInt(input) + if (isNaN(totalParticipants) || totalParticipants < 2) { + throw new Error('Total number of participants must be at least 2') + } + + this.log( + `\nEnter ${ + totalParticipants - 1 + } identities of all other participants (excluding yours) `, + ) + const identities = await ui.collectStrings('Identity', totalParticipants - 1, { + additionalStrings: [currentIdentity], + errorOnDuplicate: true, + }) + + input = await ui.inputPrompt('Enter the number of minimum signers', true) + const minSigners = parseInt(input) + if (isNaN(minSigners) || minSigners < 2) { + throw new Error('Minimum number of signers must be at least 2') + } + + if (ledger) { + const result = await this.performRound1WithLedger( + ledger, + client, + participantName, + identities, + minSigners, + ) + return { + ...result, + totalParticipants, + } + } + + this.log('\nPerforming DKG Round 1...') + const response = await client.wallet.multisig.dkg.round1({ + participantName, + participants: identities.map((identity) => ({ identity })), + minSigners, + }) + + return { + round1: { + secretPackage: response.content.round1SecretPackage, + publicPackage: response.content.round1PublicPackage, + }, + totalParticipants, + } + } + + async performRound2WithLedger( + ledger: Ledger, + round1PublicPackages: string[], + round1SecretPackage: string, + ): Promise<{ + round2: { secretPackage: string; publicPackage: string } + }> { + // TODO(hughy): determine how to handle multiple identities using index + const { publicPackage, secretPackage } = await ledger.dkgRound2( + 0, + round1PublicPackages, + round1SecretPackage, + ) + + return { + round2: { + secretPackage: secretPackage.toString('hex'), + publicPackage: publicPackage.toString('hex'), + }, + } + } + + async performRound2( + client: RpcClient, + participantName: string, + round1Result: { secretPackage: string; publicPackage: string }, + totalParticipants: number, + ledger: Ledger | undefined, + ): Promise<{ + round2: { secretPackage: string; publicPackage: string } + round1PublicPackages: string[] + }> { + this.log(`\nEnter ${totalParticipants - 1} Round 1 Public Packages (excluding yours) `) + + const round1PublicPackages = await ui.collectStrings( + 'Round 1 Public Package', + totalParticipants - 1, + { + additionalStrings: [round1Result.publicPackage], + errorOnDuplicate: true, + }, + ) + + this.log('\nPerforming DKG Round 2...') + + if (ledger) { + const result = await this.performRound2WithLedger( + ledger, + round1PublicPackages, + round1Result.secretPackage, + ) + return { + ...result, + round1PublicPackages, + } + } + + const response = await client.wallet.multisig.dkg.round2({ + participantName, + round1SecretPackage: round1Result.secretPackage, + round1PublicPackages, + }) + + return { + round2: { + secretPackage: response.content.round2SecretPackage, + publicPackage: response.content.round2PublicPackage, + }, + round1PublicPackages, + } + } + + async performRound3WithLedger( + ledger: Ledger, + client: RpcClient, + accountName: string, + participantName: string, + round1PublicPackagesStr: string[], + round2PublicPackagesStr: string[], + round2SecretPackage: string, + ): Promise { + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + // Sort packages by identity + const round1PublicPackages = round1PublicPackagesStr + .map(deserializePublicPackage) + .sort((a, b) => a.identity.localeCompare(b.identity)) + + // Filter out packages not intended for participant and sort by sender identity + const round2CombinedPublicPackages = round2PublicPackagesStr.map( + deserializeRound2CombinedPublicPackage, + ) + const round2PublicPackages = round2CombinedPublicPackages + .flatMap((combined) => + combined.packages.filter((pkg) => pkg.recipientIdentity === identity), + ) + .sort((a, b) => a.senderIdentity.localeCompare(b.senderIdentity)) + + // Extract raw parts from round1 and round2 public packages + const participants = [] + const round1FrostPackages = [] + const gskBytes = [] + for (const pkg of round1PublicPackages) { + // Exclude participant's own identity and round1 public package + if (pkg.identity !== identity) { + participants.push(pkg.identity) + round1FrostPackages.push(pkg.frostPackage) + } + + gskBytes.push(pkg.groupSecretKeyShardEncrypted) + } + + const round2FrostPackages = round2PublicPackages.map((pkg) => pkg.frostPackage) + + // Perform round3 with Ledger + await ledger.dkgRound3( + 0, + participants, + round1FrostPackages, + round2FrostPackages, + round2SecretPackage, + gskBytes, + ) + + // Retrieve all multisig account keys and publicKeyPackage + const dkgKeys = await ledger.dkgRetrieveKeys() + + const publicKeyPackage = await ledger.dkgGetPublicPackage() + + const accountImport = { + ...dkgKeys, + multisigKeys: { + publicKeyPackage: publicKeyPackage.toString('hex'), + identity, + }, + version: 4, + name: accountName, + spendingKey: null, + createdAt: null, + } + + // Import multisig account + const response = await client.wallet.importAccount({ + account: encodeAccountImport(accountImport, AccountFormat.Base64Json), + }) + + this.log() + this.log( + `Account ${response.content.name} imported with public address: ${dkgKeys.publicAddress}`, + ) + + this.log() + this.log('Creating an encrypted backup of multisig keys from your Ledger device...') + this.log() + + const encryptedKeys = await ledger.dkgBackupKeys() + + this.log() + this.log('Encrypted Ledger Multisig Backup:') + this.log(encryptedKeys.toString('hex')) + this.log() + this.log('Please save the encrypted keys show above.') + this.log( + 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', + ) + } + + async performRound3( + client: RpcClient, + accountName: string, + participantName: string, + round2Result: { secretPackage: string; publicPackage: string }, + round1PublicPackages: string[], + totalParticipants: number, + ledger: Ledger | undefined, + ): Promise { + this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) + + const round2PublicPackages = await ui.collectStrings( + 'Round 2 Public Package', + totalParticipants - 1, + { + additionalStrings: [round2Result.publicPackage], + errorOnDuplicate: true, + }, + ) + + if (ledger) { + await this.performRound3WithLedger( + ledger, + client, + accountName, + participantName, + round1PublicPackages, + round2PublicPackages, + round2Result.secretPackage, + ) + return + } + + const response = await client.wallet.multisig.dkg.round3({ + participantName: participantName, + accountName: accountName, + round2SecretPackage: round2Result.secretPackage, + round1PublicPackages, + round2PublicPackages, + }) + + this.log(`Account Name: ${response.content.name}`) + this.log(`Public Address: ${response.content.publicAddress}`) + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index b26085cf0c..b76c12317e 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -1,6 +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 { RpcClient } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' @@ -71,7 +72,7 @@ export class DkgRound1Command extends IronfishCommand { } if (flags.ledger) { - await this.performRound1WithLedger() + await this.performRound1WithLedger(client, participantName, identities, minSigners) return } @@ -93,10 +94,15 @@ export class DkgRound1Command extends IronfishCommand { this.log('Send the round 1 public package to each participant') } - async performRound1WithLedger(): Promise { + async performRound1WithLedger( + client: RpcClient, + participantName: string, + identities: string[], + minSigners: number, + ): Promise { const ledger = new Ledger(this.logger) try { - await ledger.connect() + await ledger.connect(true) } catch (e) { if (e instanceof Error) { this.error(e.message) @@ -104,5 +110,26 @@ export class DkgRound1Command extends IronfishCommand { throw e } } + + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + if (!identities.includes(identity)) { + identities.push(identity) + } + + // TODO(hughy): determine how to handle multiple identities using index + const { publicPackage, secretPackage } = await ledger.dkgRound1(0, identities, minSigners) + + this.log('\nRound 1 Encrypted Secret Package:\n') + this.log(secretPackage.toString('hex')) + this.log() + + this.log('\nRound 1 Public Package:\n') + this.log(publicPackage.toString('hex')) + this.log() + + this.log('Next step:') + this.log('Send the round 1 public package to each participant') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index d2b5b4027b..bdf82b2429 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -70,7 +70,7 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages = round1PublicPackages.map((i) => i.trim()) if (flags.ledger) { - await this.performRound2WithLedger() + await this.performRound2WithLedger(round1PublicPackages, round1SecretPackage) return } @@ -93,10 +93,13 @@ export class DkgRound2Command extends IronfishCommand { this.log('Send the round 2 public package to each participant') } - async performRound2WithLedger(): Promise { + async performRound2WithLedger( + round1PublicPackages: string[], + round1SecretPackage: string, + ): Promise { const ledger = new Ledger(this.logger) try { - await ledger.connect() + await ledger.connect(true) } catch (e) { if (e instanceof Error) { this.error(e.message) @@ -104,5 +107,24 @@ export class DkgRound2Command extends IronfishCommand { throw e } } + + // TODO(hughy): determine how to handle multiple identities using index + const { publicPackage, secretPackage } = await ledger.dkgRound2( + 0, + round1PublicPackages, + round1SecretPackage, + ) + + this.log('\nRound 2 Encrypted Secret Package:\n') + this.log(secretPackage.toString('hex')) + this.log() + + this.log('\nRound 2 Public Package:\n') + this.log(publicPackage.toString('hex')) + this.log() + + this.log() + this.log('Next step:') + this.log('Send the round 2 public package to each participant') } } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 12034755fd..098ad08f3f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -1,10 +1,21 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { + deserializePublicPackage, + deserializeRound2CombinedPublicPackage, +} from '@ironfish/rust-nodejs' +import { + ACCOUNT_SCHEMA_VERSION, + AccountFormat, + encodeAccountImport, + RpcClient, +} from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' +import { importAccount } from '../../../../utils' import { Ledger } from '../../../../utils/ledger' export class DkgRound3Command extends IronfishCommand { @@ -107,7 +118,13 @@ export class DkgRound3Command extends IronfishCommand { round2PublicPackages = round2PublicPackages.map((i) => i.trim()) if (flags.ledger) { - await this.performRound3WithLedger() + await this.performRound3WithLedger( + client, + participantName, + round1PublicPackages, + round2PublicPackages, + round2SecretPackage, + ) return } @@ -125,10 +142,16 @@ export class DkgRound3Command extends IronfishCommand { ) } - async performRound3WithLedger(): Promise { + async performRound3WithLedger( + client: RpcClient, + participantName: string, + round1PublicPackagesStr: string[], + round2PublicPackagesStr: string[], + round2SecretPackage: string, + ): Promise { const ledger = new Ledger(this.logger) try { - await ledger.connect() + await ledger.connect(true) } catch (e) { if (e instanceof Error) { this.error(e.message) @@ -136,5 +159,91 @@ export class DkgRound3Command extends IronfishCommand { throw e } } + + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + // Sort packages by identity + const round1PublicPackages = round1PublicPackagesStr + .map(deserializePublicPackage) + .sort((a, b) => a.identity.localeCompare(b.identity)) + + // Filter out packages not intended for participant and sort by sender identity + const round2CombinedPublicPackages = round2PublicPackagesStr.map( + deserializeRound2CombinedPublicPackage, + ) + const round2PublicPackages = round2CombinedPublicPackages + .flatMap((combined) => + combined.packages.filter((pkg) => pkg.recipientIdentity === identity), + ) + .sort((a, b) => a.senderIdentity.localeCompare(b.senderIdentity)) + + // Extract raw parts from round1 and round2 public packages + const participants = [] + const round1FrostPackages = [] + const gskBytes = [] + for (const pkg of round1PublicPackages) { + // Exclude participant's own identity and round1 public package + if (pkg.identity !== identity) { + participants.push(pkg.identity) + round1FrostPackages.push(pkg.frostPackage) + } + + gskBytes.push(pkg.groupSecretKeyShardEncrypted) + } + + const round2FrostPackages = round2PublicPackages.map((pkg) => pkg.frostPackage) + + // Perform round3 with Ledger + await ledger.dkgRound3( + 0, + participants, + round1FrostPackages, + round2FrostPackages, + round2SecretPackage, + gskBytes, + ) + + // Retrieve all multisig account keys and publicKeyPackage + const dkgKeys = await ledger.dkgRetrieveKeys() + + const publicKeyPackage = await ledger.dkgGetPublicPackage() + + const accountImport = { + ...dkgKeys, + multisigKeys: { + publicKeyPackage: publicKeyPackage.toString('hex'), + identity, + }, + version: ACCOUNT_SCHEMA_VERSION, + name: participantName, + spendingKey: null, + createdAt: null, + } + + // Import multisig account + const { name } = await importAccount( + client, + encodeAccountImport(accountImport, AccountFormat.Base64Json), + this.logger, + ) + + this.log() + this.log(`Account ${name} imported with public address: ${dkgKeys.publicAddress}`) + + this.log() + this.log('Creating an encrypted backup of multisig keys from your Ledger device...') + this.log() + + const encryptedKeys = await ledger.dkgBackupKeys() + + this.log() + this.log('Encrypted Ledger Multisig Backup:') + this.log(encryptedKeys.toString('hex')) + this.log() + this.log('Please save the encrypted keys show above.') + this.log( + 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', + ) } } diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts new file mode 100644 index 0000000000..587e6e3d5b --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IronfishCommand } from '../../../../command' +import { Ledger } from '../../../../utils/ledger' + +export class MultisigLedgerBackup extends IronfishCommand { + static description = `show encrypted multisig keys from a Ledger device` + + async start(): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + const encryptedKeys = await ledger.dkgBackupKeys() + + this.log() + this.log('Encrypted Ledger Multisig Backup:') + this.log(encryptedKeys.toString('hex')) + this.log() + this.log('Please save the encrypted keys shown above.') + this.log( + 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', + ) + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts new file mode 100644 index 0000000000..3db2ffcbdc --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/import.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 { ACCOUNT_SCHEMA_VERSION, AccountFormat, encodeAccountImport } from '@ironfish/sdk' +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import { RemoteFlags } from '../../../../flags' +import * as ui from '../../../../ui' +import { importAccount } from '../../../../utils' +import { Ledger } from '../../../../utils/ledger' + +export class MultisigLedgerImport extends IronfishCommand { + static description = `import a multisig account from a Ledger device` + + static flags = { + ...RemoteFlags, + name: Flags.string({ + description: 'Name to use for the account', + char: 'n', + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigLedgerImport) + + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + const name = flags.name ?? (await ui.inputPrompt('Enter a name for the account', true)) + + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + const identity = await ledger.dkgGetIdentity(0) + const dkgKeys = await ledger.dkgRetrieveKeys() + const publicKeyPackage = await ledger.dkgGetPublicPackage() + + const accountImport = { + ...dkgKeys, + multisigKeys: { + publicKeyPackage: publicKeyPackage.toString('hex'), + identity: identity.toString('hex'), + }, + version: ACCOUNT_SCHEMA_VERSION, + name, + spendingKey: null, + createdAt: null, + } + + const { name: accountName } = await importAccount( + client, + encodeAccountImport(accountImport, AccountFormat.Base64Json), + this.logger, + ) + + this.log() + this.log(`Account ${accountName} imported with public address: ${dkgKeys.publicAddress}`) + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts new file mode 100644 index 0000000000..08014d3835 --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Flags } from '@oclif/core' +import { IronfishCommand } from '../../../../command' +import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' + +export class MultisigLedgerRestore extends IronfishCommand { + static description = `restore encrypted multisig keys to a Ledger device` + + static flags = { + backup: Flags.string({ + description: 'Encrypted multisig key backup from your Ledger device', + char: 'b', + }), + } + + async start(): Promise { + const { flags } = await this.parse(MultisigLedgerRestore) + + let encryptedKeys = flags.backup + if (!encryptedKeys) { + encryptedKeys = await ui.longPrompt( + 'Enter the encrypted multisig key backup to restore to your Ledger device', + ) + } + + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + await ledger.dkgRestoreKeys(encryptedKeys) + + this.log() + this.log('Encrypted multisig key backup restored to Ledger.') + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index 1621686631..bca0f41b1f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -6,6 +6,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 MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` @@ -16,6 +17,11 @@ export class MultisigIdentityCreate extends IronfishCommand { char: 'n', description: 'Name to associate with the identity', }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + hidden: true, + }), } async start(): Promise { @@ -29,14 +35,27 @@ export class MultisigIdentityCreate extends IronfishCommand { name = await ui.inputPrompt('Enter a name for the identity', true) } + let identity + if (flags.ledger) { + identity = await this.getIdentityFromLedger() + } + let response while (!response) { try { - response = await client.wallet.multisig.createParticipant({ name }) + if (identity) { + response = await client.wallet.multisig.importParticipant({ + name, + identity: identity.toString('hex'), + }) + } else { + response = await client.wallet.multisig.createParticipant({ name }) + } } catch (e) { if ( e instanceof RpcRequestError && - e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() + (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() || + e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString()) ) { this.log() this.log(e.codeMessage) @@ -50,4 +69,20 @@ export class MultisigIdentityCreate extends IronfishCommand { this.log('Identity:') this.log(response.content.identity) } + + async getIdentityFromLedger(): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + // TODO(hughy): support multiple identities using index + return ledger.dkgGetIdentity(0) + } } diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts new file mode 100644 index 0000000000..00f53c9ddb --- /dev/null +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -0,0 +1,372 @@ +/* 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 { + CurrencyUtils, + Identity, + RpcClient, + Transaction, + UnsignedTransaction, +} from '@ironfish/sdk' +import { Flags, ux } from '@oclif/core' +import { IronfishCommand } from '../../../command' +import { RemoteFlags } from '../../../flags' +import * as ui from '../../../ui' +import { Ledger } from '../../../utils/ledger' +import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' + +// todo(patnir): this command does not differentiate between a participant and an account. +// there is a possibility that the account and participant have different names. + +type MultisigParticipant = { + name: string + identity: Identity + hasSecret: boolean +} + +export class SignMultisigTransactionCommand extends IronfishCommand { + static description = 'Interactive command sign a transaction with a multisig account' + + static flags = { + ...RemoteFlags, + unsignedTransaction: Flags.string({ + char: 'u', + description: 'The unsigned transaction that needs to be signed', + }), + account: Flags.string({ + char: 'a', + description: 'Name of the account to use for signing the transaction', + }), + ledger: Flags.boolean({ + default: false, + description: 'Perform operation with a ledger device', + }), + } + + async start(): Promise { + const { flags } = await this.parse(SignMultisigTransactionCommand) + const client = await this.connectRpc() + await ui.checkWalletUnlocked(client) + + let ledger: Ledger | undefined = undefined + + if (flags.ledger) { + ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + } + + let multisigAccountName: string + if (!flags.account) { + multisigAccountName = await ui.accountPrompt(client) + } else { + multisigAccountName = flags.account + const account = (await client.wallet.getAccounts()).content.accounts.find( + (a) => a === multisigAccountName, + ) + if (!account) { + this.error(`Account ${multisigAccountName} not found`) + } + } + + const accountIdentities = ( + await client.wallet.multisig.getAccountIdentities({ account: multisigAccountName }) + ).content.identities + const participants = (await client.wallet.multisig.getIdentities()).content.identities + + const matchingIdentities = participants.filter((identity) => + accountIdentities.includes(identity.identity), + ) + + if (matchingIdentities.length === 0) { + this.error(`No matching identities found for account ${multisigAccountName}`) + } + + let participant: MultisigParticipant + + if (matchingIdentities.length === 1) { + participant = matchingIdentities[0] + } else { + participant = await ui.listPrompt( + 'Select identity for signing', + matchingIdentities, + (i) => i.name, + ) + } + + const unsignedTransactionInput = + flags.unsignedTransaction ?? + (await ui.longPrompt('Enter the unsigned transaction', { required: true })) + const unsignedTransaction = new UnsignedTransaction( + Buffer.from(unsignedTransactionInput, 'hex'), + ) + await renderUnsignedTransactionDetails( + client, + unsignedTransaction, + multisigAccountName, + this.logger, + ) + + const { commitment, identities } = await ui.retryStep( + async () => { + return this.performCreateSigningCommitment( + client, + multisigAccountName, + participant, + unsignedTransaction, + unsignedTransactionInput, + ledger, + ) + }, + this.logger, + true, + ) + + this.log('\n============================================') + this.log('\nCommitment:') + this.log(commitment) + this.log('\n============================================') + + this.log('\nShare your commitment with other participants.') + + const signingPackage = await ui.retryStep(() => { + return this.performAggregateCommitments( + client, + multisigAccountName, + commitment, + identities, + unsignedTransaction, + ) + }, this.logger) + + this.log('\n============================================') + this.log('\nSigning Package:') + this.log(signingPackage) + this.log('\n============================================') + + const signatureShare = await ui.retryStep( + () => + this.performCreateSignatureShare( + client, + multisigAccountName, + participant, + signingPackage, + unsignedTransaction, + ledger, + ), + this.logger, + true, + ) + + this.log('\n============================================') + this.log('\nSignature Share:') + this.log(signatureShare) + this.log('\n============================================') + + this.log('\nShare your signature share with other participants.') + + await ui.retryStep( + () => + this.performAggregateSignatures( + client, + multisigAccountName, + signingPackage, + signatureShare, + identities.length, + ), + this.logger, + ) + + this.log('Mutlisignature sign process completed!') + } + + private async performAggregateSignatures( + client: RpcClient, + accountName: string, + signingPackage: string, + signatureShare: string, + totalParticipants: number, + ): Promise { + const signatureShares = await ui.collectStrings('Signature Share', totalParticipants - 1, { + additionalStrings: [signatureShare], + errorOnDuplicate: true, + }) + + const broadcast = await ui.confirmPrompt('Do you want to broadcast the transaction?') + const watch = await ui.confirmPrompt('Do you want to watch the transaction?') + + ux.action.start('Signing the multisig transaction') + + const response = await client.wallet.multisig.aggregateSignatureShares({ + account: accountName, + broadcast, + signingPackage, + signatureShares, + }) + + const bytes = Buffer.from(response.content.transaction, 'hex') + const transaction = new Transaction(bytes) + + ux.action.stop() + + if (broadcast && response.content.accepted === false) { + this.warn( + `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, + ) + } + + if (broadcast && response.content.broadcasted === false) { + this.warn(`Transaction '${transaction.hash().toString('hex')}' failed to broadcast`) + } + + this.log(`Transaction: ${response.content.transaction}`) + this.log(`Hash: ${transaction.hash().toString('hex')}`) + this.log(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) + + if (watch) { + this.log('') + + await watchTransaction({ + client, + logger: this.logger, + account: accountName, + hash: transaction.hash().toString('hex'), + }) + } + } + + private async performCreateSignatureShare( + client: RpcClient, + accountName: string, + identity: MultisigParticipant, + signingPackageString: string, + unsignedTransaction: UnsignedTransaction, + ledger: Ledger | undefined, + ): Promise { + let signatureShare: string + + const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) + + if (ledger) { + await ledger.reviewTransaction(unsignedTransaction.serialize().toString('hex')) + + const frostSignatureShare = await ledger.dkgSign( + unsignedTransaction.publicKeyRandomness(), + signingPackage.frostSigningPackage().toString('hex'), + unsignedTransaction.hash().toString('hex'), + ) + + signatureShare = multisig.SignatureShare.fromFrost( + frostSignatureShare, + Buffer.from(identity.identity, 'hex'), + ) + .serialize() + .toString('hex') + } else { + signatureShare = ( + await client.wallet.multisig.createSignatureShare({ + account: accountName, + signingPackage: signingPackageString, + }) + ).content.signatureShare + } + + return signatureShare + } + + private async performAggregateCommitments( + client: RpcClient, + accountName: string, + commitment: string, + identities: string[], + unsignedTransaction: UnsignedTransaction, + ) { + const commitments = await ui.collectStrings('Commitment', identities.length - 1, { + additionalStrings: [commitment], + errorOnDuplicate: true, + }) + + const signingPackageResponse = await client.wallet.multisig.createSigningPackage({ + account: accountName, + unsignedTransaction: unsignedTransaction.serialize().toString('hex'), + commitments, + }) + + return signingPackageResponse.content.signingPackage + } + + private async performCreateSigningCommitment( + client: RpcClient, + accountName: string, + participant: MultisigParticipant, + unsignedTransaction: UnsignedTransaction, + unsignedTransactionInput: string, + ledger: Ledger | undefined, + ) { + const input = await ui.inputPrompt( + 'Enter the number of participants in signing this transaction', + true, + ) + const totalParticipants = parseInt(input) + + if (totalParticipants < 2) { + this.error('Minimum number of participants must be at least 2') + } + + const identities = await ui.collectStrings('Identity', totalParticipants, { + additionalStrings: [], + errorOnDuplicate: true, + }) + + let commitment + + if (ledger) { + commitment = await this.createSigningCommitmentWithLedger( + ledger, + participant, + unsignedTransaction.hash(), + identities, + ) + } else { + commitment = ( + await client.wallet.multisig.createSigningCommitment({ + account: accountName, + unsignedTransaction: unsignedTransactionInput, + signers: identities.map((identity) => ({ identity })), + }) + ).content.commitment + } + + return { + commitment, + identities, + } + } + + async createSigningCommitmentWithLedger( + ledger: Ledger, + participant: MultisigParticipant, + transactionHash: Buffer, + signers: string[], + ): Promise { + const rawCommitments = await ledger.dkgGetCommitments(transactionHash.toString('hex')) + + const sigingCommitment = multisig.SigningCommitment.fromRaw( + participant.identity, + rawCommitments, + transactionHash, + signers, + ) + + return sigingCommitment.serialize().toString('hex') + } +} diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index a156964794..68ae20b2ca 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -2,11 +2,12 @@ * 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 { UnsignedTransaction } from '@ironfish/sdk' +import { RpcClient, UnsignedTransaction } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' +import { Ledger } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -30,6 +31,10 @@ export class CreateSignatureShareCommand extends IronfishCommand { path: Flags.string({ description: 'Path to a JSON file containing multisig transaction data', }), + ledger: Flags.boolean({ + default: false, + description: 'Create signature share using a Ledger device', + }), } async start(): Promise { @@ -41,6 +46,11 @@ export class CreateSignatureShareCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) + let participantName = flags.account + if (!participantName) { + participantName = await ui.multisigSecretPrompt(client) + } + let signingPackageString = options.signingPackage if (!signingPackageString) { signingPackageString = await ui.longPrompt('Enter the signing package') @@ -56,7 +66,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { await renderUnsignedTransactionDetails( client, unsignedTransaction, - flags.account, + participantName, this.logger, ) @@ -64,8 +74,18 @@ export class CreateSignatureShareCommand extends IronfishCommand { await ui.confirmOrQuit('Confirm new signature share creation') } + if (flags.ledger) { + await this.createSignatureShareWithLedger( + client, + participantName, + unsignedTransaction, + signingPackage.frostSigningPackage().toString('hex'), + ) + return + } + const signatureShareResponse = await client.wallet.multisig.createSignatureShare({ - account: flags.account, + account: participantName, signingPackage: signingPackageString, }) @@ -93,4 +113,50 @@ export class CreateSignatureShareCommand extends IronfishCommand { this.log(signer.toString('hex')) } } + + async createSignatureShareWithLedger( + client: RpcClient, + participantName: string, + unsignedTransaction: UnsignedTransaction, + frostSigningPackage: string, + ): Promise { + const ledger = new Ledger(this.logger) + try { + await ledger.connect(true) + } catch (e) { + if (e instanceof Error) { + this.error(e.message) + } else { + throw e + } + } + + const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) + const identity = identityResponse.content.identity + + const transactionHash = await ledger.reviewTransaction( + unsignedTransaction.serialize().toString('hex'), + ) + + const frostSignatureShare = await ledger.dkgSign( + unsignedTransaction.publicKeyRandomness(), + frostSigningPackage, + transactionHash.toString('hex'), + ) + + const signatureShare = multisig.SignatureShare.fromFrost( + frostSignatureShare, + Buffer.from(identity, 'hex'), + ) + + this.log() + this.log('Signature Share:') + this.log(signatureShare.serialize().toString('hex')) + + this.log() + this.log('Next step:') + this.log( + 'Send the signature to the coordinator. They will aggregate the signatures from all participants and sign the transaction.', + ) + } } diff --git a/ironfish-cli/src/ui/index.ts b/ironfish-cli/src/ui/index.ts index 9ae9ad0931..25752ab000 100644 --- a/ironfish-cli/src/ui/index.ts +++ b/ironfish-cli/src/ui/index.ts @@ -8,5 +8,6 @@ export * from './longPrompt' export * from './progressBar' export * from './prompt' export * from './prompts' +export * from './retry' export * from './table' export * from './wallet' diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index f670e28044..91c6fbc687 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -4,6 +4,37 @@ import { ux } from '@oclif/core' import inquirer from 'inquirer' +import { longPrompt } from './longPrompt' + +export async function collectStrings( + itemName: string, + itemAmount: number, + options?: { + additionalStrings: string[] + errorOnDuplicate: boolean + }, +): Promise { + const array = [] + + for (let i = 0; i < itemAmount; i++) { + const input = await longPrompt(`${itemName} #${i + 1}`, { required: true }) + array.push(input) + } + + const additionalStrings = options?.additionalStrings || [] + + const strings = [...array, ...additionalStrings] + + if (!options?.errorOnDuplicate) { + const withoutDuplicates = [...new Set(strings)] + + if (withoutDuplicates.length !== strings.length) { + throw new Error(`Duplicate ${itemName} found in the list`) + } + } + + return strings +} async function _inputPrompt(message: string, options?: { password: boolean }): Promise { const result: { prompt: string } = await inquirer.prompt({ diff --git a/ironfish-cli/src/ui/retry.ts b/ironfish-cli/src/ui/retry.ts new file mode 100644 index 0000000000..da206c5fb8 --- /dev/null +++ b/ironfish-cli/src/ui/retry.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 { Logger } from '@ironfish/sdk' +import { confirmPrompt } from './prompt' + +export async function retryStep( + stepFunction: () => Promise, + logger: Logger, + askToRetry: boolean = false, + maxRetries: number = 10, +): Promise { + // eslint-disable-next-line no-constant-condition + let retries = 0 + while (retries < maxRetries) { + try { + const result = await stepFunction() + return result + } catch (error) { + logger.log(`An Error Occurred: ${(error as Error).message}`) + if (askToRetry) { + const continueResponse = await confirmPrompt('Do you want to retry this step?') + if (!continueResponse) { + throw new Error('User chose to not continue') + } + } + } + retries++ + } + + throw new Error('Max retries reached') +} diff --git a/ironfish-cli/src/utils/account.ts b/ironfish-cli/src/utils/account.ts index a867d86707..2f0f20dbb4 100644 --- a/ironfish-cli/src/utils/account.ts +++ b/ironfish-cli/src/utils/account.ts @@ -2,8 +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 { RpcClient } from '@ironfish/sdk' +import { + ImportResponse, + Logger, + RPC_ERROR_CODES, + RpcClient, + RpcRequestError, +} from '@ironfish/sdk' import * as ui from '../ui' +import { inputPrompt } from '../ui' export async function useAccount( client: RpcClient, @@ -27,3 +34,57 @@ export async function useAccount( return ui.accountPrompt(client, message) } + +export async function importAccount( + client: RpcClient, + account: string, + logger: Logger, + accountName?: string, + createdAt?: number, + rescan?: boolean, +): Promise { + let name = accountName + + let result + while (!result) { + try { + result = await client.wallet.importAccount({ + account, + name, + rescan, + createdAt, + }) + } catch (e) { + 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.DUPLICATE_IDENTITY_NAME.toString()) + ) { + const message = 'Enter a name for the account' + + if (e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString()) { + logger.info('') + logger.info(e.codeMessage) + } + + if (e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString()) { + logger.info('') + logger.info(e.codeMessage) + } + + const inputName = await inputPrompt(message, true) + if (inputName === name) { + throw new Error(`Entered the same name: '${name}'`) + } + + name = inputName + continue + } + + throw e + } + } + + return result.content +} diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 9a560e5ce5..64a813033f 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -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/. */ import { + ACCOUNT_SCHEMA_VERSION, + AccountImport, createRootLogger, CurrencyUtils, Logger, @@ -10,16 +12,20 @@ import { RpcClient, Transaction, } from '@ironfish/sdk' -import { AccountImport } from '@ironfish/sdk/src/wallet/exporter' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import { Errors, ux } from '@oclif/core' import IronfishApp, { IronfishKeys, + KeyResponse, ResponseAddress, + ResponseDkgRound1, + ResponseDkgRound2, + ResponseIdentity, ResponseProofGenKey, ResponseSign, ResponseViewKey, } from '@zondax/ledger-ironfish' +import { ResponseError } from '@zondax/ledger-js' import * as ui from '../ui' import { watchTransaction } from './transaction' @@ -33,35 +39,50 @@ export class Ledger { this.logger = logger ? logger : createRootLogger() } - connect = async () => { - const transport = await TransportNodeHid.create(3000, 3000) + tryInstruction = async (promise: Promise) => { + try { + return await promise + } catch (error: unknown) { + if (isResponseError(error)) { + this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) + if (error.returnCode === LedgerDeviceLockedError.returnCode) { + throw new LedgerDeviceLockedError('Please unlock your Ledger device.') + } else if (error.returnCode === LedgerAppNotOpenError.returnCode) { + throw new LedgerAppNotOpenError( + 'Please open the Iron Fish app on your Ledger device.', + ) + } + + throw new LedgerError(error.errorMessage) + } + + throw error + } + } + + connect = async (dkg = false) => { + const transport = await TransportNodeHid.create(3000) if (transport.deviceModel) { this.logger.debug(`${transport.deviceModel.productName} found.`) } - const app = new IronfishApp(transport) + const app = new IronfishApp(transport, dkg) - const appInfo = await app.appInfo() - this.logger.debug(appInfo.appName ?? 'no app name') + // TODO: remove this condition if appInfo is available in the DKG app + if (!dkg) { + const appInfo = await this.tryInstruction(app.appInfo()) - if (appInfo.appName !== 'Ironfish') { this.logger.debug(appInfo.appName ?? 'no app name') - this.logger.debug(appInfo.returnCode.toString(16)) - this.logger.debug(appInfo.errorMessage.toString()) - - // references: - // https://github.com/LedgerHQ/ledger-live/blob/173bb3c84cc855f83ab8dc49362bc381afecc31e/libs/ledgerjs/packages/errors/src/index.ts#L263 - // https://github.com/Zondax/ledger-ironfish/blob/bf43a4b8d403d15138699ee3bb1a3d6dfdb428bc/docs/APDUSPEC.md?plain=1#L25 - if (appInfo.returnCode === 0x5515) { - throw new Error('Please unlock your Ledger device.') - } - throw new Error('Please open the Iron Fish app on your ledger device.') - } + if (appInfo.appName !== 'Ironfish') { + this.logger.debug(appInfo.appName ?? 'no app name') + throw new Error('Please open the Iron Fish app on your ledger device.') + } - if (appInfo.appVersion) { - this.logger.debug(`Ironfish App Version: ${appInfo.appVersion}`) + if (appInfo.appVersion) { + this.logger.debug(`Ironfish App Version: ${appInfo.appVersion}`) + } } this.app = app @@ -74,65 +95,48 @@ export class Ledger { throw new Error('Connect to Ledger first') } - const response: ResponseAddress = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.PublicAddress, - false, + const response = await this.tryInstruction( + this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), ) - if (!response.publicAddress) { - this.logger.debug(`No public address returned.`) - this.logger.debug(response.returnCode.toString()) - throw new Error(response.errorMessage) + if (!isResponseAddress(response)) { + throw new Error(`No public address returned`) + } else { + return response.publicAddress.toString('hex') } - - return response.publicAddress.toString('hex') } - importAccount = async () => { + importAccount = async (): Promise => { if (!this.app) { throw new Error('Connect to Ledger first') } - const responseAddress: ResponseAddress = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.PublicAddress, - false, + const responseAddress: KeyResponse = await this.tryInstruction( + this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), ) - if (!responseAddress.publicAddress) { - this.logger.debug(`No public address returned.`) - this.logger.debug(responseAddress.returnCode.toString()) - throw new Error(responseAddress.errorMessage) + if (!isResponseAddress(responseAddress)) { + throw new Error(`No public address returned.`) } - this.logger.log('Please confirm the request on your ledger device.') - - const responseViewKey: ResponseViewKey = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.ViewKey, - true, + const responseViewKey = await this.tryInstruction( + this.app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), ) - if (!responseViewKey.viewKey || !responseViewKey.ovk || !responseViewKey.ivk) { - this.logger.debug(`No view key returned.`) - this.logger.debug(responseViewKey.returnCode.toString()) - throw new Error(responseViewKey.errorMessage) + if (!isResponseViewKey(responseViewKey)) { + throw new Error(`No view key returned.`) } - const responsePGK: ResponseProofGenKey = await this.app.retrieveKeys( - this.PATH, - IronfishKeys.ProofGenerationKey, - false, + const responsePGK: KeyResponse = await this.tryInstruction( + this.app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false), ) - if (!responsePGK.ak || !responsePGK.nsk) { - this.logger.debug(`No proof authorizing key returned.`) - throw new Error(responsePGK.errorMessage) + if (!isResponseProofGenKey(responsePGK)) { + throw new Error(`No proof authorizing key returned.`) } const accountImport: AccountImport = { - version: 4, // ACCOUNT_SCHEMA_VERSION as of 2024-05 + version: ACCOUNT_SCHEMA_VERSION, name: 'ledger', viewKey: responseViewKey.viewKey.toString('hex'), incomingViewKey: responseViewKey.ivk.toString('hex'), @@ -160,16 +164,226 @@ export class Ledger { throw new Error('Transaction size is too large, must be less than 16kb.') } - const response: ResponseSign = await this.app.sign(this.PATH, buffer) + const response: ResponseSign = await this.tryInstruction(this.app.sign(this.PATH, buffer)) - if (!response.signature) { - this.logger.debug(`No signatures returned.`) - this.logger.debug(response.returnCode.toString()) - throw new Error(response.errorMessage) + return response.signature + } + + dkgGetIdentity = async (index: number): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') } - return response.signature + this.logger.log('Retrieving identity from ledger device.') + + const response: ResponseIdentity = await this.tryInstruction( + this.app.dkgGetIdentity(index, false), + ) + + return response.identity + } + + dkgRound1 = async ( + index: number, + identities: string[], + minSigners: number, + ): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction(this.app.dkgRound1(index, identities, minSigners)) } + + dkgRound2 = async ( + index: number, + round1PublicPackages: string[], + round1SecretPackage: string, + ): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction( + this.app.dkgRound2(index, round1PublicPackages, round1SecretPackage), + ) + } + + dkgRound3 = async ( + index: number, + participants: string[], + round1PublicPackages: string[], + round2PublicPackages: string[], + round2SecretPackage: string, + gskBytes: string[], + ): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please approve the request on your ledger device.') + + return this.tryInstruction( + this.app.dkgRound3Min( + index, + participants, + round1PublicPackages, + round2PublicPackages, + round2SecretPackage, + gskBytes, + ), + ) + } + + dkgRetrieveKeys = async (): Promise<{ + publicAddress: string + viewKey: string + incomingViewKey: string + outgoingViewKey: string + proofAuthorizingKey: string + }> => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const responseAddress: KeyResponse = await this.tryInstruction( + this.app.dkgRetrieveKeys(IronfishKeys.PublicAddress), + ) + + if (!isResponseAddress(responseAddress)) { + throw new Error(`No public address returned.`) + } + + const responseViewKey = await this.tryInstruction( + this.app.dkgRetrieveKeys(IronfishKeys.ViewKey), + ) + + if (!isResponseViewKey(responseViewKey)) { + throw new Error(`No view key returned.`) + } + + const responsePGK: KeyResponse = await this.tryInstruction( + this.app.dkgRetrieveKeys(IronfishKeys.ProofGenerationKey), + ) + + if (!isResponseProofGenKey(responsePGK)) { + throw new Error(`No proof authorizing key returned.`) + } + + return { + publicAddress: responseAddress.publicAddress.toString('hex'), + viewKey: responseViewKey.viewKey.toString('hex'), + incomingViewKey: responseViewKey.ivk.toString('hex'), + outgoingViewKey: responseViewKey.ovk.toString('hex'), + proofAuthorizingKey: responsePGK.nsk.toString('hex'), + } + } + + dkgGetPublicPackage = async (): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const response = await this.tryInstruction(this.app.dkgGetPublicPackage()) + + return response.publicPackage + } + + reviewTransaction = async (transaction: string): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.info( + 'Please review and approve the outputs of this transaction on your ledger device.', + ) + + const { hash } = await this.tryInstruction(this.app.reviewTransaction(transaction)) + + return hash + } + + dkgGetCommitments = async (transactionHash: string): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const { commitments } = await this.tryInstruction( + this.app.dkgGetCommitments(transactionHash), + ) + + return commitments + } + + dkgSign = async ( + randomness: string, + frostSigningPackage: string, + transactionHash: string, + ): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const { signature } = await this.tryInstruction( + this.app.dkgSign(randomness, frostSigningPackage, transactionHash), + ) + + return signature + } + + dkgBackupKeys = async (): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please approve the request on your ledger device.') + + const { encryptedKeys } = await this.tryInstruction(this.app.dkgBackupKeys()) + + return encryptedKeys + } + + dkgRestoreKeys = async (encryptedKeys: string): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please approve the request on your ledger device.') + + await this.tryInstruction(this.app.dkgRestoreKeys(encryptedKeys)) + } +} + +function isResponseAddress(response: KeyResponse): response is ResponseAddress { + return 'publicAddress' in response +} + +function isResponseViewKey(response: KeyResponse): response is ResponseViewKey { + return 'viewKey' in response +} + +function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey { + return 'ak' in response +} + +function isResponseError(error: unknown): error is ResponseError { + return 'errorMessage' in (error as object) && 'returnCode' in (error as object) +} + +export class LedgerError extends Error { + name = this.constructor.name +} + +export class LedgerDeviceLockedError extends LedgerError { + static returnCode = 0x5515 +} + +export class LedgerAppNotOpenError extends LedgerError { + static returnCode = 0x6f00 } export async function sendTransactionWithLedger( diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index 66a20de65e..95c2a201c1 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -243,13 +243,20 @@ async function _renderTransactionDetails( } logger.log('') + const verifiedAssetMetadata = assetLookup[note.assetId].verification + const renderedAmount = CurrencyUtils.render( note.value, true, note.assetId, - assetLookup[note.assetId].verification, + verifiedAssetMetadata, ) logger.log(`Amount: ${renderedAmount}`) + + if (verifiedAssetMetadata.symbol) { + logger.log(`Asset ID: ${note.assetId}`) + } + logger.log(`Memo: ${note.memo}`) logger.log(`Recipient: ${note.owner}`) logger.log(`Sender: ${note.sender}`) @@ -267,13 +274,20 @@ async function _renderTransactionDetails( } logger.log('') + const verifiedAssetMetadata = assetLookup[note.assetId].verification + const renderedAmount = CurrencyUtils.render( note.value, true, note.assetId, - assetLookup[note.assetId].verification, + verifiedAssetMetadata, ) logger.log(`Amount: ${renderedAmount}`) + + if (verifiedAssetMetadata.symbol) { + logger.log(`Asset ID: ${note.assetId}`) + } + logger.log(`Memo: ${note.memo}`) logger.log(`Recipient: ${note.owner}`) logger.log(`Sender: ${note.sender}`) diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index eb4ed16352..a53f89fa9e 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -337,6 +337,7 @@ export namespace multisig { static fromFrost(frostSignatureShare: Buffer, identity: Buffer): NativeSignatureShare identity(): Buffer frostSignatureShare(): Buffer + serialize(): Buffer } export class ParticipantSecret { constructor(jsBytes: Buffer) diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index dde0171e5f..52f8ca989f 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -190,6 +190,11 @@ impl NativeSignatureShare { .as_slice(), ) } + + #[napi] + pub fn serialize(&self) -> Buffer { + Buffer::from(self.signature_share.serialize().as_slice()) + } } #[napi(namespace = "multisig")] diff --git a/ironfish/src/primitives/unsignedTransaction.ts b/ironfish/src/primitives/unsignedTransaction.ts index df644f472b..86a2b50fda 100644 --- a/ironfish/src/primitives/unsignedTransaction.ts +++ b/ironfish/src/primitives/unsignedTransaction.ts @@ -164,4 +164,16 @@ export class UnsignedTransaction { return result } + + hash(): Buffer { + const hash = this.takeReference().hash() + this.returnReference() + return hash + } + + publicKeyRandomness(): string { + const publicKeyRandomness = this.takeReference().publicKeyRandomness() + this.returnReference() + return publicKeyRandomness + } } diff --git a/ironfish/src/rpc/adapters/errors.ts b/ironfish/src/rpc/adapters/errors.ts index 5835d90c0d..31898515de 100644 --- a/ironfish/src/rpc/adapters/errors.ts +++ b/ironfish/src/rpc/adapters/errors.ts @@ -11,6 +11,7 @@ export enum RPC_ERROR_CODES { INSUFFICIENT_BALANCE = 'insufficient-balance', UNAUTHENTICATED = 'unauthenticated', NOT_FOUND = 'not-found', + IDENTITY_NOT_FOUND = 'identity-not-found', DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name', DUPLICATE_IDENTITY_NAME = 'duplicate-identity-name', IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required', diff --git a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts index 881b559970..5c98a4f83f 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/getIdentity.ts @@ -2,7 +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 { RpcValidationError } from '../../../adapters/errors' +import { RPC_ERROR_CODES, RpcValidationError } from '../../../adapters/errors' import { ApiNamespace } from '../../namespaces' import { routes } from '../../router' import { AssertHasRpcContext } from '../../rpcContext' @@ -37,7 +37,11 @@ routes.register( const identity = await context.wallet.walletDb.getMultisigIdentityByName(name) if (identity === undefined) { - throw new RpcValidationError(`No identity found with name ${name}`, 404) + throw new RpcValidationError( + `No identity found with name ${name}`, + 404, + RPC_ERROR_CODES.IDENTITY_NOT_FOUND, + ) } request.end({ identity: identity.toString('hex') }) diff --git a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts index 0e1de10590..19b1f48909 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/importParticipant.ts @@ -45,13 +45,12 @@ routes.register Date: Tue, 24 Sep 2024 15:00:03 -0700 Subject: [PATCH 10/37] Allows `--metadata ''` when minting an asset (#5424) --- ironfish-cli/src/commands/wallet/mint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/src/commands/wallet/mint.ts b/ironfish-cli/src/commands/wallet/mint.ts index c3f92df229..bcea04220d 100644 --- a/ironfish-cli/src/commands/wallet/mint.ts +++ b/ironfish-cli/src/commands/wallet/mint.ts @@ -151,7 +151,7 @@ This will create tokens and increase supply for a given asset.` name = await ui.inputPrompt('Enter the name for the new asset', true) } - if (!metadata) { + if (metadata == null) { metadata = await ui.inputPrompt('Enter metadata for the new asset') } From d91aef6a0eda606a11638f65ca53282c60f2fced Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:36:16 -0700 Subject: [PATCH 11/37] adds exportable devUtils module (#5423) --- ironfish/src/devUtils/index.ts | 8 ++++++++ ironfish/src/{testUtilities => devUtils}/witness.ts | 0 ironfish/src/index.ts | 1 + ironfish/src/multisig.test.slow.ts | 2 +- ironfish/src/primitives/rawTransaction.test.slow.ts | 3 ++- ironfish/src/primitives/rawTransaction.test.ts | 2 +- ironfish/src/testUtilities/index.ts | 1 - 7 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 ironfish/src/devUtils/index.ts rename ironfish/src/{testUtilities => devUtils}/witness.ts (100%) diff --git a/ironfish/src/devUtils/index.ts b/ironfish/src/devUtils/index.ts new file mode 100644 index 0000000000..951be1475c --- /dev/null +++ b/ironfish/src/devUtils/index.ts @@ -0,0 +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/. */ + +// The devUtils module contains exportable utilities for testing and +// development. This module must NOT import any test libraries, fixtures, mocks, +// or other dev dependencies. +export * from './witness' diff --git a/ironfish/src/testUtilities/witness.ts b/ironfish/src/devUtils/witness.ts similarity index 100% rename from ironfish/src/testUtilities/witness.ts rename to ironfish/src/devUtils/witness.ts diff --git a/ironfish/src/index.ts b/ironfish/src/index.ts index 1ee9e55d5b..a8da92f77e 100644 --- a/ironfish/src/index.ts +++ b/ironfish/src/index.ts @@ -28,3 +28,4 @@ export * from './package' export * from './platform' export * from './primitives' export { getFeeRate } from './memPool' +export * as devUtils from './devUtils' diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts index f07eaa92c6..c21d7dad40 100644 --- a/ironfish/src/multisig.test.slow.ts +++ b/ironfish/src/multisig.test.slow.ts @@ -3,9 +3,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Asset, multisig, Note as NativeNote, verifyTransactions } from '@ironfish/rust-nodejs' +import { makeFakeWitness } from './devUtils' import { Note, RawTransaction } from './primitives' import { Transaction, TransactionVersion } from './primitives/transaction' -import { makeFakeWitness } from './testUtilities' describe('multisig', () => { describe('dkg', () => { diff --git a/ironfish/src/primitives/rawTransaction.test.slow.ts b/ironfish/src/primitives/rawTransaction.test.slow.ts index cf7e19128f..faf298d2b6 100644 --- a/ironfish/src/primitives/rawTransaction.test.slow.ts +++ b/ironfish/src/primitives/rawTransaction.test.slow.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 { Asset, generateKey, Note as NativeNote } from '@ironfish/rust-nodejs' -import { makeFakeWitness, useAccountFixture, useTxFixture } from '../testUtilities' +import { makeFakeWitness } from '../devUtils' +import { useAccountFixture, useTxFixture } from '../testUtilities' import { createNodeTest } from '../testUtilities/nodeTest' import { SpendingAccount } from '../wallet' import { Note } from './note' diff --git a/ironfish/src/primitives/rawTransaction.test.ts b/ironfish/src/primitives/rawTransaction.test.ts index e29ebc9ee4..bf08438d47 100644 --- a/ironfish/src/primitives/rawTransaction.test.ts +++ b/ironfish/src/primitives/rawTransaction.test.ts @@ -4,8 +4,8 @@ import { Asset, generateKey, Note as NativeNote } from '@ironfish/rust-nodejs' import { BufferMap } from 'buffer-map' import { Assert } from '../assert' +import { makeFakeWitness } from '../devUtils' import { IsNoteWitnessEqual } from '../merkletree/witness' -import { makeFakeWitness } from '../testUtilities' import { useAccountFixture, useMinerBlockFixture, diff --git a/ironfish/src/testUtilities/index.ts b/ironfish/src/testUtilities/index.ts index 7a28f36b4b..bcd264d9ea 100644 --- a/ironfish/src/testUtilities/index.ts +++ b/ironfish/src/testUtilities/index.ts @@ -8,4 +8,3 @@ export * from './fixtures' export * from './keys' export * from './nodeTest' export * from './utils' -export * from './witness' From 44c1e2bf51f1c005cdf415022a0fa14e04da94c8 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:07:17 -0700 Subject: [PATCH 12/37] separates Ledger DKG logic from single signer (#5425) * separates Ledger DKG logic from single signer defines a separate class, LedgerDkg, for interacting with the Ledger Ironfish DKG app updates all DKG and multisig commands to use LedgerDkg adds aliased dependencies @zondax/ledger-js-dkg and @zondax/ledger-ironfish-dkg to use the versions of these packages that we need for the DKG app restores the previous version of the Ledger class for use with the single signer Ledger app uses v0.1.2 of @zondax/ledger-ironfish for compatibility with the single signer app * Update ironfish-cli/src/utils/ledger.ts Co-authored-by: Rahul Patni --------- Co-authored-by: Rahul Patni --- ironfish-cli/package.json | 6 +- .../wallet/multisig/commitment/create.ts | 6 +- .../commands/wallet/multisig/dkg/create.ts | 22 +- .../commands/wallet/multisig/dkg/round1.ts | 6 +- .../commands/wallet/multisig/dkg/round2.ts | 6 +- .../commands/wallet/multisig/dkg/round3.ts | 6 +- .../commands/wallet/multisig/ledger/backup.ts | 6 +- .../commands/wallet/multisig/ledger/import.ts | 6 +- .../wallet/multisig/ledger/restore.ts | 6 +- .../wallet/multisig/participant/create.ts | 6 +- .../src/commands/wallet/multisig/sign.ts | 14 +- .../wallet/multisig/signature/create.ts | 6 +- ironfish-cli/src/utils/ledger.ts | 274 +++++++++++------- yarn.lock | 56 +++- 14 files changed, 256 insertions(+), 170 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index 1ff2ff7ce2..f8bef9a431 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -68,7 +68,9 @@ "@oclif/plugin-warn-if-update-available": "3.1.8", "@types/keccak": "3.0.4", "@types/tar": "6.1.1", - "@zondax/ledger-ironfish": "0.4.0", + "@zondax/ledger-ironfish": "0.1.2", + "@zondax/ledger-ironfish-dkg": "npm:@zondax/ledger-ironfish@0.4.0", + "@zondax/ledger-js-dkg": "npm:@zondax/ledger-js@1.0.1", "axios": "1.7.2", "bech32": "2.0.0", "blessed": "0.1.81", @@ -148,4 +150,4 @@ "url": "https://github.com/iron-fish/ironfish/issues" }, "homepage": "https://ironfish.network" -} \ No newline at end of file +} diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index f94271f962..a78872b9fa 100644 --- a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts @@ -7,7 +7,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -124,9 +124,9 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, signers: string[], ): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 775512b042..f91d28529f 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -11,7 +11,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class DkgCreateCommand extends IronfishCommand { static description = 'Interactive command to create a multisignature account using DKG' @@ -37,12 +37,12 @@ export class DkgCreateCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - let ledger: Ledger | undefined = undefined + let ledger: LedgerDkg | undefined = undefined if (flags.ledger) { - ledger = new Ledger(this.logger) + ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) @@ -170,7 +170,7 @@ export class DkgCreateCommand extends IronfishCommand { } async getIdentityFromLedger( - ledger: Ledger, + ledger: LedgerDkg, client: RpcClient, name?: string, ): Promise<{ @@ -226,7 +226,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound1WithLedger( - ledger: Ledger, + ledger: LedgerDkg, client: RpcClient, participantName: string, identities: string[], @@ -256,7 +256,7 @@ export class DkgCreateCommand extends IronfishCommand { client: RpcClient, participantName: string, currentIdentity: string, - ledger: Ledger | undefined, + ledger: LedgerDkg | undefined, ): Promise<{ round1: { secretPackage: string; publicPackage: string } totalParticipants: number @@ -316,7 +316,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound2WithLedger( - ledger: Ledger, + ledger: LedgerDkg, round1PublicPackages: string[], round1SecretPackage: string, ): Promise<{ @@ -342,7 +342,7 @@ export class DkgCreateCommand extends IronfishCommand { participantName: string, round1Result: { secretPackage: string; publicPackage: string }, totalParticipants: number, - ledger: Ledger | undefined, + ledger: LedgerDkg | undefined, ): Promise<{ round2: { secretPackage: string; publicPackage: string } round1PublicPackages: string[] @@ -388,7 +388,7 @@ export class DkgCreateCommand extends IronfishCommand { } async performRound3WithLedger( - ledger: Ledger, + ledger: LedgerDkg, client: RpcClient, accountName: string, participantName: string, @@ -490,7 +490,7 @@ export class DkgCreateCommand extends IronfishCommand { round2Result: { secretPackage: string; publicPackage: string }, round1PublicPackages: string[], totalParticipants: number, - ledger: Ledger | undefined, + ledger: LedgerDkg | undefined, ): Promise { this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index b76c12317e..8b8c9d195c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts @@ -6,7 +6,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -100,9 +100,9 @@ export class DkgRound1Command extends IronfishCommand { identities: string[], minSigners: number, ): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index bdf82b2429..f7f569bcf0 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts @@ -5,7 +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' +import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -97,9 +97,9 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages: string[], round1SecretPackage: string, ): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 098ad08f3f..2a9a520905 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -16,7 +16,7 @@ import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' import { importAccount } from '../../../../utils' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -149,9 +149,9 @@ export class DkgRound3Command extends IronfishCommand { round2PublicPackagesStr: string[], round2SecretPackage: string, ): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts index 587e6e3d5b..a2b26435b5 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -2,15 +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 { IronfishCommand } from '../../../../command' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerBackup extends IronfishCommand { static description = `show encrypted multisig keys from a Ledger device` async start(): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts index 3db2ffcbdc..a3e1b2d2ad 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts @@ -7,7 +7,7 @@ import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' import { importAccount } from '../../../../utils' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerImport extends IronfishCommand { static description = `import a multisig account from a Ledger device` @@ -28,9 +28,9 @@ export class MultisigLedgerImport extends IronfishCommand { const name = flags.name ?? (await ui.inputPrompt('Enter a name for the account', true)) - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index 08014d3835..f0e995b7ab 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts @@ -4,7 +4,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerRestore extends IronfishCommand { static description = `restore encrypted multisig keys to a Ledger device` @@ -26,9 +26,9 @@ export class MultisigLedgerRestore extends IronfishCommand { ) } - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index bca0f41b1f..3630fcb43c 100644 --- a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts @@ -6,7 +6,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` @@ -71,9 +71,9 @@ export class MultisigIdentityCreate extends IronfishCommand { } async getIdentityFromLedger(): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 00f53c9ddb..9e9f0e5591 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -14,7 +14,7 @@ import { Flags, ux } from '@oclif/core' import { IronfishCommand } from '../../../command' import { RemoteFlags } from '../../../flags' import * as ui from '../../../ui' -import { Ledger } from '../../../utils/ledger' +import { LedgerDkg } from '../../../utils/ledger' import { renderUnsignedTransactionDetails, watchTransaction } from '../../../utils/transaction' // todo(patnir): this command does not differentiate between a participant and an account. @@ -50,12 +50,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - let ledger: Ledger | undefined = undefined + let ledger: LedgerDkg | undefined = undefined if (flags.ledger) { - ledger = new Ledger(this.logger) + ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) @@ -250,7 +250,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { identity: MultisigParticipant, signingPackageString: string, unsignedTransaction: UnsignedTransaction, - ledger: Ledger | undefined, + ledger: LedgerDkg | undefined, ): Promise { let signatureShare: string @@ -310,7 +310,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { participant: MultisigParticipant, unsignedTransaction: UnsignedTransaction, unsignedTransactionInput: string, - ledger: Ledger | undefined, + ledger: LedgerDkg | undefined, ) { const input = await ui.inputPrompt( 'Enter the number of participants in signing this transaction', @@ -353,7 +353,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { } async createSigningCommitmentWithLedger( - ledger: Ledger, + ledger: LedgerDkg, participant: MultisigParticipant, transactionHash: Buffer, signers: string[], diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 68ae20b2ca..db52317a67 100644 --- a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts @@ -7,7 +7,7 @@ import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' import * as ui from '../../../../ui' -import { Ledger } from '../../../../utils/ledger' +import { LedgerDkg } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -120,9 +120,9 @@ export class CreateSignatureShareCommand extends IronfishCommand { unsignedTransaction: UnsignedTransaction, frostSigningPackage: string, ): Promise { - const ledger = new Ledger(this.logger) + const ledger = new LedgerDkg(this.logger) try { - await ledger.connect(true) + await ledger.connect() } catch (e) { if (e instanceof Error) { this.error(e.message) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 64a813033f..7cf2ed9a45 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -16,21 +16,27 @@ import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import { Errors, ux } from '@oclif/core' import IronfishApp, { IronfishKeys, - KeyResponse, ResponseAddress, - ResponseDkgRound1, - ResponseDkgRound2, - ResponseIdentity, ResponseProofGenKey, ResponseSign, ResponseViewKey, } from '@zondax/ledger-ironfish' -import { ResponseError } from '@zondax/ledger-js' +import { + KeyResponse, + ResponseAddress as ResponseAddressDkg, + ResponseDkgRound1, + ResponseDkgRound2, + ResponseIdentity, + ResponseProofGenKey as ResponseProofGenKeyDkg, + ResponseViewKey as ResponseViewKeyDkg, +} from '@zondax/ledger-ironfish-dkg' +import { default as IronfishDkgApp } from '@zondax/ledger-ironfish-dkg' +import { ResponseError } from '@zondax/ledger-js-dkg' import * as ui from '../ui' import { watchTransaction } from './transaction' -export class Ledger { - app: IronfishApp | undefined +export class LedgerDkg { + app: IronfishDkgApp | undefined logger: Logger PATH = "m/44'/1338'/0" @@ -60,115 +66,20 @@ export class Ledger { } } - connect = async (dkg = false) => { + connect = async () => { const transport = await TransportNodeHid.create(3000) if (transport.deviceModel) { this.logger.debug(`${transport.deviceModel.productName} found.`) } - const app = new IronfishApp(transport, dkg) - - // TODO: remove this condition if appInfo is available in the DKG app - if (!dkg) { - const appInfo = await this.tryInstruction(app.appInfo()) - - this.logger.debug(appInfo.appName ?? 'no app name') - - if (appInfo.appName !== 'Ironfish') { - this.logger.debug(appInfo.appName ?? 'no app name') - throw new Error('Please open the Iron Fish app on your ledger device.') - } - - if (appInfo.appVersion) { - this.logger.debug(`Ironfish App Version: ${appInfo.appVersion}`) - } - } + const app = new IronfishDkgApp(transport, true) this.app = app return { app, PATH: this.PATH } } - getPublicAddress = async () => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - const response = await this.tryInstruction( - this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), - ) - - if (!isResponseAddress(response)) { - throw new Error(`No public address returned`) - } else { - return response.publicAddress.toString('hex') - } - } - - importAccount = async (): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - const responseAddress: KeyResponse = await this.tryInstruction( - this.app.retrieveKeys(this.PATH, IronfishKeys.PublicAddress, false), - ) - - if (!isResponseAddress(responseAddress)) { - throw new Error(`No public address returned.`) - } - - const responseViewKey = await this.tryInstruction( - this.app.retrieveKeys(this.PATH, IronfishKeys.ViewKey, true), - ) - - if (!isResponseViewKey(responseViewKey)) { - throw new Error(`No view key returned.`) - } - - const responsePGK: KeyResponse = await this.tryInstruction( - this.app.retrieveKeys(this.PATH, IronfishKeys.ProofGenerationKey, false), - ) - - if (!isResponseProofGenKey(responsePGK)) { - throw new Error(`No proof authorizing key returned.`) - } - - const accountImport: AccountImport = { - version: ACCOUNT_SCHEMA_VERSION, - name: 'ledger', - viewKey: responseViewKey.viewKey.toString('hex'), - incomingViewKey: responseViewKey.ivk.toString('hex'), - outgoingViewKey: responseViewKey.ovk.toString('hex'), - publicAddress: responseAddress.publicAddress.toString('hex'), - proofAuthorizingKey: responsePGK.nsk.toString('hex'), - spendingKey: null, - createdAt: null, - } - - return accountImport - } - - sign = async (message: string): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - this.logger.log('Please confirm the request on your ledger device.') - - const buffer = Buffer.from(message, 'hex') - - // max size of a transaction is 16kb - if (buffer.length > 16 * 1024) { - throw new Error('Transaction size is too large, must be less than 16kb.') - } - - const response: ResponseSign = await this.tryInstruction(this.app.sign(this.PATH, buffer)) - - return response.signature - } - dkgGetIdentity = async (index: number): Promise => { if (!this.app) { throw new Error('Connect to Ledger first') @@ -358,15 +269,164 @@ export class Ledger { } } -function isResponseAddress(response: KeyResponse): response is ResponseAddress { +export class Ledger { + app: IronfishApp | undefined + logger: Logger + PATH = "m/44'/1338'/0" + + constructor(logger?: Logger) { + this.app = undefined + this.logger = logger ? logger : createRootLogger() + } + + connect = async () => { + const transport = await TransportNodeHid.create(3000) + + if (transport.deviceModel) { + this.logger.debug(`${transport.deviceModel.productName} found.`) + } + + const app = new IronfishApp(transport) + + const appInfo = await app.appInfo() + this.logger.debug(appInfo.appName ?? 'no app name') + + if (appInfo.appName !== 'Ironfish') { + this.logger.debug(appInfo.appName ?? 'no app name') + this.logger.debug(appInfo.returnCode.toString(16)) + this.logger.debug(appInfo.errorMessage.toString()) + + // references: + // https://github.com/LedgerHQ/ledger-live/blob/173bb3c84cc855f83ab8dc49362bc381afecc31e/libs/ledgerjs/packages/errors/src/index.ts#L263 + // https://github.com/Zondax/ledger-ironfish/blob/bf43a4b8d403d15138699ee3bb1a3d6dfdb428bc/docs/APDUSPEC.md?plain=1#L25 + if (appInfo.returnCode === 0x5515) { + throw new Error('Please unlock your Ledger device.') + } + + throw new Error('Please open the Iron Fish app on your ledger device.') + } + + if (appInfo.appVersion) { + this.logger.debug(`Ironfish App Version: ${appInfo.appVersion}`) + } + + this.app = app + + return { app, PATH: this.PATH } + } + + getPublicAddress = async () => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const response: ResponseAddress = await this.app.retrieveKeys( + this.PATH, + IronfishKeys.PublicAddress, + false, + ) + + if (!response.publicAddress) { + this.logger.debug(`No public address returned.`) + this.logger.debug(response.returnCode.toString()) + throw new Error(response.errorMessage) + } + + return response.publicAddress.toString('hex') + } + + importAccount = async () => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + const responseAddress: ResponseAddress = await this.app.retrieveKeys( + this.PATH, + IronfishKeys.PublicAddress, + false, + ) + + if (!responseAddress.publicAddress) { + this.logger.debug(`No public address returned.`) + this.logger.debug(responseAddress.returnCode.toString()) + throw new Error(responseAddress.errorMessage) + } + + this.logger.log('Please confirm the request on your ledger device.') + + const responseViewKey: ResponseViewKey = await this.app.retrieveKeys( + this.PATH, + IronfishKeys.ViewKey, + true, + ) + + if (!responseViewKey.viewKey || !responseViewKey.ovk || !responseViewKey.ivk) { + this.logger.debug(`No view key returned.`) + this.logger.debug(responseViewKey.returnCode.toString()) + throw new Error(responseViewKey.errorMessage) + } + + const responsePGK: ResponseProofGenKey = await this.app.retrieveKeys( + this.PATH, + IronfishKeys.ProofGenerationKey, + false, + ) + + if (!responsePGK.ak || !responsePGK.nsk) { + this.logger.debug(`No proof authorizing key returned.`) + throw new Error(responsePGK.errorMessage) + } + + const accountImport: AccountImport = { + version: ACCOUNT_SCHEMA_VERSION, + name: 'ledger', + viewKey: responseViewKey.viewKey.toString('hex'), + incomingViewKey: responseViewKey.ivk.toString('hex'), + outgoingViewKey: responseViewKey.ovk.toString('hex'), + publicAddress: responseAddress.publicAddress.toString('hex'), + proofAuthorizingKey: responsePGK.nsk.toString('hex'), + spendingKey: null, + createdAt: null, + } + + return accountImport + } + + sign = async (message: string): Promise => { + if (!this.app) { + throw new Error('Connect to Ledger first') + } + + this.logger.log('Please confirm the request on your ledger device.') + + const buffer = Buffer.from(message, 'hex') + + // max size of a transaction is 16kb + if (buffer.length > 16 * 1024) { + throw new Error('Transaction size is too large, must be less than 16kb.') + } + + const response: ResponseSign = await this.app.sign(this.PATH, buffer) + + if (!response.signature) { + this.logger.debug(`No signatures returned.`) + this.logger.debug(response.returnCode.toString()) + throw new Error(response.errorMessage) + } + + return response.signature + } +} + +function isResponseAddress(response: KeyResponse): response is ResponseAddressDkg { return 'publicAddress' in response } -function isResponseViewKey(response: KeyResponse): response is ResponseViewKey { +function isResponseViewKey(response: KeyResponse): response is ResponseViewKeyDkg { return 'viewKey' in response } -function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKey { +function isResponseProofGenKey(response: KeyResponse): response is ResponseProofGenKeyDkg { return 'ak' in response } diff --git a/yarn.lock b/yarn.lock index 6ba0a16292..ee245b3297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,6 +1495,16 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@ledgerhq/devices@^8.0.0", "@ledgerhq/devices@^8.4.2": + version "8.4.3" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.3.tgz#4c296df4dd4af6f1085d728609b6931a640baf86" + integrity sha512-+ih+M27E6cm6DHrmw3GbS3mEaznCyFc0e62VdQux40XK2psgYhL2yBPftM4KCrBYm1UbHqXzqLN+Jb7rNIzsHg== + dependencies: + "@ledgerhq/errors" "^6.19.0" + "@ledgerhq/logs" "^6.12.0" + rxjs "^7.8.1" + semver "^7.3.5" + "@ledgerhq/devices@^8.4.0": version "8.4.0" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.0.tgz#f3a03576d4a53d731bdaa212a00bd0adbfb86fb1" @@ -1505,26 +1515,16 @@ rxjs "^7.8.1" semver "^7.3.5" -"@ledgerhq/devices@^8.4.2": - version "8.4.3" - resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.3.tgz#4c296df4dd4af6f1085d728609b6931a640baf86" - integrity sha512-+ih+M27E6cm6DHrmw3GbS3mEaznCyFc0e62VdQux40XK2psgYhL2yBPftM4KCrBYm1UbHqXzqLN+Jb7rNIzsHg== - dependencies: - "@ledgerhq/errors" "^6.19.0" - "@ledgerhq/logs" "^6.12.0" - rxjs "^7.8.1" - semver "^7.3.5" +"@ledgerhq/errors@^6.12.3", "@ledgerhq/errors@^6.18.0", "@ledgerhq/errors@^6.19.0": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.0.tgz#ed4f01df3dabfcdeb0b073159d66cb5f2d086243" + integrity sha512-c3Jid7euMSnpHFp8H7iPtsmKDjwbTjlG46YKdw+RpCclsqtBx1uQDlYmcbP1Yv9201kVlUFUhhP4H623k8xzlQ== "@ledgerhq/errors@^6.17.0": version "6.17.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.17.0.tgz#0d56361fe6eb7de3b239e661710679f933f1fcca" integrity sha512-xnOVpy/gUUkusEORdr2Qhw3Vd0MGfjyVGgkGR9Ck6FXE26OIdIQ3tNmG5BdZN+gwMMFJJVxxS4/hr0taQfZ43w== -"@ledgerhq/errors@^6.18.0", "@ledgerhq/errors@^6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.0.tgz#ed4f01df3dabfcdeb0b073159d66cb5f2d086243" - integrity sha512-c3Jid7euMSnpHFp8H7iPtsmKDjwbTjlG46YKdw+RpCclsqtBx1uQDlYmcbP1Yv9201kVlUFUhhP4H623k8xzlQ== - "@ledgerhq/hw-transport-node-hid-noevents@^6.30.1": version "6.30.1" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.30.1.tgz#e84854c809dda02bcb74a6d3dcc20b6014b5210d" @@ -1550,6 +1550,15 @@ node-hid "2.1.2" usb "2.9.0" +"@ledgerhq/hw-transport@6.28.1": + version "6.28.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.1.tgz#cb22fe9bc23af4682c30f2aac7fe6f7ab13ed65a" + integrity sha512-RaZe+abn0zBIz82cE9tp7Y7aZkHWWbEaE2yJpfxT8AhFz3fx+BU0kLYzuRN9fmA7vKueNJ1MTVUCY+Ex9/CHSQ== + dependencies: + "@ledgerhq/devices" "^8.0.0" + "@ledgerhq/errors" "^6.12.3" + events "^3.3.0" + "@ledgerhq/hw-transport@6.31.2": version "6.31.2" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.31.2.tgz#79c95f7928a64a0e3b5bc4ea7b5be04b9f738322" @@ -3902,20 +3911,35 @@ dependencies: argparse "^2.0.1" -"@zondax/ledger-ironfish@0.4.0": +"@zondax/ledger-ironfish-dkg@npm:@zondax/ledger-ironfish@0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.4.0.tgz#424d213a73688f8ec33035325d45cb0f0d7915da" integrity sha512-ifZPJl0WKKvTxAZCGRPARRJJv+qssU6PJYZEJTPHe+Vy2GSbcpfwbIzoyLqKI1vlPBQ1InbZYBP5BOmU1zRWnQ== dependencies: "@zondax/ledger-js" "^1.0.1" -"@zondax/ledger-js@^1.0.1": +"@zondax/ledger-ironfish@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@zondax/ledger-ironfish/-/ledger-ironfish-0.1.2.tgz#2ff93139c706734eb0d6800f743a9e0c2ae5268d" + integrity sha512-a9qnSOHxAf76pMonJBy5jI9oauR2W7WpVu/cCBs151uEW78NeSu4IMHOLGCo8KNiTPzpGwGa/7+1bpzxlQiEng== + dependencies: + "@zondax/ledger-js" "^0.2.1" + +"@zondax/ledger-js-dkg@npm:@zondax/ledger-js@1.0.1", "@zondax/ledger-js@^1.0.1": + name "@zondax/ledger-js-dkg" version "1.0.1" resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-1.0.1.tgz#a1c51943c5b7d1370cea588b193197234485d196" integrity sha512-9h+aIXyEK+Rdic5Ppsmq+tptDFwPTacG1H6tpZHFdhtBFHYFOLLkKTTmq5rMTv84aAPS1v0tnsF1e2Il6M05Cg== dependencies: "@ledgerhq/hw-transport" "6.31.2" +"@zondax/ledger-js@^0.2.1": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-0.2.2.tgz#b334cecaa372a8bfb91ae4fc5dd0d1c52411da4e" + integrity sha512-7wOUlRF2+kRaRU2KSzKb7XjPfScwEg3Cjg6NH/p+ikQLJ9eMkGC45NhSxYn8lixIIk+TgZ4yzTNOzFvF836gQw== + dependencies: + "@ledgerhq/hw-transport" "6.28.1" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" From 61254dfd61031d957c0a4cf349a6917324d10cb7 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 25 Sep 2024 11:16:58 -0700 Subject: [PATCH 13/37] Error for too many participants (#5426) At the moment, ledger dkg only supports up to 4 participants. This commit adds an error message when the user tries to create a dkg with more than 4 participants. --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index f91d28529f..4f874d816d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -269,6 +269,10 @@ export class DkgCreateCommand extends IronfishCommand { throw new Error('Total number of participants must be at least 2') } + if (ledger && totalParticipants > 4) { + throw new Error('DKG with Ledger supports a maximum of 4 participants') + } + this.log( `\nEnter ${ totalParticipants - 1 From e9a0d952675f1b8a18fccff7b8b034e8327bf18a Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 25 Sep 2024 14:12:48 -0700 Subject: [PATCH 14/37] Ask for confirmation while retrying ledger connect (#5428) --- .../src/commands/wallet/multisig/dkg/create.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 4f874d816d..b1ddd4595d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -55,10 +55,14 @@ export class DkgCreateCommand extends IronfishCommand { const accountName = await this.getAccountName(client, flags.newAccount) const { name: participantName, identity } = ledger - ? await ui.retryStep(() => { - Assert.isNotUndefined(ledger) - return this.getIdentityFromLedger(ledger, client, flags.participant) - }, this.logger) + ? await ui.retryStep( + () => { + Assert.isNotUndefined(ledger) + return this.getIdentityFromLedger(ledger, client, flags.participant) + }, + this.logger, + true, + ) : await this.getParticipant(client, flags.participant) this.log(`Identity for ${participantName}: \n${identity} \n`) From b36956e7ccc8681c50c18b38ed03185c4f27d6cf Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:14:42 -0700 Subject: [PATCH 15/37] fix resolution of @zondax/ledger-js package (#5427) * fix resolution of @zondax/ledger-js package we have two versions of @zondax/ledger-ironfish in our dependencies that require different versions of @zondax/ledger-js we need to make sure that each version's dependencies resolve correctly in order for both the DKG and single signer apps to work * removes resolutions block from package.json --- ironfish-cli/package.json | 2 +- ironfish-cli/src/utils/ledger.ts | 2 +- yarn.lock | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index f8bef9a431..e6839270e6 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -70,7 +70,7 @@ "@types/tar": "6.1.1", "@zondax/ledger-ironfish": "0.1.2", "@zondax/ledger-ironfish-dkg": "npm:@zondax/ledger-ironfish@0.4.0", - "@zondax/ledger-js-dkg": "npm:@zondax/ledger-js@1.0.1", + "@zondax/ledger-js": "^1.0.1", "axios": "1.7.2", "bech32": "2.0.0", "blessed": "0.1.81", diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 7cf2ed9a45..0968207a40 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -31,7 +31,7 @@ import { ResponseViewKey as ResponseViewKeyDkg, } from '@zondax/ledger-ironfish-dkg' import { default as IronfishDkgApp } from '@zondax/ledger-ironfish-dkg' -import { ResponseError } from '@zondax/ledger-js-dkg' +import { ResponseError } from '@zondax/ledger-js' import * as ui from '../ui' import { watchTransaction } from './transaction' diff --git a/yarn.lock b/yarn.lock index ee245b3297..dc9a6999ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3925,14 +3925,6 @@ dependencies: "@zondax/ledger-js" "^0.2.1" -"@zondax/ledger-js-dkg@npm:@zondax/ledger-js@1.0.1", "@zondax/ledger-js@^1.0.1": - name "@zondax/ledger-js-dkg" - version "1.0.1" - resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-1.0.1.tgz#a1c51943c5b7d1370cea588b193197234485d196" - integrity sha512-9h+aIXyEK+Rdic5Ppsmq+tptDFwPTacG1H6tpZHFdhtBFHYFOLLkKTTmq5rMTv84aAPS1v0tnsF1e2Il6M05Cg== - dependencies: - "@ledgerhq/hw-transport" "6.31.2" - "@zondax/ledger-js@^0.2.1": version "0.2.2" resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-0.2.2.tgz#b334cecaa372a8bfb91ae4fc5dd0d1c52411da4e" @@ -3940,6 +3932,13 @@ dependencies: "@ledgerhq/hw-transport" "6.28.1" +"@zondax/ledger-js@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@zondax/ledger-js/-/ledger-js-1.0.1.tgz#a1c51943c5b7d1370cea588b193197234485d196" + integrity sha512-9h+aIXyEK+Rdic5Ppsmq+tptDFwPTacG1H6tpZHFdhtBFHYFOLLkKTTmq5rMTv84aAPS1v0tnsF1e2Il6M05Cg== + dependencies: + "@ledgerhq/hw-transport" "6.31.2" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" From 4756156ed1fe8c5c13d1d62e018553403fbbb37c Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 25 Sep 2024 16:18:17 -0700 Subject: [PATCH 16/37] Review ledger transaction before commitment (#5429) This fits with the latest changes in the ledger app/ sdk. --- ironfish-cli/src/commands/wallet/multisig/sign.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 9e9f0e5591..dd34a5ba08 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -257,8 +257,6 @@ export class SignMultisigTransactionCommand extends IronfishCommand { const signingPackage = new multisig.SigningPackage(Buffer.from(signingPackageString, 'hex')) if (ledger) { - await ledger.reviewTransaction(unsignedTransaction.serialize().toString('hex')) - const frostSignatureShare = await ledger.dkgSign( unsignedTransaction.publicKeyRandomness(), signingPackage.frostSigningPackage().toString('hex'), @@ -330,6 +328,8 @@ export class SignMultisigTransactionCommand extends IronfishCommand { let commitment if (ledger) { + await ledger.reviewTransaction(unsignedTransaction.serialize().toString('hex')) + commitment = await this.createSigningCommitmentWithLedger( ledger, participant, From 11f24809b1bee307eadcc034544410108898e970 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 25 Sep 2024 16:48:03 -0700 Subject: [PATCH 17/37] Expect to participant in signing (#5431) Previously, this command was written in a way where the user performing this command didn't have to be a participant. This changes that to make sure that the user performing this command is a participant in the multisig wallet. This is because this command is catering to the most common use case. --- .../src/commands/wallet/multisig/sign.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index dd34a5ba08..1aab822652 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -196,6 +196,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { signatureShare: string, totalParticipants: number, ): Promise { + this.log( + `Enter ${ + totalParticipants - 1 + } signature shares of the participants (excluding your own)`, + ) + const signatureShares = await ui.collectStrings('Signature Share', totalParticipants - 1, { additionalStrings: [signatureShare], errorOnDuplicate: true, @@ -288,6 +294,10 @@ export class SignMultisigTransactionCommand extends IronfishCommand { identities: string[], unsignedTransaction: UnsignedTransaction, ) { + this.log( + `Enter ${identities.length - 1} commitments of the participants (excluding your own)`, + ) + const commitments = await ui.collectStrings('Commitment', identities.length - 1, { additionalStrings: [commitment], errorOnDuplicate: true, @@ -320,8 +330,12 @@ export class SignMultisigTransactionCommand extends IronfishCommand { this.error('Minimum number of participants must be at least 2') } - const identities = await ui.collectStrings('Identity', totalParticipants, { - additionalStrings: [], + this.log( + `Enter ${totalParticipants - 1} identities of the participants (excluding your own)`, + ) + + const identities = await ui.collectStrings('Identity', totalParticipants - 1, { + additionalStrings: [participant.identity], errorOnDuplicate: true, }) From f383662c9634e48e66671db8ffbcb9a27e66139e Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Wed, 25 Sep 2024 16:49:06 -0700 Subject: [PATCH 18/37] Fix logical error on catching duplicates (#5432) --- ironfish-cli/src/ui/prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index 91c6fbc687..d29801f26d 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -25,7 +25,7 @@ export async function collectStrings( const strings = [...array, ...additionalStrings] - if (!options?.errorOnDuplicate) { + if (options?.errorOnDuplicate) { const withoutDuplicates = [...new Set(strings)] if (withoutDuplicates.length !== strings.length) { From 4113f66330987c94db29c2481ae5e5422e11d68e Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:40:52 -0700 Subject: [PATCH 19/37] optionally sets createdAt when importing multisig accounts (#5430) * optionally sets createdAt when importing multisig accounts adds '--createdAt' flag to 'wallet:multisig:dkg:create', 'wallet:multisig:dkg:round3', and 'wallet:multisig:ledger:import' updates round3 RPC to accept optional 'accountCreatedAt' parameter allows user to set the 'createdAt' sequence when creating a multisig account or importing one from a ledger device. this allows users to start using their accounts more quickly without waiting for the account to scan the entire chain * defaults dkg createdAt to head of node's chain keeps the createdAt flag in place in case user wants to set it on an account when their node isn't synced, but defaults to the chain head --- .../commands/wallet/multisig/dkg/create.ts | 27 ++++++++++++++++--- .../commands/wallet/multisig/dkg/round3.ts | 15 +++++++++++ .../commands/wallet/multisig/ledger/import.ts | 5 ++++ .../routes/wallet/multisig/dkg/round3.test.ts | 10 +++++++ .../rpc/routes/wallet/multisig/dkg/round3.ts | 6 ++++- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index b1ddd4595d..e2720acd6d 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -6,7 +6,13 @@ import { deserializePublicPackage, deserializeRound2CombinedPublicPackage, } from '@ironfish/rust-nodejs' -import { AccountFormat, Assert, encodeAccountImport, RpcClient } from '@ironfish/sdk' +import { + ACCOUNT_SCHEMA_VERSION, + AccountFormat, + Assert, + encodeAccountImport, + RpcClient, +} from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../../../command' import { RemoteFlags } from '../../../../flags' @@ -30,6 +36,10 @@ export class DkgCreateCommand extends IronfishCommand { default: false, description: 'Perform operation with a ledger device', }), + createdAt: Flags.integer({ + description: + "Block sequence to begin scanning from for the created account. Uses node's chain head by default", + }), } async start(): Promise { @@ -54,6 +64,12 @@ export class DkgCreateCommand extends IronfishCommand { const accountName = await this.getAccountName(client, flags.newAccount) + let accountCreatedAt = flags.createdAt + if (!accountCreatedAt) { + const statusResponse = await client.node.getStatus() + accountCreatedAt = statusResponse.content.blockchain.head.sequence + } + const { name: participantName, identity } = ledger ? await ui.retryStep( () => { @@ -112,6 +128,7 @@ export class DkgCreateCommand extends IronfishCommand { round1PublicPackages, totalParticipants, ledger, + accountCreatedAt, ) }, this.logger, @@ -403,6 +420,7 @@ export class DkgCreateCommand extends IronfishCommand { round1PublicPackagesStr: string[], round2PublicPackagesStr: string[], round2SecretPackage: string, + accountCreatedAt?: number, ): Promise { const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity @@ -459,15 +477,16 @@ export class DkgCreateCommand extends IronfishCommand { publicKeyPackage: publicKeyPackage.toString('hex'), identity, }, - version: 4, + version: ACCOUNT_SCHEMA_VERSION, name: accountName, - spendingKey: null, createdAt: null, + spendingKey: null, } // Import multisig account const response = await client.wallet.importAccount({ account: encodeAccountImport(accountImport, AccountFormat.Base64Json), + createdAt: accountCreatedAt, }) this.log() @@ -499,6 +518,7 @@ export class DkgCreateCommand extends IronfishCommand { round1PublicPackages: string[], totalParticipants: number, ledger: LedgerDkg | undefined, + accountCreatedAt?: number, ): Promise { this.log(`\nEnter ${totalParticipants - 1} Round 2 Public Packages (excluding yours) `) @@ -520,6 +540,7 @@ export class DkgCreateCommand extends IronfishCommand { round1PublicPackages, round2PublicPackages, round2Result.secretPackage, + accountCreatedAt, ) return } diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index 2a9a520905..b056c572fe 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -53,6 +53,10 @@ export class DkgRound3Command extends IronfishCommand { description: 'Perform operation with a ledger device', hidden: true, }), + createdAt: Flags.integer({ + description: + "Block sequence to begin scanning from for the created account. Uses node's chain head by default.", + }), } async start(): Promise { @@ -117,6 +121,12 @@ export class DkgRound3Command extends IronfishCommand { } round2PublicPackages = round2PublicPackages.map((i) => i.trim()) + let accountCreatedAt = flags.createdAt + if (!accountCreatedAt) { + const statusResponse = await client.node.getStatus() + accountCreatedAt = statusResponse.content.blockchain.head.sequence + } + if (flags.ledger) { await this.performRound3WithLedger( client, @@ -124,6 +134,7 @@ export class DkgRound3Command extends IronfishCommand { round1PublicPackages, round2PublicPackages, round2SecretPackage, + accountCreatedAt, ) return } @@ -134,6 +145,7 @@ export class DkgRound3Command extends IronfishCommand { round2SecretPackage, round1PublicPackages, round2PublicPackages, + accountCreatedAt, }) this.log() @@ -148,6 +160,7 @@ export class DkgRound3Command extends IronfishCommand { round1PublicPackagesStr: string[], round2PublicPackagesStr: string[], round2SecretPackage: string, + accountCreatedAt?: number, ): Promise { const ledger = new LedgerDkg(this.logger) try { @@ -226,6 +239,8 @@ export class DkgRound3Command extends IronfishCommand { client, encodeAccountImport(accountImport, AccountFormat.Base64Json), this.logger, + participantName, + accountCreatedAt, ) this.log() diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts index a3e1b2d2ad..1641e382c5 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/import.ts @@ -18,6 +18,9 @@ export class MultisigLedgerImport extends IronfishCommand { description: 'Name to use for the account', char: 'n', }), + createdAt: Flags.integer({ + description: 'Block sequence to begin scanning from for the imported account', + }), } async start(): Promise { @@ -59,6 +62,8 @@ export class MultisigLedgerImport extends IronfishCommand { client, encodeAccountImport(accountImport, AccountFormat.Base64Json), this.logger, + name, + flags.createdAt, ) this.log() 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 9fbba85f09..f6d3068881 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.test.ts @@ -52,6 +52,8 @@ describe('Route multisig/dkg/round3', () => { ), ) + const accountCreatedAt = 2 + // Perform DKG round 3 const round3Responses = await Promise.all( participantNames.map((participantName, index) => @@ -61,6 +63,7 @@ describe('Route multisig/dkg/round3', () => { round2SecretPackage: round2Packages[index].content.round2SecretPackage, round1PublicPackages: round1Packages.map((pkg) => pkg.content.round1PublicPackage), round2PublicPackages: round2Packages.map((pkg) => pkg.content.round2PublicPackage), + accountCreatedAt, }), ), ) @@ -98,6 +101,13 @@ describe('Route multisig/dkg/round3', () => { .sort() expect(knownIdentities).toStrictEqual(expectedIdentities) } + + // Check that all imported accounts have createdAt sequence set + for (const accountName of accountNames) { + const account = routeTest.wallet.getAccountByName(accountName) + Assert.isNotNull(account) + expect(account.createdAt?.sequence).toEqual(accountCreatedAt) + } }) it('should fail if not all round 1 packages are passed as an input', async () => { diff --git a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts index 0912b324db..448ec7fe44 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/dkg/round3.ts @@ -16,6 +16,7 @@ export type DkgRound3Request = { round1PublicPackages: Array round2PublicPackages: Array accountName?: string + accountCreatedAt?: number } export type DkgRound3Response = { @@ -30,6 +31,7 @@ export const DkgRound3RequestSchema: yup.ObjectSchema = yup round1PublicPackages: yup.array().of(yup.string().defined()).defined(), round2PublicPackages: yup.array().of(yup.string().defined()).defined(), accountName: yup.string().optional(), + accountCreatedAt: yup.number().optional(), }) .defined() @@ -92,7 +94,9 @@ routes.register( }, } - const account = await node.wallet.importAccount(accountImport) + const account = await node.wallet.importAccount(accountImport, { + createdAt: request.data.accountCreatedAt, + }) await node.wallet.skipRescan(account) request.end({ From 3e811a499cc176fd6fca5800bcfdd5aeec1ed341 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:57:52 -0700 Subject: [PATCH 20/37] fixes rendering of tx assets if node not synced (#5436) when testing multisig signing we found that rendering unsigned transaction details failed with the error 'No asset found with identifier' when the transaction contained a custom asset, but the wallet had not scanned a transaction that included that asset users will likely use multisig signing offline, so they may not have access to synced asset data updates '_renderTransactionDetails' to use 'getAssetVerificationByIds', which handles errors from missing assets --- ironfish-cli/src/utils/transaction.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index 95c2a201c1..b87aef8900 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -23,7 +23,7 @@ import { } from '@ironfish/sdk' import { ux } from '@oclif/core' import { ProgressBar, ProgressBarPresets } from '../ui' -import { getAssetsByIDs, getAssetVerificationByIds } from './asset' +import { getAssetVerificationByIds } from './asset' export class TransactionTimer { private progressBar: ProgressBar | undefined @@ -167,7 +167,7 @@ async function _renderTransactionDetails( logger = logger ?? createRootLogger() const assetIds = collectAssetIds(mints, burns, notes) - const assetLookup = await getAssetsByIDs(client, assetIds, account, undefined) + const assetLookup = await getAssetVerificationByIds(client, assetIds, account, undefined) if (mints.length > 0) { logger.log('') @@ -185,7 +185,7 @@ async function _renderTransactionDetails( mint.value, false, mint.asset.id().toString('hex'), - assetLookup[mint.asset.id().toString('hex')].verification, + assetLookup[mint.asset.id().toString('hex')], ) logger.log(`Asset ID: ${mint.asset.id().toString('hex')}`) logger.log(`Name: ${mint.asset.name().toString('utf8')}`) @@ -218,7 +218,7 @@ async function _renderTransactionDetails( burn.value, false, burn.assetId.toString('hex'), - assetLookup[burn.assetId.toString('hex')].verification, + assetLookup[burn.assetId.toString('hex')], ) logger.log(`Asset ID: ${burn.assetId.toString('hex')}`) logger.log(`Amount: ${renderedAmount}`) @@ -243,7 +243,7 @@ async function _renderTransactionDetails( } logger.log('') - const verifiedAssetMetadata = assetLookup[note.assetId].verification + const verifiedAssetMetadata = assetLookup[note.assetId] const renderedAmount = CurrencyUtils.render( note.value, @@ -253,7 +253,7 @@ async function _renderTransactionDetails( ) logger.log(`Amount: ${renderedAmount}`) - if (verifiedAssetMetadata.symbol) { + if (verifiedAssetMetadata?.symbol) { logger.log(`Asset ID: ${note.assetId}`) } @@ -274,7 +274,7 @@ async function _renderTransactionDetails( } logger.log('') - const verifiedAssetMetadata = assetLookup[note.assetId].verification + const verifiedAssetMetadata = assetLookup[note.assetId] const renderedAmount = CurrencyUtils.render( note.value, @@ -284,7 +284,7 @@ async function _renderTransactionDetails( ) logger.log(`Amount: ${renderedAmount}`) - if (verifiedAssetMetadata.symbol) { + if (verifiedAssetMetadata?.symbol) { logger.log(`Asset ID: ${note.assetId}`) } From 360a6a5e05a4d8da27a45297dace4cb5fe66fc4a Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 25 Sep 2024 21:01:50 -0400 Subject: [PATCH 21/37] Use Chainport fallback token list API (#5437) --- ironfish-cli/src/utils/chainport/requests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironfish-cli/src/utils/chainport/requests.ts b/ironfish-cli/src/utils/chainport/requests.ts index d538ab338b..d2a1b7bd92 100644 --- a/ironfish-cli/src/utils/chainport/requests.ts +++ b/ironfish-cli/src/utils/chainport/requests.ts @@ -37,7 +37,7 @@ export const fetchChainportVerifiedTokens = async ( networkId: number, ): Promise => { const config = getConfig(networkId) - const url = `${config.endpoint}/token/list?network_name=IRONFISH` + const url = `${config.endpoint}/token_list?network_name=IRONFISH` return (await makeChainportRequest<{ verified_tokens: ChainportVerifiedToken[] }>(url)) .verified_tokens From ab46cf1406f2a932136890fffad4f48a79a48be2 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:38:42 -0700 Subject: [PATCH 22/37] Rename interactive step Identity to Participant Identity (#5434) --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 2 +- ironfish-cli/src/commands/wallet/multisig/sign.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index e2720acd6d..aa70a15fff 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -299,7 +299,7 @@ export class DkgCreateCommand extends IronfishCommand { totalParticipants - 1 } identities of all other participants (excluding yours) `, ) - const identities = await ui.collectStrings('Identity', totalParticipants - 1, { + const identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { additionalStrings: [currentIdentity], errorOnDuplicate: true, }) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index 1aab822652..d93560a0c7 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -334,7 +334,7 @@ export class SignMultisigTransactionCommand extends IronfishCommand { `Enter ${totalParticipants - 1} identities of the participants (excluding your own)`, ) - const identities = await ui.collectStrings('Identity', totalParticipants - 1, { + const identities = await ui.collectStrings('Participant Identity', totalParticipants - 1, { additionalStrings: [participant.identity], errorOnDuplicate: true, }) From e1e0a585a33961d41c848b1736a3bffe59305640 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:38:58 -0700 Subject: [PATCH 23/37] Rename newAccount flag in dkg create command to name (#5435) --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index aa70a15fff..3945969fb9 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -28,7 +28,7 @@ export class DkgCreateCommand extends IronfishCommand { char: 'n', description: 'The name of the secret to use for encryption during DKG', }), - newAccount: Flags.string({ + name: Flags.string({ char: 'a', description: 'The name to set for multisig account to be created', }), @@ -62,7 +62,7 @@ export class DkgCreateCommand extends IronfishCommand { } } - const accountName = await this.getAccountName(client, flags.newAccount) + const accountName = await this.getAccountName(client, flags.name) let accountCreatedAt = flags.createdAt if (!accountCreatedAt) { From 05b597334c4fad470946058012795238a6372e33 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 26 Sep 2024 10:41:04 -0700 Subject: [PATCH 24/37] chainport config update with mainnet fields (#5422) * feat: Add MAINNET CLI support for Chainport * update error message --- .../src/commands/wallet/chainport/send.ts | 36 +++++++++++-------- ironfish-cli/src/ui/prompts.ts | 10 +++--- ironfish-cli/src/utils/chainport/config.ts | 15 ++++++-- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/chainport/send.ts b/ironfish-cli/src/commands/wallet/chainport/send.ts index 879839afc2..bcce482551 100644 --- a/ironfish-cli/src/commands/wallet/chainport/send.ts +++ b/ironfish-cli/src/commands/wallet/chainport/send.ts @@ -2,10 +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/. */ -import { Asset } from '@ironfish/rust-nodejs' import { CreateTransactionRequest, CurrencyUtils, + MAINNET, RawTransaction, RawTransactionSerde, RpcAsset, @@ -84,8 +84,8 @@ export class BridgeCommand extends IronfishCommand { const networkId = (await client.chain.getNetworkInfo()).content.networkId - if (networkId !== TESTNET.id) { - this.error(`Chainport transactions are only available on testnet.`) + if (networkId !== TESTNET.id && networkId !== MAINNET.id) { + this.error(`Chainport transactions are only available on testnet and mainnet.`) } if (!flags.offline) { @@ -184,22 +184,31 @@ export class BridgeCommand extends IronfishCommand { const tokens = await fetchChainportVerifiedTokens(networkId) - if (assetId == null) { + const tokenNames = tokens.map( + (t, index) => `${index + 1}. ${t.name} (${t.symbol}) - ${t.web3_address}`, + ) + + if (!assetId) { const asset = await ui.assetPrompt(client, from, { action: 'send', showNativeAsset: true, showNonCreatorAsset: true, - showSingleAssetChoice: false, + showSingleAssetChoice: true, filter: (asset) => { return tokens.some((t) => t.web3_address === asset.id) }, }) - assetId = asset?.id - - if (!assetId) { - assetId = Asset.nativeId().toString('hex') + if (!asset) { + this.logger.error( + `No supported Chainport asset found for this account. Here are the supported tokens: \n\n${tokenNames.join( + '\n', + )}\n`, + ) + this.exit(1) } + + assetId = asset.id } const asset: ChainportVerifiedToken | undefined = tokens.find( @@ -207,15 +216,12 @@ export class BridgeCommand extends IronfishCommand { ) if (!asset) { - const names = tokens.map( - (t, index) => `${index + 1}. ${t.name} (${t.symbol}) - ${t.web3_address}`, - ) - - this.error( - `Asset ${assetId} not supported by Chainport. Here are the supported tokens: \n\n${names.join( + this.logger.error( + `Asset ${assetId} not supported by Chainport. Here are the supported tokens: \n\n${tokenNames.join( '\n', )}\n`, ) + this.exit(1) } const targetNetworks = asset.target_networks diff --git a/ironfish-cli/src/ui/prompts.ts b/ironfish-cli/src/ui/prompts.ts index 8c2fead622..fff33c80ce 100644 --- a/ironfish-cli/src/ui/prompts.ts +++ b/ironfish-cli/src/ui/prompts.ts @@ -73,6 +73,11 @@ export async function assetPrompt( ) } + const filter = options.filter + if (filter) { + balances = balances.filter((balance) => filter(assetLookup[balance.assetId])) + } + if (balances.length === 0) { return undefined } @@ -85,11 +90,6 @@ export async function assetPrompt( } } - 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' diff --git a/ironfish-cli/src/utils/chainport/config.ts b/ironfish-cli/src/utils/chainport/config.ts index 015d0556a9..b8405e283f 100644 --- a/ironfish-cli/src/utils/chainport/config.ts +++ b/ironfish-cli/src/utils/chainport/config.ts @@ -2,7 +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 { TESTNET } from '@ironfish/sdk' +import { MAINNET, TESTNET } from '@ironfish/sdk' const config = { [TESTNET.id]: { @@ -17,7 +17,18 @@ const config = { '06102d319ab7e77b914a1bd135577f3e266fd82a3e537a02db281421ed8b3d13', ]), }, -} // MAINNET support to follow + [MAINNET.id]: { + chainportId: 22, + endpoint: 'https://api.chainport.io', + outgoingAddresses: new Set([ + '576ffdcc27e11d81f5180d3dc5690294941170d492b2d9503c39130b1f180405', + '7ac2d6a59e19e66e590d014af013cd5611dc146e631fa2aedf0ee3ed1237eebe', + ]), + incomingAddresses: new Set([ + '1216302193e8f1ad020f458b54a163039403d803e98673c6a85e59b5f4a1a900', + ]), + }, +} export const isNetworkSupportedByChainport = (networkId: number) => { return !!config[networkId] From a1d86b167bd0ac8e6bb6e34397bd7d3de01e2cae Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:41:14 -0700 Subject: [PATCH 25/37] Fix minor typos (show -> shown) (#5433) --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 2 +- ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 3945969fb9..2c54aaaca2 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -504,7 +504,7 @@ export class DkgCreateCommand extends IronfishCommand { this.log('Encrypted Ledger Multisig Backup:') this.log(encryptedKeys.toString('hex')) this.log() - this.log('Please save the encrypted keys show above.') + this.log('Please save the encrypted keys shown above.') this.log( 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', ) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index b056c572fe..7d4bc8083a 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -256,7 +256,7 @@ export class DkgRound3Command extends IronfishCommand { this.log('Encrypted Ledger Multisig Backup:') this.log(encryptedKeys.toString('hex')) this.log() - this.log('Please save the encrypted keys show above.') + this.log('Please save the encrypted keys shown above.') this.log( 'Use `ironfish wallet:multisig:ledger:restore` if you need to restore the keys to your Ledger.', ) From 5e8786df93a52deee3a47bc0df9968e0a41f990d Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 26 Sep 2024 12:28:50 -0700 Subject: [PATCH 26/37] signing app error display improvement (#5439) --- ironfish-cli/src/commands/wallet/import.ts | 7 ++++--- ironfish-cli/src/utils/ledger.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index f80fed408d..7c8e54badf 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -8,7 +8,7 @@ import { RemoteFlags } from '../../flags' import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' import { importAccount } from '../../utils' -import { Ledger } from '../../utils/ledger' +import { Ledger, LedgerError } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { static description = `import an account` @@ -123,8 +123,9 @@ export class ImportCommand extends IronfishCommand { const account = await ledger.importAccount() return encodeAccountImport(account, AccountFormat.Base64Json) } catch (e) { - if (e instanceof Error) { - this.error(e.message) + if (e instanceof LedgerError) { + this.logger.error(e.message + '\n') + this.exit(1) } else { this.error('Unknown error while importing account from ledger device.') } diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 0968207a40..f732aa538d 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -300,10 +300,10 @@ export class Ledger { // https://github.com/LedgerHQ/ledger-live/blob/173bb3c84cc855f83ab8dc49362bc381afecc31e/libs/ledgerjs/packages/errors/src/index.ts#L263 // https://github.com/Zondax/ledger-ironfish/blob/bf43a4b8d403d15138699ee3bb1a3d6dfdb428bc/docs/APDUSPEC.md?plain=1#L25 if (appInfo.returnCode === 0x5515) { - throw new Error('Please unlock your Ledger device.') + throw new LedgerError('Please unlock your Ledger device.') } - throw new Error('Please open the Iron Fish app on your ledger device.') + throw new LedgerError('Please open the Iron Fish app on your ledger device.') } if (appInfo.appVersion) { From c159fe0d27349a6582916dac590cc4bcb26095fd Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:37:05 -0700 Subject: [PATCH 27/37] Temporarily assign different URL for mainnet (#5441) --- ironfish-cli/src/utils/chainport/requests.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ironfish-cli/src/utils/chainport/requests.ts b/ironfish-cli/src/utils/chainport/requests.ts index d2a1b7bd92..11124fe83c 100644 --- a/ironfish-cli/src/utils/chainport/requests.ts +++ b/ironfish-cli/src/utils/chainport/requests.ts @@ -1,6 +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 { MAINNET } from '@ironfish/sdk' import axios from 'axios' import { getConfig } from './config' import { @@ -37,7 +38,12 @@ export const fetchChainportVerifiedTokens = async ( networkId: number, ): Promise => { const config = getConfig(networkId) - const url = `${config.endpoint}/token_list?network_name=IRONFISH` + let url + if (networkId === MAINNET.id) { + url = `${config.endpoint}/token/list?network_name=IRONFISH` + } else { + url = `${config.endpoint}/token_list?network_name=IRONFISH` + } return (await makeChainportRequest<{ verified_tokens: ChainportVerifiedToken[] }>(url)) .verified_tokens From 5218bd87a52cda548d1a29755312d4dca72f2b36 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 26 Sep 2024 14:20:45 -0700 Subject: [PATCH 28/37] explicityly asking to broadcast ledger transaction (#5440) --- ironfish-cli/src/utils/ledger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index f732aa538d..49880d6dac 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -22,6 +22,7 @@ import IronfishApp, { ResponseViewKey, } from '@zondax/ledger-ironfish' import { + default as IronfishDkgApp, KeyResponse, ResponseAddress as ResponseAddressDkg, ResponseDkgRound1, @@ -30,7 +31,6 @@ import { ResponseProofGenKey as ResponseProofGenKeyDkg, ResponseViewKey as ResponseViewKeyDkg, } from '@zondax/ledger-ironfish-dkg' -import { default as IronfishDkgApp } from '@zondax/ledger-ironfish-dkg' import { ResponseError } from '@zondax/ledger-js' import * as ui from '../ui' import { watchTransaction } from './transaction' @@ -501,7 +501,7 @@ export async function sendTransactionWithLedger( ux.stdout(`\nHash: ${transaction.hash().toString('hex')}`) ux.stdout(`Fee: ${CurrencyUtils.render(transaction.fee(), true)}`) - await ui.confirmOrQuit('', confirm) + await ui.confirmOrQuit('Would you like to broadcast this transaction?', confirm) const addTransactionResponse = await client.wallet.addTransaction({ transaction: signedTransaction, From 8cc37bea04b056f0782ab490a06c670ed9d67a28 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Thu, 26 Sep 2024 14:20:55 -0700 Subject: [PATCH 29/37] Detects whether ironfish dkg app is open (#5442) * Detects whether ironfish dkg app is open Calling app.getVersion errors when the app is locked and when the app is not open. This is sufficient enough for us to detect whether the app is open or not. If it isn't open, we just end the process for the dkg commands. * commetn explaining why we call this and do nothing with the response --- ironfish-cli/src/utils/ledger.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 49880d6dac..16ee2716d2 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -75,6 +75,9 @@ export class LedgerDkg { const app = new IronfishDkgApp(transport, true) + // If the app isn't open or the device is locked, this will throw an error. + await app.getVersion() + this.app = app return { app, PATH: this.PATH } From 0eb71a209994d084bc554b7d84bb9adc9ef5ac5b Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:58:52 -0700 Subject: [PATCH 30/37] refresh Ledger connection before each instruction (#5444) some errors cause the CLI to disconnect from the device. this can make it impossible to continue running commands until a new connection is made handles 'disconnect' events on the app transport by closing the connection refreshes the connection before each instruction: checks if the app is undefined and establishes a new connection refactors 'tryInstruction' to take a function instead of a promise --- ironfish-cli/src/utils/ledger.ts | 77 +++++++++++++++----------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 16ee2716d2..500ec8365f 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -4,6 +4,7 @@ import { ACCOUNT_SCHEMA_VERSION, AccountImport, + Assert, createRootLogger, CurrencyUtils, Logger, @@ -45,9 +46,12 @@ export class LedgerDkg { this.logger = logger ? logger : createRootLogger() } - tryInstruction = async (promise: Promise) => { + tryInstruction = async (instruction: (app: IronfishDkgApp) => Promise) => { + await this.refreshConnection() + Assert.isNotUndefined(this.app, 'Unable to establish connection with Ledger device') + try { - return await promise + return await instruction(this.app) } catch (error: unknown) { if (isResponseError(error)) { this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) @@ -69,6 +73,11 @@ export class LedgerDkg { connect = async () => { const transport = await TransportNodeHid.create(3000) + transport.on('disconnect', async () => { + await transport.close() + this.app = undefined + }) + if (transport.deviceModel) { this.logger.debug(`${transport.deviceModel.productName} found.`) } @@ -83,15 +92,17 @@ export class LedgerDkg { return { app, PATH: this.PATH } } - dkgGetIdentity = async (index: number): Promise => { + private refreshConnection = async () => { if (!this.app) { - throw new Error('Connect to Ledger first') + await this.connect() } + } + dkgGetIdentity = async (index: number): Promise => { this.logger.log('Retrieving identity from ledger device.') - const response: ResponseIdentity = await this.tryInstruction( - this.app.dkgGetIdentity(index, false), + const response: ResponseIdentity = await this.tryInstruction((app) => + app.dkgGetIdentity(index, false), ) return response.identity @@ -102,13 +113,9 @@ export class LedgerDkg { identities: string[], minSigners: number, ): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction(this.app.dkgRound1(index, identities, minSigners)) + return this.tryInstruction((app) => app.dkgRound1(index, identities, minSigners)) } dkgRound2 = async ( @@ -116,14 +123,10 @@ export class LedgerDkg { round1PublicPackages: string[], round1SecretPackage: string, ): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction( - this.app.dkgRound2(index, round1PublicPackages, round1SecretPackage), + return this.tryInstruction((app) => + app.dkgRound2(index, round1PublicPackages, round1SecretPackage), ) } @@ -135,14 +138,10 @@ export class LedgerDkg { round2SecretPackage: string, gskBytes: string[], ): Promise => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction( - this.app.dkgRound3Min( + return this.tryInstruction((app) => + app.dkgRound3Min( index, participants, round1PublicPackages, @@ -160,28 +159,24 @@ export class LedgerDkg { outgoingViewKey: string proofAuthorizingKey: string }> => { - if (!this.app) { - throw new Error('Connect to Ledger first') - } - - const responseAddress: KeyResponse = await this.tryInstruction( - this.app.dkgRetrieveKeys(IronfishKeys.PublicAddress), + const responseAddress: KeyResponse = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.PublicAddress), ) if (!isResponseAddress(responseAddress)) { throw new Error(`No public address returned.`) } - const responseViewKey = await this.tryInstruction( - this.app.dkgRetrieveKeys(IronfishKeys.ViewKey), + const responseViewKey = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.ViewKey), ) if (!isResponseViewKey(responseViewKey)) { throw new Error(`No view key returned.`) } - const responsePGK: KeyResponse = await this.tryInstruction( - this.app.dkgRetrieveKeys(IronfishKeys.ProofGenerationKey), + const responsePGK: KeyResponse = await this.tryInstruction((app) => + app.dkgRetrieveKeys(IronfishKeys.ProofGenerationKey), ) if (!isResponseProofGenKey(responsePGK)) { @@ -202,7 +197,7 @@ export class LedgerDkg { throw new Error('Connect to Ledger first') } - const response = await this.tryInstruction(this.app.dkgGetPublicPackage()) + const response = await this.tryInstruction((app) => app.dkgGetPublicPackage()) return response.publicPackage } @@ -216,7 +211,7 @@ export class LedgerDkg { 'Please review and approve the outputs of this transaction on your ledger device.', ) - const { hash } = await this.tryInstruction(this.app.reviewTransaction(transaction)) + const { hash } = await this.tryInstruction((app) => app.reviewTransaction(transaction)) return hash } @@ -226,8 +221,8 @@ export class LedgerDkg { throw new Error('Connect to Ledger first') } - const { commitments } = await this.tryInstruction( - this.app.dkgGetCommitments(transactionHash), + const { commitments } = await this.tryInstruction((app) => + app.dkgGetCommitments(transactionHash), ) return commitments @@ -242,8 +237,8 @@ export class LedgerDkg { throw new Error('Connect to Ledger first') } - const { signature } = await this.tryInstruction( - this.app.dkgSign(randomness, frostSigningPackage, transactionHash), + const { signature } = await this.tryInstruction((app) => + app.dkgSign(randomness, frostSigningPackage, transactionHash), ) return signature @@ -256,7 +251,7 @@ export class LedgerDkg { this.logger.log('Please approve the request on your ledger device.') - const { encryptedKeys } = await this.tryInstruction(this.app.dkgBackupKeys()) + const { encryptedKeys } = await this.tryInstruction((app) => app.dkgBackupKeys()) return encryptedKeys } @@ -268,7 +263,7 @@ export class LedgerDkg { this.logger.log('Please approve the request on your ledger device.') - await this.tryInstruction(this.app.dkgRestoreKeys(encryptedKeys)) + await this.tryInstruction((app) => app.dkgRestoreKeys(encryptedKeys)) } } From 5d7f6ffac3af8c78249572718d0c8b9dd861c456 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:59:13 -0700 Subject: [PATCH 31/37] displays own identity at start of signing process (#5445) users must enter the identities of all signers (excluding their own) before creating a signing commitment displays the user's own identity and asks the user to share it with other signers before prompting for the identities of other signers --- ironfish-cli/src/commands/wallet/multisig/sign.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/multisig/sign.ts b/ironfish-cli/src/commands/wallet/multisig/sign.ts index d93560a0c7..4f75574b05 100644 --- a/ironfish-cli/src/commands/wallet/multisig/sign.ts +++ b/ironfish-cli/src/commands/wallet/multisig/sign.ts @@ -320,6 +320,9 @@ export class SignMultisigTransactionCommand extends IronfishCommand { unsignedTransactionInput: string, ledger: LedgerDkg | undefined, ) { + this.log(`Identity for ${participant.name}: \n${participant.identity} \n`) + this.log('Share your participant identity with other signers.') + const input = await ui.inputPrompt( 'Enter the number of participants in signing this transaction', true, From 4dff297d8e68fbb6be7aa51330ea95050cabd0f6 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:59:27 -0700 Subject: [PATCH 32/37] ensures that min signers entered is <= total participants (#5446) the minimum number of signers cannot be greater than the number of total participants throw an error when the user enters an invalid number instead of failing during round1 --- ironfish-cli/src/commands/wallet/multisig/dkg/create.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 2c54aaaca2..22ef7ee9af 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -308,6 +308,10 @@ export class DkgCreateCommand extends IronfishCommand { const minSigners = parseInt(input) if (isNaN(minSigners) || minSigners < 2) { throw new Error('Minimum number of signers must be at least 2') + } else if (minSigners > totalParticipants) { + throw new Error( + 'Minimum number of signers cannot be more than total number of participants', + ) } if (ledger) { From fed15464dd55bd5c0f53c6b6e182f65a659606e6 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:59:43 -0700 Subject: [PATCH 33/37] changes 'backup' from flag to arg in 'restore' (#5447) updates 'wallet:multisig:ledger:restore' to accept the encrypted keys backup as an arg instead of as a flag this makes the command consistent with our cli style guide --- .../src/commands/wallet/multisig/ledger/restore.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index f0e995b7ab..cad4c91e16 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.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 { Flags } from '@oclif/core' +import { Args } from '@oclif/core' import { IronfishCommand } from '../../../../command' import * as ui from '../../../../ui' import { LedgerDkg } from '../../../../utils/ledger' @@ -9,17 +9,17 @@ import { LedgerDkg } from '../../../../utils/ledger' export class MultisigLedgerRestore extends IronfishCommand { static description = `restore encrypted multisig keys to a Ledger device` - static flags = { - backup: Flags.string({ + static args = { + backup: Args.string({ + required: false, description: 'Encrypted multisig key backup from your Ledger device', - char: 'b', }), } async start(): Promise { - const { flags } = await this.parse(MultisigLedgerRestore) + const { args } = await this.parse(MultisigLedgerRestore) - let encryptedKeys = flags.backup + let encryptedKeys = args.backup if (!encryptedKeys) { encryptedKeys = await ui.longPrompt( 'Enter the encrypted multisig key backup to restore to your Ledger device', From 427c53f3b73a30f121b8018dde0e9bc0b654e022 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:47:54 -0700 Subject: [PATCH 34/37] uses actionable error message for common Ledger errors (#5448) we've seen several errors occur commonly when the Ledger app is unavailable either because the device is locked or the app isn't open catches errors with these codes and displays an error message telling the user that the error may be due to a locked device or closed app --- ironfish-cli/src/utils/ledger.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index 500ec8365f..106051fab3 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -57,10 +57,8 @@ export class LedgerDkg { this.logger.debug(`Ledger ResponseError returnCode: ${error.returnCode.toString(16)}`) if (error.returnCode === LedgerDeviceLockedError.returnCode) { throw new LedgerDeviceLockedError('Please unlock your Ledger device.') - } else if (error.returnCode === LedgerAppNotOpenError.returnCode) { - throw new LedgerAppNotOpenError( - 'Please open the Iron Fish app on your Ledger device.', - ) + } else if (LedgerAppUnavailableError.returnCodes.includes(error.returnCode)) { + throw new LedgerAppUnavailableError() } throw new LedgerError(error.errorMessage) @@ -440,8 +438,18 @@ export class LedgerDeviceLockedError extends LedgerError { static returnCode = 0x5515 } -export class LedgerAppNotOpenError extends LedgerError { - static returnCode = 0x6f00 +export class LedgerAppUnavailableError extends LedgerError { + static returnCodes = [ + 0x6d00, // Instruction not supported + 0xffff, // Unknown transport error + 0x6f00, // Technical error + ] + + constructor() { + super( + `Unable to connect to Ironfish app on Ledger. Please check that the device is unlocked and the app is open.`, + ) + } } export async function sendTransactionWithLedger( From 0ce965d76cb94aa8f1185385c07017791b650f08 Mon Sep 17 00:00:00 2001 From: mat-if <97762857+mat-if@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:34:47 -0700 Subject: [PATCH 35/37] wallet:multisig:account:participants displays your identity separately (#5450) --- .../wallet/multisig/account/participants.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts index 1c7cba014f..3896f54087 100644 --- a/ironfish-cli/src/commands/wallet/multisig/account/participants.ts +++ b/ironfish-cli/src/commands/wallet/multisig/account/participants.ts @@ -23,11 +23,29 @@ export class MultisigAccountParticipants extends IronfishCommand { const client = await this.connectRpc() await ui.checkWalletUnlocked(client) - const response = await client.wallet.multisig.getAccountIdentities({ - account: flags.account, - }) + const accountIdentities = ( + await client.wallet.multisig.getAccountIdentities({ + account: flags.account, + }) + ).content.identities - for (const identity of response.content.identities) { + const participants = (await client.wallet.multisig.getIdentities()).content.identities + + const matchingIdentities = participants.filter((identity) => + accountIdentities.includes(identity.identity), + ) + + let participant: string | undefined + if (matchingIdentities.length === 1) { + participant = matchingIdentities[0].identity + this.log(`Your identity:\n${participant}`) + this.log('\nOther participating identities:') + } + + for (const identity of accountIdentities) { + if (participant && participant === identity) { + continue + } this.log(identity) } } From 8e9bde7f17288485f5eb148d80f1d3820f9c1f7e Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Fri, 27 Sep 2024 11:53:21 -0700 Subject: [PATCH 36/37] inputNumberPrompt and usage (#5443) * Adds prompt specific to numbers The problem with parse int is that even if you pass it something like "1234123asdf" it will return a success and return 1234123. This is not what we want. We want to make sure that the user has entered a valid number. We also want to retry the prompt if the user enters an invalid number or a decimal when an integer is expected. * adding min signers check back: lint fix --- .../commands/wallet/multisig/dkg/create.ts | 25 +++++++---- ironfish-cli/src/ui/prompt.ts | 42 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts index 22ef7ee9af..12377e2ead 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/create.ts @@ -284,9 +284,13 @@ export class DkgCreateCommand extends IronfishCommand { }> { this.log('\nCollecting Participant Info and Performing Round 1...') - let input = await ui.inputPrompt('Enter the total number of participants', true) - const totalParticipants = parseInt(input) - if (isNaN(totalParticipants) || totalParticipants < 2) { + const totalParticipants = await ui.inputNumberPrompt( + this.logger, + 'Enter the total number of participants', + { required: true, integer: true }, + ) + + if (totalParticipants < 2) { throw new Error('Total number of participants must be at least 2') } @@ -304,13 +308,15 @@ export class DkgCreateCommand extends IronfishCommand { errorOnDuplicate: true, }) - input = await ui.inputPrompt('Enter the number of minimum signers', true) - const minSigners = parseInt(input) - if (isNaN(minSigners) || minSigners < 2) { - throw new Error('Minimum number of signers must be at least 2') - } else if (minSigners > totalParticipants) { + const minSigners = await ui.inputNumberPrompt( + this.logger, + 'Enter the number of minimum signers', + { required: true, integer: true }, + ) + + if (minSigners < 2 || minSigners > totalParticipants) { throw new Error( - 'Minimum number of signers cannot be more than total number of participants', + 'Minimum number of signers must be between 2 and the total number of participants', ) } @@ -322,6 +328,7 @@ export class DkgCreateCommand extends IronfishCommand { identities, minSigners, ) + return { ...result, totalParticipants, diff --git a/ironfish-cli/src/ui/prompt.ts b/ironfish-cli/src/ui/prompt.ts index d29801f26d..5685bfdd05 100644 --- a/ironfish-cli/src/ui/prompt.ts +++ b/ironfish-cli/src/ui/prompt.ts @@ -2,6 +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 { Logger } from '@ironfish/sdk' import { ux } from '@oclif/core' import inquirer from 'inquirer' import { longPrompt } from './longPrompt' @@ -45,6 +46,47 @@ async function _inputPrompt(message: string, options?: { password: boolean }): P return result.prompt.trim() } +export async function inputNumberPrompt( + logger: Logger, + message: string, + options: { + required?: boolean + integer?: boolean + }, +): Promise { + const validateNumber = (input: string): number => { + const num = Number(input) + + if (isNaN(num)) { + throw new Error('Input must be a number') + } + + if (options.integer && num % 1 !== 0) { + throw new Error('Input must be an integer') + } + + return num + } + + if (options.required) { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const userInput = await _inputPrompt(message) + return validateNumber(userInput) + } catch (e) { + if (e instanceof Error) { + logger.error(e.message) + } else { + logger.error('An error occurred. Please try again.') + } + } + } + } + + return validateNumber(await _inputPrompt(message)) +} + export async function inputPrompt( message: string, required: boolean = false, From dd2733285eac82498a2fcd623c592cc4f90e656e Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:58:12 -0700 Subject: [PATCH 37/37] bumps versions for v2.7.0 (#5452) --- 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 e6839270e6..cfff8cf806 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "2.6.0", + "version": "2.7.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.6.0", - "@ironfish/sdk": "2.6.0", + "@ironfish/rust-nodejs": "2.7.0", + "@ironfish/sdk": "2.7.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 eeddf59ab5..0c09d5c874 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.6.0", + "version": "2.7.0", "os": [ "darwin" ], diff --git a/ironfish-rust-nodejs/npm/darwin-x64/package.json b/ironfish-rust-nodejs/npm/darwin-x64/package.json index d34397a1f7..7485c7d570 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.6.0", + "version": "2.7.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 9a3ab7c0f6..2a6630ddcf 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.6.0", + "version": "2.7.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 fabe279e18..e8e3dd548e 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.6.0", + "version": "2.7.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 0041a8bae1..3df3885f9a 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.6.0", + "version": "2.7.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 0f10be1936..8fe9f2d953 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.6.0", + "version": "2.7.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 2a7e344884..f3809d4733 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.6.0", + "version": "2.7.0", "os": [ "win32" ], diff --git a/ironfish-rust-nodejs/package.json b/ironfish-rust-nodejs/package.json index 4b61d3bad9..11b3fa9a29 100644 --- a/ironfish-rust-nodejs/package.json +++ b/ironfish-rust-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/rust-nodejs", - "version": "2.6.0", + "version": "2.7.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 2e3e59653b..8c68235061 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "2.6.0", + "version": "2.7.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.6.0", + "@ironfish/rust-nodejs": "2.7.0", "@napi-rs/blake-hash": "1.3.3", "axios": "1.7.2", "bech32": "2.0.0",