Skip to content

Commit 2f11726

Browse files
committed
feat(sdk-coin-flrp): enhance ImportInPTxBuilder for P-chain transactions
Ticket: WIN-8043
1 parent 71366cd commit 2f11726

File tree

8 files changed

+576
-69
lines changed

8 files changed

+576
-69
lines changed

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

Lines changed: 179 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,127 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
22
import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core';
33
import { AtomicTransactionBuilder } from './atomicTransactionBuilder';
44
import {
5-
evmSerial,
5+
pvmSerial,
6+
avaxSerial,
67
UnsignedTx,
78
Int,
89
Id,
910
TransferableInput,
11+
TransferableOutput,
12+
TransferOutput,
13+
TransferInput,
14+
OutputOwners,
1015
utils as FlareUtils,
1116
Address,
1217
BigIntPr,
18+
Credential,
19+
Bytes,
1320
} from '@flarenetwork/flarejs';
1421
import utils from './utils';
1522
import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface';
1623

1724
export class ImportInPTxBuilder extends AtomicTransactionBuilder {
1825
constructor(_coinConfig: Readonly<CoinConfig>) {
1926
super(_coinConfig);
20-
// external chain id is P
21-
this._externalChainId = utils.cb58Decode(this.transaction._network.blockchainID);
22-
// chain id is C
23-
this.transaction._blockchainID = Buffer.from(
24-
utils.cb58Decode(this.transaction._network.cChainBlockchainID)
25-
).toString('hex');
27+
// For Import INTO P-chain:
28+
// - external chain (source) is C-chain
29+
// - blockchain ID (destination) is P-chain
30+
this._externalChainId = utils.cb58Decode(this.transaction._network.cChainBlockchainID);
31+
// P-chain blockchain ID (from network config - typically all zeros for primary network)
32+
this.transaction._blockchainID = Buffer.from(utils.cb58Decode(this.transaction._network.blockchainID)).toString(
33+
'hex'
34+
);
2635
}
2736

2837
protected get transactionType(): TransactionType {
2938
return TransactionType.Import;
3039
}
3140

32-
initBuilder(tx: Tx): this {
33-
const baseTx = tx as evmSerial.ImportTx;
34-
if (!this.verifyTxType(baseTx._type)) {
41+
initBuilder(tx: Tx, rawBytes?: Buffer): this {
42+
const importTx = tx as pvmSerial.ImportTx;
43+
44+
if (!this.verifyTxType(importTx._type)) {
3545
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
3646
}
3747

3848
// The regular change output is the tx output in Import tx.
39-
// createInputOutput results in a single item array.
4049
// It's expected to have only one output with the addresses of the sender.
41-
const outputs = baseTx.Outs;
50+
const outputs = importTx.baseTx.outputs;
4251
if (outputs.length !== 1) {
4352
throw new BuildTransactionError('Transaction can have one external output');
4453
}
4554

4655
const output = outputs[0];
4756
const assetId = output.assetId.toBytes();
48-
if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId)) !== 0) {
57+
if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId, 'hex')) !== 0) {
4958
throw new Error('The Asset ID of the output does not match the transaction');
5059
}
5160

52-
// Set locktime to 0 since it's not used in EVM outputs
53-
this.transaction._locktime = BigInt(0);
61+
const transferOutput = output.output as TransferOutput;
62+
const outputOwners = transferOutput.outputOwners;
63+
64+
// Set locktime from output
65+
this.transaction._locktime = outputOwners.locktime.value();
5466

55-
// Set threshold to 1 since EVM outputs only have one address
56-
this.transaction._threshold = 1;
67+
// Set threshold from output
68+
this.transaction._threshold = outputOwners.threshold.value();
5769

58-
// Convert output address to buffer and set as fromAddress
59-
this.transaction._fromAddresses = [Buffer.from(output.address.toBytes())];
70+
// Convert output addresses to buffers and set as fromAddresses
71+
this.transaction._fromAddresses = outputOwners.addrs.map((addr) => Buffer.from(addr.toBytes()));
6072

