Skip to content

Commit 7acb615

Browse files
authored
Merge pull request #6410 from BitGo/COIN-4742-add-flush-token-tx-builder
Coin 4742 add flush token tx builder
2 parents 3566e4a + f28db93 commit 7acb615

File tree

13 files changed

+391
-36
lines changed

13 files changed

+391
-36
lines changed

modules/sdk-coin-vet/src/lib/iface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface VetTransactionData {
2323
value?: string;
2424
deployedAddress?: string;
2525
to?: string;
26+
tokenAddress?: string;
2627
}
2728

2829
export interface VetTransactionExplanation extends BaseTransactionExplanation {

modules/sdk-coin-vet/src/lib/transaction/addressInitializationTransaction.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export class AddressInitializationTransaction extends Transaction {
1212
private _feeAddress: string;
1313
private _salt: string;
1414
private _initCode: string;
15-
private _transactionData: string;
1615
private _deployedAddress: string;
1716

1817
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -52,14 +51,6 @@ export class AddressInitializationTransaction extends Transaction {
5251
this._initCode = initCode;
5352
}
5453

55-
get transactionData(): string {
56-
return this._transactionData;
57-
}
58-
59-
set transactionData(transactionData: string) {
60-
this._transactionData = transactionData;
61-
}
62-
6354
get deployedAddress(): string {
6455
return this._deployedAddress;
6556
}
@@ -140,7 +131,7 @@ export class AddressInitializationTransaction extends Transaction {
140131
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
141132
this.dependsOn = body.dependsOn || null;
142133
this.nonce = typeof body.nonce === 'number' ? body.nonce : Number(body.nonce) || 0;
143-
// Set recipients from clauses
134+
// Set data from clauses
144135
this.contract = body.clauses[0]?.to || '0x0';
145136
this.transactionData = body.clauses[0]?.data || '0x0';
146137

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
4+
5+
import { Transaction } from './transaction';
6+
import { VetTransactionData } from '../iface';
7+
8+
export class FlushTokenTransaction extends Transaction {
9+
private _forwarderVersion: number;
10+
private _tokenAddress: string;
11+
12+
constructor(_coinConfig: Readonly<CoinConfig>) {
13+
super(_coinConfig);
14+
this._type = TransactionType.FlushTokens;
15+
}
16+
17+
get forwarderVersion(): number {
18+
return this._forwarderVersion;
19+
}
20+
21+
set forwarderVersion(forwarderVersion: number) {
22+
this._forwarderVersion = forwarderVersion;
23+
}
24+
25+
get tokenAddress(): string {
26+
return this._tokenAddress;
27+
}
28+
29+
set tokenAddress(address: string) {
30+
this._tokenAddress = address;
31+
}
32+
33+
/** @inheritdoc */
34+
buildClauses(): void {
35+
this._clauses = [
36+
{
37+
to: this._contract,
38+
value: '0x0',
39+
data: this._transactionData,
40+
},
41+
];
42+
}
43+
44+
/** @inheritdoc */
45+
toJson(): VetTransactionData {
46+
const json: VetTransactionData = {
47+
id: this.id,
48+
chainTag: this.chainTag,
49+
blockRef: this.blockRef,
50+
expiration: this.expiration,
51+
gasPriceCoef: this.gasPriceCoef,
52+
gas: this.gas,
53+
dependsOn: this.dependsOn,
54+
nonce: this.nonce,
55+
data: this.transactionData,
56+
value: '0',
57+
sender: this.sender,
58+
to: this.contract,
59+
tokenAddress: this.tokenAddress,
60+
};
61+
return json;
62+
}
63+
64+
/** @inheritdoc */
65+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
66+
try {
67+
if (!signedTx || !signedTx.body) {
68+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
69+
}
70+
71+
// Store the raw transaction
72+
this.rawTransaction = signedTx;
73+
74+
// Set transaction body properties
75+
const body = signedTx.body;
76+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
77+
this.blockRef = body.blockRef || '0x0';
78+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
79+
this.clauses = body.clauses || [];
80+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
81+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
82+
this.dependsOn = body.dependsOn || null;
83+
this.nonce = typeof body.nonce === 'number' ? body.nonce : Number(body.nonce) || 0;
84+
// Set data from clauses
85+
this.contract = body.clauses[0]?.to || '0x0';
86+
this.transactionData = body.clauses[0]?.data || '0x0';
87+
88+
// Set sender address
89+
if (signedTx.origin) {
90+
this.sender = signedTx.origin.toString().toLowerCase();
91+
}
92+
93+
// Set signatures if present
94+
if (signedTx.signature) {
95+
// First signature is sender's signature
96+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
97+
98+
// If there's additional signature data, it's the fee payer's signature
99+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
100+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
101+
}
102+
}
103+
} catch (e) {
104+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
105+
}
106+
}
107+
}

modules/sdk-coin-vet/src/lib/transaction/transaction.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class Transaction extends BaseTransaction {
2626
protected _recipients: TransactionRecipient[];
2727
protected _clauses: TransactionClause[];
2828
protected _contract: string;
29+
protected _transactionData: string;
2930
private _chainTag: number;
3031
private _blockRef: string;
3132
private _expiration: number;
@@ -202,6 +203,14 @@ export class Transaction extends BaseTransaction {
202203
this._contract = address;
203204
}
204205

206+
get transactionData(): string {
207+
return this._transactionData;
208+
}
209+
210+
set transactionData(transactionData: string) {
211+
this._transactionData = transactionData;
212+
}
213+
205214
/**
206215
* Get all signatures associated with this transaction
207216
* Required by BaseTransaction
@@ -393,6 +402,9 @@ export class Transaction extends BaseTransaction {
393402
case TransactionType.AddressInitialization:
394403
this._type = TransactionType.AddressInitialization;
395404
break;
405+
case TransactionType.FlushTokens:
406+
this._type = TransactionType.FlushTokens;
407+
break;
396408
case TransactionType.Send:
397409
this._type = TransactionType.Send;
398410
const totalAmount = this._recipients.reduce(

modules/sdk-coin-vet/src/lib/transactionBuilder/addressInitializationBuilder.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,4 @@ export class AddressInitializationBuilder extends TransactionBuilder {
175175
const args = EthereumAbi.rawEncode(createForwarderTypes, createForwarderParams);
176176
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
177177
}
178-
179-
/** @inheritdoc */
180-
protected fromImplementation(rawTransaction: string): Transaction {
181-
const tx = new AddressInitializationTransaction(this._coinConfig);
182-
this.validateRawTransaction(rawTransaction);
183-
184-
tx.fromRawTransaction(rawTransaction);
185-
this.initBuilder(tx);
186-
this.validateTransaction(tx);
187-
return this.transaction;
188-
}
189178
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import assert from 'assert';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionType, BuildTransactionError } from '@bitgo/sdk-core';
4+
import { decodeFlushTokensData, flushTokensData } from '@bitgo/abstract-eth';
5+
import { TransactionClause } from '@vechain/sdk-core';
6+
7+
import { TransactionBuilder } from './transactionBuilder';
8+
import { Transaction } from '../transaction/transaction';
9+
import { FlushTokenTransaction } from '../transaction/flushTokenTransaction';
10+
import utils from '../utils';
11+
12+
export class FlushTokenTransactionBuilder extends TransactionBuilder {
13+
/**
14+
* Creates a new FlushTokenTransactionBuilder instance.
15+
*
16+
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
17+
*/
18+
constructor(_coinConfig: Readonly<CoinConfig>) {
19+
super(_coinConfig);
20+
}
21+
22+
/**
23+
* Initializes the builder with an existing FlushTokenTransaction.
24+
*
25+
* @param {FlushTokenTransaction} tx - The transaction to initialize the builder with
26+
*/
27+
initBuilder(tx: FlushTokenTransaction): void {
28+
this._transaction = tx;
29+
}
30+
31+
/**
32+
* Gets the flush token transaction instance.
33+
*
34+
* @returns {FlushTokenTransaction} The flush token transaction
35+
*/
36+
get flushTokenTransaction(): FlushTokenTransaction {
37+
return this._transaction as FlushTokenTransaction;
38+
}
39+
40+
/**
41+
* Gets the transaction type for flush token.
42+
*
43+
* @returns {TransactionType} The transaction type
44+
*/
45+
protected get transactionType(): TransactionType {
46+
return TransactionType.FlushTokens;
47+
}
48+
49+
/**
50+
* Validates the transaction clauses for flush token transaction.
51+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
52+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
53+
*/
54+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
55+
try {
56+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
57+
return false;
58+
}
59+
60+
const clause = clauses[0];
61+
62+
if (!clause.to || !utils.isValidAddress(clause.to)) {
63+
return false;
64+
}
65+
66+
// For address init transactions, value must be exactly 0
67+
if (clause.value !== 0) {
68+
return false;
69+
}
70+
71+
const { tokenAddress } = decodeFlushTokensData(clause.data, clause.to);
72+
73+
if (!utils.isValidAddress(tokenAddress as string)) {
74+
return false;
75+
}
76+
77+
return true;
78+
} catch (e) {
79+
return false;
80+
}
81+
}
82+
83+
/**
84+
* Sets the token address for this token flush tx.
85+
*
86+
* @param {string} address - The token address to be set for the token flush transaction
87+
* @returns {FlushTokenTransactionBuilder} This transaction builder
88+
*/
89+
tokenAddress(address: string): this {
90+
this.validateAddress({ address });
91+
this.flushTokenTransaction.tokenAddress = address;
92+
return this;
93+
}
94+
95+
/**
96+
* Sets the forwarder version for this token flush transaction.
97+
* The forwarder version must be 4 or higher.
98+
*
99+
* @param {number} version - The forwarder version to use (must be >= 4)
100+
* @returns {FlushTokenTransactionBuilder} This transaction builder
101+
* @throws {BuildTransactionError} When version is less than 4
102+
*/
103+
forwarderVersion(version: number): this {
104+
if (version < 4) {
105+
throw new BuildTransactionError(`Invalid forwarder version: ${version}`);
106+
}
107+
108+
this.flushTokenTransaction.forwarderVersion = version;
109+
return this;
110+
}
111+
112+
/** @inheritdoc */
113+
validateTransaction(transaction?: FlushTokenTransaction): void {
114+
if (!transaction) {
115+
throw new Error('transaction not defined');
116+
}
117+
assert(transaction.contract, 'Contract address is required');
118+
assert(transaction.tokenAddress, 'Token address is required');
119+
120+
this.validateAddress({ address: transaction.contract });
121+
this.validateAddress({ address: transaction.tokenAddress });
122+
}
123+
124+
/** @inheritdoc */
125+
protected async buildImplementation(): Promise<Transaction> {
126+
const transactionData = this.getFlushTokenTransactionData();
127+
this.transaction.type = this.transactionType;
128+
this.flushTokenTransaction.transactionData = transactionData;
129+
await this.flushTokenTransaction.build();
130+
return this.transaction;
131+
}
132+
133+
/**
134+
* Generates the transaction data for flush token transaction
135+
*
136+
* @private
137+
* @returns {string} The encoded transaction data as a hex string
138+
*/
139+
private getFlushTokenTransactionData(): string {
140+
const flushTokenData = flushTokensData(
141+
this.flushTokenTransaction.contract,
142+
this.flushTokenTransaction.tokenAddress,
143+
this.flushTokenTransaction.forwarderVersion
144+
);
145+
return flushTokenData;
146+
}
147+
}

modules/sdk-coin-vet/src/lib/transactionBuilder/transactionBuilder.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
180180
validateKey(key: BaseKey): void {
181181
throw new Error('Method not implemented.');
182182
}
183+
184+
/** @inheritdoc */
185+
protected fromImplementation(rawTransaction: string): Transaction {
186+
this.validateRawTransaction(rawTransaction);
187+
this.transaction.fromRawTransaction(rawTransaction);
188+
return this.transaction;
189+
}
183190
}

modules/sdk-coin-vet/src/lib/transactionBuilder/transferBuilder.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,6 @@ export class TransferBuilder extends TransactionBuilder {
1717
return TransactionType.Send;
1818
}
1919

20-
/** @inheritdoc */
21-
protected fromImplementation(rawTransaction: string): Transaction {
22-
const tx = new Transaction(this._coinConfig);
23-
this.validateRawTransaction(rawTransaction);
24-
25-
tx.fromRawTransaction(rawTransaction);
26-
this.initBuilder(tx);
27-
this.validateTransaction(tx);
28-
return this.transaction;
29-
}
30-
3120
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
3221
try {
3322
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {

0 commit comments

Comments
 (0)