From 7d43929a54fc4791c65860f0075292f3bd4e4448 Mon Sep 17 00:00:00 2001 From: neha-kri Date: Wed, 30 Sep 2026 10:13:17 +0530 Subject: [PATCH] feat(sdk-core): surface pendingApprovalId from TSS message signing The message-signing policy (DEFI-653) parks signMessage/signTypedStructuredData txRequests behind a pending approval, but the SDK's message-sign paths never inspected the txRequest state: they ran all MPC signing rounds against a parked request and failed at the final send with a wrapped 'Expected transaction request to be in state pendingDelivery' error, leaving the UI unable to recover the pending approval id. After approval, the only retry path (signAndSendTxRequest) signs through the transaction path and crashes on message-only requests (transactions[0].unsignedTx of undefined). - signMessageTss/signTypedDataTss: after create/fetch, return the pendingApprovalId (with txRequestId) when the txRequest is parked behind a pending approval instead of attempting to sign, mirroring the tx flow's early return in prebuildAndSignTransaction - SignedMessage: txHash/signature are now optional and a pendingApprovalId field marks the parked result; callers should treat a present pendingApprovalId as awaiting-approval data, not a failure - new signAndSendMessageTxRequest({ txRequestId, walletPassphrase }): signs a message txRequest through the message path (WP-persisted messageEncoded as bufferToSign), for resuming a parked request after approval; resolves with the pendingApprovalId if the request is still parked - rethrow the original signing errors instead of wrapping them in 'failed to sign message/typed data ...' so downstream callers can classify ApiResponseError results (e.g. the TxRequestPendingApprovalError 409) - sdk-coin-xdc: signXdcKycMessage fails loudly when the KYC sign request is parked behind a pending approval instead of returning an unusable signature-less result Refs: DEFI-1014 --- modules/bitgo/test/v2/unit/wallet.ts | 227 ++++++++++++++++++ modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts | 11 + .../sdk-coin-xdc/test/unit/xdcKycMessage.ts | 15 ++ .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 40 ++- modules/sdk-core/src/bitgo/wallet/iWallet.ts | 7 + modules/sdk-core/src/bitgo/wallet/wallet.ts | 141 ++++++++++- 6 files changed, 429 insertions(+), 12 deletions(-) diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index b37ffb4b866..034b8695e1e 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -4517,6 +4517,7 @@ describe('V2 Wallet:', function () { let signTxRequestForMessage; const messageSigningCoins = ['teth', 'tpolygon']; const expected: SignedMessage = { + state: 'delivered', txRequestId: reqId.toString(), txHash, signature: txHash, @@ -4696,6 +4697,7 @@ describe('V2 Wallet:', function () { ], }; const solExpectedSignedMessage: SignedMessage = { + state: 'delivered', txRequestId: reqId.toString(), txHash: solTxHash, signature: solTxHash, @@ -4999,6 +5001,7 @@ describe('V2 Wallet:', function () { const txRequestId = txRequestForTypedDataSigning.txRequestId; typedDataBase.txRequestId = txRequestId; const expected: SignedMessage = { + state: 'delivered', txRequestId, messageRaw: JSON.stringify(typedMessage), signature: txHash, @@ -5147,6 +5150,230 @@ describe('V2 Wallet:', function () { }); }); + describe('Message Signing with pending approval (policy flow)', function () { + const messageRaw = 'hello world'; + const messageEncoded = Buffer.from(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`).toString( + 'hex' + ); + const txHash = '0xrrrsss1b'; + const pendingApprovalId = 'pa-11112222333344445555666677778888'; + + const signedMessagesFixture: NonNullable = [ + { + state: 'signed', + messageRaw, + derivationPath: 'm/0', + signatureShares: [{ from: SignatureShareType.USER, to: SignatureShareType.USER, share: '' }], + combineSigShare: '0:rrr:sss:3', + txHash, + messageEncoded, + }, + ]; + + const parkedTxRequest: TxRequest = { + txRequestId: 'parked-tx-request-id', + transactions: [], + intent: { intentType: 'signTypedStructuredData' }, + date: new Date().toISOString(), + latest: true, + state: 'pendingApproval', + apiVersion: 'full', + pendingApprovalId, + userId: 'userId', + walletType: 'hot', + policiesChecked: true, + version: 1, + walletId: 'walletId', + unsignedTxs: [], + unsignedMessages: [], + messages: [ + { + state: 'pendingSignature', + messageRaw, + derivationPath: 'm/0', + signatureShares: [], + messageEncoded, + }, + ], + }; + + const signableTxRequest: TxRequest = { + ...parkedTxRequest, + state: 'pendingDelivery', + }; + + const signedTxRequest: TxRequest = { + ...parkedTxRequest, + state: 'delivered', + messages: signedMessagesFixture, + }; + + const typedDataFixture: TypedData = { + typedDataRaw: JSON.stringify({ + domain: { + name: 'bitgo', + version: '1', + chainId: 1, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + primaryType: 'Message', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Message: [{ name: 'data', type: 'string' }], + }, + message: { data: 'bitgo says hello!' }, + }), + version: SignTypedDataVersion.V3, + }; + + let signTxRequestForMessageStub: sinon.SinonStub; + + beforeEach(function () { + signTxRequestForMessageStub = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'signTxRequestForMessage'); + signTxRequestForMessageStub.resolves(signedTxRequest); + sandbox.stub(Keychains.prototype, 'getKeysForSigning').resolves([ + { + commonKeychain: 'test', + id: '', + pub: '', + type: 'tss', + encryptedPrv: + '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}', + }, + ]); + }); + + /** + * Narrows the SignedMessage union after asserting its state: a should + * assertion on `state` alone does not narrow the discriminated union + * for the compiler, so variant-specific fields must be read from the + * narrowed return value. + */ + function assertSignedMessageState( + result: SignedMessage, + state: T + ): Extract { + result.state.should.equal(state); + return result as Extract; + } + + afterEach(function () { + sandbox.restore(); + nock.cleanAll(); + }); + + it('should return pendingApprovalId without signing when a fresh signMessage request is parked', async function () { + nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/msgrequests`).reply(200, parkedTxRequest); + + const result = await tssEthWallet.signMessage({ + message: { messageRaw, messageStandardType: MessageStandardType.EIP191 }, + prv: 'secretKey', + }); + + const parked = assertSignedMessageState(result, 'pendingApproval'); + parked.pendingApprovalId.should.equal(pendingApprovalId); + parked.txRequestId.should.equal(parkedTxRequest.txRequestId); + sinon.assert.notCalled(signTxRequestForMessageStub); + }); + + it('should return pendingApprovalId without signing when resuming a parked signMessage request', async function () { + nock(bgUrl) + .get(`/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${parkedTxRequest.txRequestId}&latest=true`) + .reply(200, { txRequests: [parkedTxRequest] }); + + const result = await tssEthWallet.signMessage({ + message: { + messageRaw, + txRequestId: parkedTxRequest.txRequestId, + messageStandardType: MessageStandardType.EIP191, + }, + prv: 'secretKey', + }); + + const parked = assertSignedMessageState(result, 'pendingApproval'); + parked.pendingApprovalId.should.equal(pendingApprovalId); + }); + + it('should return pendingApprovalId without signing when a fresh signTypedData request is parked', async function () { + nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`).reply(200, parkedTxRequest); + + const result = await tssEthWallet.signTypedData({ + typedData: typedDataFixture, + prv: 'secretKey', + }); + + const parked = assertSignedMessageState(result, 'pendingApproval'); + parked.pendingApprovalId.should.equal(pendingApprovalId); + sinon.assert.notCalled(signTxRequestForMessageStub); + }); + + it('should return pendingApprovalId without signing when resuming a parked signTypedData request', async function () { + nock(bgUrl) + .get(`/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${parkedTxRequest.txRequestId}&latest=true`) + .reply(200, { txRequests: [parkedTxRequest] }); + + const result = await tssEthWallet.signTypedData({ + typedData: { ...typedDataFixture, txRequestId: parkedTxRequest.txRequestId }, + prv: 'secretKey', + }); + + const parked = assertSignedMessageState(result, 'pendingApproval'); + parked.pendingApprovalId.should.equal(pendingApprovalId); + sinon.assert.notCalled(signTxRequestForMessageStub); + }); + + it('should sign a pendingDelivery message request via signAndSendMessageTxRequest', async function () { + nock(bgUrl) + .get( + `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${signableTxRequest.txRequestId}&latest=true` + ) + .reply(200, { txRequests: [signableTxRequest] }); + + const result = await tssEthWallet.signAndSendMessageTxRequest({ + txRequestId: signableTxRequest.txRequestId, + walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE as string, + }); + + const delivered = assertSignedMessageState(result, 'delivered'); + delivered.txHash.should.equal(txHash); + delivered.signature.should.equal(txHash); + delivered.txRequestId.should.equal(signableTxRequest.txRequestId); + delivered.messageRaw.should.equal(messageRaw); + sinon.assert.calledOnce(signTxRequestForMessageStub); + }); + + it('should return pendingApprovalId without signing when signAndSendMessageTxRequest hits a still-parked request', async function () { + nock(bgUrl) + .get(`/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${parkedTxRequest.txRequestId}&latest=true`) + .reply(200, { txRequests: [parkedTxRequest] }); + + const result = await tssEthWallet.signAndSendMessageTxRequest({ + txRequestId: parkedTxRequest.txRequestId, + walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE as string, + }); + + const parked = assertSignedMessageState(result, 'pendingApproval'); + parked.pendingApprovalId.should.equal(pendingApprovalId); + sinon.assert.notCalled(signTxRequestForMessageStub); + }); + + it('should reject signAndSendMessageTxRequest for non-TSS wallets', async function () { + const nonTssWallet = new Wallet(bitgo, bitgo.coin('teth'), { ...ethWalletData, multisigType: 'onchain' }); + + await nonTssWallet + .signAndSendMessageTxRequest({ + txRequestId: parkedTxRequest.txRequestId, + walletPassphrase: 'passphrase', + }) + .should.be.rejectedWith('Message signing only supported for TSS wallets'); + }); + }); + describe('Send Many', function () { const sendManyInput = { type: 'transfer', diff --git a/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts b/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts index 3dbece732a4..cd42e40928d 100644 --- a/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts +++ b/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { IWallet, MessageStandardType } from '@bitgo/sdk-core'; export interface XdcKycMessageParams { @@ -44,6 +45,16 @@ export async function signXdcKycMessage( walletPassphrase, }); + // A message-signing policy can park the sign request behind a pending + // approval (no signature is produced); the KYC flow requires a signature, + // so fail loudly instead of returning an unusable signed-message object. + if (signed.state === 'pendingApproval') { + throw new Error( + `Unable to sign XDC KYC message: the sign request is pending approval (${signed.pendingApprovalId})` + ); + } + assert(signed.signature, 'Unable to sign XDC KYC message: no signature was produced'); + return { kycAccount: account, kycMessage, diff --git a/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts b/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts index 6cb93f2397a..56ddf4cfb83 100644 --- a/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts +++ b/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts @@ -36,6 +36,7 @@ describe('signXdcKycMessage', function () { it('should call wallet.signMessage with correct messageRaw and messageStandardType', async function () { (wallet.signMessage as sinon.SinonStub).resolves({ + state: 'delivered', txHash: '', signature: SIGNATURE, messageRaw: '', @@ -54,6 +55,7 @@ describe('signXdcKycMessage', function () { it('should return kycAccount, kycMessage, and kycSignature mapped correctly', async function () { (wallet.signMessage as sinon.SinonStub).resolves({ + state: 'delivered', txHash: '', signature: SIGNATURE, messageRaw: '', @@ -79,4 +81,17 @@ describe('signXdcKycMessage', function () { /TSS signing failed/ ); }); + + it('should reject when the sign request is parked behind a pending approval', async function () { + (wallet.signMessage as sinon.SinonStub).resolves({ + state: 'pendingApproval', + messageRaw: '', + pendingApprovalId: 'pa-11112222333344445555666677778888', + }); + + await assert.rejects( + () => signXdcKycMessage(wallet as unknown as IWallet, ACCOUNT, 'passphrase'), + /pending approval \(pa-11112222333344445555666677778888\)/ + ); + }); }); diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 2e6b1eaa025..c79e0e53e0a 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -598,17 +598,49 @@ export type SignedTransaction = | SignedTransactionRequest | TxRequest; -export interface SignedMessage { +/** + * Base fields shared by every message-sign result. + */ +export interface SignedMessageBase { coin?: string; - // @deprecated - use `signature` instead - txHash: string; - signature: string; messageRaw: string; messageEncoded?: string; messageStandardType?: MessageStandardType; txRequestId: string; } +/** + * The message-sign request was parked behind a pending approval by the + * message-signing policy; no signature was produced. The discriminator is the + * server-reported txRequest state ('pendingApproval') — never inferred from + * id presence. Surface the approval, then re-sign via + * signAndSendMessageTxRequest once it is resolved. + */ +export interface SignedMessagePendingApproval extends SignedMessageBase { + state: 'pendingApproval'; + pendingApprovalId: string; +} + +/** + * The message is signed and the request is terminal (wallet-platform + * transitions a message request pendingDelivery → delivered once all + * messages are signed; the txRequest-level 'signed' state is never used for + * message requests). + */ +export interface SignedMessageDelivered extends SignedMessageBase { + state: 'delivered'; + // @deprecated - use `signature` instead + txHash: string; + signature: string; +} + +/** + * Discriminated on the wallet-platform txRequest state. BREAKING: consumers + * that previously read `txHash`/`signature` without narrowing must first + * check `state === 'delivered'`. + */ +export type SignedMessage = SignedMessagePendingApproval | SignedMessageDelivered; + export interface RecoverWalletTokenOptions { tokenContractAddress: string; wallet: IWallet; diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index e0130c883b3..11f553e3863 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -451,6 +451,12 @@ export interface SignAndSendTxRequestOptions { isTxRequestFull: boolean; } +export interface SignAndSendMessageTxRequestOptions { + txRequestId: string; + walletPassphrase: string; + reqId?: IRequestTracer; +} + export interface GetUserPrvOptions { keychain?: Keychain; key?: Keychain; @@ -1371,6 +1377,7 @@ export interface IWallet { signMessage(params: WalletSignMessageOptions): Promise; buildSignMessageRequest(params: WalletSignMessageOptions): Promise; signTypedData(params: WalletSignTypedDataOptions): Promise; + signAndSendMessageTxRequest(params: SignAndSendMessageTxRequestOptions): Promise; fetchCrossChainUTXOs(params: FetchCrossChainUTXOsOptions): Promise; getChallengesForEcdsaSigning(): Promise; getNftBalances(): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 68b7d8e0daa..930b1cf5f27 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -125,6 +125,7 @@ import { SharedKeyChain, ShareWalletOptions, SignAndSendTxRequestOptions, + SignAndSendMessageTxRequestOptions, SimulateWebhookOptions, SubmitTransactionOptions, SubWalletType, @@ -5087,6 +5088,93 @@ export class Wallet implements IWallet { return ret; } + /** + * Signs the message of a full (apiVersion === 'full') message-sign + * transaction request from a TSS (hot) wallet. Meant to be used for a + * message-sign request whose signing was parked behind a pending approval + * by the message-signing policy and resumed after the approval was + * resolved. Message-sign transaction requests are full-only: the lite + * (deprecated) request flow never carries messages. + * + * Unlike signAndSendTxRequest, this signs through the message path + * (signTxRequestForMessage with the WP-persisted messageEncoded); the + * transaction signer has no transaction to sign on a message-only request. + * The method only signs — delivery of the signature to the requestor is + * completed server-side once the signature shares are ingested. + * + * If the request is still parked behind a pending approval (txRequest + * state 'pendingApproval'), resolves with the pendingApprovalId instead + * of signing (same contract as signMessage/signTypedData). + * + * @param params + * txRequestId - The ID of the message-sign transaction request. + * walletPassphrase - The passphrase for the wallet. + * reqId - Optional request tracer. + * @returns A promise that resolves to a SignedMessage. + */ + public async signAndSendMessageTxRequest(params: SignAndSendMessageTxRequestOptions): Promise { + if (this._wallet.multisigType !== 'tss') { + throw new Error('Message signing only supported for TSS wallets'); + } + const reqId = params.reqId ?? new RequestTracer(); + this.bitgo.setRequestTracer(reqId); + + const keychains = await this.getKeychainsAndValidatePassphrase({ + reqId, + walletPassphrase: params.walletPassphrase, + }); + const userKeychain = keychains[0]; + if (!userKeychain || !userKeychain.encryptedPrv) { + throw new Error('the user keychain does not have property encryptedPrv'); + } + + const txRequest = await getTxRequest(this.bitgo, this.id(), params.txRequestId, reqId); + + // The message-signing policy may still park the request behind a pending + // approval; resolve with the pendingApprovalId instead of signing. + if (this.tssUtils?.isPendingApprovalTxRequestFull(txRequest)) { + assert(txRequest.pendingApprovalId, 'Unable to find pendingApprovalId on a pendingApproval-state txRequest'); + return { + state: 'pendingApproval', + coin: this.coin(), + messageRaw: txRequest.messages?.[0]?.messageRaw ?? '', + txRequestId: txRequest.txRequestId, + pendingApprovalId: txRequest.pendingApprovalId, + }; + } + + assert( + txRequest.messages && txRequest.messages.length > 0, + 'Unable to find messages in txRequest for message signing' + ); + const message = txRequest.messages[0]; + const messageEncoded = message.messageEncoded; + assert(messageEncoded, 'Unable to find messageEncoded in txRequest message'); + + const signedMessageRequest = await this.tssUtils!.signTxRequestForMessage({ + txRequest, + prv: await this.getUserPrv({ + keychain: userKeychain, + walletPassphrase: params.walletPassphrase, + }), + reqId, + messageRaw: message.messageRaw, + messageEncoded, + bufferToSign: Buffer.from(messageEncoded, 'hex'), + }); + assert(signedMessageRequest.messages, 'Unable to find messages in signedMessageRequest'); + assert(signedMessageRequest.messages[0].txHash, 'Unable to find txHash in signedMessageRequest.messages'); + return { + state: 'delivered', + coin: this.coin(), + txHash: signedMessageRequest.messages[0].txHash, + signature: signedMessageRequest.messages[0].txHash, + messageRaw: message.messageRaw, + messageEncoded, + txRequestId: signedMessageRequest.txRequestId, + }; + } + /** * Ensures signature shares are in a clean state before signing a transaction. * Automatically deletes signature shares for Full TxRequests before signing to prevent @@ -5341,6 +5429,20 @@ export class Wallet implements IWallet { txRequest = await getTxRequest(this.bitgo, this.id(), params.message.txRequestId, params.reqId); } + // A message-signing policy can park the request behind a pending approval. + // Signing cannot proceed until the approval is resolved; surface the + // pendingApprovalId instead of attempting MPC rounds against a parked request + // (which would fail at send with a pendingDelivery state error). + if (this.tssUtils!.isPendingApprovalTxRequestFull(txRequest)) { + return { + state: 'pendingApproval', + coin: this.coin(), + messageRaw, + txRequestId: txRequest.txRequestId, + pendingApprovalId: txRequest.pendingApprovalId, + }; + } + assert( txRequest.messages && txRequest.messages.length > 0, 'Unable to find messages in txRequest for message signing' @@ -5362,20 +5464,25 @@ export class Wallet implements IWallet { 'Unable to find combineSigShare in signedMessageRequest.messages' ); } - assert(signedMessageRequest.messages[0].txHash, 'Unable to find txHash in signedMessageRequest.messages'); + const { txHash } = signedMessageRequest.messages[0]; + assert(txHash, 'Unable to find txHash in signedMessageRequest.messages'); // messageRaw on the signed request is the WP-persisted payload that was validated and // encoded before signing; the caller's echo is unverifiable on the resume path. const signedMessageRaw = signedMessageRequest.messages[0].messageRaw ?? messageRaw; return { + state: 'delivered', coin: this.coin(), - txHash: signedMessageRequest.messages[0].txHash, - signature: signedMessageRequest.messages[0].txHash, + txHash, + signature: txHash, messageRaw: signedMessageRaw, messageEncoded, txRequestId: signedMessageRequest.txRequestId, }; } catch (e) { - throw new Error('failed to sign message ' + e); + // Rethrow the original error so callers can classify it (e.g. the 409 + // TxRequestPendingApprovalError from wallet-platform) instead of losing + // the error type inside a generic message. + throw e; } } @@ -5410,6 +5517,19 @@ export class Wallet implements IWallet { txRequest = await getTxRequest(this.bitgo, this.id(), params.typedData.txRequestId, params.reqId); } + // A message-signing policy can park the request behind a pending approval. + // Signing cannot proceed until the approval is resolved; surface the + // pendingApprovalId instead of attempting MPC rounds against a parked request. + if (this.tssUtils!.isPendingApprovalTxRequestFull(txRequest)) { + return { + state: 'pendingApproval', + coin: this.coin(), + messageRaw: params.typedData.typedDataRaw, + txRequestId: txRequest.txRequestId, + pendingApprovalId: txRequest.pendingApprovalId, + }; + } + const signedTypedDataRequest = await this.tssUtils!.signTxRequestForMessage({ txRequest, prv: params.prv, @@ -5423,17 +5543,22 @@ export class Wallet implements IWallet { signedTypedDataRequest.messages[0].combineSigShare, 'Unable to find combineSigShare in signedTypedDataRequest.messages' ); - assert(signedTypedDataRequest.messages[0].txHash, 'Unable to find txHash in signedTypedDataRequest.messages'); + const { txHash } = signedTypedDataRequest.messages[0]; + assert(txHash, 'Unable to find txHash in signedTypedDataRequest.messages'); return { + state: 'delivered', coin: this.coin(), - txHash: signedTypedDataRequest.messages[0].txHash, - signature: signedTypedDataRequest.messages[0].txHash, + txHash, + signature: txHash, messageRaw: params.typedData.typedDataRaw, messageEncoded: params.typedData.typedDataEncoded!.toString('hex'), txRequestId: signedTypedDataRequest.txRequestId, }; } catch (e) { - throw new Error('failed to sign typed data ' + e); + // Rethrow the original error so callers can classify it (e.g. the 409 + // TxRequestPendingApprovalError from wallet-platform) instead of losing + // the error type inside a generic message. + throw e; } }