diff --git a/packages/stellar-wallet-snap/CHANGELOG.md b/packages/stellar-wallet-snap/CHANGELOG.md index 47bd0888..4dab92bd 100644 --- a/packages/stellar-wallet-snap/CHANGELOG.md +++ b/packages/stellar-wallet-snap/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `signProofOfOwnershipBatch` for signing multiple proof-of-ownership messages in one request. ([#267](https://github.com/MetaMask/internal-snaps/pull/267)) - Add `signProofOfOwnership` client request for silent proof-of-ownership signing (SEP-0053) ([#186](https://github.com/MetaMask/internal-snaps/pull/186)) - Add `exportAccount` keyring method for base32 Stellar secret-seed export ([#187](https://github.com/MetaMask/internal-snaps/pull/187)) - Add `TrustlineExceedLimitException` for send simulation when a payment would exceed the destination trustline limit (previously a generic `TransactionValidationException`) ([#185](https://github.com/MetaMask/internal-snaps/pull/185)) diff --git a/packages/stellar-wallet-snap/snap.manifest.json b/packages/stellar-wallet-snap/snap.manifest.json index 81e9fa82..92c996e2 100644 --- a/packages/stellar-wallet-snap/snap.manifest.json +++ b/packages/stellar-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "df2jwrVqYLs741TWeyLYKABatgBkFCEDWJdwpnNwiXw=", + "shasum": "SHnUG47ZILT1CsFbGHOFPRyGwRTlXQwuuMiX/U4pjMs=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/stellar-wallet-snap/src/context.ts b/packages/stellar-wallet-snap/src/context.ts index d29ec967..74da094f 100644 --- a/packages/stellar-wallet-snap/src/context.ts +++ b/packages/stellar-wallet-snap/src/context.ts @@ -15,6 +15,7 @@ import { OnAddressInputHandler } from './handlers/clientRequest/onAddressInput'; import { OnAmountInputHandler } from './handlers/clientRequest/onAmountInput'; import { SignAndSendTransactionHandler } from './handlers/clientRequest/signAndSendTransaction'; import { SignProofOfOwnershipHandler } from './handlers/clientRequest/signProofOfOwnership'; +import { SignProofOfOwnershipBatchHandler } from './handlers/clientRequest/signProofOfOwnershipBatch'; import type { ICronjobRequestHandler } from './handlers/cronjob/api'; import { BackgroundEventMethod } from './handlers/cronjob/api'; import { @@ -290,6 +291,12 @@ const signProofOfOwnershipHandler = new SignProofOfOwnershipHandler({ accountResolver, }); +const signProofOfOwnershipBatchHandler = new SignProofOfOwnershipBatchHandler({ + logger, + accountService, + walletService, +}); + const clientRequestMethodHandlers: Record< ClientRequestMethod, IClientRequestHandler @@ -301,6 +308,8 @@ const clientRequestMethodHandlers: Record< [ClientRequestMethod.SignAndSendTransaction]: signAndSendTransactionHandler, [ClientRequestMethod.ComputeFee]: computeFeeHandler, [ClientRequestMethod.SignProofOfOwnership]: signProofOfOwnershipHandler, + [ClientRequestMethod.SignProofOfOwnershipBatch]: + signProofOfOwnershipBatchHandler, }; const clientRequestHandler = new ClientRequestHandler({ diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/api.test.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/api.test.ts index b04c0f69..85176167 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/api.test.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/api.test.ts @@ -21,6 +21,8 @@ import { ConfirmSendJsonRpcResponseStruct, SignAndSendTransactionJsonRpcRequestStruct, SignAndSendTransactionJsonRpcResponseStruct, + SignProofOfOwnershipBatchJsonRpcRequestStruct, + SignProofOfOwnershipBatchJsonRpcResponseStruct, SignProofOfOwnershipJsonRpcRequestStruct, SignProofOfOwnershipJsonRpcResponseStruct, } from './api'; @@ -1072,3 +1074,90 @@ describe('SignProofOfOwnershipJsonRpcResponseStruct', () => { ).toThrow(StructError); }); }); + +describe('SignProofOfOwnershipBatchJsonRpcRequestStruct', () => { + const nonce = 'a1b2c3d4e5f6789012345678'; + + it('accepts a valid signProofOfOwnershipBatch request', () => { + expect(() => + assert( + { + jsonrpc: '2.0', + id: 1, + method: ClientRequestMethod.SignProofOfOwnershipBatch, + params: { + items: [ + { + accountId, + message: `metamask:proof-of-ownership:${nonce}:${stellarAddress}`, + }, + ], + }, + }, + SignProofOfOwnershipBatchJsonRpcRequestStruct, + ), + ).not.toThrow(); + }); + + it.each([ + { + method: ClientRequestMethod.SignProofOfOwnership, + params: { items: [] }, + }, + { + method: ClientRequestMethod.SignProofOfOwnershipBatch, + params: {}, + }, + { + method: ClientRequestMethod.SignProofOfOwnershipBatch, + params: { items: [{ accountId }] }, + }, + { + method: ClientRequestMethod.SignProofOfOwnershipBatch, + params: { items: [{ accountId: 'not-a-uuid', message: 'message' }] }, + }, + ])( + 'rejects an invalid signProofOfOwnershipBatch request', + ({ method, params }) => { + expect(() => + assert( + { jsonrpc: '2.0', id: 1, method, params }, + SignProofOfOwnershipBatchJsonRpcRequestStruct, + ), + ).toThrow(StructError); + }, + ); +}); + +describe('SignProofOfOwnershipBatchJsonRpcResponseStruct', () => { + it('accepts per-item success and error results', () => { + expect(() => + assert( + { + results: [ + { + accountId, + signature: `0x${'ab'.repeat(64)}`, + }, + { + accountId: '22222222-2222-4222-8222-222222222222', + error: 'Account not found', + }, + ], + }, + SignProofOfOwnershipBatchJsonRpcResponseStruct, + ), + ).not.toThrow(); + }); + + it.each([ + {}, + { results: [{ accountId, signature: 'not-a-signature' }] }, + { results: [{ accountId, error: 123 }] }, + { results: [{ accountId: 'not-a-uuid', error: 'bad id' }] }, + ])('rejects an invalid signProofOfOwnershipBatch response', (response) => { + expect(() => + assert(response, SignProofOfOwnershipBatchJsonRpcResponseStruct), + ).toThrow(StructError); + }); +}); diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/api.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/api.ts index 63733454..569ef888 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/api.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/api.ts @@ -1,5 +1,9 @@ import { AssetStruct, FeeType } from '@metamask/keyring-api'; -import { UuidStruct } from '@metamask/snap-networks-utils'; +import { + ProofOfOwnershipBatchErrorStruct as SignProofOfOwnershipBatchErrorStruct, + ProofOfOwnershipBatchRequestParamsStruct, + UuidStruct, +} from '@metamask/snap-networks-utils'; import type { Infer } from '@metamask/superstruct'; import { enums, @@ -39,6 +43,19 @@ import { import { isSep41Id } from '../../utils'; import { parseProofOfOwnershipMessage } from './utils'; +/** + * Validation struct for one signProofOfOwnershipBatch request item. + * + * Messages are validated inside the handler so invalid proof messages can be + * returned as per-item errors instead of rejecting the whole batch. + */ +export { ProofOfOwnershipBatchRequestItemStruct as SignProofOfOwnershipBatchJsonRpcRequestItemStruct } from '@metamask/snap-networks-utils'; + +/** + * Validation struct for one failed signProofOfOwnershipBatch result. + */ +export { ProofOfOwnershipBatchErrorStruct as SignProofOfOwnershipBatchErrorStruct } from '@metamask/snap-networks-utils'; + /** * Enum for the client request method. */ @@ -55,6 +72,11 @@ export const ClientRequestMethod = { * SIP-31 client-only. */ SignProofOfOwnership: 'signProofOfOwnership', + /** + * Silent batch proof-of-ownership signing for + * `@metamask/profile-metrics-controller`. SIP-31 client-only. + */ + SignProofOfOwnershipBatch: 'signProofOfOwnershipBatch', /** -------------------------------- Stellar Specific -------------------------------- */ ChangeTrustOpt: 'changeTrustOpt', } as const; @@ -409,6 +431,12 @@ export const ProofOfOwnershipMessageStruct = refine( }, ); +/** + * Validation struct for a 64-byte value encoded as lowercase hex with a leading + * `0x` prefix. + */ +export const SixtyFourByte0xHexStruct = pattern(string(), /^0x[0-9a-f]{128}$/u); + /** * Validation struct for the signProofOfOwnership JSON-RPC request. * Coerces `nonce` and `address` from `message` (clients send only accountId + message). @@ -453,7 +481,41 @@ export const SignProofOfOwnershipJsonRpcRequestStruct = coerce( * The `0x` prefix is not part of the 64-byte signature length. */ export const SignProofOfOwnershipJsonRpcResponseStruct = object({ - signature: pattern(string(), /^0x[0-9a-f]{128}$/u), + signature: SixtyFourByte0xHexStruct, +}); + +/** + * Validation struct for the signProofOfOwnershipBatch JSON-RPC request. + */ +export const SignProofOfOwnershipBatchJsonRpcRequestStruct = assign( + JsonRpcRequestStruct, + object({ + method: literal(ClientRequestMethod.SignProofOfOwnershipBatch), + params: ProofOfOwnershipBatchRequestParamsStruct, + }), +); + +/** + * Validation struct for one successful signProofOfOwnershipBatch result. + */ +export const SignProofOfOwnershipBatchSuccessStruct = object({ + accountId: UuidStruct, + signature: SixtyFourByte0xHexStruct, +}); + +/** + * Validation struct for one signProofOfOwnershipBatch result. + */ +export const SignProofOfOwnershipBatchItemResponseStruct = union([ + SignProofOfOwnershipBatchSuccessStruct, + SignProofOfOwnershipBatchErrorStruct, +]); + +/** + * Validation struct for the signProofOfOwnershipBatch JSON-RPC response. + */ +export const SignProofOfOwnershipBatchJsonRpcResponseStruct = object({ + results: array(SignProofOfOwnershipBatchItemResponseStruct), }); /** @@ -561,3 +623,17 @@ export type SignProofOfOwnershipJsonRpcRequest = Infer< export type SignProofOfOwnershipJsonRpcResponse = Infer< typeof SignProofOfOwnershipJsonRpcResponseStruct >; + +/** + * Type for the signProofOfOwnershipBatch JSON-RPC request. + */ +export type SignProofOfOwnershipBatchJsonRpcRequest = Infer< + typeof SignProofOfOwnershipBatchJsonRpcRequestStruct +>; + +/** + * Type for the signProofOfOwnershipBatch JSON-RPC response. + */ +export type SignProofOfOwnershipBatchJsonRpcResponse = Infer< + typeof SignProofOfOwnershipBatchJsonRpcResponseStruct +>; diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/index.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/index.ts index 2f949a4c..d7d72e28 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/index.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/index.ts @@ -1,4 +1,5 @@ export * from './changeTrustOpt'; export * from './clientRequest'; export * from './api'; +export * from './signProofOfOwnershipBatch'; export type { IClientRequestHandler } from './base'; diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.test.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.test.ts new file mode 100644 index 00000000..fd55bf9a --- /dev/null +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.test.ts @@ -0,0 +1,216 @@ +import type { JsonRpcRequest } from '@metamask/utils'; + +import { AccountService } from '../../services/account'; +import type { StellarKeyringAccount } from '../../services/account'; +import { generateStellarKeyringAccount } from '../../services/account/__mocks__/account.fixtures'; +import { mockOnChainAccountService } from '../../services/on-chain-account/__mocks__/onChainAccount.fixtures'; +import { WalletService } from '../../services/wallet'; +import { getTestWallet } from '../../services/wallet/__mocks__/wallet.fixtures'; +import { logger } from '../../utils/logger'; +import { ClientRequestMethod } from './api'; +import { SignProofOfOwnershipBatchHandler } from './signProofOfOwnershipBatch'; + +jest.mock('../../utils/logger'); + +describe('SignProofOfOwnershipBatchHandler', () => { + const accountId1 = '11111111-1111-4111-8111-111111111111'; + const accountId2 = '22222222-2222-4222-8222-222222222222'; + const missingAccountId = '33333333-3333-4333-8333-333333333333'; + const nonce = 'a1b2c3d4e5f6789012345678'; + + type SetupResult = { + handler: SignProofOfOwnershipBatchHandler; + account1: StellarKeyringAccount; + account2: StellarKeyringAccount; + wallet1: ReturnType; + wallet2: ReturnType; + findByIdsSpy: jest.SpyInstance; + getWalletResolverSpy: jest.SpyInstance; + walletResolver: jest.Mock; + buildProofMessage: (proofAddress: string, proofNonce?: string) => string; + createRequest: ( + items: { accountId: string; message: string }[], + ) => JsonRpcRequest; + }; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function setup(): SetupResult { + const wallet1 = getTestWallet(); + const wallet2 = getTestWallet(); + const account1 = generateStellarKeyringAccount( + accountId1, + wallet1.address, + 'entropy-source-1', + 0, + ); + const account2 = generateStellarKeyringAccount( + accountId2, + wallet2.address, + 'entropy-source-1', + 1, + ); + + const { accountService, walletService } = mockOnChainAccountService(); + const findByIdsSpy = jest + .spyOn(AccountService.prototype, 'findByIds') + .mockResolvedValue([account2, account1]); + const walletResolver = jest + .fn() + .mockResolvedValueOnce(wallet1) + .mockResolvedValueOnce(wallet2); + const getWalletResolverSpy = jest + .spyOn(WalletService.prototype, 'getWalletResolver') + .mockResolvedValue(walletResolver); + + const handler = new SignProofOfOwnershipBatchHandler({ + logger, + accountService, + walletService, + }); + + const buildProofMessage = ( + proofAddress: string, + proofNonce: string = nonce, + ): string => `metamask:proof-of-ownership:${proofNonce}:${proofAddress}`; + + const createRequest = ( + items: { accountId: string; message: string }[], + ): JsonRpcRequest => ({ + jsonrpc: '2.0', + id: 1, + method: ClientRequestMethod.SignProofOfOwnershipBatch, + params: { items }, + }); + + return { + handler, + account1, + account2, + wallet1, + wallet2, + findByIdsSpy, + getWalletResolverSpy, + walletResolver, + buildProofMessage, + createRequest, + }; + } + + it('signs proof messages and returns results in input order', async () => { + const { + handler, + account1, + account2, + wallet1, + wallet2, + findByIdsSpy, + getWalletResolverSpy, + walletResolver, + buildProofMessage, + createRequest, + } = setup(); + const message1 = buildProofMessage(wallet1.address); + const message2 = buildProofMessage(wallet2.address); + + const result = await handler.handle( + createRequest([ + { accountId: account1.id, message: message1 }, + { accountId: account2.id, message: message2 }, + ]), + ); + + expect(findByIdsSpy).toHaveBeenCalledWith([account1.id, account2.id]); + expect(getWalletResolverSpy).toHaveBeenCalledTimes(1); + expect(getWalletResolverSpy).toHaveBeenCalledWith('entropy-source-1'); + expect(walletResolver).toHaveBeenCalledWith(0); + expect(walletResolver).toHaveBeenCalledWith(1); + expect(result).toStrictEqual({ + results: [ + { + accountId: account1.id, + signature: `0x${wallet1.signMessage(message1, 'hex')}`, + }, + { + accountId: account2.id, + signature: `0x${wallet2.signMessage(message2, 'hex')}`, + }, + ], + }); + }); + + it('returns item-level errors for missing accounts and address mismatches', async () => { + const { + handler, + account1, + wallet1, + wallet2, + buildProofMessage, + createRequest, + walletResolver, + } = setup(); + const validMessage = buildProofMessage(wallet1.address); + const mismatchedMessage = buildProofMessage(wallet2.address); + + const result = await handler.handle( + createRequest([ + { accountId: account1.id, message: validMessage }, + { accountId: missingAccountId, message: validMessage }, + { accountId: account1.id, message: mismatchedMessage }, + ]), + ); + + expect(walletResolver).toHaveBeenCalledTimes(1); + expect(result).toStrictEqual({ + results: [ + { + accountId: account1.id, + signature: `0x${wallet1.signMessage(validMessage, 'hex')}`, + }, + { + accountId: missingAccountId, + error: `Account not found: ${missingAccountId}`, + }, + { + accountId: account1.id, + error: `Address in proof-of-ownership message (${wallet2.address}) does not match signing account address (${wallet1.address})`, + }, + ], + }); + }); + + it('returns item-level errors for invalid messages and wallet resolver failures', async () => { + const { + handler, + account1, + wallet1, + getWalletResolverSpy, + buildProofMessage, + createRequest, + } = setup(); + const validMessage = buildProofMessage(wallet1.address); + getWalletResolverSpy.mockRejectedValueOnce(new Error('resolver failed')); + + const result = await handler.handle( + createRequest([ + { accountId: account1.id, message: 'not-a-proof-message' }, + { accountId: account1.id, message: validMessage }, + ]), + ); + + expect(result).toStrictEqual({ + results: [ + { + accountId: account1.id, + error: 'Message must start with "metamask:proof-of-ownership:"', + }, + { + accountId: account1.id, + error: 'resolver failed', + }, + ], + }); + }); +}); diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.ts new file mode 100644 index 00000000..9569fd68 --- /dev/null +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/signProofOfOwnershipBatch.ts @@ -0,0 +1,211 @@ +import { normalizeError } from '@metamask/snap-networks-utils'; +import type { Logger } from '@metamask/snap-networks-utils'; +import { add0x } from '@metamask/utils'; + +import type { + AccountService, + StellarKeyringAccount, +} from '../../services/account'; +import { assertSameAddress } from '../../services/account'; +import type { WalletService } from '../../services/wallet'; +import { BaseHandler } from '../base'; +import type { + SignProofOfOwnershipBatchJsonRpcRequest, + SignProofOfOwnershipBatchJsonRpcResponse, +} from './api'; +import { + SignProofOfOwnershipBatchJsonRpcRequestStruct, + SignProofOfOwnershipBatchJsonRpcResponseStruct, +} from './api'; +import type { IClientRequestHandler } from './base'; +import { parseProofOfOwnershipMessage } from './utils'; + +/** + * Validated proof-of-ownership signing request with its original input index. + */ +type SigningRequest = { + /** + * Original batch item index, used to preserve response ordering. + */ + index: number; + + /** + * Account ID from the original request item. + */ + accountId: string; + + /** + * Resolved Stellar account used for address validation and wallet lookup. + */ + account: StellarKeyringAccount; + + /** + * Plaintext proof-of-ownership message to sign. + */ + message: string; +}; + +/** + * Handles silent batch signing of proof-of-ownership messages. + * + * Used by `@metamask/profile-metrics-controller` to prove wallet control for + * multiple Stellar accounts in a single Snap request. This is a silent sign + * path: it intentionally skips the regular sign-message confirmation flow. + */ +export class SignProofOfOwnershipBatchHandler + extends BaseHandler< + SignProofOfOwnershipBatchJsonRpcRequest, + SignProofOfOwnershipBatchJsonRpcResponse + > + implements IClientRequestHandler +{ + readonly #accountService: AccountService; + + readonly #walletService: WalletService; + + constructor({ + logger, + accountService, + walletService, + }: { + logger: Logger; + accountService: AccountService; + walletService: WalletService; + }) { + super({ + logger: logger.withPrefix('[🔏 SignProofOfOwnershipBatchHandler]'), + requestStruct: SignProofOfOwnershipBatchJsonRpcRequestStruct, + responseStruct: SignProofOfOwnershipBatchJsonRpcResponseStruct, + }); + this.#accountService = accountService; + this.#walletService = walletService; + } + + /** + * Validates each request item, derives wallets grouped by entropy source, and + * returns one success or error result per input item. + * + * @param request - The JSON-RPC batch proof-of-ownership request. + * @returns Batch proof-of-ownership signing results in input order. + */ + protected async handleRequest( + request: SignProofOfOwnershipBatchJsonRpcRequest, + ): Promise { + const { items } = request.params; + const uniqueAccountIds = [ + ...new Set(items.map(({ accountId }) => accountId)), + ]; + const accounts = await this.#accountService.findByIds(uniqueAccountIds); + const accountsById = new Map( + accounts.map((account) => [account.id.toLowerCase(), account]), + ); + const results: SignProofOfOwnershipBatchJsonRpcResponse['results'] = + new Array(items.length); + const signingRequests: SigningRequest[] = []; + + items.forEach(({ accountId, message }, index) => { + const account = accountsById.get(accountId.toLowerCase()); + if (account === undefined) { + results[index] = { + accountId, + error: `Account not found: ${accountId}`, + }; + return; + } + + try { + const { address: messageAddress } = + parseProofOfOwnershipMessage(message); + + if (messageAddress !== account.address) { + results[index] = { + accountId, + error: `Address in proof-of-ownership message (${messageAddress}) does not match signing account address (${account.address})`, + }; + return; + } + + signingRequests.push({ + index, + accountId, + account, + message, + }); + } catch (error) { + results[index] = { + accountId, + error: normalizeError(error).message, + }; + } + }); + + await this.#signValidRequests(signingRequests, results); + + return { results }; + } + + /** + * Signs already-validated requests, grouping wallet derivation by entropy + * source so the coin-type node is fetched once per source. + * + * @param signingRequests - Requests that passed account and message checks. + * @param results - Mutable output array indexed to match the original input. + */ + async #signValidRequests( + signingRequests: SigningRequest[], + results: SignProofOfOwnershipBatchJsonRpcResponse['results'], + ): Promise { + const requestsByEntropySource = new Map(); + for (const signingRequest of signingRequests) { + const entropyRequests = + requestsByEntropySource.get(signingRequest.account.entropySource) ?? []; + entropyRequests.push(signingRequest); + requestsByEntropySource.set( + signingRequest.account.entropySource, + entropyRequests, + ); + } + + await Promise.all( + [...requestsByEntropySource.values()].map( + async (entropySourceRequests) => { + const firstRequest = entropySourceRequests[0] as SigningRequest; + const { entropySource } = firstRequest.account; + + try { + const walletResolver = + await this.#walletService.getWalletResolver(entropySource); + + for (const { + account, + accountId, + index, + message, + } of entropySourceRequests) { + try { + const wallet = await walletResolver(account.index); + assertSameAddress(account.address, wallet.address); + results[index] = { + accountId, + signature: add0x(wallet.signMessage(message, 'hex')), + }; + } catch (error) { + results[index] = { + accountId, + error: normalizeError(error).message, + }; + } + } + } catch (error) { + for (const { accountId, index } of entropySourceRequests) { + results[index] = { + accountId, + error: normalizeError(error).message, + }; + } + } + }, + ), + ); + } +} diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/utils.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/utils.ts index 3e0e48d5..578e0d5d 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/utils.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/utils.ts @@ -1,3 +1,6 @@ +import { parseProofOfOwnershipMessage as parseSharedProofOfOwnershipMessage } from '@metamask/snap-networks-utils'; +import type { ProofOfOwnershipMessage } from '@metamask/snap-networks-utils'; + import { StellarAddressStruct } from '../../api/address'; import type { Transaction } from '../../services/transaction'; import { @@ -109,36 +112,15 @@ export function getTxnErrorMessageKey( * @returns The parsed nonce and Stellar address. * @throws Error if the message format is invalid. */ -export function parseProofOfOwnershipMessage(message: string): { - nonce: string; - address: string; -} { - const messagePrefix = 'metamask:proof-of-ownership:'; - - if (!message.startsWith(messagePrefix)) { - throw new Error(`Message must start with "${messagePrefix}"`); - } - - const remainder = message.slice(messagePrefix.length); - const separatorIdx = remainder.lastIndexOf(':'); - if (separatorIdx === -1) { - throw new Error( - 'Message must follow the format "metamask:proof-of-ownership:{nonce}:{address}"', - ); - } - - const nonce = remainder.slice(0, separatorIdx); - const address = remainder.slice(separatorIdx + 1); - - if (nonce === '') { - throw new Error( - 'Proof-of-ownership message must contain a non-empty nonce', - ); - } +export function parseProofOfOwnershipMessage( + message: string, +): ProofOfOwnershipMessage { + const proofMessage = parseSharedProofOfOwnershipMessage(message); + const { address } = proofMessage; if (!StellarAddressStruct.is(address)) { throw new Error('Invalid Stellar address in proof-of-ownership message'); } - return { nonce, address }; + return proofMessage; }