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
98 changes: 82 additions & 16 deletions modules/sdk-coin-flrp/src/lib/ExportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,57 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));

// When credentials were extracted, use them directly to preserve existing signatures
// Otherwise, create empty credentials with embedded addresses for slot identification
// Otherwise, create empty credentials with dynamic ordering based on addressesIndex
// Match avaxp behavior: order depends on UTXO address positions
const txCredentials =
credentials.length > 0
? credentials
: exportTx.baseTx.inputs.map((input) => {
: exportTx.baseTx.inputs.map((input, inputIdx) => {
const transferInput = input.input as TransferInput;
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;
// Create empty signatures with embedded addresses for slot identification
const sigSlots: ReturnType<typeof utils.createEmptySigWithAddress>[] = [];
for (let i = 0; i < inputThreshold; i++) {
const addrHex = Buffer.from(sortedAddresses[i]).toString('hex');
sigSlots.push(utils.createEmptySigWithAddress(addrHex));

// Get UTXO for this input to determine addressesIndex
const utxo = this.transaction._utxos[inputIdx];

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

// If UTXO has addresses, compute dynamic ordering
if (utxo && utxo.addresses && utxo.addresses.length > 0) {
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
const addressesIndex = this.transaction._fromAddresses.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

// Dynamic ordering based on addressesIndex
let sigSlots: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first: [zeros, userAddress]
sigSlots = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
),
];
} else {
// User comes first: [userAddress, zeros]
sigSlots = [
utils.createEmptySigWithAddress(
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
),
utils.createNewSig(''),
];
}
return new Credential(sigSlots);
} else {
// Fallback: use all zeros if no UTXO addresses available
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < inputThreshold; i++) {
sigSlots.push(utils.createNewSig(''));
}
return new Credential(sigSlots);
}
return new Credential(sigSlots);
});

// Create address maps for signing - one per input/credential
Expand Down Expand Up @@ -277,14 +314,43 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {

inputs.push(transferableInput);

// Create credential with empty signatures that have embedded addresses for slot identification
// This allows the signing logic to determine which slot belongs to which address
const sortedAddrs = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
const emptySignatures = sigIndices.map((idx) => {
const addrHex = Buffer.from(sortedAddrs[idx]).toString('hex');
return utils.createEmptySigWithAddress(addrHex);
});
credentials.push(new Credential(emptySignatures));
// Create credential with empty signatures for slot identification
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
const hasAddresses =
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;

if (!hasAddresses) {
// If addresses not available, use all zeros
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
} else {
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
const addressesIndex = this.transaction._fromAddresses.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

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

// Dynamic ordering based on addressesIndex
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first in signature order: [zeros, userAddress]
emptySignatures = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
];
} else {
// User comes first in signature order: [userAddress, zeros]
emptySignatures = [
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
utils.createNewSig(''),
];
}
credentials.push(new Credential(emptySignatures));
}
}

// Create change output if there is remaining amount after export and fee
Expand Down
44 changes: 39 additions & 5 deletions modules/sdk-coin-flrp/src/lib/ImportInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,12 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
const addressMaps = new FlareUtils.AddressMaps([addressMap]);

// When credentials were extracted, use them directly to preserve existing signatures
// For initBuilder, _fromAddresses may not be set yet, so use all zeros for credential slots
let txCredentials: Credential[];
if (credentials.length > 0) {
txCredentials = credentials;
} else {
// Create empty credential with threshold number of signature slots
// Create empty credential with threshold number of signature slots (all zeros)
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < inputThreshold; i++) {
emptySignatures.push(utils.createNewSig(''));
Expand Down Expand Up @@ -227,10 +228,43 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {

inputs.push(transferableInput);

// Create empty credential for each input with threshold signers
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
const credential = new Credential(emptySignatures);
credentials.push(credential);
// Create credential with empty signatures for slot identification
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
const hasAddresses =
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;

if (!hasAddresses) {
// If addresses not available, use all zeros
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
} else {
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
const addressesIndex = this.transaction._fromAddresses.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

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

// Dynamic ordering based on addressesIndex
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first in signature order: [zeros, userAddress]
emptySignatures = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
];
} else {
// User comes first in signature order: [userAddress, zeros]
emptySignatures = [
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
utils.createNewSig(''),
];
}
credentials.push(new Credential(emptySignatures));
}
});

return {
Expand Down
83 changes: 79 additions & 4 deletions modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,51 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
const addressMaps = sortedAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]]));

// When credentials were extracted, use them directly to preserve existing signatures
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
const txCredentials =
credentials.length > 0
? credentials
: [new Credential(sortedAddresses.slice(0, this.transaction._threshold).map(() => utils.createNewSig('')))];
: this.transaction._utxos.map((utxo) => {
// either user (0) or recovery (2)
const firstIndex = this.recoverSigner ? 2 : 0;
const bitgoIndex = 1;

// If UTXO has addresses, compute dynamic ordering
if (utxo && utxo.addresses && utxo.addresses.length > 0) {
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
const addressesIndex = this.transaction._fromAddresses.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

// Dynamic ordering based on addressesIndex
let sigSlots: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first: [zeros, userAddress]
sigSlots = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
),
];
} else {
// User comes first: [userAddress, zeros]
sigSlots = [
utils.createEmptySigWithAddress(
Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')
),
utils.createNewSig(''),
];
}
return new Credential(sigSlots);
} else {
// Fallback: use all zeros if no UTXO addresses available
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
for (let i = 0; i < this.transaction._threshold; i++) {
sigSlots.push(utils.createNewSig(''));
}
return new Credential(sigSlots);
}
});

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

