Skip to content
Open
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
25 changes: 23 additions & 2 deletions modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,29 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {

this.computeAddressesIndexFromParsed();

// Use parsed credentials if available, otherwise create new ones based on sigIndices
// The sigIndices from the parsed transaction (stored in addressesIndex) determine
// the correct credential ordering for on-chain verification
const txCredentials =
credentials.length > 0
? credentials
: this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
// Use sigIndices-based method if we have valid sigIndices from parsed transaction
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createCredentialForUtxo(utxo, utxoThreshold);
});

// Create addressMaps using sigIndices from parsed transaction for consistency
const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createAddressMapForUtxo(utxo, utxoThreshold);
});

Expand Down Expand Up @@ -178,19 +191,27 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
}

const transferInput = input.input as TransferInput;
const actualSigIndices = transferInput.sigIndicies();

return {
...originalUtxo,
addressesIndex: originalUtxo.addressesIndex,
addresses: originalUtxo.addresses,
threshold: originalUtxo.threshold || this.transaction._threshold,
actualSigIndices,
};
});

this.transaction._utxos = utxosWithIndex;

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

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

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

Expand Down
29 changes: 24 additions & 5 deletions modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,31 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {

this.computeAddressesIndexFromParsed();

// Create addressMaps using sigIndices from parsed transaction for consistency
const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createAddressMapForUtxo(utxo, utxoThreshold);
});

const flareAddressMaps = new FlareUtils.AddressMaps(addressMaps);

// Use parsed credentials if available, otherwise create new ones based on sigIndices
let txCredentials: Credential[];
if (credentials.length > 0) {
txCredentials = credentials;
} else {
txCredentials = this.transaction._utxos.map((utxo) =>
this.createCredentialForUtxo(utxo, utxo.threshold || this.transaction._threshold)
);
txCredentials = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createCredentialForUtxo(utxo, utxoThreshold);
});
}

const unsignedTx = new UnsignedTx(baseTx, [], flareAddressMaps, txCredentials);
Expand Down Expand Up @@ -178,19 +189,27 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
}

const transferInput = input.input as TransferInput;
const actualSigIndices = transferInput.sigIndicies();

return {
...originalUtxo,
addressesIndex: originalUtxo.addressesIndex,
addresses: originalUtxo.addresses,
threshold: originalUtxo.threshold || this.transaction._threshold,
actualSigIndices,
};
});

this.transaction._utxos = utxosWithIndex;

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

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

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

Expand Down
25 changes: 23 additions & 2 deletions modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,29 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {

this.computeAddressesIndexFromParsed();

// Use parsed credentials if available, otherwise create new ones based on sigIndices
// The sigIndices from the parsed transaction (stored in addressesIndex) determine
// the correct credential ordering for on-chain verification
const txCredentials =
credentials.length > 0
? credentials
: this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
// Use sigIndices-based method if we have valid sigIndices from parsed transaction
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createCredentialForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createCredentialForUtxo(utxo, utxoThreshold);
});

// Create addressMaps using sigIndices from parsed transaction for consistency
const addressMaps = this.transaction._utxos.map((utxo) => {
const utxoThreshold = utxo.threshold || this.transaction._threshold;
const sigIndices = utxo.addressesIndex ?? [];
if (sigIndices.length >= utxoThreshold && sigIndices.every((idx) => idx >= 0)) {
return this.createAddressMapForUtxoWithSigIndices(utxo, utxoThreshold, sigIndices);
}
return this.createAddressMapForUtxo(utxo, utxoThreshold);
});

Expand Down Expand Up @@ -209,19 +222,27 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
throw new BuildTransactionError(`Could not find matching UTXO for input ${inputTxid}:${inputOutputIdx}`);
}

const transferInput = input.input as TransferInput;
const actualSigIndices = transferInput.sigIndicies();

return {
...originalUtxo,
addressesIndex: originalUtxo.addressesIndex,
addresses: originalUtxo.addresses,
threshold: originalUtxo.threshold || this.transaction._threshold,
actualSigIndices,
};
});

this.transaction._utxos = utxosWithIndex;

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

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

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

Expand Down
130 changes: 130 additions & 0 deletions modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,134 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {

return addressMap;
}

/**
* Create credential using the ACTUAL sigIndices from FlareJS.
*
* This method determines which sender addresses correspond to which sigIndex positions,
* then creates the credential with signatures in the correct order matching the sigIndices.
*
* sigIndices tell us which positions in the UTXO's owner addresses need to sign.
* We need to figure out which sender addresses are at those positions and create
* signature slots in the same order as sigIndices.
*
* @param utxo - The UTXO to create credential for
* @param threshold - Number of signatures required
* @param actualSigIndices - The actual sigIndices from FlareJS's built input
* @returns Credential with signatures ordered to match sigIndices
* @protected
*/
protected createCredentialForUtxoWithSigIndices(
utxo: DecodedUtxoObj,
threshold: number,
actualSigIndices: number[]
): Credential {
const sender = this.transaction._fromAddresses;
const addressesIndex = utxo.addressesIndex ?? [];

// either user (0) or recovery (2)
const firstIndex = this.recoverSigner ? 2 : 0;
const bitgoIndex = 1;

if (threshold === 1) {
if (sender && sender.length > firstIndex && addressesIndex[firstIndex] !== undefined) {
return new Credential([utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex'))]);
}
return new Credential([utils.createNewSig('')]);
}

// For threshold >= 2, use the actual sigIndices order from FlareJS
// sigIndices[i] = position in UTXO's owner addresses that needs to sign
// addressesIndex[senderIdx] = position in UTXO's owner addresses for that sender
//
// We need to find which sender corresponds to each sigIndex and create signatures
// in the sigIndices order.
if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) {
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];

for (const sigIdx of actualSigIndices) {
// Find which sender address is at this UTXO position
// addressesIndex[senderIdx] tells us which UTXO position each sender is at
const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx);

if (senderIdx === bitgoIndex) {
// This sigIndex slot is for BitGo (HSM) - empty signature
emptySignatures.push(utils.createNewSig(''));
} else if (senderIdx === firstIndex) {
// This sigIndex slot is for user/recovery - embed their address
emptySignatures.push(utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')));
} else {
// Fallback for unknown sender - empty signature
emptySignatures.push(utils.createNewSig(''));
}
}

return new Credential(emptySignatures);
}

// Fallback: create threshold empty signatures
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < threshold; i++) {
emptySignatures.push(utils.createNewSig(''));
}
return new Credential(emptySignatures);
}

