-
Notifications
You must be signed in to change notification settings - Fork 574
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Implemented the multisig:dkg:create cli command to facilitate the creation of multisignature accounts using Distributed Key Generation (DKG). - Added methods for participant management, and DKG rounds (1, 2, and 3) with user prompts for input. - Integrated error handling and retry logic for every step. - Enhanced user experience with informative logging and prompts throughout the process.
- Loading branch information
Showing
3 changed files
with
242 additions
and
2 deletions.
There are no files selected for viewing
235 changes: 235 additions & 0 deletions
235
ironfish-cli/src/commands/wallet/multisig/dkg/create.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
/* 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 { RPC_ERROR_CODES, RpcClient, RpcRequestError } from '@ironfish/sdk' | ||
import { Flags } from '@oclif/core' | ||
import { IronfishCommand } from '../../../../command' | ||
import { RemoteFlags } from '../../../../flags' | ||
import * as ui from '../../../../ui' | ||
|
||
export class DkgCreateCommand extends IronfishCommand { | ||
static description = 'Interactive command to create a multisignature account using DKG' | ||
|
||
static flags = { | ||
...RemoteFlags, | ||
participantName: Flags.string({ | ||
char: 'n', | ||
description: 'The name of the participant to use for DKG', | ||
}), | ||
accountName: Flags.string({ | ||
char: 'a', | ||
description: 'The name to set for the imported account', | ||
}), | ||
} | ||
|
||
async start(): Promise<void> { | ||
const { flags } = await this.parse(DkgCreateCommand) | ||
const client = await this.connectRpc() | ||
await ui.checkWalletUnlocked(client) | ||
|
||
const participantName = flags.participantName | ||
const accountName = flags.accountName | ||
|
||
const { name, identity } = await this.retryStep('Get or create participant', async () => { | ||
return this.getOrCreateParticipant(client, participantName) | ||
}) | ||
|
||
const { round1Result } = await this.retryStep( | ||
'Collect participant info and perform Round 1', | ||
async () => { | ||
return this.performRound1(client, name, identity) | ||
}, | ||
) | ||
|
||
const { round2Result, round1PublicPackages } = await this.retryStep( | ||
'Collect Round 1 public packages and perform Round 2', | ||
async () => { | ||
return this.performRound2(client, name, round1Result) | ||
}, | ||
) | ||
|
||
await this.retryStep('Perform Round 3', async () => { | ||
await this.performRound3(client, name, accountName, round2Result, round1PublicPackages) | ||
}) | ||
|
||
this.log('DKG process completed successfully!') | ||
} | ||
|
||
private async retryStep<T>(stepName: string, stepFunction: () => Promise<T>): Promise<T> { | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
try { | ||
this.log(`\nStarting step: ${stepName}`) | ||
const result = await stepFunction() | ||
this.log(`Step completed: ${stepName}`) | ||
return result | ||
} catch (error) { | ||
this.logger.log(`An error occurred during ${stepName}: ${(error as Error).message}`) | ||
await ui.confirmOrQuit('Do you want to retry this step?') | ||
} | ||
} | ||
} | ||
|
||
async getOrCreateParticipant( | ||
client: RpcClient, | ||
name?: string, | ||
): Promise<{ name: string; identity: string }> { | ||
if (!name) { | ||
name = await ui.inputPrompt('Enter a name for your participant identity', true) | ||
} | ||
|
||
let identity: string | ||
|
||
try { | ||
identity = (await client.wallet.multisig.getIdentity({ name })).content.identity | ||
this.log(`Using existing participant: ${name}`) | ||
} catch (error) { | ||
if ( | ||
error instanceof RpcRequestError && | ||
error.code === RPC_ERROR_CODES.IDENTITY_NOT_FOUND.toString() | ||
) { | ||
this.log(`Creating new participant: ${name}`) | ||
identity = (await client.wallet.multisig.createParticipant({ name })).content.identity | ||
} else { | ||
throw error | ||
} | ||
} | ||
|
||
this.log(`Participant identity: ${identity} for ${name}`) | ||
|
||
return { name, identity } | ||
} | ||
|
||
async performRound1( | ||
client: RpcClient, | ||
participantName: string, | ||
currentIdentity: string, | ||
): Promise<{ round1Result: { secretPackage: string; publicPackage: string } }> { | ||
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') | ||
} | ||
|
||
input = await ui.longPrompt( | ||
'Enter the identities of all other participants (excluding yours), separated by commas', | ||
{ required: true }, | ||
) | ||
const inputIdentities = input.split(',').map((i) => i.trim()) | ||
|
||
if (inputIdentities.length !== totalParticipants - 1) { | ||
throw new Error( | ||
`Number of input identities must be ${totalParticipants - 1}, ${ | ||
inputIdentities.length | ||
} provided`, | ||
) | ||
} | ||
|
||
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') | ||
} | ||
|
||
const identities = [...inputIdentities, currentIdentity] | ||
|
||
this.log('\nPerforming DKG Round 1...') | ||
const response = await client.wallet.multisig.dkg.round1({ | ||
participantName, | ||
participants: identities.map((identity) => ({ identity })), | ||
minSigners, | ||
}) | ||
|
||
this.log('\nRound 1 Encrypted Secret Package:') | ||
this.log(response.content.round1SecretPackage) | ||
|
||
this.log('\nRound 1 Public Package:') | ||
this.log(response.content.round1PublicPackage) | ||
|
||
this.log('\nShare your Round 1 Public Package with other participants.') | ||
return { | ||
round1Result: { | ||
secretPackage: response.content.round1SecretPackage, | ||
publicPackage: response.content.round1PublicPackage, | ||
}, | ||
} | ||
} | ||
|
||
async performRound2( | ||
client: RpcClient, | ||
participantName: string, | ||
round1Result: { secretPackage: string; publicPackage: string }, | ||
): Promise<{ | ||
round2Result: { secretPackage: string; publicPackage: string } | ||
round1PublicPackages: string[] | ||
}> { | ||
this.log('\nCollecting Round 1 Public Packages...') | ||
const input = await ui.longPrompt( | ||
"Enter all other participants' (excluding yours) Round 1 Public Packages, separated by commas", | ||
{ required: true }, | ||
) | ||
const otherPackages = input.split(',').map((p) => p.trim()) | ||
const round1PublicPackages = [...otherPackages, round1Result.publicPackage] | ||
|
||
this.log('\nPerforming DKG Round 2...') | ||
|
||
const response = await client.wallet.multisig.dkg.round2({ | ||
participantName, | ||
round1SecretPackage: round1Result.secretPackage, | ||
round1PublicPackages, | ||
}) | ||
|
||
this.log('\nRound 2 Encrypted Secret Package:') | ||
this.log(response.content.round2SecretPackage) | ||
|
||
this.log('\nRound 2 Public Package:') | ||
this.log(response.content.round2PublicPackage) | ||
|
||
this.log('\nShare your Round 2 Public Package with other participants.') | ||
|
||
return { | ||
round2Result: { | ||
secretPackage: response.content.round2SecretPackage, | ||
publicPackage: response.content.round2PublicPackage, | ||
}, | ||
round1PublicPackages, | ||
} | ||
} | ||
|
||
async performRound3( | ||
client: RpcClient, | ||
participantName: string, | ||
accountName: string | undefined, | ||
round2Result: { secretPackage: string; publicPackage: string }, | ||
round1PublicPackages: string[], | ||
): Promise<void> { | ||
this.log('\nPerforming DKG Round 3...') | ||
|
||
if (!accountName) { | ||
accountName = await ui.inputPrompt('Enter a name for the multisig account', true) | ||
} | ||
|
||
const round2PublicPackagesInput = await ui.longPrompt( | ||
'Enter all Round 2 Public Packages (excluding yours), separated by commas', | ||
{ required: true }, | ||
) | ||
|
||
const round2PublicPackages = round2PublicPackagesInput.split(',').map((p) => p.trim()) | ||
round2PublicPackages.push(round2Result.publicPackage) | ||
|
||
const response = await client.wallet.multisig.dkg.round3({ | ||
participantName, | ||
accountName, | ||
round2SecretPackage: round2Result.secretPackage, | ||
round1PublicPackages, | ||
round2PublicPackages, | ||
}) | ||
|
||
this.log('\nMultisig account created successfully!') | ||
this.log(`Account Name: ${response.content.name}`) | ||
this.log(`Public Address: ${response.content.publicAddress}`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters