Skip to content

Commit 7b3d49d

Browse files
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. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6c87b40 commit 7b3d49d

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,10 +572,10 @@ export abstract class AbstractUtxoCoin
572572
return (chainhead as any).height;
573573
}
574574

575-
checkRecipient(recipient: { address: string; amount: number | string }): void {
575+
checkRecipient(recipient: { address?: string; amount: number | string }): void {
576576
assertValidTransactionRecipient(recipient);
577-
if (!isScriptRecipient(recipient.address)) {
578-
super.checkRecipient(recipient);
577+
if (recipient.address && !isScriptRecipient(recipient.address)) {
578+
super.checkRecipient({ address: recipient.address, amount: recipient.amount });
579579
}
580580
}
581581

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import assert from 'assert';
2+
3+
import { getUtxoCoin } from '../util/utxoCoins';
4+
5+
describe('AbstractUtxoCoin.checkRecipient', function () {
6+
const coin = getUtxoCoin('btc');
7+
8+
it('does not throw for OP_RETURN output with no address field', function () {
9+
// Simulates { amount: '0', script: '6a0c...' } coming from buildParams.recipients
10+
assert.doesNotThrow(() => {
11+
coin.checkRecipient({ amount: '0' });
12+
});
13+
});
14+
15+
it('does not throw for script-prefixed address with zero amount', function () {
16+
assert.doesNotThrow(() => {
17+
coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '0' });
18+
});
19+
});
20+
21+
it('does not throw for a regular address', function () {
22+
// A valid mainnet P2PKH address
23+
assert.doesNotThrow(() => {
24+
coin.checkRecipient({ address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf', amount: '1000' });
25+
});
26+
});
27+
28+
it('throws when OP_RETURN output (no address) has non-zero amount', function () {
29+
assert.throws(() => {
30+
coin.checkRecipient({ amount: '1000' });
31+
}, /Only zero amounts allowed for non-encodeable scriptPubkeys/);
32+
});
33+
34+
it('throws when script-prefixed address has non-zero amount', function () {
35+
assert.throws(() => {
36+
coin.checkRecipient({ address: 'scriptPubKey:6a0c68656c6c6f20776f726c64', amount: '500' });
37+
}, /Only zero amounts allowed for non-encodeable scriptPubkeys/);
38+
});
39+
});

0 commit comments

Comments
 (0)