diff --git a/ironfish/src/rpc/clients/client.ts b/ironfish/src/rpc/clients/client.ts index 06e3760519..5eb7ab1589 100644 --- a/ironfish/src/rpc/clients/client.ts +++ b/ironfish/src/rpc/clients/client.ts @@ -59,6 +59,8 @@ import type { FollowChainStreamResponse, GetAccountIdentitiesRequest, GetAccountIdentitiesResponse, + GetAccountIdentityRequest, + GetAccountIdentityResponse, GetAccountNotesStreamRequest, GetAccountNotesStreamResponse, GetAccountsRequest, @@ -313,6 +315,15 @@ export abstract class RpcClient { ).waitForEnd() }, + getAccountIdentity: ( + params: GetAccountIdentityRequest, + ): Promise> => { + return this.request( + `${ApiNamespace.wallet}/multisig/getAccountIdentity`, + params, + ).waitForEnd() + }, + dkg: { round1: (params: DkgRound1Request): Promise> => { return this.request( diff --git a/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/getAccountIdentity.test.ts.fixture b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/getAccountIdentity.test.ts.fixture new file mode 100644 index 0000000000..a4402ce31e --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/__fixtures__/getAccountIdentity.test.ts.fixture @@ -0,0 +1,33 @@ +{ + "Route multisig/getAccountIdentity throws an error if the account is not a multisig account": [ + { + "value": { + "encrypted": false, + "version": 4, + "id": "7481f108-9a5f-442b-9281-383084698d23", + "name": "test", + "spendingKey": "f9b5b5e38073cf7429dff092832748e39a38e82736f6107ae80f1f6a3a20f1f0", + "viewKey": "945f43341b4b2ce2c18c48b146222bda226de0301db715b398f1285b552914941e732c78e4cb4296213bd71d85e75cf3142968d42e0dfdae0d2e4293a505f849", + "incomingViewKey": "5a102178f7479d00d4a5ae7e4c067e9e1e57fb2cc9a039cf92e4e6b43098a905", + "outgoingViewKey": "52b6c3b9716133406a5292585d34c8fb4f64e3f051662c7db76f4bd35681e922", + "publicAddress": "ec93a7a17f2991a3638804711c527eee8bec696d9fce1de7bb55e3c035cfbd6a", + "createdAt": { + "sequence": 1, + "hash": { + "type": "Buffer", + "data": "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "scanningEnabled": true, + "proofAuthorizingKey": "e738313bc11134cc4821ee7f8bd1dab37129f60c577c981421ee8099ace29801" + }, + "head": { + "hash": { + "type": "Buffer", + "data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY=" + }, + "sequence": 1 + } + } + ] +} \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.test.ts b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.test.ts new file mode 100644 index 0000000000..d24db21354 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.test.ts @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { useAccountFixture } from '../../../../testUtilities' +import { createRouteTest } from '../../../../testUtilities/routeTest' +import { ACCOUNT_SCHEMA_VERSION, AccountImport, JsonEncoder } from '../../../../wallet' + +describe('Route multisig/getAccountIdentity', () => { + const routeTest = createRouteTest() + + it('returns the identity belonging to an account', async () => { + const identity1 = await routeTest.client.wallet.multisig.createParticipant({ + name: 'identity1', + }) + const identity2 = await routeTest.client.wallet.multisig.createParticipant({ + name: 'identity2', + }) + + const participants = [ + { identity: identity1.content.identity }, + { identity: identity2.content.identity }, + ] + + const request = { minSigners: 2, participants } + const response = await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage( + request, + ) + + const importAccount = await routeTest.client.wallet.importAccount({ + account: response.content.participantAccounts[0].account, + }) + + const accountName = importAccount.content.name + + const accountIdentity = await routeTest.client.wallet.multisig.getAccountIdentity({ + account: accountName, + }) + + expect(accountIdentity.content.identity).toEqual(participants[0].identity) + }) + + it('throws an error for a coordinator account', async () => { + const identity1 = await routeTest.client.wallet.multisig.createParticipant({ + name: 'identity1', + }) + const identity2 = await routeTest.client.wallet.multisig.createParticipant({ + name: 'identity2', + }) + + const participants = [ + { identity: identity1.content.identity }, + { identity: identity2.content.identity }, + ] + + const request = { minSigners: 2, participants } + const response = await routeTest.client.wallet.multisig.createTrustedDealerKeyPackage( + request, + ) + + const account: AccountImport = { + name: 'coordinator', + version: ACCOUNT_SCHEMA_VERSION, + createdAt: null, + spendingKey: null, + viewKey: response.content.viewKey, + incomingViewKey: response.content.incomingViewKey, + outgoingViewKey: response.content.outgoingViewKey, + publicAddress: response.content.publicAddress, + proofAuthorizingKey: response.content.proofAuthorizingKey, + multisigKeys: { + publicKeyPackage: response.content.publicKeyPackage, + }, + } + + await routeTest.client.wallet.importAccount({ + account: new JsonEncoder().encode(account), + }) + + await expect( + routeTest.client.wallet.multisig.getAccountIdentity({ + account: 'coordinator', + }), + ).rejects.toThrow('does not have a multisig identity') + }) + + it('throws an error if the account is not a multisig account', async () => { + const account = await useAccountFixture(routeTest.wallet) + + await expect( + routeTest.client.wallet.multisig.getAccountIdentity({ + account: account.name, + }), + ).rejects.toThrow('is not a multisig account') + }) +}) diff --git a/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.ts b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.ts new file mode 100644 index 0000000000..53752eca67 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/multisig/getAccountIdentity.ts @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import * as yup from 'yup' +import { Assert } from '../../../../assert' +import { WithRequired } from '../../../../utils' +import { Account, AssertMultisig } from '../../../../wallet' +import { + MultisigHardwareSigner, + MultisigSigner, +} from '../../../../wallet/interfaces/multisigKeys' +import { ApiNamespace } from '../../namespaces' +import { routes } from '../../router' +import { AssertHasRpcContext } from '../../rpcContext' +import { getAccount } from '../utils' + +export type GetAccountIdentityRequest = { + account?: string +} + +export type GetAccountIdentityResponse = { + identity: string +} +export const GetAccountIdentityRequestSchema: yup.ObjectSchema = yup + .object({ + account: yup.string().optional(), + }) + .defined() + +export const GetAccountIdentityResponseSchema: yup.ObjectSchema = + yup + .object({ + identity: yup.string().defined(), + }) + .defined() + +routes.register( + `${ApiNamespace.wallet}/multisig/getAccountIdentity`, + GetAccountIdentityRequestSchema, + (request, context): void => { + AssertHasRpcContext(request, context, 'wallet') + + const account = getAccount(context.wallet, request.data.account) + AssertMultisigOwner(account) + + request.end({ identity: account.multisigKeys.identity }) + }, +) + +type MultisigOwnerAccount = WithRequired & { + multisigKeys: MultisigSigner | MultisigHardwareSigner +} + +function AssertMultisigOwner(account: Account): asserts account is MultisigOwnerAccount { + AssertMultisig(account) + Assert.isTrue( + 'identity' in account.multisigKeys, + `Account '${account.name}' does not have a multisig identity`, + ) +} diff --git a/ironfish/src/rpc/routes/wallet/multisig/index.ts b/ironfish/src/rpc/routes/wallet/multisig/index.ts index c141f997fd..b63d4cd74e 100644 --- a/ironfish/src/rpc/routes/wallet/multisig/index.ts +++ b/ironfish/src/rpc/routes/wallet/multisig/index.ts @@ -11,5 +11,6 @@ export * from './createParticipant' export * from './getIdentity' export * from './getIdentities' export * from './getAccountIdentities' +export * from './getAccountIdentity' export * from './importParticipant' export * from './dkg'