Expand Down Expand Up @@ -221,9 +262,43 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {

inputs.push(transferableInput);

// Create credential with empty signatures for threshold signers
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
// Create credential with empty signatures for slot identification
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
const hasAddresses =
this.transaction._fromAddresses && this.transaction._fromAddresses.length >= this.transaction._threshold;

if (!hasAddresses) {
// If addresses not available, use all zeros
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
} else {
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
const utxoAddresses = utxo.addresses.map((a) => utils.parseAddress(a));
const addressesIndex = this.transaction._fromAddresses.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

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

// Dynamic ordering based on addressesIndex
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first in signature order: [zeros, userAddress]
emptySignatures = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
];
} else {
// User comes first in signature order: [userAddress, zeros]
emptySignatures = [
utils.createEmptySigWithAddress(Buffer.from(this.transaction._fromAddresses[firstIndex]).toString('hex')),
utils.createNewSig(''),
];
}
credentials.push(new Credential(emptySignatures));
}
});

return {
Expand Down
39 changes: 36 additions & 3 deletions modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,42 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
Object.assign(transferableInput, { input });
inputs.push(transferableInput);

// Create empty credential for each input
const emptySignatures = sender.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
// Create credential with empty signatures for slot identification
// Match avaxp behavior: dynamic ordering based on addressesIndex from UTXO
const hasAddresses = sender && sender.length >= (this.transaction as Transaction)._threshold;

if (!hasAddresses) {
// If addresses not available, use all zeros
const emptySignatures = sender.map(() => utils.createNewSig(''));
credentials.push(new Credential(emptySignatures));
} else {
// Compute addressesIndex: position of each _fromAddresses in UTXO's address list
const utxoAddresses = utxo.addresses.map((a: string) => utils.parseAddress(a));
const addressesIndex = sender.map((a) =>
utxoAddresses.findIndex((u) => Buffer.compare(Buffer.from(u), Buffer.from(a)) === 0)
);

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

// Dynamic ordering based on addressesIndex
let emptySignatures: ReturnType<typeof utils.createNewSig>[];
if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
// Bitgo comes first in signature order: [zeros, userAddress]
emptySignatures = [
utils.createNewSig(''),
utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')),
];
} else {
// User comes first in signature order: [userAddress, zeros]
emptySignatures = [
utils.createEmptySigWithAddress(Buffer.from(sender[firstIndex]).toString('hex')),
utils.createNewSig(''),
];
}
credentials.push(new Credential(emptySignatures));
}
});

// Create output if there is change
Expand Down
15 changes: 8 additions & 7 deletions modules/sdk-coin-flrp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,14 @@ export class Transaction extends BaseTransaction {
return FlareUtils.bufferToHex(this._rawSignedBytes);
}
const unsignedTx = this._flareTransaction as UnsignedTx;
// For signed transactions, return the full signed tx with credentials
// Check signature.length for robustness
if (this.signature.length > 0) {
return FlareUtils.bufferToHex(unsignedTx.getSignedTx().toBytes());
}
// For unsigned transactions, return just the transaction bytes
return FlareUtils.bufferToHex(unsignedTx.toBytes());
const signedTxBytes = unsignedTx.getSignedTx().toBytes();

// Both P-chain and C-chain transactions include checksum (matching avaxp behavior)
// avaxp P-chain: transaction.ts uses addChecksum() explicitly
// avaxp C-chain: deprecatedTransaction.ts uses Tx.toStringHex() which internally adds checksum
const rawTx = FlareUtils.bufferToHex(utils.addChecksum(signedTxBytes));
console.log('rawTx in toBroadcastFormat:', rawTx);
return rawTx;
}

toJson(): TxData {
Expand Down
8 changes: 5 additions & 3 deletions modules/sdk-coin-flrp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,12 @@ export class Utils implements BaseUtils {

/**
* Adds a checksum to a Buffer and returns the concatenated result
* Uses last 4 bytes of SHA256 hash as checksum (matching avaxp behavior)
*/
private addChecksum(buff: Buffer): Buffer {
const hashSlice = createHash('sha256').update(buff).digest().slice(28);
return Buffer.concat([buff, hashSlice]);
public addChecksum(buff: Buffer | Uint8Array): Uint8Array {
const buffer = Buffer.from(buff);
const hashSlice = createHash('sha256').update(buffer).digest().slice(28);
return new Uint8Array(Buffer.concat([buffer, hashSlice]));
}

// In utils.ts, add this method to the Utils class:
Expand Down
Loading