Skip to content

Commit cf627cd

Browse files
Merge pull request #6904 from BitGo/WIN-6320
feat: (skeleton) export in c chain
2 parents b10e9c7 + f86f215 commit cf627cd

File tree

3 files changed

+278
-2
lines changed

3 files changed

+278
-2
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { AtomicTransactionBuilder } from './atomicTransactionBuilder';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import utils from './utils';
4+
import { BuildTransactionError } from '@bitgo/sdk-core';
5+
6+
interface FlareChainNetworkMeta {
7+
blockchainID?: string; // P-chain id (external)
8+
cChainBlockchainID?: string; // C-chain id (local)
9+
[k: string]: unknown;
10+
}
11+
12+
interface FeeShape {
13+
fee?: string; // legacy
14+
feeRate?: string; // per unit rate
15+
}
16+
17+
/**
18+
* Flare P->C atomic import/export style builder (C-chain context). This adapts the AVAXP logic
19+
* removing direct Avalanche SDK dependencies. Network / chain ids are expected to be provided
20+
* in the transaction._network object by a higher-level factory once Flare network constants
21+
* are finalized. For now we CB58-decode placeholders if present and default to zero buffers.
22+
*/
23+
export abstract class AtomicInCTransactionBuilder extends AtomicTransactionBuilder {
24+
// Placeholder fixed fee (can be overridden by subclasses or network config)
25+
protected fixedFee = 0n;
26+
constructor(_coinConfig: Readonly<CoinConfig>) {
27+
super(_coinConfig);
28+
this.initializeChainIds();
29+
}
30+
31+
/**
32+
* Set base fee (already scaled to Flare C-chain native decimals). Accept bigint | number | string.
33+
*/
34+
feeRate(baseFee: bigint | number | string): this {
35+
const n = typeof baseFee === 'bigint' ? baseFee : BigInt(baseFee);
36+
this.validateFee(n);
37+
this.setFeeRate(n);
38+
return this;
39+
}
40+
41+
/**
42+
* Recreate builder state from raw tx (hex). Flare C-chain support TBD; for now validate & stash.
43+
*/
44+
protected fromImplementation(rawTransaction: string): { _tx?: unknown } {
45+
// If utils has validateRawTransaction use it; otherwise basic check
46+
if ((utils as unknown as { validateRawTransaction?: (r: string) => void }).validateRawTransaction) {
47+
(utils as unknown as { validateRawTransaction: (r: string) => void }).validateRawTransaction(rawTransaction);
48+
}
49+
this.transaction.setTransaction(rawTransaction);
50+
return this.transaction;
51+
}
52+
53+
private validateFee(fee: bigint): void {
54+
if (fee <= 0n) {
55+
throw new BuildTransactionError('Fee must be greater than 0');
56+
}
57+
}
58+
59+
private initializeChainIds(): void {
60+
const meta = this.transaction._network as FlareChainNetworkMeta;
61+
if (meta?.blockchainID) {
62+
this._externalChainId = utils.cb58Decode(meta.blockchainID);
63+
}
64+
if (meta?.cChainBlockchainID) {
65+
this.transaction._blockchainID = utils.cb58Decode(meta.cChainBlockchainID);
66+
}
67+
}
68+
69+
private setFeeRate(n: bigint): void {
70+
const currentContainer = this.transaction as unknown as { _fee: FeeShape };
71+
const current = currentContainer._fee || { fee: '0' };
72+
currentContainer._fee = { ...current, feeRate: n.toString() };
73+
}
74+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ export abstract class AtomicTransactionBuilder {
1818
_blockchainID: Buffer;
1919
_assetId: Buffer;
2020
_fromAddresses: string[];
21+
_to: string[];
2122
_locktime: bigint;
2223
_threshold: number;
23-
fee: { fee: string };
24+
_fee: { fee: string; feeRate?: string; size?: number };
2425
hasCredentials: boolean;
2526
_tx?: unknown;
2627
setTransaction: (tx: unknown) => void;
@@ -30,9 +31,10 @@ export abstract class AtomicTransactionBuilder {
3031
_blockchainID: Buffer.alloc(0),
3132
_assetId: Buffer.alloc(0),
3233
_fromAddresses: [],
34+
_to: [],
3335
_locktime: 0n,
3436
_threshold: 1,
35-
fee: { fee: '0' },
37+
_fee: { fee: '0' },
3638
hasCredentials: false,
3739
setTransaction: function (_tx: unknown) {
3840
this._tx = _tx;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { AtomicInCTransactionBuilder } from './atomicInCTransactionBuilder';
4+
5+
// Lightweight interface placeholders replacing Avalanche SDK transaction shapes
6+
interface FlareExportInputShape {
7+
address: string;
8+
amount: bigint; // includes exported amount + fee
9+
assetId: Buffer;
10+
nonce: bigint;
11+
}
12+
13+
interface FlareExportOutputShape {
14+
addresses: string[]; // destination P-chain addresses
15+
amount: bigint; // exported amount
16+
assetId: Buffer;
17+
}
18+
19+
interface FlareUnsignedExportTx {
20+
networkId: number;
21+
sourceBlockchainId: Buffer; // C-chain id
22+
destinationBlockchainId: Buffer; // P-chain id
23+
inputs: FlareExportInputShape[];
24+
outputs: FlareExportOutputShape[];
25+
}
26+
27+
interface FlareSignedExportTx {
28+
unsignedTx: FlareUnsignedExportTx;
29+
// credentials placeholder
30+
credentials: unknown[];
31+
}
32+
33+
type RawFlareExportTx = FlareSignedExportTx; // for now treat them the same
34+
35+
export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
36+
private _amount?: bigint;
37+
private _nonce?: bigint;
38+
39+
constructor(_coinConfig: Readonly<CoinConfig>) {
40+
super(_coinConfig);
41+
}
42+
43+
/**
44+
* Utxos are not required in Export Tx in C-Chain.
45+
* Override utxos to prevent used by throwing a error.
46+
*
47+
* @param {DecodedUtxoObj[]} value ignored
48+
*/
49+
utxos(_value: unknown[]): this {
50+
throw new BuildTransactionError('utxos are not required in Export Tx in C-Chain');
51+
}
52+
53+
/**
54+
* Amount is a long that specifies the quantity of the asset that this output owns. Must be positive.
55+
* The transaction output amount add a fixed fee that will be paid upon import.
56+
*
57+
* @param {BN | string} amount The withdrawal amount
58+
*/
59+
amount(amount: bigint | number | string): this {
60+
const n = typeof amount === 'bigint' ? amount : BigInt(amount);
61+
this.validateAmount(n);
62+
this._amount = n;
63+
return this;
64+
}
65+
66+
/**
67+
* Set the nonce of C-Chain sender address
68+
*
69+
* @param {number | string} nonce - number that can be only used once
70+
*/
71+
nonce(nonce: bigint | number | string): this {
72+
const n = typeof nonce === 'bigint' ? nonce : BigInt(nonce);
73+
this.validateNonce(n);
74+
this._nonce = n;
75+
return this;
76+
}
77+
78+
/**
79+
* Export tx target P wallet.
80+
*
81+
* @param pAddresses
82+
*/
83+
to(pAddresses: string | string[]): this {
84+
const pubKeys = Array.isArray(pAddresses) ? pAddresses : pAddresses.split('~');
85+
// For now ensure they are stored as bech32 / string addresses directly
86+
this.transaction._to = pubKeys.map((a) => a.toString());
87+
return this;
88+
}
89+
90+
protected get transactionType(): TransactionType {
91+
return TransactionType.Export;
92+
}
93+
94+
initBuilder(raw: RawFlareExportTx): this {
95+
// Basic shape validation
96+
const unsigned = raw.unsignedTx;
97+
if (unsigned.networkId !== this.transaction._networkID) {
98+
throw new Error('Network ID mismatch');
99+
}
100+
if (!unsigned.sourceBlockchainId.equals(this.transaction._blockchainID)) {
101+
throw new Error('Blockchain ID mismatch');
102+
}
103+
if (unsigned.outputs.length !== 1) {
104+
throw new BuildTransactionError('Transaction can have one output');
105+
}
106+
if (unsigned.inputs.length !== 1) {
107+
throw new BuildTransactionError('Transaction can have one input');
108+
}
109+
const out = unsigned.outputs[0];
110+
const inp = unsigned.inputs[0];
111+
if (!out.assetId.equals(this.transaction._assetId) || !inp.assetId.equals(this.transaction._assetId)) {
112+
throw new Error('AssetID mismatch');
113+
}
114+
this.transaction._to = out.addresses;
115+
this._amount = out.amount;
116+
const fee = inp.amount - out.amount;
117+
if (fee < 0n) {
118+
throw new BuildTransactionError('Computed fee is negative');
119+
}
120+
const fixed = this.fixedFee;
121+
const feeRate = fee - fixed;
122+
this.transaction._fee.feeRate = feeRate.toString();
123+
this.transaction._fee.fee = fee.toString();
124+
this.transaction._fee.size = 1;
125+
this.transaction._fromAddresses = [inp.address];
126+
this._nonce = inp.nonce;
127+
this.transaction.setTransaction(raw);
128+
return this;
129+
}
130+
131+
// For parity with Avalanche builder interfaces; always returns true for placeholder
132+
//TODO: WIN-6322
133+
static verifyTxType(_tx: unknown): _tx is FlareUnsignedExportTx {
134+
return true;
135+
}
136+
137+
verifyTxType(_tx: unknown): _tx is FlareUnsignedExportTx {
138+
return ExportInCTxBuilder.verifyTxType(_tx);
139+
}
140+
141+
/**
142+
* Build the export in C-chain transaction
143+
* @protected
144+
*/
145+
protected buildFlareTransaction(): void {
146+
if (this.transaction.hasCredentials) return; // placeholder: credentials not yet implemented
147+
if (this._amount === undefined) throw new Error('amount is required');
148+
if (this.transaction._fromAddresses.length !== 1) throw new Error('sender is one and required');
149+
if (this.transaction._to.length === 0) throw new Error('to is required');
150+
if (!this.transaction._fee.feeRate) throw new Error('fee rate is required');
151+
if (this._nonce === undefined) throw new Error('nonce is required');
152+
153+
// Compose placeholder unsigned tx shape
154+
const feeRate = BigInt(this.transaction._fee.feeRate);
155+
const fixed = this.fixedFee;
156+
const totalFee = feeRate + fixed;
157+
const input: FlareExportInputShape = {
158+
address: this.transaction._fromAddresses[0],
159+
amount: this._amount + totalFee,
160+
assetId: this.transaction._assetId,
161+
nonce: this._nonce,
162+
};
163+
const output: FlareExportOutputShape = {
164+
addresses: this.transaction._to,
165+
amount: this._amount,
166+
assetId: this.transaction._assetId,
167+
};
168+
const unsigned: FlareUnsignedExportTx = {
169+
networkId: this.transaction._networkID,
170+
sourceBlockchainId: this.transaction._blockchainID,
171+
destinationBlockchainId: this._externalChainId || Buffer.alloc(0),
172+
inputs: [input],
173+
outputs: [output],
174+
};
175+
const signed: FlareSignedExportTx = { unsignedTx: unsigned, credentials: [] };
176+
this.transaction._fee.fee = totalFee.toString();
177+
this.transaction._fee.size = 1;
178+
this.transaction.setTransaction(signed);
179+
}
180+
181+
/** @inheritdoc */
182+
protected fromImplementation(raw: string | RawFlareExportTx): { _tx?: unknown } {
183+
if (typeof raw === 'string') {
184+
// Future: parse hex or serialized form. For now treat as opaque raw tx.
185+
this.transaction.setTransaction(raw);
186+
return this.transaction;
187+
}
188+
return this.initBuilder(raw).transaction;
189+
}
190+
191+
/**
192+
* Check the amount is positive.
193+
* @param amount
194+
*/
195+
validateNonce(nonce: bigint): void {
196+
if (nonce < 0n) {
197+
throw new BuildTransactionError('Nonce must be greater or equal than 0');
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)