Skip to content

Commit 95b673f

Browse files
Marzooqaclaude
authored andcommitted
fix(sdk-coin-ada): mark change outputs in explainTransaction
ADA's explainTransaction() returned all outputs in a flat array with no change marker, causing intent verification to count change outputs as recipients and fire false-positive TransactionFailsIntentVerification alerts. Add _changeAddress to Transaction, set it from all three build paths in TransactionBuilder, and mark matching outputs with change: true in explainTransaction(). Also add change?: boolean to the explainTransaction return type. Ticket: WCI-633 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Session-Id: 9d7a233b-1b25-4206-87ef-85d2a74410a4 Task-Id: 2eafb02e-6fe8-44a3-a0e9-1825491a8e31
1 parent c057fe1 commit 95b673f

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

modules/sdk-coin-ada/src/lib/transaction.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export class Transaction extends BaseTransaction {
101101
private _transaction: CardanoWasm.Transaction;
102102
private _fee: string;
103103
private _pledgeDetails?: PledgeDetails;
104+
private _changeAddress?: string;
104105

105106
constructor(coinConfig: Readonly<CoinConfig>) {
106107
super(coinConfig);
@@ -391,7 +392,7 @@ export class Transaction extends BaseTransaction {
391392

392393
/** @inheritdoc */
393394
explainTransaction(): {
394-
outputs: { amount: string; address: string; multiAssets?: Asset[] }[];
395+
outputs: { amount: string; address: string; multiAssets?: Asset[]; change?: boolean }[];
395396
certificates: Cert[];
396397
changeOutputs: string[];
397398
outputAmount: string;
@@ -426,10 +427,12 @@ export class Transaction extends BaseTransaction {
426427
id: txJson.id,
427428
outputs: txJson.outputs.map((o) => {
428429
const multiAssets = Transaction.parseMultiAssets(o.multiAssets as CardanoWasm.MultiAsset | undefined);
430+
const isChange = this._changeAddress !== undefined && o.address === this._changeAddress;
429431
return {
430432
address: o.address,
431433
amount: o.amount,
432434
...(multiAssets && { multiAssets }),
435+
...(isChange && { change: true }),
433436
};
434437
}),
435438
outputAmount: outputAmount,
@@ -485,6 +488,14 @@ export class Transaction extends BaseTransaction {
485488
fee(fee: string) {
486489
this._fee = fee;
487490
}
491+
492+
set changeAddress(address: string) {
493+
this._changeAddress = address;
494+
}
495+
496+
get changeAddress(): string | undefined {
497+
return this._changeAddress;
498+
}
488499
}
489500

490501
export interface SponsorshipInfo {

modules/sdk-coin-ada/src/lib/transactionBuilder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
227227
this.setMutableSenderAssetList();
228228
this.addOutputs(outputs);
229229
this._transaction.transaction = this.prepareAdaTransactionDraft(inputs, outputs, true);
230+
if (this._changeAddress) {
231+
this._transaction.changeAddress = this._changeAddress;
232+
}
230233
return this.transaction;
231234
}
232235

@@ -573,6 +576,9 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
573576

574577
const finalOutputs = this.buildExplicitOutputsCollection(this._fee);
575578
this._transaction.transaction = this.prepareAdaTransactionDraft(inputs, finalOutputs, true);
579+
if (this._changeAddress) {
580+
this._transaction.changeAddress = this._changeAddress;
581+
}
576582
return this.transaction;
577583
}
578584

@@ -924,6 +930,9 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
924930
});
925931
witnessSet.set_vkeys(vkeyWitnesses);
926932
this._transaction.transaction = CardanoWasm.Transaction.new(txRaw, witnessSet);
933+
if (this._changeAddress) {
934+
this._transaction.changeAddress = this._changeAddress;
935+
}
927936
return this.transaction;
928937
}
929938

modules/sdk-coin-ada/test/unit/transactionBuilder.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,4 +549,71 @@ describe('ADA Transaction Builder', async () => {
549549
// console.log(err);
550550
// }
551551
// });
552+
553+
describe('explainTransaction change output marking', () => {
554+
it('should mark the change output with change: true for a shelley send tx', async () => {
555+
const txBuilder = factory.getTransferBuilder();
556+
txBuilder.input({
557+
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
558+
transaction_index: 1,
559+
});
560+
const outputAmount = 7823121;
561+
txBuilder.output({
562+
address: testData.rawTx.outputAddress1.address,
563+
amount: outputAmount.toString(),
564+
});
565+
const totalInput = 21032023;
566+
txBuilder.changeAddress(testData.rawTx.outputAddress2.address, totalInput.toString());
567+
txBuilder.ttl(800000000);
568+
const tx = (await txBuilder.build()) as Transaction;
569+
const explained = tx.explainTransaction();
570+
explained.outputs.length.should.equal(2);
571+
const recipientOutput = explained.outputs.find((o) => o.address === testData.rawTx.outputAddress1.address);
572+
const changeOutput = explained.outputs.find((o) => o.address === testData.rawTx.outputAddress2.address);
573+
should.exist(recipientOutput);
574+
should.exist(changeOutput);
575+
should.not.exist(recipientOutput!.change);
576+
changeOutput!.change!.should.be.true();
577+
});
578+
579+
it('should mark the change output with change: true for a byron send tx', async () => {
580+
const txBuilder = factory.getTransferBuilder();
581+
txBuilder.input({
582+
transaction_id: '1b53331e069a6e58fe77919d30c0cf299d13a2f5b3d9970ce473c1a66d71bf03',
583+
transaction_index: 1,
584+
});
585+
const outputAmount = 200000000;
586+
txBuilder.output({
587+
address: testData.rawTxByron.outputAddress1.address,
588+
amount: outputAmount.toString(),
589+
});
590+
const totalInput = 999600000;
591+
txBuilder.changeAddress(testData.rawTxByron.outputAddress2.address, totalInput.toString());
592+
txBuilder.ttl(800000000);
593+
const tx = (await txBuilder.build()) as Transaction;
594+
const explained = tx.explainTransaction();
595+
explained.outputs.length.should.equal(2);
596+
const recipientOutput = explained.outputs.find((o) => o.address === testData.rawTxByron.outputAddress1.address);
597+
const changeOutput = explained.outputs.find((o) => o.address === testData.rawTxByron.outputAddress2.address);
598+
should.exist(recipientOutput);
599+
should.exist(changeOutput);
600+
should.not.exist(recipientOutput!.change);
601+
changeOutput!.change!.should.be.true();
602+
});
603+
604+
it('should not set change on any output when no change address is set (consolidation)', async () => {
605+
const txBuilder = factory.getTransferBuilder();
606+
txBuilder.input({
607+
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
608+
transaction_index: 1,
609+
});
610+
const totalInput = 20000000;
611+
txBuilder.changeAddress(testData.rawTx.outputAddress1.address, totalInput.toString());
612+
txBuilder.ttl(800000000);
613+
const tx = (await txBuilder.build()) as Transaction;
614+
const explained = tx.explainTransaction();
615+
explained.outputs.length.should.equal(1);
616+
explained.outputs[0].change!.should.be.true();
617+
});
618+
});
552619
});

0 commit comments

Comments
 (0)