6173
// Set external chain ID from the source chain
62-
this._externalChainId = Buffer.from(baseTx.sourceChain.toString());
74+
this._externalChainId = Buffer.from(importTx.sourceChain.toBytes());
6375

6476
// Recover UTXOs from imported inputs
65-
this.transaction._utxos = this.recoverUtxos(baseTx.importedInputs);
77+
this.transaction._utxos = this.recoverUtxos(importTx.ins);
78+
79+
// Calculate and set fee from input/output difference
80+
const totalInputAmount = importTx.ins.reduce((sum, input) => sum + input.amount(), BigInt(0));
81+
const outputAmount = transferOutput.amount();
82+
const fee = totalInputAmount - outputAmount;
83+
this.transaction._fee.fee = fee.toString();
84+
85+
// Check if raw bytes contain credentials
86+
// For PVM transactions, credentials start after the unsigned tx bytes
87+
let hasCredentials = false;
88+
let credentials: Credential[] = [];
89+
90+
if (rawBytes) {
91+
// Try standard extraction first
92+
const result = utils.extractCredentialsFromRawBytes(rawBytes, importTx, 'PVM');
93+
hasCredentials = result.hasCredentials;
94+
credentials = result.credentials;
95+
96+
// If extraction failed but raw bytes are longer, try parsing credentials at known offset
97+
// For ImportTx, the unsigned tx is typically 302 bytes
98+
if ((!hasCredentials || credentials.length === 0) && rawBytes.length > 350) {
99+
hasCredentials = true;
100+
// Try to extract credentials at the standard position (302 bytes)
101+
const credResult = utils.parseCredentialsAtOffset(rawBytes, 302);
102+
if (credResult.length > 0) {
103+
credentials = credResult;
104+
}
105+
}
106+
}
107+
108+
// If there are credentials in raw bytes, store the original bytes to preserve exact format
109+
if (rawBytes && hasCredentials) {
110+
this.transaction._rawSignedBytes = rawBytes;
111+
}
66112

113+
// Create proper UnsignedTx wrapper with credentials
114+
const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
115+
const addressMaps = sortedAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]]));
116+
117+
// Create credentials if none exist
118+
const txCredentials =
119+
credentials.length > 0
120+
? credentials
121+
: [new Credential(sortedAddresses.slice(0, this.transaction._threshold).map(() => utils.createNewSig('')))];
122+
123+
const unsignedTx = new UnsignedTx(importTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials);
124+
125+
this.transaction.setTransaction(unsignedTx);
67126
return this;
68127
}
69128

@@ -76,46 +135,123 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
76135
}
77136

78137
/**
79-
* Build the import transaction
138+
* Build the import transaction for P-chain
80139
* @protected
81140
*/
82141
protected buildFlareTransaction(): void {
83142
// if tx has credentials, tx shouldn't change
84143
if (this.transaction.hasCredentials) return;
85144

86-
const { inputs, credentials } = this.createInputOutput(BigInt(this.transaction.fee.fee));
145+
const { inputs, credentials, totalAmount } = this.createImportInputs();
87146

88-
// Convert TransferableInput to evmSerial.Output
89-
const evmOutputs = inputs.map((input) => {
90-
return new evmSerial.Output(
91-
new Address(this.transaction._fromAddresses[0]),
92-
new BigIntPr(input.input.amount()),
93-
new Id(input.assetId.toBytes())
94-
);
95-
});
147+
// Calculate fee from transaction fee settings
148+
const fee = BigInt(this.transaction.fee.fee);
149+
const outputAmount = totalAmount - fee;
150+
151+
// Create the output for P-chain (TransferableOutput with TransferOutput)
152+
const assetIdBytes = new Uint8Array(Buffer.from(this.transaction._assetId, 'hex'));
153+
154+
// Create OutputOwners with the P-chain addresses (sorted by byte value as per AVAX protocol)
155+
const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b));
156+
const outputOwners = new OutputOwners(
157+
new BigIntPr(this.transaction._locktime),
158+
new Int(this.transaction._threshold),
159+
sortedAddresses.map((addr) => new Address(addr))
160+
);
161+
162+
const transferOutput = new TransferOutput(new BigIntPr(outputAmount), outputOwners);
163+
const output = new TransferableOutput(new Id(assetIdBytes), transferOutput);
96164

