Skip to content

Commit e867c03

Browse files
authored
Merge pull request #6487 from BitGo/coin-4887
feat(sdk-coin-vet): add token transaction builder for vechain
2 parents 515d9b8 + 86036b8 commit e867c03

File tree

12 files changed

+490
-17
lines changed

12 files changed

+490
-17
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const VET_TRANSACTION_ID_LENGTH = 64;
22
export const VET_ADDRESS_LENGTH = 40;
33
export const VET_BLOCK_ID_LENGTH = 64;
4+
5+
export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export class Transaction extends BaseTransaction {
371371
nonce: this.nonce,
372372
};
373373

374-
if (this.type === TransactionType.Send) {
374+
if (this.type === TransactionType.Send || this.type === TransactionType.SendToken) {
375375
transactionBody.reserved = {
376376
features: 1, // mark transaction as delegated i.e. will use gas payer
377377
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { addHexPrefix } from 'ethereumjs-util';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionType } from '@bitgo/sdk-core';
4+
import { TransactionClause } from '@vechain/sdk-core';
5+
6+
import { TransactionBuilder } from './transactionBuilder';
7+
import { TokenTransaction } from '../transaction/tokenTransaction';
8+
import utils from '../utils';
9+
10+
export class TokenTransactionBuilder extends TransactionBuilder {
11+
constructor(_coinConfig: Readonly<CoinConfig>) {
12+
super(_coinConfig);
13+
}
14+
15+
initBuilder(tx: TokenTransaction): void {
16+
this._transaction = tx;
17+
}
18+
19+
get tokenTransaction(): TokenTransaction {
20+
return this._transaction as TokenTransaction;
21+
}
22+
23+
protected get transactionType(): TransactionType {
24+
return TransactionType.Send;
25+
}
26+
27+
/**
28+
* Validates the transaction clauses for flush token transaction.
29+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
30+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
31+
*/
32+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
33+
try {
34+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
35+
return false;
36+
}
37+
38+
const clause = clauses[0];
39+
40+
if (!clause.to || !utils.isValidAddress(clause.to)) {
41+
return false;
42+
}
43+
44+
// For token transactions, the value should be 0
45+
if (clause.value !== 0) {
46+
return false;
47+
}
48+
49+
const { address } = utils.decodeTransferTokenData(clause.data);
50+
const recipientAddress = addHexPrefix(address.toString()).toLowerCase();
51+
52+
if (!recipientAddress || !utils.isValidAddress(recipientAddress)) {
53+
return false;
54+
}
55+
56+
return true;
57+
} catch (e) {
58+
return false;
59+
}
60+
}
61+
62+
tokenAddress(address: string): this {
63+
this.validateAddress({ address });
64+
this.tokenTransaction.tokenAddress = address;
65+
return this;
66+
}
67+
68+
/** @inheritdoc */
69+
validateTransaction(transaction?: TokenTransaction): void {
70+
if (!transaction) {
71+
throw new Error('transaction not defined');
72+
}
73+
74+
if (!transaction.tokenAddress) {
75+
throw new Error('Token address is required');
76+
}
77+
78+
this.validateAddress({ address: transaction.tokenAddress });
79+
}
80+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Transaction } from './transaction/transaction';
99
import utils from './utils';
1010
import { AddressInitializationTransaction } from './transaction/addressInitializationTransaction';
1111
import { FlushTokenTransaction } from './transaction/flushTokenTransaction';
12+
import { TokenTransactionBuilder } from './transactionBuilder/tokenTransactionBuilder';
13+
import { TokenTransaction } from './transaction/tokenTransaction';
1214

1315
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1416
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -33,6 +35,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3335
const flushTokenTx = new FlushTokenTransaction(this._coinConfig);
3436
flushTokenTx.fromDeserializedSignedTransaction(signedTx);
3537
return this.getFlushTokenTransactionBuilder(flushTokenTx);
38+
case TransactionType.SendToken:
39+
const tokenTransferTx = new TokenTransaction(this._coinConfig);
40+
tokenTransferTx.fromDeserializedSignedTransaction(signedTx);
41+
return this.getTokenTransactionBuilder(tokenTransferTx);
3642
default:
3743
throw new InvalidTransactionError('Invalid transaction type');
3844
}
@@ -54,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
5460
return this.initializeBuilder(tx, new FlushTokenTransactionBuilder(this._coinConfig));
5561
}
5662