/**
* Create AddressMap using the ACTUAL sigIndices from FlareJS.
*
* Maps sender addresses to signature slots based on the actual sigIndices order.
*
* @param utxo - The UTXO to create AddressMap for
* @param threshold - Number of signatures required
* @param actualSigIndices - The actual sigIndices from FlareJS's built input
* @returns AddressMap that maps addresses to signature slots
* @protected
*/
protected createAddressMapForUtxoWithSigIndices(
utxo: DecodedUtxoObj,
threshold: number,
actualSigIndices: number[]
): FlareUtils.AddressMap {
const addressMap = new FlareUtils.AddressMap();
const sender = this.transaction._fromAddresses;
const addressesIndex = utxo.addressesIndex ?? [];

const firstIndex = this.recoverSigner ? 2 : 0;
const bitgoIndex = 1;

if (threshold === 1) {
if (sender && sender.length > firstIndex) {
addressMap.set(new Address(sender[firstIndex]), 0);
} else if (sender && sender.length > 0) {
addressMap.set(new Address(sender[0]), 0);
}
return addressMap;
}

// For threshold >= 2, map addresses based on actual sigIndices order
if (actualSigIndices.length >= 2 && addressesIndex.length >= 2 && sender && sender.length >= threshold) {
actualSigIndices.forEach((sigIdx, slotIdx) => {
// Find which sender is at this UTXO position
const senderIdx = addressesIndex.findIndex((utxoPos) => utxoPos === sigIdx);

if (senderIdx === bitgoIndex) {
addressMap.set(new Address(sender[bitgoIndex]), slotIdx);
} else if (senderIdx === firstIndex) {
addressMap.set(new Address(sender[firstIndex]), slotIdx);
}
});

return addressMap;
}

// Fallback
if (sender && sender.length >= threshold) {
sender.slice(0, threshold).forEach((addr, i) => {
addressMap.set(new Address(addr), i);
});
}

return addressMap;
}
}
16 changes: 13 additions & 3 deletions modules/sdk-coin-flrp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,16 @@ export class Transaction extends BaseTransaction {
const signature = await secp256k1.sign(unsignedBytes, prv);
let signatureSet = false;

if (hasMatchingAddress) {
// Check if any credential has embedded addresses - if so, we MUST use address-based matching
const hasEmbeddedAddressesInCredentials = unsignedTx.credentials.some((credential) => {
const signatures = credential.getSignatures();
return signatures.some((sig) => isEmptySignature(sig) && hasEmbeddedAddress(sig));
});

// Use address-based slot matching if:
// 1. addressMap contains matching address, OR
// 2. credentials have embedded addresses (prioritize this to ensure correct slot placement)
if (hasMatchingAddress || hasEmbeddedAddressesInCredentials) {
// Use address-based slot matching (like AVAX-P)
let checkSign: CheckSignature | undefined = undefined;

Expand All @@ -209,8 +218,9 @@ export class Transaction extends BaseTransaction {
}

// Fallback: If address-based matching didn't work (e.g., ImportInC loaded from unsigned tx
// where P-chain addresses aren't in addressMaps), sign ALL empty slots across ALL credentials.
// This handles multisig where each UTXO needs a credential signed by the same key.
// where P-chain addresses aren't in addressMaps AND no embedded addresses), sign ALL empty
// slots across ALL credentials. This handles multisig where each UTXO needs a credential
// signed by the same key.
if (!signatureSet) {
for (const credential of unsignedTx.credentials) {
const signatures = credential.getSignatures();
Expand Down
8 changes: 7 additions & 1 deletion modules/sdk-coin-flrp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,14 @@ export class Utils implements BaseUtils {
/**
* Sort addresses lexicographically by their byte representation.
* This matches how addresses are stored on-chain in Avalanche/Flare P-chain UTXOs.
*
* IMPORTANT: This sorting MUST be consistent with FlareJS's internal address sorting.
* FlareJS uses the same lexicographic comparison: `hexA.localeCompare(hexB)`.
* The sigIndices in transaction inputs depend on this sorted order, so any deviation
* would cause signature order mismatches and on-chain verification failures.
*
* @param addresses - Array of bech32 address strings (e.g., "P-costwo1...")
* @returns Array of addresses sorted by hex value
* @returns Array of addresses sorted by hex value (ascending lexicographic order)
*/
public sortAddressesByHex(addresses: string[]): string[] {
return [...addresses].sort((a, b) => {
Expand Down
Loading