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
61 changes: 36 additions & 25 deletions modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,26 +76,15 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
const txCredentials =
credentials.length > 0
? credentials
: exportTx.baseTx.inputs.map((input, inputIdx) => {
const transferInput = input.input as TransferInput;
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;

const utxo = this.transaction._utxos[inputIdx];

if (inputThreshold === this.transaction._threshold) {
return this.createCredentialForUtxo(utxo, this.transaction._threshold);
} else {
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < inputThreshold; i++) {
sigSlots.push(utils.createNewSig(''));
}
return new Credential(sigSlots);
}
: this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
return this.createCredentialForUtxo(utxo, utxoThreshold);
});

const addressMaps = txCredentials.map((credential, credIdx) =>
this.createAddressMapForUtxo(this.transaction._utxos[credIdx], this.transaction._threshold)
);
const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
return this.createAddressMapForUtxo(utxo, utxoThreshold);
});

const unsignedTx = new UnsignedTx(exportTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
this.transaction.setTransaction(unsignedTx);
Expand Down Expand Up @@ -167,29 +156,51 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
this.transaction._context
);

this.transaction.setTransaction(exportTx);
const flareUnsignedTx = exportTx as UnsignedTx;
const innerTx = flareUnsignedTx.getTx() as pvmSerial.ExportTx;

const utxosWithIndex = innerTx.baseTx.inputs.map((input, idx) => {
const transferInput = input.input as TransferInput;
const addressesIndex = transferInput.sigIndicies();
return {
...this.transaction._utxos[idx],
addressesIndex,
addresses: [],
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
};
});

const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));

const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));

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

this.transaction.setTransaction(fixedUnsignedTx);
}

/**
* Recover UTXOs from inputs
* Extract addressesIndex from sigIndicies for proper signature ordering
* @param inputs Array of TransferableInput
* @returns Array of decoded UTXO objects
*/
private recoverUtxos(inputs: TransferableInput[]): DecodedUtxoObj[] {
return inputs.map((input) => {
const utxoId = input.utxoID;
const transferInput = input.input as TransferInput;
const inputThreshold = transferInput.sigIndicies().length;
return {
const addressesIndex = transferInput.sigIndicies();

const utxo: DecodedUtxoObj = {
outputID: SECP256K1_Transfer_Output,
amount: input.amount().toString(),
txid: utils.cb58Encode(Buffer.from(utxoId.txID.toBytes())),
outputidx: utxoId.outputIdx.value().toString(),
threshold: inputThreshold || this.transaction._threshold,
addresses: this.transaction._fromAddresses.map((addr) =>
utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr))
),
threshold: addressesIndex.length || this.transaction._threshold,
addresses: [],
addressesIndex,
};
return utxo;
});
}
}
72 changes: 49 additions & 23 deletions modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,42 +74,48 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
fee: fee.toString(),
};

const firstUtxo = this.transaction._utxos[0];
let addressMap: FlareUtils.AddressMap;
if (
firstUtxo &&
firstUtxo.addresses &&
firstUtxo.addresses.length > 0 &&
this.transaction._fromAddresses &&
this.transaction._fromAddresses.length >= this.transaction._threshold
) {
addressMap = this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold);
} else {
addressMap = new FlareUtils.AddressMap();
if (this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold) {
this.transaction._fromAddresses.slice(0, this.transaction._threshold).forEach((addr, i) => {
const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
if (
utxo.addressesIndex &&
utxo.addressesIndex.length >= utxoThreshold &&
this.transaction._fromAddresses &&
this.transaction._fromAddresses.length >= utxoThreshold
) {
return this.createAddressMapForUtxo(utxo, utxoThreshold);
}
if (
utxo.addresses &&
utxo.addresses.length > 0 &&
this.transaction._fromAddresses &&
this.transaction._fromAddresses.length >= utxoThreshold
) {
return this.createAddressMapForUtxo(utxo, utxoThreshold);
}
const addressMap = new FlareUtils.AddressMap();
if (this.transaction._fromAddresses && this.transaction._fromAddresses.length >= utxoThreshold) {
this.transaction._fromAddresses.slice(0, utxoThreshold).forEach((addr, i) => {
addressMap.set(new Address(addr), i);
});
} else {
const toAddress = new Address(output.address.toBytes());
addressMap.set(toAddress, 0);
}
}
return addressMap;
});

const addressMaps = new FlareUtils.AddressMaps([addressMap]);
const flareAddressMaps = new FlareUtils.AddressMaps(addressMaps);

let txCredentials: Credential[];
if (credentials.length > 0) {
txCredentials = credentials;
} else {
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < inputThreshold; i++) {
emptySignatures.push(utils.createNewSig(''));
}
txCredentials = [new Credential(emptySignatures)];
txCredentials = this.transaction._utxos.map((utxo) =>
this.createCredentialForUtxo(utxo, utxo.threshold || this.transaction._threshold)
);
}

const unsignedTx = new UnsignedTx(baseTx, [], addressMaps, txCredentials);
const unsignedTx = new UnsignedTx(baseTx, [], flareAddressMaps, txCredentials);

this.transaction.setTransaction(unsignedTx);
return this;
Expand Down Expand Up @@ -175,7 +181,27 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
actualFeeNFlr
);