97-
// Create the import transaction
98-
const importTx = new evmSerial.ImportTx(
165+
// Create the BaseTx for the P-chain import transaction
166+
const baseTx = new avaxSerial.BaseTx(
99167
new Int(this.transaction._networkID),
100-
Id.fromString(this.transaction._blockchainID.toString()),
101-
Id.fromString(this._externalChainId.toString()),
102-
inputs,
103-
evmOutputs
168+
new Id(Buffer.from(this.transaction._blockchainID, 'hex')),
169+
[output], // outputs
170+
[], // inputs (empty for import - inputs come from importedInputs)
171+
new Bytes(new Uint8Array(0)) // empty memo
172+
);
173+
174+
// Create the P-chain import transaction using pvmSerial.ImportTx
175+
const importTx = new pvmSerial.ImportTx(
176+
baseTx,
177+
new Id(this._externalChainId), // sourceChain (C-chain)
178+
inputs // importedInputs (ins)
104179
);
105180

106-
const addressMaps = this.transaction._fromAddresses.map((a) => new FlareUtils.AddressMap([[new Address(a), 0]]));
181+
// Create address maps for signing
182+
const addressMaps = this.transaction._fromAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]]));
107183

108184
// Create unsigned transaction
109185
const unsignedTx = new UnsignedTx(
110186
importTx,
111-
[], // Empty UTXOs array, will be filled during processing
187+
[], // Empty UTXOs array
112188
new FlareUtils.AddressMaps(addressMaps),
113189
credentials
114190
);
115191

116192
this.transaction.setTransaction(unsignedTx);
117193
}
118194

