Skip to content

Commit 56ab36f

Browse files
committed
refactor(sdk-coin-flrp): streamline txn builders to utilize addressesIndex
Ticket: WIN-8657
1 parent d09e37d commit 56ab36f

6 files changed

Lines changed: 353 additions & 54 deletions

File tree

modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,15 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
7676
const txCredentials =
7777
credentials.length > 0
7878
? credentials
79-
: exportTx.baseTx.inputs.map((input, inputIdx) => {
80-
const transferInput = input.input as TransferInput;
81-
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;
82-
83-
const utxo = this.transaction._utxos[inputIdx];
84-
85-
if (inputThreshold === this.transaction._threshold) {
86-
return this.createCredentialForUtxo(utxo, this.transaction._threshold);
87-
} else {
88-
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
89-
for (let i = 0; i < inputThreshold; i++) {
90-
sigSlots.push(utils.createNewSig(''));
91-
}
92-
return new Credential(sigSlots);
93-
}
79+
: this.transaction._utxos.map((utxo) => {
80+
const utxoThreshold = utxo.threshold || this.transaction._threshold;
81+
return this.createCredentialForUtxo(utxo, utxoThreshold);
9482
});
9583

96-
const addressMaps = txCredentials.map((credential, credIdx) =>
97-
this.createAddressMapForUtxo(this.transaction._utxos[credIdx], this.transaction._threshold)
98-
);
84+
const addressMaps = this.transaction._utxos.map((utxo) => {
85+
const utxoThreshold = utxo.threshold || this.transaction._threshold;
86+
return this.createAddressMapForUtxo(utxo, utxoThreshold);
87+
});
9988

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

170-
this.transaction.setTransaction(exportTx);
159+
const flareUnsignedTx = exportTx as UnsignedTx;
160+
const innerTx = flareUnsignedTx.getTx() as pvmSerial.ExportTx;
161+
162+
const utxosWithIndex = innerTx.baseTx.inputs.map((input, idx) => {
163+
const transferInput = input.input as TransferInput;
164+
const addressesIndex = transferInput.sigIndicies();
165+
return {
166+
...this.transaction._utxos[idx],
167+
addressesIndex,
168+
addresses: [],
169+
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
170+
};
171+
});
172+
173+
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
174+
175+
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
176+
177+
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
178+
179+
this.transaction.setTransaction(fixedUnsignedTx);
171180
}
172181

173182
/**
174183
* Recover UTXOs from inputs
184+
* Extract addressesIndex from sigIndicies for proper signature ordering
175185
* @param inputs Array of TransferableInput
176186
* @returns Array of decoded UTXO objects
177187
*/
178188
private recoverUtxos(inputs: TransferableInput[]): DecodedUtxoObj[] {
179189
return inputs.map((input) => {
180190
const utxoId = input.utxoID;
181191
const transferInput = input.input as TransferInput;
182-
const inputThreshold = transferInput.sigIndicies().length;
183-
return {
192+
const addressesIndex = transferInput.sigIndicies();
193+
194+
const utxo: DecodedUtxoObj = {
184195
outputID: SECP256K1_Transfer_Output,
185196
amount: input.amount().toString(),
186197
txid: utils.cb58Encode(Buffer.from(utxoId.txID.toBytes())),
187198
outputidx: utxoId.outputIdx.value().toString(),
188-
threshold: inputThreshold || this.transaction._threshold,
189-
addresses: this.transaction._fromAddresses.map((addr) =>
190-
utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr))
191-
),
199+
threshold: addressesIndex.length || this.transaction._threshold,
200+
addresses: [],
201+
addressesIndex,
192202
};
203+
return utxo;
193204
});
194205
}
195206
}

modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,42 +74,48 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
7474
fee: fee.toString(),
7575
};
7676

