diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 326d7d66a8..ff6953ae42 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -909,7 +909,12 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici // descriptor Psbt, skipping the fixedScriptWallet.BitGoPsbt intermediate. return explainTx(decodeDescriptorPsbt(params), { ...params, wallet }, this.wasmName, this.addressCodec); } - return explainTx(this.decodeTransactionFromPrebuild(params), { ...params, wallet }, this.wasmName, this.addressCodec); + return explainTx( + this.decodeTransactionFromPrebuild(params), + { ...params, wallet }, + this.wasmName, + this.addressCodec + ); } /** diff --git a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts index 09d6a3775a..935a85828e 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/parseTransaction.ts @@ -112,7 +112,7 @@ function toExpectedOutputs( }); if (txParams.allowExternalChangeAddress && txParams.changeAddress) { expectedOutputs.push({ - script: addressCodec.decodeChangeScript(txParams.changeAddress), + script: Buffer.from(addressCodec.decodeChangeAddress(txParams.changeAddress)), // When an external change address is explicitly specified, count all outputs going towards that // address in the expected outputs (regardless of the output amount) value: 'max', @@ -254,10 +254,9 @@ export async function parseTransaction( function toComparableOutputsWithExternal(outputs: Output[]): ComparableOutputWithAddress[] { return outputs.map((output) => ({ // Change/custom-change outputs are always transparent wallet addresses. - script: - output.external === false - ? addressCodec.decodeChangeScript(output.address) - : addressCodec.fromExtendedAddressFormatToScript(output.address), + script: output.external + ? Buffer.from(addressCodec.decodeExternalAddress(output.address)) + : Buffer.from(addressCodec.decodeChangeAddress(output.address)), value: output.amount === 'max' ? 'max' : (BigInt(output.amount) as bigint | 'max'), external: output.external, address: output.address, @@ -297,7 +296,7 @@ export async function parseTransaction( function toOutputs(outputs: ExpectedOutputWithAddress[] | ComparableOutputWithAddress[]): Output[] { return outputs.map((output) => ({ - address: addressCodec.outputScriptToAddress(output.script, output.address), + address: addressCodec.toExtendedAddressFormat(output.script, output.address), amount: output.value.toString(), external: output.external, })); diff --git a/modules/abstract-utxo/src/transaction/recipient.ts b/modules/abstract-utxo/src/transaction/recipient.ts index 807fcb5d40..cbec296d4d 100644 --- a/modules/abstract-utxo/src/transaction/recipient.ts +++ b/modules/abstract-utxo/src/transaction/recipient.ts @@ -56,19 +56,10 @@ export class AddressCodec { return this.decode(address); } - /** Resolve a transparent change address directly to a Buffer script. */ - decodeChangeScript(address: string): Buffer { - return Buffer.from(this.decodeChangeAddress(address)); - } - - /** - * Convert an output's scriptPubKey back to the address form the output should report. The - * base implementation encodes the script. Coins whose output scripts cannot always be - * re-encoded (e.g. Zcash shielded recipients, whose raw Orchard receiver has no scriptPubKey - * encoding) override this and may fall back to the output's original address. - */ - outputScriptToAddress(script: Buffer, address?: string): string { - return this.toExtendedAddressFormat(script); + /** Resolve an external address, including extended script recipients, to its script. */ + decodeExternalAddress(address: string): Uint8Array { + const result = AddressCodec.fromExtendedAddressFormat(address); + return 'script' in result ? Buffer.from(result.script, 'hex') : this.decode(result.address); } encode(script: Uint8Array): string { @@ -84,13 +75,10 @@ export class AddressCodec { } fromExtendedAddressFormatToScript(extendedAddress: string): Buffer { - const result = AddressCodec.fromExtendedAddressFormat(extendedAddress); - if ('script' in result) { - return Buffer.from(result.script, 'hex'); - } - return Buffer.from(this.decode(result.address)); + return Buffer.from(this.decodeExternalAddress(extendedAddress)); } + /** Check whether a parsed output address resolves to its raw script. */ isMatchingScript(output: AddressCodecOutput): boolean { if (output.address === undefined || output.address === null) { return true; @@ -116,7 +104,13 @@ export class AddressCodec { throw new Error('invalid input'); } - toExtendedAddressFormat(script: Buffer): string { + toExtendedAddressFormat(script: Buffer, address?: string): string { + if (address !== undefined && !this.isMatchingScript({ address, script })) { + throw new Error(`address ${address} does not match output script`); + } + if (address !== undefined) { + return address; + } return script[0] === OP_RETURN ? `${ScriptRecipientPrefix}${script.toString('hex')}` : this.encode(script); } } diff --git a/modules/abstract-utxo/test/unit/bip322.ts b/modules/abstract-utxo/test/unit/bip322.ts index 13f664d95d..e90b32aee0 100644 --- a/modules/abstract-utxo/test/unit/bip322.ts +++ b/modules/abstract-utxo/test/unit/bip322.ts @@ -441,7 +441,10 @@ describe('BIP322', function () { it('should successfully run with a user nonce', function () { const psbt = createUnsignedPsbt(); assertCommon( - explainPsbtWasm(psbt, walletKeys, { addressCodec: new AddressCodec('btc'), replayProtection: { publicKeys: [] } }), + explainPsbtWasm(psbt, walletKeys, { + addressCodec: new AddressCodec('btc'), + replayProtection: { publicKeys: [] }, + }), 0 ); }); @@ -450,7 +453,10 @@ describe('BIP322', function () { const psbt = createUnsignedPsbt(); psbt.sign(BIP32.fromBase58(xprivs[0])); assertCommon( - explainPsbtWasm(psbt, walletKeys, { addressCodec: new AddressCodec('btc'), replayProtection: { publicKeys: [] } }), + explainPsbtWasm(psbt, walletKeys, { + addressCodec: new AddressCodec('btc'), + replayProtection: { publicKeys: [] }, + }), 1 ); }); @@ -460,7 +466,10 @@ describe('BIP322', function () { psbt.sign(BIP32.fromBase58(xprivs[0])); psbt.sign(BIP32.fromBase58(xprivs[2])); assertCommon( - explainPsbtWasm(psbt, walletKeys, { addressCodec: new AddressCodec('btc'), replayProtection: { publicKeys: [] } }), + explainPsbtWasm(psbt, walletKeys, { + addressCodec: new AddressCodec('btc'), + replayProtection: { publicKeys: [] }, + }), 2 ); });