From 5f0d1edcfa2021ff4580d41730b955f96a530921 Mon Sep 17 00:00:00 2001 From: Mohammad Al Faiyaz Date: Tue, 28 Apr 2026 15:43:01 -0400 Subject: [PATCH] fix(abstract-utxo): guard address in checkRecipient for OP_RETURN outputs What changed: - make address optional in checkRecipient parameter type - add recipient.address guard before calling isScriptRecipient - pass explicit object to super.checkRecipient to satisfy its address: string type - add unit tests covering OP_RETURN and script-prefixed recipient cases Why: OP_RETURN recipients have no address field at runtime; calling isScriptRecipient(undefined) unconditionally invoked undefined.toLowerCase() causing a crash when approving pending approvals containing OP_RETURN outputs. TICKET: WAL-826 Co-Authored-By: Claude Sonnet 4.6 --- modules/abstract-utxo/src/abstractUtxoCoin.ts | 6 +-- .../test/unit/transaction/recipient.ts | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 modules/abstract-utxo/test/unit/transaction/recipient.ts diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index b5d04a47a7..6248c96e08 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -572,10 +572,10 @@ export abstract class AbstractUtxoCoin return (chainhead as any).height; } - checkRecipient(recipient: { address: string; amount: number | string }): void { + checkRecipient(recipient: { address?: string; amount: number | string }): void { assertValidTransactionRecipient(recipient); - if (!isScriptRecipient(recipient.address)) { - super.checkRecipient(recipient); + if (recipient.address && !isScriptRecipient(recipient.address)) { + super.checkRecipient({ address: recipient.address, amount: recipient.amount }); } } diff --git a/modules/abstract-utxo/test/unit/transaction/recipient.ts b/modules/abstract-utxo/test/unit/transaction/recipient.ts new file mode 100644 index 0000000000..1dbf838fba --- /dev/null +++ b/modules/abstract-utxo/test/unit/transaction/recipient.ts @@ -0,0 +1,39 @@ +import assert from 'assert'; + +import { getUtxoCoin } from '../util/utxoCoins'; + +describe('AbstractUtxoCoin.checkRecipient', function () { + const coin = getUtxoCoin('btc'); + + it('does not throw for OP_RETURN output with no address field', function () { + // Simulates { amount: '0', script: '6a0c...' } coming from buildParams.recipients + assert.doesNotThrow(() => { + coin.checkRecipient({ amount: '0' }); + }); + }); + + it('does not throw for script-prefixed address with zero amount', function () { + assert.doesNotThrow(() => { + coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '0' }); + }); + }); + + it('does not throw for a regular address', function () { + // A valid mainnet P2PKH address + assert.doesNotThrow(() => { + coin.checkRecipient({ address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf', amount: '1000' }); + }); + }); + + it('throws when OP_RETURN output (no address) has non-zero amount', function () { + assert.throws(() => { + coin.checkRecipient({ amount: '1000' }); + }, /Only zero amounts allowed for non-encodeable scriptPubkeys/); + }); + + it('throws when script-prefixed address has non-zero amount', function () { + assert.throws(() => { + coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '500' }); + }, /Only zero amounts allowed for non-encodeable scriptPubkeys/); + }); +});