77-
const firstUtxo = this.transaction._utxos[0];
78-
let addressMap: FlareUtils.AddressMap;
79-
if (
80-
firstUtxo &&
81-
firstUtxo.addresses &&
82-
firstUtxo.addresses.length > 0 &&
83-
this.transaction._fromAddresses &&
84-
this.transaction._fromAddresses.length >= this.transaction._threshold
85-
) {
86-
addressMap = this.createAddressMapForUtxo(firstUtxo, this.transaction._threshold);
87-
} else {
88-
addressMap = new FlareUtils.AddressMap();
89-
if (this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold) {
90-
this.transaction._fromAddresses.slice(0, this.transaction._threshold).forEach((addr, i) => {
77+
const addressMaps = this.transaction._utxos.map((utxo) => {
78+
const utxoThreshold = utxo.threshold || this.transaction._threshold;
79+
if (
80+
utxo.addressesIndex &&
81+
utxo.addressesIndex.length >= utxoThreshold &&
82+
this.transaction._fromAddresses &&
83+
this.transaction._fromAddresses.length >= utxoThreshold
84+
) {
85+
return this.createAddressMapForUtxo(utxo, utxoThreshold);
86+
}
87+
if (
88+
utxo.addresses &&
89+
utxo.addresses.length > 0 &&
90+
this.transaction._fromAddresses &&
91+
this.transaction._fromAddresses.length >= utxoThreshold
92+
) {
93+
return this.createAddressMapForUtxo(utxo, utxoThreshold);
94+
}
95+
const addressMap = new FlareUtils.AddressMap();
96+
if (this.transaction._fromAddresses && this.transaction._fromAddresses.length >= utxoThreshold) {
97+
this.transaction._fromAddresses.slice(0, utxoThreshold).forEach((addr, i) => {
9198
addressMap.set(new Address(addr), i);
9299
});
93100
} else {
94101
const toAddress = new Address(output.address.toBytes());
95102
addressMap.set(toAddress, 0);
96103
}
97-
}
104+
return addressMap;
105+
});
98106

99-
const addressMaps = new FlareUtils.AddressMaps([addressMap]);
107+
const flareAddressMaps = new FlareUtils.AddressMaps(addressMaps);
100108

101109
let txCredentials: Credential[];
102110
if (credentials.length > 0) {
103111
txCredentials = credentials;
104112
} else {
105-
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
106-
for (let i = 0; i < inputThreshold; i++) {
107-
emptySignatures.push(utils.createNewSig(''));
108-
}
109-
txCredentials = [new Credential(emptySignatures)];
113+
txCredentials = this.transaction._utxos.map((utxo) =>
114+
this.createCredentialForUtxo(utxo, utxo.threshold || this.transaction._threshold)
115+
);
110116
}
111117

112-
const unsignedTx = new UnsignedTx(baseTx, [], addressMaps, txCredentials);
118+
const unsignedTx = new UnsignedTx(baseTx, [], flareAddressMaps, txCredentials);
113119

114120
this.transaction.setTransaction(unsignedTx);
115121
return this;
@@ -175,7 +181,27 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
175181
actualFeeNFlr
176182
);
177183

178-
this.transaction.setTransaction(importTx);
184+
const flareUnsignedTx = importTx as UnsignedTx;
185+
const innerTx = flareUnsignedTx.getTx() as evmSerial.ImportTx;
186+
187+
const utxosWithIndex = innerTx.importedInputs.map((input, idx) => {
188+
const transferInput = input.input as TransferInput;
189+
const addressesIndex = transferInput.sigIndicies();
190+
return {
191+
...this.transaction._utxos[idx],
192+
addressesIndex,
193+
addresses: [],
194+
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
195+
};
196+
});
197+
198+
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
199+
200+
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
201+
202+
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
203+
204+
this.transaction.setTransaction(fixedUnsignedTx);
179205
}
180206

181207
/**

modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,15 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
9595
const txCredentials =
9696
credentials.length > 0
9797
? credentials
98-
: this.transaction._utxos.map((utxo) => this.createCredentialForUtxo(utxo, this.transaction._threshold));
99-
100-
const addressMaps = this.transaction._utxos.map((utxo) =>
101-
this.createAddressMapForUtxo(utxo, this.transaction._threshold)
102-
);
98+
: this.transaction._utxos.map((utxo) => {
99+
const utxoThreshold = utxo.threshold || this.transaction._threshold;
100+
return this.createCredentialForUtxo(utxo, utxoThreshold);
101+
});
102+
103+
const addressMaps = this.transaction._utxos.map((utxo) => {
104+
const utxoThreshold = utxo.threshold || this.transaction._threshold;
105+
return this.createAddressMapForUtxo(utxo, utxoThreshold);
106+
});
103107

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

@@ -186,7 +190,27 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
186190
this.transaction._context
187191
);
188192

189-
this.transaction.setTransaction(importTx);
193+
const flareUnsignedTx = importTx as UnsignedTx;
194+
const innerTx = flareUnsignedTx.getTx() as pvmSerial.ImportTx;
195+
196+
const utxosWithIndex = innerTx.ins.map((input, idx) => {
197+
const transferInput = input.input as TransferInput;
198+
const addressesIndex = transferInput.sigIndicies();
199+
return {
200+
...this.transaction._utxos[idx],
201+
addressesIndex,
202+
addresses: [],
203+
threshold: addressesIndex.length || this.transaction._utxos[idx].threshold,
204+
};
205+
});
206+
207+
const txCredentials = utxosWithIndex.map((utxo) => this.createCredentialForUtxo(utxo, utxo.threshold));
208+
209+
const addressMaps = utxosWithIndex.map((utxo) => this.createAddressMapForUtxo(utxo, utxo.threshold));
210+
211+
const fixedUnsignedTx = new UnsignedTx(innerTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
212+
213+
this.transaction.setTransaction(fixedUnsignedTx);
190214
}
191215

192216
/**

modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,86 @@ describe('Flrp Export In P Tx Builder', () => {
147147
err.message.should.be.equal('Private key cannot sign the transaction');
148148
});
149149
});
150+
151+
describe('addressesIndex extraction and signature ordering', () => {
152+
it('should extract addressesIndex from parsed transaction inputs', async () => {
153+
const txBuilder = factory.from(testData.halfSigntxHex);
154+
const tx = await txBuilder.build();
155+
const txJson = tx.toJson();
156+
157+
txJson.type.should.equal(22);
158+
txJson.signatures.length.should.be.greaterThan(0);
159+
});
160+
161+
it('should correctly handle fresh build with proper signature ordering', async () => {
162+
const txBuilder = factory
163+
.getExportInPBuilder()
164+
.threshold(testData.threshold)
165+
.locktime(testData.locktime)
166+
.fromPubKey(testData.pAddresses)
167+
.amount(testData.amount)
168+
.externalChainId(testData.sourceChainId)
169+
.feeState(testData.feeState)
170+
.context(testData.context)
171+
.decodedUtxos(testData.utxos);
172+
173+
txBuilder.sign({ key: testData.privateKeys[2] });
174+
const tx = await txBuilder.build();
175+
const txJson = tx.toJson();
176+
177+
txJson.type.should.equal(22);
178+
tx.toBroadcastFormat().should.be.a.String();
179+
});
180+
181+
it('should correctly build and sign with different UTXO address ordering', async () => {
182+
const reorderedUtxos = testData.utxos.map((utxo) => ({
183+
...utxo,
184+
addresses: [testData.pAddresses[1], testData.pAddresses[2], testData.pAddresses[0]],
185+
}));
186+
187+
const txBuilder = factory
188+
.getExportInPBuilder()
189+
.threshold(testData.threshold)
190+
.locktime(testData.locktime)
191+
.fromPubKey(testData.pAddresses)
192+
.amount(testData.amount)
193+
.externalChainId(testData.sourceChainId)
194+
.feeState(testData.feeState)
195+
.context(testData.context)
196+
.decodedUtxos(reorderedUtxos);
197+
198+
txBuilder.sign({ key: testData.privateKeys[2] });
199+
const tx = await txBuilder.build();
200+
const txJson = tx.toJson();
201+
202+
txJson.type.should.equal(22);
203+
tx.toBroadcastFormat().should.be.a.String();
204+
});
205+
206+
it('should handle parse-sign-parse-sign flow correctly', async () => {
207+
const txBuilder = factory
208+
.getExportInPBuilder()
209+
.threshold(testData.threshold)
210+
.locktime(testData.locktime)
211+
.fromPubKey(testData.pAddresses)
212+
.amount(testData.amount)
213+
.externalChainId(testData.sourceChainId)
214+
.feeState(testData.feeState)
215+
.context(testData.context)
216+
.decodedUtxos(testData.utxos);
217+
218+
txBuilder.sign({ key: testData.privateKeys[2] });
219+
const halfSignedTx = await txBuilder.build();
220+
const halfSignedHex = halfSignedTx.toBroadcastFormat();
221+
222+
const txBuilder2 = factory.from(halfSignedHex);
223+
txBuilder2.sign({ key: testData.privateKeys[0] });
224+
const fullSignedTx = await txBuilder2.build();
225+
const fullSignedJson = fullSignedTx.toJson();
226+
227+
fullSignedJson.type.should.equal(22);
228+
fullSignedJson.signatures.length.should.be.greaterThan(0);
229+
fullSignedTx.toBroadcastFormat().should.be.a.String();
230+
});
231+
});
150232
});

modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,41 @@ describe('Flrp Import In C Tx Builder', () => {
208208
txJson.signatures.length.should.equal(0);
209209
});
210210
});
211+
212+
describe('fresh build with different UTXO address order for ImportInC', () => {
213+
it('should correctly complete full sign flow with different UTXO address order for ImportInC', async () => {
214+
const builder1 = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex);
215+
const unsignedTx = await builder1.build();
216+
const unsignedHex = unsignedTx.toBroadcastFormat();
217+
218+
const builder2 = new TransactionBuilderFactory(coins.get('tflrp')).from(unsignedHex);
219+
builder2.sign({ key: testData.privateKeys[2] });
220+
const halfSignedTx = await builder2.build();
221+
const halfSignedHex = halfSignedTx.toBroadcastFormat();
222+
223+
halfSignedTx.toJson().signatures.length.should.equal(1);
224+
225+
const builder3 = new TransactionBuilderFactory(coins.get('tflrp')).from(halfSignedHex);
226+
builder3.sign({ key: testData.privateKeys[0] });
227+
const fullSignedTx = await builder3.build();
228+
229+
fullSignedTx.toJson().signatures.length.should.equal(2);
230+
231+
const txId = fullSignedTx.id;
232+
txId.should.be.a.String();
233+
txId.length.should.be.greaterThan(0);
234+
});
235+
236+
it('should handle ImportInC signing in different order and still produce valid tx', async () => {
237+
const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex);
238+
239+
txBuilder.sign({ key: testData.privateKeys[0] });
240+
txBuilder.sign({ key: testData.privateKeys[2] });
241+
242+
const tx = await txBuilder.build();
243+
const txJson = tx.toJson();
244+
245+
txJson.signatures.length.should.equal(2);
246+
});
247+
});
211248
});

0 commit comments

Comments
 (0)