63+
getTokenTransactionBuilder(tx?: Transaction): TokenTransactionBuilder {
64+
return this.initializeBuilder(tx, new TokenTransactionBuilder(this._coinConfig));
65+
}
66+
5767
/** @inheritdoc */
5868
getWalletInitializationBuilder(): void {
5969
throw new Error('Method not implemented.');

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
import { BaseUtils, TransactionType } from '@bitgo/sdk-core';
2-
import { v4CreateForwarderMethodId, flushForwarderTokensMethodIdV4 } from '@bitgo/abstract-eth';
3-
import { VET_ADDRESS_LENGTH, VET_BLOCK_ID_LENGTH, VET_TRANSACTION_ID_LENGTH } from './constants';
4-
import { KeyPair } from './keyPair';
51
import { HexUInt, Transaction, TransactionClause } from '@vechain/sdk-core';
2+
import EthereumAbi from 'ethereumjs-abi';
3+
import { addHexPrefix, BN } from 'ethereumjs-util';
4+
import { BaseUtils, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
5+
import {
6+
v4CreateForwarderMethodId,
7+
flushForwarderTokensMethodIdV4,
8+
getRawDecoded,
9+
getBufferedByteCode,
10+
} from '@bitgo/abstract-eth';
11+
import {
12+
TRANSFER_TOKEN_METHOD_ID,
13+
VET_ADDRESS_LENGTH,
14+
VET_BLOCK_ID_LENGTH,
15+
VET_TRANSACTION_ID_LENGTH,
16+
} from './constants';
17+
import { KeyPair } from './keyPair';
618

719
export class Utils implements BaseUtils {
820
isValidAddress(address: string): boolean {
@@ -64,10 +76,35 @@ export class Utils implements BaseUtils {
6476
return TransactionType.AddressInitialization;
6577
} else if (clauses[0].data.startsWith(flushForwarderTokensMethodIdV4)) {
6678
return TransactionType.FlushTokens;
79+
} else if (clauses[0].data.startsWith(TRANSFER_TOKEN_METHOD_ID)) {
80+
return TransactionType.SendToken;
6781
} else {
6882
return TransactionType.SendToken;
6983
}
7084
}
85+
86+
getTransferTokenData(toAddress: string, amountWei: string): string {
87+
const methodName = 'transfer';
88+
const types = ['address', 'uint256'];
89+
const params = [toAddress, new BN(amountWei)];
90+
91+
const method = EthereumAbi.methodID(methodName, types);
92+
const args = EthereumAbi.rawEncode(types, params);
93+
94+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
95+
}
96+
97+
decodeTransferTokenData(data: string): TransactionRecipient {
98+
const [address, amount] = getRawDecoded(
99+
['address', 'uint256'],
100+
getBufferedByteCode(TRANSFER_TOKEN_METHOD_ID, data)
101+
);
102+
const recipientAddress = addHexPrefix(address.toString()).toLowerCase();
103+
return {
104+
address: recipientAddress,
105+
amount: amount.toString(),
106+
};
107+
}
71108
}
72109

73110
const utils = new Utils();

modules/sdk-coin-vet/test/resources/vet.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ export const AMOUNT = 100000000000000000; // 0.1 VET in base units
66
export const SPONSORED_TRANSACTION =
77
'0xf8bc2788014e9cad44bade0940e0df94e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec88016345785d8a0000808180825208808302d6b5c101b882ee76129c1259eb0c8a2b3b3e5f2b089cd11068da1c0db32a9e22228db83dd4be5c721858bc813514141fbd5cf641d0972ce47ceb9be61133fa2ebf0ea37c1f290011fdce201f56d639d827035a5ed8bcef42a42f6eb562bc76a6d95c7736cf8cf340122d1e2fb034668dc491d47b7d3bb10724ba2338a6e79df87bce9617fdce9c00';
88

9+
export const SPONSORED_TOKEN_TRANSACTION =
10+
'0xf8fb278801543ac0a0b3aaf940f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a0000818082c93e808307e669c101b8825dc39997717119cec9e65bfc927d1ad6ef31956e4747f95cea49f76f9e66164a737a155a6694f86287bcec043a83c7f9a3169fd3bb57ce617ab3fd4c59da437701277f6455fc041e672082bff003e75520c3167ad61edd98fba7e0ed87eb5323fc47feb3f51bfe2faa31c2174a2f7bb548e9727d7628f15137b4007a26651413fd00';
11+
12+
export const VALID_TOKEN_SIGNABLE_PAYLOAD =
13+
'f8762788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101';
14+
915
export const UNSIGNED_TRANSACTION =
1016
'0xf4278801536ce9e9fb063840dddc94c52584d1c56e7bddcb6f65d50ff00f71e0ef897a85e8d4a510008081808252088083061c70c0';
1117

1218
export const UNSIGNED_TRANSACTION_2 =
1319
'0xf72788014ead140e77bbc140e0df94e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec88016345785d8a00008081808252088082faf8c101';
1420

21+
export const UNSIGNED_TRANSACTION_3 =
22+
'0xf8762788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101';
23+
1524
export const ADDRESS_INITIALIZATION_TRANSACTION =
1625
'0xf8952788014ead140e77bbc140f87ef87c9465343e18c376d2fc8c3cf10cd146d63e2e0dc9ef80b86413b2f75c00000000000000000000000055b00b5c807d5696197b48d4affa40bb876df2400000000000000000000000007c87b9ffc6fd6c167c0e4fa9418720f3d659358e000000000000000000000000000000000000000000000000000000000000000181808252088082faf8c0';
1726

@@ -63,12 +72,24 @@ export const senderSig =
6372
export const feePayerSig =
6473
'47ae79e23effd233206180ece3ab3d6d496310c03c55bd0566a54bb7d42095c97a90ca9ff4cfb94ec579a7ec7f79de4814dfc816d0a130189c07b746945482fb00';
6574

75+
export const senderSig2 =
76+
'9d05e90a00c538afd68652f63a4e21481d1d082fdbd4a300b04b561dc76185ad292bb48d70846bd0fb8b526782b9d2f940dbcee3bc554bfb7b53e362bbcb39d701';
77+
78+
export const feePayerSig2 =
79+
'45d8aec4d2ed5f74b788695ceb76d18dd3597503bc0466d667493b2aff2c2b095d352788a345b2e3b49e9ad19bc28b0bdee45d0cce986962e61b0febe60a259301';
80+
6681
export const senderSignedSerializedTxHex =
6782
'0xf72788014ead140e77bbc140e0df94e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec88016345785d8a00008081808252088082faf8c101';
6883

84+
export const senderSignedSerializedTxHex2 =
85+
'0xf8762788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101';
86+
6987
export const completeSignedSerializedHex =
7088
'0xf8bb2788014ead140e77bbc140e0df94e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec88016345785d8a00008081808252088082faf8c101b882062080cc77db9d5ce5db84f1cfb57c9906c9bfdce78e4d53757bbd3d536c731e1e5e406a4e783a4f63446aa28d21dc27796aae90fac79151c3d0b3ba68cde3680147ae79e23effd233206180ece3ab3d6d496310c03c55bd0566a54bb7d42095c97a90ca9ff4cfb94ec579a7ec7f79de4814dfc816d0a130189c07b746945482fb00';
7189

90+
export const completeSignedSerializedHex2 =
91+
'0xf8fa2788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101b8829d05e90a00c538afd68652f63a4e21481d1d082fdbd4a300b04b561dc76185ad292bb48d70846bd0fb8b526782b9d2f940dbcee3bc554bfb7b53e362bbcb39d70145d8aec4d2ed5f74b788695ceb76d18dd3597503bc0466d667493b2aff2c2b095d352788a345b2e3b49e9ad19bc28b0bdee45d0cce986962e61b0febe60a259301';
92+
7293
export const blockIds: { validBlockIds: string[]; invalidBlockIds: string[] } = {
7394
validBlockIds: [
7495
'0x014f12ed94c4b4770f7f9a73e2aa41a9dfbac02a49f36ec05acfdba8c7244ff0',

modules/sdk-coin-vet/test/transactionBuilder/addressInitializationBuilder.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { TransactionBuilderFactory, Transaction } from '../../src';
2-
import { coins } from '@bitgo/statics';
3-
import * as testData from '../resources/vet';
41
import should from 'should';
52
import { TransactionType } from '@bitgo/sdk-core';
6-
7-
import { AddressInitializationTransaction } from '../../src/lib/transaction/addressInitializationTransaction';
3+
import { coins } from '@bitgo/statics';
4+
import * as testData from '../resources/vet';
5+
import { TransactionBuilderFactory, Transaction, AddressInitializationTransaction } from '../../src';
86

97
describe('Address Initialisation Transaction', () => {
108
const factory = new TransactionBuilderFactory(coins.get('tvet'));

modules/sdk-coin-vet/test/transactionBuilder/flushTokenTransactionBuilder.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { TransactionBuilderFactory } from '../../src';
2-
import { coins } from '@bitgo/statics';
31
import should from 'should';
2+
import { coins } from '@bitgo/statics';
43
import { TransactionType } from '@bitgo/sdk-core';
5-
4+
import { TransactionBuilderFactory, FlushTokenTransaction } from '../../src';
65
import * as testData from '../resources/vet';
7-
import { FlushTokenTransaction } from '../../src/lib/transaction/flushTokenTransaction';
86

97
describe('Flush Token Transaction', () => {
108
const factory = new TransactionBuilderFactory(coins.get('tvet'));

0 commit comments

Comments
 (0)