Skip to content

Commit 6062764

Browse files
authored
Merge pull request #5372 from iron-fish/rahul/account-import-multisig-identity
feat: Import multisig identity during account import
2 parents 5ef3368 + e0d08af commit 6062764

File tree

8 files changed

+194
-13
lines changed

8 files changed

+194
-13
lines changed

ironfish-cli/src/commands/wallet/import.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export class ImportCommand extends IronfishCommand {
116116
if (
117117
e instanceof RpcRequestError &&
118118
(e.code === RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME.toString() ||
119-
e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString())
119+
e.code === RPC_ERROR_CODES.IMPORT_ACCOUNT_NAME_REQUIRED.toString() ||
120+
e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString())
120121
) {
121122
const message = 'Enter a name for the account'
122123

@@ -125,6 +126,11 @@ export class ImportCommand extends IronfishCommand {
125126
this.log(e.codeMessage)
126127
}
127128

129+
if (e.code === RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME.toString()) {
130+
this.log()
131+
this.log(e.codeMessage)
132+
}
133+
128134
const name = await inputPrompt(message, true)
129135
if (name === flags.name) {
130136
this.error(`Entered the same name: '${name}'`)

ironfish/src/rpc/adapters/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum RPC_ERROR_CODES {
1212
UNAUTHENTICATED = 'unauthenticated',
1313
NOT_FOUND = 'not-found',
1414
DUPLICATE_ACCOUNT_NAME = 'duplicate-account-name',
15+
DUPLICATE_IDENTITY_NAME = 'duplicate-identity-name',
1516
IMPORT_ACCOUNT_NAME_REQUIRED = 'import-account-name-required',
1617
MULTISIG_SECRET_NOT_FOUND = 'multisig-secret-not-found',
1718
WALLET_ALREADY_DECRYPTED = 'wallet-already-decrypted',

ironfish/src/rpc/routes/wallet/__fixtures__/importAccount.test.ts.fixture

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66
"previousBlockHash": "4791D7AE9F97DF100EF1558E84772D6A09B43762388283F75C6F20A32A88AA86",
77
"noteCommitment": {
88
"type": "Buffer",
9-
"data": "base64:OrFh1gNPebpwyD0EOBJXhvWSWcNIqvOofpmIUdQuMxg="
9+
"data": "base64:0KDCZq74l7x/xBpk6pN4GKueDfxqDpBfVVbEBle3piE="
1010
},
1111
"transactionCommitment": {
1212
"type": "Buffer",
13-
"data": "base64:o67WawihP0SziWZCyKRf8E9IJLHEAKJRxtHegUciYYU="
13+
"data": "base64:V+JntG6Mh/CV2y6OUBo0XHM1SsQd2BuBUpVQjfd8UTY="
1414
},
1515
"target": "9282972777491357380673661573939192202192629606981189395159182914949423",
1616
"randomness": "0",
17-
"timestamp": 1718920385337,
17+
"timestamp": 1726270914810,
1818
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
1919
"noteSize": 4,
2020
"work": "0"
2121
},
2222
"transactions": [
2323
{
2424
"type": "Buffer",
25-
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAKVqg+LxkGFUdgVMSHhIIrp1oG0U1gg3WxwKonNL3I16NB/NqRoZxzc+mlx6IX3v2Ch+S+aHHTzXnVwJoMNsy5KGWbqGPvb1RP+fM/H6mmbCnik9BEtu8uYv12x4XjDqAohBRAPoUG0AjYAAhpQLSXnvQ7bF6grHEj+agByDw45kVAnz+nVhTFp3ODOCH4HGMqylTTG7329MZgUGXXOSIqWKCLYjIpACsiyEsz1Y/XP+54h9oHmUwm2ObkGOLFXb6D0PHxfKTEWg+jJNU6XVGUDY9BVt6SgYyWE+qA8ForJYDgxHly6kPsDy1WMYhAHpLpvSF4oyvwUoVa8qQDmVhtS8k/F7aRjCICP94DaPs/IvvesJkXI/gmp6mBsZyS/hxwQUNPi/OOqUZzDKvVthYcKwTIJcxt2W99bVKv2UnIc9+ay/WZ6m/Tm8zm0EO2lCqZXOer6eR7sQUStydjRgl8TYyHyl4htkK8gsS5hWQarJLIgGClFAYs+zicp/me4Pn8aK+8G1h4BuLApM+TJotvIzbISyMnkOB9c9x1H01T3txQYAeAgoLtLhNbI6Pz6VPNTes9GYWCiCH6FBudyyW+7ug95kJSU54xrOvdkBjHNX6bd17okg8Rklyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwDj/3ANmFakpfmgyAW39nTxSIjLTJION9L6HdYthgi2QyZ4ClO1gzlXZKRnMfu8E6JLAtC7M6ZEfe0m+JysV5Ag=="
25+
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAyk5bfvKdSTdwi+EqeXx2tuNHJaRs6/OyyTqqtfXiOpeCwrIXEHclFCfWxdYf26U+WL6RmJa2EXCOEU6DWMvSdRv9DjGNEdEs3psBNEoBsPyCxX3g2+2YZOL4q/khIPoKWnEYwtr4Y0LK4CZ2fXcBrwD6RGx/rx6XQI8KCln/DXoY8XogZd5TzPBkEgje6l7AnKSMYJSaj4NxsNHZImLJOcvJsJDKPYaupuA57kTbb9+2mXV4Ww2wX0AzPXgdbtML2WmWSDPhMqNTQ4fSEf+wej8Bdu19SfPHAVcnGM5IlmMnWxLmLpoWJSMqyLHyuIm7U+awFCB2Rnp/ctiDv5Pvw0zXsHnTD1m8FGTtDBXsCLSrcnkAScYq2k9TKSPoebA0pNNqXuK2yCCcuFwGMgno/kepNOjgIQC2NxnzhjxnVdJiw9tTgMgXqSAlXqWpSVP/px2gtax7dFibMtdv8ytLigKMfy9fAAl1L8IVrk2QclO4WHGelqhidGNNRCofwiL43lWwTeX3xFhwLetRp4coJ8xk/zu/IOiLxeL+Pgj3gCsMSgHzwWiLTjWVB0j7zm75Idrgbm+sgS8A6rEnPdlkHvW7aNLYrTHxigi7q6LdHC1m0tzkMPLTqUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwPTtFNObdUhDGdWBuYySM8KCwmFmBaUol9uIxYe/w3Ip9yh0GHfuULUqZHBBCfl/McTVbJl4h86Tb0qRJSoJABQ=="
2626
}
2727
]
2828
}

ironfish/src/rpc/routes/wallet/importAccount.test.ts

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import path from 'path'
77
import { createTrustedDealerKeyPackages, useMinerBlockFixture } from '../../../testUtilities'
88
import { createRouteTest } from '../../../testUtilities/routeTest'
99
import { JsonEncoder } from '../../../wallet'
10+
import { isMultisigSignerImport } from '../../../wallet/exporter'
1011
import { AccountFormat, encodeAccountImport } from '../../../wallet/exporter/account'
1112
import { AccountImport } from '../../../wallet/exporter/accountImport'
1213
import { Bech32Encoder } from '../../../wallet/exporter/encoders/bech32'
1314
import { Bech32JsonEncoder } from '../../../wallet/exporter/encoders/bech32json'
1415
import { encryptEncodedAccount } from '../../../wallet/exporter/encryption'
15-
import { RpcClient } from '../../clients'
16+
import { RPC_ERROR_CODES } from '../../adapters'
17+
import { RpcClient, RpcRequestError } from '../../clients'
1618

1719
describe('Route wallet/importAccount', () => {
1820
const routeTest = createRouteTest(true)
@@ -51,7 +53,7 @@ describe('Route wallet/importAccount', () => {
5153
})
5254

5355
it('should import a multisig account that has no spending key', async () => {
54-
const trustedDealerPackages = createTrustedDealerKeyPackages()
56+
const { dealer: trustedDealerPackages } = createTrustedDealerKeyPackages()
5557

5658
const account: AccountImport = {
5759
version: 1,
@@ -412,4 +414,117 @@ describe('Route wallet/importAccount', () => {
412414
const accountHead = await account?.getHead()
413415
expect(accountHead?.sequence).toEqual(createdAtSequence - 1)
414416
})
417+
418+
it('should not import account with duplicate name', async () => {
419+
const name = 'duplicateNameTest'
420+
const spendingKey = generateKey().spendingKey
421+
422+
await routeTest.client.wallet.importAccount({
423+
account: spendingKey,
424+
name,
425+
rescan: false,
426+
})
427+
428+
try {
429+
await routeTest.client.wallet.importAccount({
430+
account: spendingKey,
431+
name,
432+
rescan: false,
433+
})
434+
} catch (e: unknown) {
435+
if (!(e instanceof RpcRequestError)) {
436+
throw e
437+
}
438+
expect(e.status).toBe(400)
439+
expect(e.code).toBe(RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME)
440+
}
441+
442+
expect.assertions(2)
443+
})
444+
445+
it('should not import multisig account with duplicate identity name', async () => {
446+
const name = 'duplicateIdentityNameTest'
447+
448+
const {
449+
dealer: trustedDealerPackages,
450+
secrets,
451+
identities,
452+
} = createTrustedDealerKeyPackages()
453+
454+
await routeTest.node.wallet.walletDb.putMultisigIdentity(
455+
Buffer.from(identities[0], 'hex'),
456+
{
457+
secret: secrets[0].serialize(),
458+
name,
459+
},
460+
)
461+
462+
const indentityCountBefore = (await routeTest.client.wallet.multisig.getIdentities())
463+
.content.identities.length
464+
465+
const account: AccountImport = {
466+
version: 1,
467+
name,
468+
viewKey: trustedDealerPackages.viewKey,
469+
incomingViewKey: trustedDealerPackages.incomingViewKey,
470+
outgoingViewKey: trustedDealerPackages.outgoingViewKey,
471+
publicAddress: trustedDealerPackages.publicAddress,
472+
spendingKey: null,
473+
createdAt: null,
474+
proofAuthorizingKey: trustedDealerPackages.proofAuthorizingKey,
475+
multisigKeys: {
476+
publicKeyPackage: trustedDealerPackages.publicKeyPackage,
477+
keyPackage: trustedDealerPackages.keyPackages[1].keyPackage.toString(),
478+
secret: secrets[1].serialize().toString('hex'),
479+
},
480+
}
481+
482+
try {
483+
await routeTest.client.wallet.importAccount({
484+
account: new JsonEncoder().encode(account),
485+
name,
486+
rescan: false,
487+
})
488+
} catch (e: unknown) {
489+
if (!(e instanceof RpcRequestError)) {
490+
throw e
491+
}
492+
493+
/**
494+
* These assertions ensures that we cannot import multiple identities with the same name.
495+
* This is done by creating an identity, storing it and attempting to import another identity but give it the same name.
496+
*/
497+
expect(e.status).toBe(400)
498+
expect(e.code).toBe(RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME)
499+
}
500+
501+
if (account.multisigKeys && isMultisigSignerImport(account.multisigKeys)) {
502+
account.multisigKeys.secret = secrets[0].serialize().toString('hex')
503+
} else {
504+
throw new Error('Invalid multisig keys')
505+
}
506+
507+
const response = await routeTest.client.wallet.importAccount({
508+
account: new JsonEncoder().encode(account),
509+
name: 'account2',
510+
rescan: false,
511+
})
512+
513+
expect(response.status).toBe(200)
514+
expect(response.content.name).toEqual('account2')
515+
516+
const identitiesAfter = (await routeTest.client.wallet.multisig.getIdentities()).content
517+
.identities
518+
const newIdentity = identitiesAfter.find((identity) => identity.name === name)
519+
520+
/**
521+
* These assertions ensure that if we try to import an identity with the same secret but different name, it will pass.
522+
* However, the identity name will remain the same as the original identity that was imported first.
523+
*/
524+
expect(identitiesAfter.length).toBe(indentityCountBefore)
525+
expect(newIdentity).toBeDefined()
526+
expect(newIdentity?.name).toBe(name)
527+
528+
expect.assertions(7)
529+
})
415530
})

ironfish/src/rpc/routes/wallet/importAccount.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44
import * as yup from 'yup'
55
import { DecodeInvalidName } from '../../../wallet'
6-
import { DuplicateAccountNameError } from '../../../wallet/errors'
6+
import { DuplicateAccountNameError, DuplicateIdentityNameError } from '../../../wallet/errors'
77
import { decodeAccountImport } from '../../../wallet/exporter/account'
88
import { decryptEncodedAccount } from '../../../wallet/exporter/encryption'
99
import { RPC_ERROR_CODES, RpcValidationError } from '../../adapters'
@@ -70,6 +70,9 @@ routes.register<typeof ImportAccountRequestSchema, ImportResponse>(
7070
isDefaultAccount,
7171
})
7272
} catch (e) {
73+
if (e instanceof DuplicateIdentityNameError) {
74+
throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_IDENTITY_NAME)
75+
}
7376
if (e instanceof DuplicateAccountNameError) {
7477
throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME)
7578
} else if (e instanceof DecodeInvalidName) {

ironfish/src/testUtilities/keys.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@ import { multisig } from '@ironfish/rust-nodejs'
66
export function createTrustedDealerKeyPackages(
77
minSigners: number = 2,
88
maxSigners: number = 2,
9-
): multisig.TrustedDealerKeyPackages {
10-
const identities = Array.from({ length: maxSigners }, () =>
11-
multisig.ParticipantSecret.random().toIdentity().serialize().toString('hex'),
12-
)
13-
return multisig.generateAndSplitKey(minSigners, identities)
9+
): {
10+
dealer: multisig.TrustedDealerKeyPackages
11+
identities: string[]
12+
secrets: multisig.ParticipantSecret[]
13+
} {
14+
const secrets = Array.from({ length: maxSigners }, () => multisig.ParticipantSecret.random())
15+
16+
const identities = secrets.map((secret) => {
17+
return secret.toIdentity().serialize().toString('hex')
18+
})
19+
20+
const dealer = multisig.generateAndSplitKey(minSigners, identities)
21+
22+
return {
23+
dealer,
24+
identities,
25+
secrets,
26+
}
1427
}

ironfish/src/wallet/errors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ export class DuplicateAccountNameError extends Error {
4343
}
4444
}
4545

46+
export class DuplicateIdentityNameError extends Error {
47+
name = this.constructor.name
48+
49+
constructor(name: string) {
50+
super()
51+
this.message = `Multisig identity already exists with the name ${name}`
52+
}
53+
}
54+
4655
export class DuplicateSpendingKeyError extends Error {
4756
name = this.constructor.name
4857

ironfish/src/wallet/wallet.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ import { EncryptedAccount } from './account/encryptedAccount'
4646
import { AssetBalances } from './assetBalances'
4747
import {
4848
DuplicateAccountNameError,
49+
DuplicateIdentityNameError,
4950
DuplicateMultisigSecretNameError,
5051
DuplicateSpendingKeyError,
5152
MaxMemoLengthError,
5253
NotEnoughFundsError,
5354
} from './errors'
55+
import { isMultisigSignerImport } from './exporter'
5456
import { AccountImport, validateAccountImport } from './exporter/accountImport'
5557
import { isMultisigSignerTrustedDealerImport } from './exporter/multisig'
5658
import { MintAssetOptions } from './interfaces/mintAssetOptions'
@@ -1415,6 +1417,7 @@ export class Wallet {
14151417
options?: { createdAt?: number; passphrase?: string },
14161418
): Promise<Account> {
14171419
let multisigKeys = accountValue.multisigKeys
1420+
let secret: Buffer | undefined
14181421
const name = accountValue.name
14191422

14201423
if (
@@ -1433,6 +1436,11 @@ export class Wallet {
14331436
publicKeyPackage: accountValue.multisigKeys.publicKeyPackage,
14341437
secret: multisigIdentity.secret.toString('hex'),
14351438
}
1439+
secret = multisigIdentity.secret
1440+
}
1441+
1442+
if (accountValue.multisigKeys && isMultisigSignerImport(accountValue.multisigKeys)) {
1443+
secret = Buffer.from(accountValue.multisigKeys.secret, 'hex')
14361444
}
14371445

14381446
if (name && this.getAccountByName(name)) {
@@ -1492,6 +1500,32 @@ export class Wallet {
14921500
await this.walletDb.setAccount(account, tx)
14931501
}
14941502

1503+
if (secret) {
1504+
const identitySerialized = new multisig.ParticipantSecret(secret)
1505+
.toIdentity()
1506+
.serialize()
1507+
const multisigIdentity = await this.walletDb.getMultisigIdentity(identitySerialized, tx)
1508+
1509+
if (!multisigIdentity) {
1510+
const duplicateSecret = await this.walletDb.getMultisigSecretByName(
1511+
accountValue.name,
1512+
tx,
1513+
)
1514+
if (duplicateSecret) {
1515+
throw new DuplicateIdentityNameError(accountValue.name)
1516+
}
1517+
1518+
await this.walletDb.putMultisigIdentity(
1519+
identitySerialized,
1520+
{
1521+
name: account.name,
1522+
secret,
1523+
},
1524+
tx,
1525+
)
1526+
}
1527+
}
1528+
14951529
if (createdAt !== null) {
14961530
const previousBlock = await this.chainGetBlock({ sequence: createdAt.sequence - 1 })
14971531

0 commit comments

Comments
 (0)