Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,28 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
const flareUnsignedTx = exportTx as UnsignedTx;
const innerTx = flareUnsignedTx.getTx() as pvmSerial.ExportTx;

const utxosWithIndex = innerTx.baseTx.inputs.map((input) => {
const changeOutputs = innerTx.baseTx.outputs;
let correctedExportTx: pvmSerial.ExportTx = innerTx;

if (changeOutputs.length > 0) {
const correctedChangeOutputs = changeOutputs.map((output) => {
const transferOut = output.output as TransferOutput;
const originalOwners = transferOut.outputOwners;

const assetIdStr = utils.flareIdString(Buffer.from(output.assetId.toBytes()).toString('hex')).toString();
return TransferableOutput.fromNative(
assetIdStr,
transferOut.amount(),
originalOwners.addrs.map((addr) => Buffer.from(addr.toBytes())),
this.transaction._locktime,
this.transaction._threshold
);
});

correctedExportTx = this.createCorrectedExportTx(innerTx, correctedChangeOutputs);
}

const utxosWithIndex = correctedExportTx.baseTx.inputs.map((input) => {
const inputTxid = utils.cb58Encode(Buffer.from(input.utxoID.txID.toBytes()));
const inputOutputIdx = input.utxoID.outputIdx.value().toString();

Expand Down Expand Up @@ -213,11 +234,46 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
this.createAddressMapForUtxoWithSigIndices(utxo, utxo.threshold, utxo.actualSigIndices)
);

const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
const fixedUnsignedTx = new UnsignedTx(
correctedExportTx,
[],
new FlareUtils.AddressMaps(addressMaps),
txCredentials
);

this.transaction.setTransaction(fixedUnsignedTx);
}

/**
* Create a corrected ExportTx with the given change outputs.
* This is necessary because FlareJS's newExportTx doesn't support setting
* the threshold and locktime for change outputs.
*
* FlareJS declares baseTx.outputs as readonly, so we use Object.defineProperty
* to override the property with the corrected outputs. This is a workaround until
* FlareJS adds proper support for change output thresholds.
*
* @param originalTx - The original ExportTx
* @param correctedOutputs - The corrected change outputs with proper threshold
* @returns A new ExportTx with the corrected change outputs
*/
private createCorrectedExportTx(
originalTx: pvmSerial.ExportTx,
correctedOutputs: TransferableOutput[]
): pvmSerial.ExportTx {
// FlareJS declares baseTx.outputs as `public readonly outputs: readonly TransferableOutput[]`
// We use Object.defineProperty to override the readonly property with our corrected outputs.
// This is necessary because FlareJS's newExportTx doesn't support change output threshold/locktime.
Object.defineProperty(originalTx.baseTx, 'outputs', {
value: correctedOutputs,
writable: false,
enumerable: true,
configurable: true,
});

return originalTx;
}

/**
* Recover UTXOs from inputs.
* Uses output addresses as proxy for UTXO addresses.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// Test data for export with single UTXO
// Transaction ID: 2R4iE6sX6BtAeTNrVdzifczs7qRGQw3yiaFTXaw9j9A4R6D5FW
export const EXPORT_IN_P = {
txhash: '2R4iE6sX6BtAeTNrVdzifczs7qRGQw3yiaFTXaw9j9A4R6D5FW',
txhash: '2HN4sSQeyS1T8ztWdsVv4Cyj3D8zJxGTMaSZeqWVmBfJEiYsiQ',
// Unsigned tx from script (with empty signatures + credential structure)
unsignedHex:
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650012845ffc4',
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f910000000200000009000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001000000090000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf36650016aec5964',
halfSigntxHex:
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000184918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a00000000090000000284918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001f945a1ff',
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000020000000900000001ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000900000002ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e918a5e8083ae4c9f2f0ed77055c24bf3665001329c4a92',
fullSigntxHex:
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000001000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002000000090000000184918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a00000000090000000284918d7e399e192bf14262c9f9622feb13f412e987f6c411b90215175e4ef4d61c629593188d703e53579a88cef6973414b7c2bef10c582ea883b1767be2622a0067902f8f061a628182a3ec1d768b100a3d13fcc01966a993c3300bf3cb5d3dc32870442f83c67741d22f5e7a1168f5ef7f22f26d7b62a183528fcbfd0fdede9300f1e3d1ac',
'0x0000000000120000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000000daba24000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f9100000002b489f3c616329c3d56a29c24702c348522e87e1ad0e23f02fbb405be599fbae30000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd0000000500000000015d35f30000000100000000e65421a9e3307ed1f644e6e44855f94e01c35ea2bce2cdd9005a77e48decbd5c0000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002e7b2b80000000200000000000000010000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da55524790000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000003473bc0000000000000000000000002000000033329be7d01cd3ebaae6654d7327dd9f17a2e15817e918a5e8083ae4c9f2f0ed77055c24bf3665001c7324437c96c7c8a6a152da2385c1db5c3ab1f91000000020000000900000001ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000000000900000002ef504cec2443f9f68078976afa43eb518d7c6b8455389a743fe25fdde1b8e04f5bc94002efbdbd4a1feb3f5739f944970484336ae51d6aff3802493b0014e85c000fd603ba6d1c63b84c0bfb50e8a2eef0a745379fbf687a09d15f2b411907eb886d725ff52060e7863258c5abe506a690446923ec9f00b97487c2df781aea88ab0181fe5c0a',
amount: '55000000', // 0.055 (0.05 FLR + 0.005 FLR fee)
pAddresses: [
'P-costwo106gc5h5qswhye8e0pmthq4wzf0ekv5qppsrvpu',
Expand Down
138 changes: 138 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EXPORT_IN_P as testData } from '../../resources/transactionData/exportI
import { TransactionBuilderFactory } from '../../../src/lib';
import { coins } from '@bitgo/statics';
import signFlowTest from './signFlowTestSuit';
import { pvmSerial, UnsignedTx, TransferOutput } from '@flarenetwork/flarejs';

describe('Flrp Export In P Tx Builder', () => {
const coinConfig = coins.get('tflrp');
Expand Down Expand Up @@ -364,4 +365,141 @@ describe('Flrp Export In P Tx Builder', () => {
fullSignedTx.toBroadcastFormat().should.be.a.String();
});
});

describe('Change output threshold fix', () => {
/**
* This test suite verifies the fix for the change output threshold bug.
*
* The issue: FlareJS's pvm.e.newExportTx() defaults change outputs to threshold=1,
* but for multisig wallets we need threshold=2 to maintain proper security.
*
* The fix: After building the transaction, we correct the change outputs to use
* the wallet's threshold (typically 2 for multisig).
*/

it('should create change output with threshold=2 for multisig wallets', async () => {
const txBuilder = factory
.getExportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.amount(testData.amount)
.externalChainId(testData.sourceChainId)
.feeState(testData.feeState)
.context(testData.context)
.decodedUtxos(testData.utxos);

const tx = await txBuilder.build();

const flareTransaction = (tx as any)._flareTransaction as UnsignedTx;
const innerTx = flareTransaction.getTx() as pvmSerial.ExportTx;
const changeOutputs = innerTx.baseTx.outputs;

changeOutputs.length.should.be.greaterThan(0);

changeOutputs.forEach((output, index) => {
const transferOut = output.output as TransferOutput;
const threshold = transferOut.outputOwners.threshold.value();

threshold.should.equal(
testData.threshold,
`Change output ${index} should have threshold=${testData.threshold}, but has threshold=${threshold}`
);
});
});

it('should create change output with correct locktime for multisig wallets', async () => {
const txBuilder = factory
.getExportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.amount(testData.amount)
.externalChainId(testData.sourceChainId)
.feeState(testData.feeState)
.context(testData.context)
.decodedUtxos(testData.utxos);

const tx = await txBuilder.build();

const flareTransaction = (tx as any)._flareTransaction as UnsignedTx;
const innerTx = flareTransaction.getTx() as pvmSerial.ExportTx;
const changeOutputs = innerTx.baseTx.outputs;

changeOutputs.length.should.be.greaterThan(0);

changeOutputs.forEach((output, index) => {
const transferOut = output.output as TransferOutput;
const locktime = transferOut.outputOwners.locktime.value();

locktime.should.equal(
BigInt(testData.locktime),
`Change output ${index} should have locktime=${testData.locktime}, but has locktime=${locktime}`
);
});
});

it('should maintain threshold=2 after parsing and rebuilding', async () => {
const txBuilder = factory
.getExportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.amount(testData.amount)
.externalChainId(testData.sourceChainId)
.feeState(testData.feeState)
.context(testData.context)
.decodedUtxos(testData.utxos);

const tx = await txBuilder.build();
const txHex = tx.toBroadcastFormat();

const parsedTxBuilder = factory.from(txHex);
const parsedTx = await parsedTxBuilder.build();

const flareTransaction = (parsedTx as any)._flareTransaction as UnsignedTx;
const innerTx = flareTransaction.getTx() as pvmSerial.ExportTx;
const changeOutputs = innerTx.baseTx.outputs;

changeOutputs.length.should.be.greaterThan(0);

changeOutputs.forEach((output, index) => {
const transferOut = output.output as TransferOutput;
const threshold = transferOut.outputOwners.threshold.value();

threshold.should.equal(
testData.threshold,
`After parsing, change output ${index} should have threshold=${testData.threshold}, but has threshold=${threshold}`
);
});
});

it('should have change output addresses matching wallet addresses', async () => {
const txBuilder = factory
.getExportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.amount(testData.amount)
.externalChainId(testData.sourceChainId)
.feeState(testData.feeState)
.context(testData.context)
.decodedUtxos(testData.utxos);

const tx = await txBuilder.build();

const flareTransaction = (tx as any)._flareTransaction as UnsignedTx;
const innerTx = flareTransaction.getTx() as pvmSerial.ExportTx;
const changeOutputs = innerTx.baseTx.outputs;

changeOutputs.length.should.be.greaterThan(0);

changeOutputs.forEach((output) => {
const transferOut = output.output as TransferOutput;
const addresses = transferOut.outputOwners.addrs;

addresses.length.should.equal(3, 'Change output should have 3 addresses for multisig wallet');
});
});
});
});