this.transaction.setTransaction(importTx);
const flareUnsignedTx = importTx as UnsignedTx;
const innerTx = flareUnsignedTx.getTx() as evmSerial.ImportTx;

const utxosWithIndex = innerTx.importedInputs.map((input, idx) => {
const transferInput = input.input as TransferInput;
const addressesIndex = transferInput.sigIndicies();
return {
...this.transaction._utxos[idx],
addressesIndex,
addresses: [],
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
};
});

const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));

const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));

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

this.transaction.setTransaction(fixedUnsignedTx);
}

/**
Expand Down
36 changes: 30 additions & 6 deletions modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,15 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
const txCredentials =
credentials.length > 0
? credentials
: this.transaction._utxos.map((utxo) => this.createCredentialForUtxo(utxo, this.transaction._threshold));

const addressMaps = this.transaction._utxos.map((utxo) =>
this.createAddressMapForUtxo(utxo, this.transaction._threshold)
);
: this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
return this.createCredentialForUtxo(utxo, utxoThreshold);
});

const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
return this.createAddressMapForUtxo(utxo, utxoThreshold);
});

const unsignedTx = new UnsignedTx(importTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);

Expand Down Expand Up @@ -186,7 +190,27 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
this.transaction._context
);

this.transaction.setTransaction(importTx);
const flareUnsignedTx = importTx as UnsignedTx;
const innerTx = flareUnsignedTx.getTx() as pvmSerial.ImportTx;

const utxosWithIndex = innerTx.ins.map((input, idx) => {
const transferInput = input.input as TransferInput;
const addressesIndex = transferInput.sigIndicies();
return {
...this.transaction._utxos[idx],
addressesIndex,
addresses: [],
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
};
});

const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));

const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));

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

this.transaction.setTransaction(fixedUnsignedTx);
}

/**
Expand Down
82 changes: 82 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,86 @@ describe('Flrp Export In P Tx Builder', () => {
err.message.should.be.equal('Private key cannot sign the transaction');
});
});

describe('addressesIndex extraction and signature ordering', () => {
it('should extract addressesIndex from parsed transaction inputs', async () => {
const txBuilder = factory.from(testData.halfSigntxHex);
const tx = await txBuilder.build();
const txJson = tx.toJson();

txJson.type.should.equal(22);
txJson.signatures.length.should.be.greaterThan(0);
});

it('should correctly handle fresh build with proper signature ordering', 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);

txBuilder.sign({ key: testData.privateKeys[2] });
const tx = await txBuilder.build();
const txJson = tx.toJson();

txJson.type.should.equal(22);
tx.toBroadcastFormat().should.be.a.String();
});

it('should correctly build and sign with different UTXO address ordering', async () => {
const reorderedUtxos = testData.utxos.map((utxo) => ({
...utxo,
addresses: [testData.pAddresses[1], testData.pAddresses[2], testData.pAddresses[0]],
}));

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(reorderedUtxos);

txBuilder.sign({ key: testData.privateKeys[2] });
const tx = await txBuilder.build();
const txJson = tx.toJson();

txJson.type.should.equal(22);
tx.toBroadcastFormat().should.be.a.String();
});

it('should handle parse-sign-parse-sign flow correctly', 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);

txBuilder.sign({ key: testData.privateKeys[2] });
const halfSignedTx = await txBuilder.build();
const halfSignedHex = halfSignedTx.toBroadcastFormat();

const txBuilder2 = factory.from(halfSignedHex);
txBuilder2.sign({ key: testData.privateKeys[0] });
const fullSignedTx = await txBuilder2.build();
const fullSignedJson = fullSignedTx.toJson();

fullSignedJson.type.should.equal(22);
fullSignedJson.signatures.length.should.be.greaterThan(0);
fullSignedTx.toBroadcastFormat().should.be.a.String();
});
});
});
37 changes: 37 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,41 @@ describe('Flrp Import In C Tx Builder', () => {
txJson.signatures.length.should.equal(0);
});
});

describe('fresh build with different UTXO address order for ImportInC', () => {
it('should correctly complete full sign flow with different UTXO address order for ImportInC', async () => {
const builder1 = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex);
const unsignedTx = await builder1.build();
const unsignedHex = unsignedTx.toBroadcastFormat();

const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex);
builder2.sign({ key: testData.privateKeys[2] });
const halfSignedTx = await builder2.build();
const halfSignedHex = halfSignedTx.toBroadcastFormat();

halfSignedTx.toJson().signatures.length.should.equal(1);

const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex);
builder3.sign({ key: testData.privateKeys[0] });
const fullSignedTx = await builder3.build();

fullSignedTx.toJson().signatures.length.should.equal(2);

const txId = fullSignedTx.id;
txId.should.be.a.String();
txId.length.should.be.greaterThan(0);
});

it('should handle ImportInC signing in different order and still produce valid tx', async () => {
const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex);

txBuilder.sign({ key: testData.privateKeys[0] });
txBuilder.sign({ key: testData.privateKeys[2] });

const tx = await txBuilder.build();
const txJson = tx.toJson();

txJson.signatures.length.should.equal(2);
});
});
});
Loading