diff --git a/ironfish-cli/src/commands/wallet/import.ts b/ironfish-cli/src/commands/wallet/import.ts index 4ff92863f9..6e518c7126 100644 --- a/ironfish-cli/src/commands/wallet/import.ts +++ b/ironfish-cli/src/commands/wallet/import.ts @@ -12,7 +12,7 @@ import { IronfishCommand } from '../../command' import { RemoteFlags } from '../../flags' import { checkWalletUnlocked, inputPrompt } from '../../ui' import { importFile, importPipe, longPrompt } from '../../ui/longPrompt' -import { Ledger } from '../../utils/ledger' +import { initializeLedger } from '../../utils/ledger' export class ImportCommand extends IronfishCommand { static description = `import an account` @@ -155,9 +155,9 @@ export class ImportCommand extends IronfishCommand { } async importLedger(): Promise { + const ledger = await initializeLedger(false, this.logger) + try { - const ledger = new Ledger(this.logger) - await ledger.connect() const account = await ledger.importAccount() return encodeAccountImport(account, AccountFormat.Base64Json) } catch (e) { diff --git a/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts b/ironfish-cli/src/commands/wallet/multisig/commitment/create.ts index b2ce1ea3a1..1c2e327057 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 { initializeLedger } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -124,16 +124,7 @@ export class CreateSigningCommitmentCommand extends IronfishCommand { transactionHash: Buffer, 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 ledger = await initializeLedger(true, this.logger) const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round1.ts index b76c12317e..e60afe1a1f 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 { initializeLedger } from '../../../../utils/ledger' export class DkgRound1Command extends IronfishCommand { static description = 'Perform round1 of the DKG protocol for multisig account creation' @@ -100,16 +100,7 @@ export class DkgRound1Command extends IronfishCommand { identities: string[], minSigners: number, ): 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 ledger = await initializeLedger(true, this.logger) const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round2.ts index bdf82b2429..27a2b8df65 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 { initializeLedger } from '../../../../utils/ledger' export class DkgRound2Command extends IronfishCommand { static description = 'Perform round2 of the DKG protocol for multisig account creation' @@ -97,16 +97,7 @@ export class DkgRound2Command extends IronfishCommand { round1PublicPackages: string[], round1SecretPackage: 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 ledger = await initializeLedger(true, this.logger) // TODO(hughy): determine how to handle multiple identities using index const { publicPackage, secretPackage } = await ledger.dkgRound2( diff --git a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts index bd718ecd3d..801ba93121 100644 --- a/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts +++ b/ironfish-cli/src/commands/wallet/multisig/dkg/round3.ts @@ -1,16 +1,12 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { - deserializePublicPackage, - deserializeRound2CombinedPublicPackage, -} from '@ironfish/rust-nodejs' -import { AccountFormat, encodeAccountImport, RpcClient } from '@ironfish/sdk' +import { AccountFormat, RpcClient, encodeAccountImport } 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 { initializeLedger } from '../../../../utils/ledger' export class DkgRound3Command extends IronfishCommand { static description = 'Perform round3 of the DKG protocol for multisig account creation' @@ -143,66 +139,20 @@ export class DkgRound3Command extends IronfishCommand { round2PublicPackagesStr: string[], round2SecretPackage: 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 ledger = await initializeLedger(true, this.logger) 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( + const { dkgKeys, publicKeyPackage } = await ledger.dkgRound3( 0, - participants, - round1FrostPackages, - round2FrostPackages, + identity, + round1PublicPackagesStr, + round2PublicPackagesStr, round2SecretPackage, - gskBytes, ) - // Retrieve all multisig account keys and publicKeyPackage - const dkgKeys = await ledger.dkgRetrieveKeys() - - const publicKeyPackage = await ledger.dkgGetPublicPackage() - const accountImport = { ...dkgKeys, multisigKeys: { diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts index 3737bd043d..5c89f82587 100644 --- a/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts +++ b/ironfish-cli/src/commands/wallet/multisig/ledger/backup.ts @@ -2,22 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { IronfishCommand } from '../../../../command' -import { Ledger } from '../../../../utils/ledger' +import { initializeLedger } 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 ledger = await initializeLedger(true, this.logger) const encryptedKeys = await ledger.dkgBackupKeys() diff --git a/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts b/ironfish-cli/src/commands/wallet/multisig/ledger/restore.ts index 08014d3835..59270c75f2 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 { initializeLedger } from '../../../../utils/ledger' export class MultisigLedgerRestore extends IronfishCommand { static description = `restore encrypted multisig keys to a Ledger device` @@ -26,16 +26,7 @@ export class MultisigLedgerRestore extends IronfishCommand { ) } - 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 ledger = await initializeLedger(true, this.logger) await ledger.dkgRestoreKeys(encryptedKeys) diff --git a/ironfish-cli/src/commands/wallet/multisig/participant/create.ts b/ironfish-cli/src/commands/wallet/multisig/participant/create.ts index bca0f41b1f..4ec1ab59de 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 { initializeLedger } from '../../../../utils/ledger' export class MultisigIdentityCreate extends IronfishCommand { static description = `Create a multisig participant identity` @@ -71,16 +71,7 @@ export class MultisigIdentityCreate extends IronfishCommand { } 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 - } - } + const ledger = await initializeLedger(true, this.logger) // TODO(hughy): support multiple identities using index return ledger.dkgGetIdentity(0) diff --git a/ironfish-cli/src/commands/wallet/multisig/signature/create.ts b/ironfish-cli/src/commands/wallet/multisig/signature/create.ts index 1e88324a65..d0f567721c 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 { initializeLedger } from '../../../../utils/ledger' import { MultisigTransactionJson } from '../../../../utils/multisig' import { renderUnsignedTransactionDetails } from '../../../../utils/transaction' @@ -120,16 +120,7 @@ export class CreateSignatureShareCommand extends IronfishCommand { 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 ledger = await initializeLedger(true, this.logger) const identityResponse = await client.wallet.multisig.getIdentity({ name: participantName }) const identity = identityResponse.content.identity diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 66107f8355..8963df1672 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -21,7 +21,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 { initializeLedger } from '../../utils/ledger' import { getSpendPostTimeInMs, updateSpendPostTimeInMs } from '../../utils/spendPostTime' import { displayTransactionSummary, @@ -359,16 +359,7 @@ export class Send extends IronfishCommand { 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 ledger = await initializeLedger(false, this.logger) const publicKey = (await client.wallet.getAccountPublicKey({ account: from })).content .publicKey diff --git a/ironfish-cli/src/commands/wallet/transactions/sign.ts b/ironfish-cli/src/commands/wallet/transactions/sign.ts index 87b57ca0b8..21b7bdd85a 100644 --- a/ironfish-cli/src/commands/wallet/transactions/sign.ts +++ b/ironfish-cli/src/commands/wallet/transactions/sign.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 { initializeLedger } from '../../../utils/ledger' import { renderTransactionDetails, watchTransaction } from '../../../utils/transaction' export class TransactionsSignCommand extends IronfishCommand { @@ -109,16 +109,7 @@ export class TransactionsSignCommand extends IronfishCommand { } private async signWithLedger(client: RpcClient, unsignedTransaction: string) { - const ledger = new Ledger(this.logger) - try { - await ledger.connect() - } catch (e) { - if (e instanceof Error) { - this.error(e.message) - } else { - throw e - } - } + const ledger = await initializeLedger(false, this.logger) const signature = (await ledger.sign(unsignedTransaction)).toString('hex') diff --git a/ironfish-cli/src/utils/ledger.ts b/ironfish-cli/src/utils/ledger.ts index e54620560f..fc5b707e9d 100644 --- a/ironfish-cli/src/utils/ledger.ts +++ b/ironfish-cli/src/utils/ledger.ts @@ -1,7 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { AccountImport, createRootLogger, Logger } from '@ironfish/sdk' +import { + deserializePublicPackage, + deserializeRound2CombinedPublicPackage, +} from '@ironfish/rust-nodejs' +import { AccountImport, Logger, createRootLogger } from '@ironfish/sdk' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' import IronfishApp, { IronfishKeys, @@ -16,6 +20,21 @@ import IronfishApp, { } from '@zondax/ledger-ironfish' import { ResponseError } from '@zondax/ledger-js' +export const initializeLedger = async (dkg: boolean, logger?: Logger) => { + const ledger = new Ledger(logger) + try { + await ledger.connect(dkg) + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message) + } else { + throw e + } + } + + return ledger +} + export class Ledger { app: IronfishApp | undefined logger: Logger @@ -200,28 +219,77 @@ export class Ledger { dkgRound3 = async ( index: number, - participants: string[], - round1PublicPackages: string[], - round2PublicPackages: string[], + identity: string, + round1PublicPackagesStr: string[], + round2PublicPackagesStr: string[], round2SecretPackage: string, - gskBytes: string[], - ): Promise => { + ): Promise<{ + dkgKeys: { + publicAddress: string + viewKey: string + incomingViewKey: string + outgoingViewKey: string + proofAuthorizingKey: string + } + publicKeyPackage: Buffer + }> => { if (!this.app) { throw new Error('Connect to Ledger first') } + // 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) + this.logger.log('Please approve the request on your ledger device.') - return this.tryInstruction( + await this.tryInstruction( this.app.dkgRound3Min( index, participants, - round1PublicPackages, - round2PublicPackages, + round1FrostPackages, + round2FrostPackages, round2SecretPackage, gskBytes, ), ) + + // Retrieve all multisig account keys and publicKeyPackage + const dkgKeys = await this.dkgRetrieveKeys() + + const publicKeyPackage = await this.dkgGetPublicPackage() + + return { + dkgKeys, + publicKeyPackage, + } } dkgRetrieveKeys = async (): Promise<{