Skip to content

Commit 4bbd231

Browse files
authored
Merge pull request #7683 from BitGo/import-in-c
feat(sdk-coin-flrp): implemented ImportInCTxBuilder and impproved signing
2 parents 441a899 + aba135f commit 4bbd231

File tree

10 files changed

+294
-355
lines changed

10 files changed

+294
-355
lines changed

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
7575
return TransactionType.Export;
7676
}
7777

78-
initBuilder(tx: Tx, rawBytes?: Buffer): this {
78+
initBuilder(tx: Tx, rawBytes?: Buffer, parsedCredentials?: Credential[]): this {
7979
const baseTx = tx as evmSerial.ExportTx;
8080
if (!this.verifyTxType(baseTx._type)) {
8181
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
@@ -115,10 +115,9 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
115115

116116
this._nonce = input.nonce.value();
117117

118-
// Check if raw bytes contain credentials and extract them
119-
const { hasCredentials, credentials } = rawBytes
120-
? utils.extractCredentialsFromRawBytes(rawBytes, baseTx, 'EVM')
121-
: { hasCredentials: false, credentials: [] };
118+
// Use credentials passed from TransactionBuilderFactory (properly extracted using codec)
119+
const credentials = parsedCredentials || [];
120+
const hasCredentials = credentials.length > 0;
122121

123122
// If it's a signed transaction, store the original raw bytes to preserve exact format
124123
if (hasCredentials && rawBytes) {

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

Lines changed: 21 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
5151
return this;
5252
}
5353

54-
initBuilder(tx: Tx, rawBytes?: Buffer): this {
54+
initBuilder(tx: Tx, rawBytes?: Buffer, parsedCredentials?: Credential[]): this {
5555
const exportTx = tx as pvmSerial.ExportTx;
5656

5757
if (!this.verifyTxType(exportTx._type)) {
@@ -101,95 +101,34 @@ export class ExportInPTxBuilder extends AtomicTransactionBuilder {
101101
const fee = totalInputAmount - changeOutputAmount - this._amount;
102102
this.transaction._fee.fee = fee.toString();
103103

104-
// Extract credentials from raw bytes
105-
let hasCredentials = false;
106-
let credentials: Credential[] = [];
107-
108-
if (rawBytes) {
109-
// Try standard extraction first
110-
const result = utils.extractCredentialsFromRawBytes(rawBytes, exportTx, 'PVM');
111-
hasCredentials = result.hasCredentials;
112-
credentials = result.credentials;
113-
114-
// If extraction failed but raw bytes are longer, try parsing credentials at known offset
115-
if ((!hasCredentials || credentials.length === 0) && rawBytes.length > 300) {
116-
const codec = FlareUtils.getManagerForVM('PVM').getDefaultCodec();
117-
const txBytesLength = exportTx.toBytes(codec).length;
118-
119-
if (rawBytes.length > txBytesLength) {
120-
hasCredentials = true;
121-
const credResult = utils.parseCredentialsAtOffset(rawBytes, txBytesLength);
122-
if (credResult.length > 0) {
123-
credentials = credResult;
124-
}
125-
}
126-
}
127-
}
104+
// Use credentials passed from TransactionBuilderFactory (properly extracted using codec)
105+
const credentials = parsedCredentials || [];
106+
const hasCredentials = credentials.length > 0;
128107

129-
// If we have parsed credentials with the correct number of credentials for the inputs,
130-
// use them directly (preserves existing signatures)
131-
const numInputs = exportTx.baseTx.inputs.length;
132-
const useDirectCredentials = hasCredentials && credentials.length === numInputs;
133-
134-
// If there are credentials in raw bytes, store the original bytes to preserve exact format
108+
// If there are credentials, store the original bytes to preserve exact format
135109
if (rawBytes && hasCredentials) {
136110
this.transaction._rawSignedBytes = rawBytes;
137111
}
138112

139113
// Create proper UnsignedTx wrapper with credentials
140114
const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
141115

142-
// Helper function to check if a signature is empty (contains no real signature data)
143-
// A real ECDSA signature will never start with 45 bytes of zeros
144-
const isSignatureEmpty = (sig: string): boolean => {
145-
if (!sig) return true;
146-
const cleanSig = utils.removeHexPrefix(sig);
147-
if (cleanSig.length === 0) return true;
148-
// Check if the first 90 hex chars (45 bytes) are all zeros
149-
// Real signatures from secp256k1 will never have this pattern
150-
const first90Chars = cleanSig.substring(0, 90);
151-
return first90Chars === '0'.repeat(90) || first90Chars === '0'.repeat(first90Chars.length);
152-
};
153-
154-
// Build txCredentials - either use direct credentials or reconstruct with embedded addresses
155-
let txCredentials: Credential[];
156-
157-
if (useDirectCredentials) {
158-
// Use the extracted credentials directly - they already have the correct signatures
159-
// Just ensure empty slots have embedded addresses for signing identification
160-
txCredentials = credentials;
161-
} else {
162-
// Reconstruct credentials from scratch with embedded addresses
163-
txCredentials = exportTx.baseTx.inputs.map((input, idx) => {
164-
const transferInput = input.input as TransferInput;
165-
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;
166-
167-
// Get existing signatures from parsed credentials if available
168-
const existingSigs: string[] = [];
169-
if (idx < credentials.length) {
170-
const existingCred = credentials[idx];
171-
existingSigs.push(...existingCred.getSignatures());
172-
}
173-
174-
// Create credential with correct number of slots, preserving existing signatures
175-
// Empty slots get embedded addresses for slot identification
176-
const sigSlots: ReturnType<typeof utils.createNewSig>[] = [];
177-
for (let i = 0; i < inputThreshold; i++) {
178-
const existingSig = i < existingSigs.length ? existingSigs[i] : null;
179-
180-
if (existingSig && !isSignatureEmpty(existingSig)) {
181-
// Use existing non-empty signature (real signature from signing)
182-
const sigHex = utils.removeHexPrefix(existingSig);
183-
sigSlots.push(utils.createNewSig(sigHex));
184-
} else {
185-
// Empty slot - create with embedded address for slot identification
186-
const addrHex = Buffer.from(sortedAddresses[i]).toString('hex');
187-
sigSlots.push(utils.createEmptySigWithAddress(addrHex));
188-
}
189-
}
190-
return new Credential(sigSlots);
191-
});
192-
}
116+
// When credentials were extracted, use them directly to preserve existing signatures
117+
// Otherwise, create empty credentials with embedded addresses for slot identification
118+
const txCredentials =
119+
credentials.length > 0
120+
? credentials
121+
: exportTx.baseTx.inputs.map((input) => {
122+
const transferInput = input.input as TransferInput;
123+
const inputThreshold = transferInput.sigIndicies().length || this.transaction._threshold;
124+
// Create empty signatures with embedded addresses for slot identification
125+
const sigSlots: ReturnType<typeof utils.createEmptySigWithAddress>[] = [];
126+
for (let i = 0; i < inputThreshold; i++) {
127+
const addrHex = Buffer.from(sortedAddresses[i]).toString('hex');
128+
sigSlots.push(utils.createEmptySigWithAddress(addrHex));
129+
}
130+
return new Credential(sigSlots);
131+
});
193132

194133
// Create address maps for signing - one per input/credential
195134
// Each address map contains all addresses mapped to their indices

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

Lines changed: 75 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Int,
1010
Id,
1111
TransferableInput,
12-
TypeSymbols,
1312
Address,
1413
utils as FlareUtils,
1514
avmSerial,
@@ -36,7 +35,7 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
3635
return TransactionType.Import;
3736
}
3837

39-
initBuilder(tx: Tx): this {
38+
initBuilder(tx: Tx, rawBytes?: Buffer, parsedCredentials?: Credential[]): this {
4039
const baseTx = tx as evmSerial.ImportTx;
4140
if (!this.verifyTxType(baseTx._type)) {
4241
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
@@ -50,8 +49,7 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
5049
}
5150
const output = outputs[0];
5251

53-
const assetIdStr = Buffer.from(this.transaction._assetId).toString('hex');
54-
if (Buffer.from(output.assetId.toBytes()).toString('hex') !== assetIdStr) {
52+
if (Buffer.from(output.assetId.toBytes()).toString('hex') !== this.transaction._assetId) {
5553
throw new Error('AssetID are not equals');
5654
}
5755
this.transaction._to = [Buffer.from(output.address.toBytes())];
@@ -66,20 +64,55 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
6664
// Calculate fee based on input/output difference
6765
const fee = totalInputAmount - totalOutputAmount;
6866
const feeSize = this.calculateFeeSize(baseTx);
69-
const feeRate = Number(fee) / feeSize;
67+
// Use integer division to ensure feeRate can be converted back to BigInt
68+
const feeRate = Math.floor(Number(fee) / feeSize);
7069

7170
this.transaction._fee = {
7271
fee: fee.toString(),
7372
feeRate: feeRate,
7473
size: feeSize,
7574
};
7675

77-
this.transaction.setTransaction(tx);
76+
// Use credentials passed from TransactionBuilderFactory (properly extracted using codec)
77+
const credentials = parsedCredentials || [];
78+
const hasCredentials = credentials.length > 0;
79+
80+
// If it's a signed transaction, store the original raw bytes to preserve exact format
81+
if (hasCredentials && rawBytes) {
82+
this.transaction._rawSignedBytes = rawBytes;
83+
}
84+
85+
// Extract threshold from first input's sigIndicies (number of required signatures)
86+
const firstInput = inputs[0];
87+
const inputThreshold = firstInput.sigIndicies().length || this.transaction._threshold;
88+
this.transaction._threshold = inputThreshold;
89+
90+
// Create proper UnsignedTx wrapper with credentials
91+
const toAddress = new Address(output.address.toBytes());
92+
const addressMap = new FlareUtils.AddressMap([[toAddress, 0]]);
93+
const addressMaps = new FlareUtils.AddressMaps([addressMap]);
94+
95+
// When credentials were extracted, use them directly to preserve existing signatures
96+
let txCredentials: Credential[];
97+
if (credentials.length > 0) {
98+
txCredentials = credentials;
99+
} else {
100+
// Create empty credential with threshold number of signature slots
101+
const emptySignatures: ReturnType<typeof utils.createNewSig>[] = [];
102+
for (let i = 0; i < inputThreshold; i++) {
103+
emptySignatures.push(utils.createNewSig(''));
104+
}
105+
txCredentials = [new Credential(emptySignatures)];
106+
}
107+
108+
const unsignedTx = new UnsignedTx(baseTx, [], addressMaps, txCredentials);
109+
110+
this.transaction.setTransaction(unsignedTx);
78111
return this;
79112
}
80113

81114
static verifyTxType(txnType: string): boolean {
82-
return txnType === FlareTransactionType.PvmImportTx;
115+
return txnType === FlareTransactionType.EvmImportTx;
83116
}
84117

85118
verifyTxType(txnType: string): boolean {
@@ -91,8 +124,10 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
91124
* @protected
92125
*/
93126
protected buildFlareTransaction(): void {
94-
// if tx has credentials, tx shouldn't change
127+
// if tx has credentials or was already recovered from raw, tx shouldn't change
95128
if (this.transaction.hasCredentials) return;
129+
// If fee is already calculated (from initBuilder), the transaction is already built
130+
if (this.transaction._fee.fee) return;
96131
if (this.transaction._to.length !== 1) {
97132
throw new Error('to is required');
98133
}
@@ -109,14 +144,12 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
109144
this.transaction._fee.fee = fee.toString();
110145
this.transaction._fee.size = feeSize;
111146

112-
// Create output with required interface implementation
113-
const output = {
114-
_type: TypeSymbols.BaseTx,
115-
address: new Address(this.transaction._to[0]),
116-
amount: new BigIntPr(amount - fee),
117-
assetId: new Id(new Uint8Array(Buffer.from(this.transaction._assetId, 'hex'))),
118-
toBytes: () => new Uint8Array(),
119-
};
147+
// Create EVM output using proper FlareJS class
148+
const output = new evmSerial.Output(
149+
new Address(this.transaction._to[0]),
150+
new BigIntPr(amount - fee),
151+
new Id(new Uint8Array(Buffer.from(this.transaction._assetId, 'hex')))
152+
);
120153

121154
// Create the import transaction
122155
const importTx = new evmSerial.ImportTx(
@@ -127,8 +160,11 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
127160
[output]
128161
);
129162

130-
// Create unsigned transaction
131-
const addressMap = new FlareUtils.AddressMap([[new Address(this.transaction._fromAddresses[0]), 0]]);
163+
// Create unsigned transaction with all potential signers in address map
164+
const addressMap = new FlareUtils.AddressMap();
165+
this.transaction._fromAddresses.forEach((addr, i) => {
166+
addressMap.set(new Address(addr), i);
167+
});
132168
const addressMaps = new FlareUtils.AddressMaps([addressMap]);
133169

134170
const unsignedTx = new UnsignedTx(
@@ -172,49 +208,29 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
172208
const amount = BigInt(utxo.amount);
173209
totalAmount += amount;
174210

175-
// Create input with proper interface implementation
176-
const input = {
177-
_type: TypeSymbols.Input,
178-
amount: () => amount,
179-
sigIndices: sender.map((_, i) => i),
180-
toBytes: () => new Uint8Array(),
181-
};
182-
183-
// Create TransferableInput with proper UTXOID implementation
184-
const txId = new Id(new Uint8Array(Buffer.from(utxo.txid, 'hex')));
185-
const outputIdxInt = new Int(Number(utxo.outputidx));
186-
const outputIdxBytes = new Uint8Array(Buffer.alloc(4));
187-
new DataView(outputIdxBytes.buffer).setInt32(0, Number(utxo.outputidx), true);
188-
const outputIdxId = new Id(outputIdxBytes);
189-
190-
// Create asset with complete Amounter interface
191-
const assetIdBytes = new Uint8Array(Buffer.from(this.transaction._assetId, 'hex'));
192-
const assetId = {
193-
_type: TypeSymbols.BaseTx,
194-
amount: () => amount,
195-
toBytes: () => assetIdBytes,
196-
toString: () => Buffer.from(assetIdBytes).toString('hex'),
197-
};
211+
// Create signature indices for threshold
212+
const sigIndices: number[] = [];
213+
for (let i = 0; i < this.transaction._threshold; i++) {
214+
sigIndices.push(i);
215+
}
198216

199-
// Create TransferableInput with UTXOID using Int for outputIdx
200-
const transferableInput = new TransferableInput(
201-
{
202-
_type: TypeSymbols.UTXOID,
203-
txID: txId,
204-
outputIdx: outputIdxInt,
205-
ID: () => utxo.txid,
206-
toBytes: () => new Uint8Array(),
207-
},
208-
outputIdxId, // Use Id type for TransferableInput constructor
209-
assetId // Use asset with complete Amounter interface
217+
// Use fromNative to create TransferableInput (same pattern as ImportInPTxBuilder)
218+
// fromNative expects cb58-encoded strings for txId and assetId
219+
const txIdCb58 = utxo.txid; // Already cb58 encoded
220+
const assetIdCb58 = utils.cb58Encode(Buffer.from(this.transaction._assetId, 'hex'));
221+
222+
const transferableInput = TransferableInput.fromNative(
223+
txIdCb58,
224+
Number(utxo.outputidx),
225+
assetIdCb58,
226+
amount,
227+
sigIndices
210228
);
211229

212-
// Set input properties
213-
Object.assign(transferableInput, { input });
214230
inputs.push(transferableInput);
215231

216-
// Create empty credential for each input
217-
const emptySignatures = sender.map(() => utils.createNewSig(''));
232+
// Create empty credential for each input with threshold signers
233+
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
218234
const credential = new Credential(emptySignatures);
219235
credentials.push(credential);
220236
});
@@ -228,6 +244,7 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
228244

229245
/**
230246
* Calculate the fee size for the transaction
247+
* For C-chain imports, the feeRate is treated as an absolute fee value
231248
*/
232249
private calculateFeeSize(tx?: evmSerial.ImportTx): number {
233250
// If tx is provided, calculate based on actual transaction size
@@ -236,14 +253,8 @@ export class ImportInCTxBuilder extends AtomicInCTransactionBuilder {
236253
return tx.toBytes(codec).length;
237254
}
238255

239-
// Otherwise estimate based on typical import transaction size
240-
const baseSize = 256; // Base transaction size
241-
const inputSize = 128; // Size per input
242-
const outputSize = 64; // Size per output
243-
const numInputs = this.transaction._utxos.length;
244-
const numOutputs = 1; // Import tx always has 1 output
245-
246-
return baseSize + inputSize * numInputs + outputSize * numOutputs;
256+
// For C-chain imports, treat feeRate as the absolute fee (multiplier of 1)
257+
return 1;
247258
}
248259

249260
/**

0 commit comments

Comments
 (0)