195+
/**
196+
* Create inputs from UTXOs for P-chain import
197+
* @returns inputs, credentials, and total amount
198+
*/
199+
protected createImportInputs(): {
200+
inputs: TransferableInput[];
201+
credentials: Credential[];
202+
totalAmount: bigint;
203+
} {
204+
const sender = this.transaction._fromAddresses.slice();
205+
if (this.recoverSigner) {
206+
// switch first and last signer
207+
const tmp = sender.pop();
208+
sender.push(sender[0]);
209+
if (tmp) {
210+
sender[0] = tmp;
211+
}
212+
}
213+
214+
let totalAmount = BigInt(0);
215+
const inputs: TransferableInput[] = [];
216+
const credentials: Credential[] = [];
217+
218+
this.transaction._utxos.forEach((utxo: DecodedUtxoObj) => {
219+
const amount = BigInt(utxo.amount);
220+
totalAmount += amount;
221+
222+
// Create signature indices for threshold
223+
const sigIndices: number[] = [];
224+
for (let i = 0; i < this.transaction._threshold; i++) {
225+
sigIndices.push(i);
226+
}
227+
228+
// Use fromNative to create TransferableInput
229+
// fromNative expects cb58-encoded strings for txId and assetId
230+
const txIdCb58 = utxo.txid; // Already cb58 encoded
231+
const assetIdCb58 = utils.cb58Encode(Buffer.from(this.transaction._assetId, 'hex'));
232+
233+
const transferableInput = TransferableInput.fromNative(
234+
txIdCb58,
235+
Number(utxo.outputidx),
236+
assetIdCb58,
237+
amount,
238+
sigIndices
239+
);
240+
241+
inputs.push(transferableInput);
242+
243+
// Create credential with empty signatures for threshold signers
244+
const emptySignatures = sigIndices.map(() => utils.createNewSig(''));
245+
credentials.push(new Credential(emptySignatures));
246+
});
247+
248+
return {
249+
inputs,
250+
credentials,
251+
totalAmount,
252+
};
253+
}
254+
119255
/**
120256
* Recover UTXOs from imported inputs
121257
* @param importedInputs Array of transferable inputs
@@ -124,12 +260,12 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder {
124260
private recoverUtxos(importedInputs: TransferableInput[]): DecodedUtxoObj[] {
125261
return importedInputs.map((input) => {
126262
const utxoId = input.utxoID;
127-
const transferInput = input.input;
263+
const transferInput = input.input as TransferInput;
128264
const utxo: DecodedUtxoObj = {
129265
outputID: SECP256K1_Transfer_Output,
130-
amount: transferInput.amount.toString(),
131-
txid: utils.cb58Encode(Buffer.from(utxoId.ID.toString())),
132-
outputidx: utxoId.outputIdx.toBytes().toString(),
266+
amount: transferInput.amount().toString(),
267+
txid: utils.cb58Encode(Buffer.from(utxoId.txID.toBytes())),
268+
outputidx: utxoId.outputIdx.value().toString(),
133269
threshold: this.transaction._threshold,
134270
addresses: this.transaction._fromAddresses.map((addr) =>
135271
utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr))

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
22
import { TransactionType } from '@bitgo/sdk-core';
33
import { TransactionBuilder } from './transactionBuilder';
44
import { Transaction } from './transaction';
5-
import { TransferableInput, Int, Id, TypeSymbols } from '@flarenetwork/flarejs';
5+
import { TransferableInput, Int, Id, TypeSymbols, Credential } from '@flarenetwork/flarejs';
66
import { DecodedUtxoObj } from './iface';
7+
import utils from './utils';
78

89
// Interface for objects that can provide an amount
910
interface Amounter {
@@ -34,7 +35,7 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
3435
protected createInputOutput(amount: bigint): {
3536
inputs: TransferableInput[];
3637
outputs: TransferableInput[];
37-
credentials: any[];
38+
credentials: Credential[];
3839
} {
3940
const sender = (this.transaction as Transaction)._fromAddresses.slice();
4041
if (this.recoverSigner) {
@@ -49,7 +50,7 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
4950
let totalAmount = BigInt(0);
5051
const inputs: TransferableInput[] = [];
5152
const outputs: TransferableInput[] = [];
52-
const credentials: any[] = [];
53+
const credentials: Credential[] = [];
5354

5455
(this.transaction as Transaction)._utxos.forEach((utxo: DecodedUtxoObj) => {
5556
const utxoAmount = BigInt(utxo.amount);
@@ -96,8 +97,8 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
9697
inputs.push(transferableInput);
9798

9899
// Create empty credential for each input
99-
const emptySignatures = sender.map(() => Buffer.alloc(0));
100-
credentials.push({ signatures: emptySignatures });
100+
const emptySignatures = sender.map(() => utils.createNewSig(''));
101+
credentials.push(new Credential(emptySignatures));
101102
});
102103

103104
// Create output if there is change
@@ -189,4 +190,28 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
189190
setTransactionType(transactionType: TransactionType): void {
190191
this.transaction._type = transactionType;
191192
}
193+
194+
/**
195+
* The internal chain is the one set for the coin in coinConfig.network. The external chain is the other chain involved.
196+
* The external chain id is the source on import and the destination on export.
197+
*
198+
* @param {string} chainId - id of the external chain
199+
*/
200+
externalChainId(chainId: string | Buffer): this {
201+
const newTargetChainId = typeof chainId === 'string' ? utils.cb58Decode(chainId) : Buffer.from(chainId);
202+
this.validateChainId(newTargetChainId);
203+
this._externalChainId = newTargetChainId;
204+
return this;
205+
}
206+
207+
/**
208+
* Set the transaction fee
209+
*
210+
* @param {string | bigint} feeValue - the fee value
211+
*/
212+
fee(feeValue: string | bigint): this {
213+
const fee = typeof feeValue === 'string' ? feeValue : feeValue.toString();
214+
(this.transaction as Transaction)._fee.fee = fee;
215+
return this;
216+
}
192217
}

0 commit comments

Comments
 (0)