diff --git a/modules/sdk-coin-ada/src/lib/transaction.ts b/modules/sdk-coin-ada/src/lib/transaction.ts index 21c2eb6d0a..1fa4cb90ea 100644 --- a/modules/sdk-coin-ada/src/lib/transaction.ts +++ b/modules/sdk-coin-ada/src/lib/transaction.ts @@ -450,4 +450,5 @@ export class Transaction extends BaseTransaction { export interface SponsorshipInfo { feeAddress: string; feeAddressInputBalance: string; + isRebuild?: boolean; // hack to redirect the flow to the legacy build } diff --git a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts index c686a10f91..f511c0e58c 100644 --- a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts @@ -476,7 +476,14 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { /** @inheritdoc */ protected async buildImplementation(): Promise { - if (this._isTokenTransaction || (this._sponsorshipInfo && this._type === TransactionType.Send)) { + /** + * Fee address utxo reservation builds a new transaction that goes through legacy build + * rebuild flag is just a hack to redirect the flow to the legacy build + */ + if ( + this._isTokenTransaction || + (this._sponsorshipInfo && !this._sponsorshipInfo.isRebuild && this._type === TransactionType.Send) + ) { return this.processTokenBuild(); } const inputs = CardanoWasm.TransactionInputs.new(); @@ -598,6 +605,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { if (this._type !== TransactionType.Send) { vkeyWitnesses.add(vkeyWitness); } + if (this._sponsorshipInfo?.isRebuild) { + const sponsorPrv = CardanoWasm.PrivateKey.generate_ed25519(); + const sponsorVkeyWitness = CardanoWasm.make_vkey_witness(txHash, sponsorPrv); + vkeyWitnesses.add(sponsorVkeyWitness); + } } witnessSet.set_vkeys(vkeyWitnesses); diff --git a/modules/sdk-coin-ada/test/unit/tokenWithdrawal.ts b/modules/sdk-coin-ada/test/unit/tokenWithdrawal.ts index 424c658278..2dfde569d9 100644 --- a/modules/sdk-coin-ada/test/unit/tokenWithdrawal.ts +++ b/modules/sdk-coin-ada/test/unit/tokenWithdrawal.ts @@ -403,6 +403,101 @@ describe('ADA Token Operations', async () => { tx.getFee.should.equal('182485'); // Fee with two witnesses }); + it(`should rebuild a sponsored transaction from hex with isRebuild flag`, async () => { + const feeAddress = + 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'; + const quantity = '20'; + const senderInputBalance = 5000000; + const feeAddressInputBalance = 20000000; + const totalAssetList = { + [fingerprint]: { + quantity: '100', + policy_id: policyId, + asset_name: asciiEncodedName, + }, + }; + + // Step 1: Build the initial sponsored transaction + const txBuilder = factory.getTransferBuilder(); + txBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21', + transaction_index: 1, + }); + txBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba22', + transaction_index: 0, + }); + + txBuilder.output({ + address: receiverAddress, + amount: '0', + multiAssets: { + asset_name: asciiEncodedName, + policy_id: policyId, + quantity, + fingerprint, + }, + }); + + txBuilder.changeAddress(senderAddress, senderInputBalance.toString(), totalAssetList); + txBuilder.sponsorshipInfo({ + feeAddress: feeAddress, + feeAddressInputBalance: feeAddressInputBalance.toString(), + }); + txBuilder.ttl(800000000); + txBuilder.isTokenTransaction(); + const initialTx = (await txBuilder.build()) as Transaction; + const initialFee = initialTx.getFee; + const initialTxData = initialTx.toJson(); + + // Step 2: Rebuild with isRebuild = true + // This simulates rebuilding from scratch but with isRebuild flag to add sponsor witness + const rebuildTxBuilder = factory.getTransferBuilder(); + rebuildTxBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21', + transaction_index: 1, + }); + rebuildTxBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba22', + transaction_index: 0, + }); + + rebuildTxBuilder.output({ + address: receiverAddress, + amount: '0', + multiAssets: { + asset_name: asciiEncodedName, + policy_id: policyId, + quantity, + fingerprint, + }, + }); + + rebuildTxBuilder.changeAddress(senderAddress, senderInputBalance.toString(), totalAssetList); + rebuildTxBuilder.sponsorshipInfo({ + feeAddress: feeAddress, + feeAddressInputBalance: feeAddressInputBalance.toString(), + isRebuild: true, + }); + rebuildTxBuilder.ttl(800000000); + rebuildTxBuilder.isTokenTransaction(); + + const rebuiltTx = (await rebuildTxBuilder.build()) as Transaction; + const rebuiltTxData = rebuiltTx.toJson(); + + // Verify the rebuilt transaction preserves the same structure + rebuiltTxData.inputs.length.should.equal(initialTxData.inputs.length); + rebuiltTxData.outputs.length.should.equal(initialTxData.outputs.length); + + // Fee should be preserved from the original transaction + rebuiltTx.getFee.should.equal(initialFee); + + // Verify receiver output is preserved + const receiverOutput = rebuiltTxData.outputs.filter((output) => output.address === receiverAddress); + receiverOutput.length.should.equal(1); + receiverOutput[0].amount.should.equal('1500000'); + }); + describe('AdaToken verifyTransaction', () => { let bitgo: TestBitGoAPI; let adaToken;