Skip to content

Commit 43e9067

Browse files
authored
Merge pull request #6604 from BitGo/SC-2596
feat: add stakingBuilder for vechain
2 parents 1ad0d90 + e006cf2 commit 43e9067

File tree

9 files changed

+567
-3
lines changed

9 files changed

+567
-3
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const VET_ADDRESS_LENGTH = 40;
33
export const VET_BLOCK_ID_LENGTH = 64;
44

55
export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
6+
export const STAKING_METHOD_ID = '0xa694fc3a';
67
export const EXIT_DELEGATION_METHOD_ID = '0x32b7006d';
78
export const BURN_NFT_METHOD_ID = '0x42966c68';
89

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@ import {
44
TransactionRecipient,
55
} from '@bitgo/sdk-core';
66

7+
/**
8+
* Interface for ABI input parameter
9+
*/
10+
export interface AbiInput {
11+
internalType: string;
12+
name: string;
13+
type: string;
14+
}
15+
16+
/**
17+
* Interface for ABI output parameter
18+
*/
19+
export interface AbiOutput {
20+
internalType?: string;
21+
name?: string;
22+
type: string;
23+
}
24+
25+
/**
26+
* Interface for ABI function definition
27+
*/
28+
export interface AbiFunction {
29+
inputs: AbiInput[];
30+
name: string;
31+
outputs: AbiOutput[];
32+
stateMutability: string;
33+
type: string;
34+
}
35+
36+
/**
37+
* Type for contract ABI
38+
*/
39+
export type ContractAbi = AbiFunction[];
40+
741
/**
842
* The transaction data returned from the toJson() function of a transaction
943
*/
@@ -25,6 +59,8 @@ export interface VetTransactionData {
2559
to?: string;
2660
tokenAddress?: string;
2761
tokenId?: string; // Added for unstaking and burn NFT transactions
62+
stakingContractAddress?: string;
63+
amountToStake?: string;
2864
}
2965

3066
export interface VetTransactionExplanation extends BaseTransactionExplanation {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ export { Transaction } from './transaction/transaction';
77
export { AddressInitializationTransaction } from './transaction/addressInitializationTransaction';
88
export { FlushTokenTransaction } from './transaction/flushTokenTransaction';
99
export { TokenTransaction } from './transaction/tokenTransaction';
10+
export { StakingTransaction } from './transaction/stakingTransaction';
1011
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
1112
export { TransferBuilder } from './transactionBuilder/transferBuilder';
1213
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
1314
export { FlushTokenTransactionBuilder } from './transactionBuilder/flushTokenTransactionBuilder';
15+
export { StakingBuilder } from './transactionBuilder/stakingBuilder';
1416
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1517
export { Constants, Utils, Interface };
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
import { Transaction } from './transaction';
5+
import { VetTransactionData, ContractAbi } from '../iface';
6+
import utils from '../utils';
7+
8+
export class StakingTransaction extends Transaction {
9+
private _stakingContractAddress: string;
10+
private _amountToStake: string;
11+
private _stakingContractABI: ContractAbi;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
this._type = TransactionType.ContractCall;
16+
}
17+
18+
get stakingContractAddress(): string {
19+
return this._stakingContractAddress;
20+
}
21+
22+
set stakingContractAddress(address: string) {
23+
this._stakingContractAddress = address;
24+
}
25+
26+
get amountToStake(): string {
27+
return this._amountToStake;
28+
}
29+
30+
set amountToStake(amount: string) {
31+
this._amountToStake = amount;
32+
}
33+
34+
get stakingContractABI(): ContractAbi {
35+
return this._stakingContractABI;
36+
}
37+
38+
set stakingContractABI(abi: ContractAbi) {
39+
this._stakingContractABI = abi;
40+
}
41+
42+
buildClauses(): void {
43+
if (!this.stakingContractAddress) {
44+
throw new Error('Staking contract address is not set');
45+
}
46+
47+
if (!this.amountToStake) {
48+
throw new Error('Amount to stake is not set');
49+
}
50+
51+
// Generate transaction data using ethereumjs-abi
52+
const data = utils.getStakingData(this.amountToStake);
53+
this._transactionData = data;
54+
55+
// Create the clause for staking
56+
this._clauses = [
57+
{
58+
to: this.stakingContractAddress,
59+
value: this.amountToStake,
60+
data: this._transactionData,
61+
},
62+
];
63+
64+
// Set recipients based on the clauses
65+
this._recipients = [
66+
{
67+
address: this.stakingContractAddress,
68+
amount: this.amountToStake,
69+
},
70+
];
71+
}
72+
73+
toJson(): VetTransactionData {
74+
const json: VetTransactionData = {
75+
id: this.id,
76+
chainTag: this.chainTag,
77+
blockRef: this.blockRef,
78+
expiration: this.expiration,
79+
gasPriceCoef: this.gasPriceCoef,
80+
gas: this.gas,
81+
dependsOn: this.dependsOn,
82+
nonce: this.nonce,
83+
data: this.transactionData,
84+
value: this.amountToStake,
85+
sender: this.sender,
86+
to: this.stakingContractAddress,
87+
stakingContractAddress: this.stakingContractAddress,
88+
amountToStake: this.amountToStake,
89+
};
90+
91+
return json;
92+
}
93+
94+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
95+
try {
96+
if (!signedTx || !signedTx.body) {
97+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
98+
}
99+
100+
// Store the raw transaction
101+
this.rawTransaction = signedTx;
102+
103+
// Set transaction body properties
104+
const body = signedTx.body;
105+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
106+
this.blockRef = body.blockRef || '0x0';
107+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
108+
this.clauses = body.clauses || [];
109+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
110+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
111+
this.dependsOn = body.dependsOn || null;
112+
this.nonce = String(body.nonce);
113+
114+
// Set staking-specific properties
115+
if (body.clauses.length > 0) {
116+
const clause = body.clauses[0];
117+
if (clause.to) {
118+
this.stakingContractAddress = clause.to;
119+
}
120+
if (clause.value) {
121+
this.amountToStake = String(clause.value);
122+
}
123+
if (clause.data) {
124+
this.transactionData = clause.data;
125+
}
126+
}
127+
128+
// Set recipients from clauses
129+
this.recipients = body.clauses.map((clause) => ({
130+
address: (clause.to || '0x0').toString().toLowerCase(),
131+
amount: String(clause.value || '0'),
132+
}));
133+
this.loadInputsAndOutputs();
134+
135+
// Set sender address
136+
if (signedTx.signature && signedTx.origin) {
137+
this.sender = signedTx.origin.toString().toLowerCase();
138+
}
139+
140+
// Set signatures if present
141+
if (signedTx.signature) {
142+
// First signature is sender's signature
143+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
144+
145+
// If there's additional signature data, it's the fee payer's signature
146+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
147+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
148+
}
149+
}
150+
} catch (e) {
151+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
152+
}
153+
}
154+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import assert from 'assert';
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 { Transaction } from '../transaction/transaction';
8+
import { StakingTransaction } from '../transaction/stakingTransaction';
9+
import { ContractAbi } from '../iface';
10+
import utils from '../utils';
11+
12+
export class StakingBuilder extends TransactionBuilder {
13+
/**
14+
* Creates a new StakingBuilder instance.
15+
*
16+
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
17+
*/
18+
constructor(_coinConfig: Readonly<CoinConfig>) {
19+
super(_coinConfig);
20+
this._transaction = new StakingTransaction(_coinConfig);
21+
}
22+
23+
/**
24+
* Initializes the builder with an existing StakingTransaction.
25+
*
26+
* @param {StakingTransaction} tx - The transaction to initialize the builder with
27+
*/
28+
initBuilder(tx: StakingTransaction): void {
29+
this._transaction = tx;
30+
}
31+
32+
/**
33+
* Gets the staking transaction instance.
34+
*
35+
* @returns {StakingTransaction} The staking transaction
36+
*/
37+
get stakingTransaction(): StakingTransaction {
38+
return this._transaction as StakingTransaction;
39+
}
40+
41+
/**
42+
* Gets the transaction type for staking.
43+
*
44+
* @returns {TransactionType} The transaction type
45+
*/
46+
protected get transactionType(): TransactionType {
47+
return TransactionType.ContractCall;
48+
}
49+
50+
/**
51+
* Validates the transaction clauses for staking transaction.
52+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
53+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
54+
*/
55+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
56+
try {
57+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
58+
return false;
59+
}
60+
61+
const clause = clauses[0];
62+
63+
if (!clause.to || !utils.isValidAddress(clause.to)) {
64+
return false;
65+
}
66+
67+
// For staking transactions, value must be greater than 0
68+
if (!clause.value || clause.value === '0x0' || clause.value === '0') {
69+
return false;
70+
}
71+
72+
return true;
73+
} catch (e) {
74+
return false;
75+
}
76+
}
77+
78+
/**
79+
* Sets the staking contract address for this staking tx.
80+
*
81+
* @param {string} address - The staking contract address
82+
* @returns {StakingBuilder} This transaction builder
83+
*/
84+
stakingContractAddress(address: string): this {
85+
this.validateAddress({ address });
86+
this.stakingTransaction.stakingContractAddress = address;
87+
return this;
88+
}
89+
90+
/**
91+
* Sets the amount to stake for this staking tx.
92+
*
93+
* @param {string} amount - The amount to stake in wei
94+
* @returns {StakingBuilder} This transaction builder
95+
*/
96+
amountToStake(amount: string): this {
97+
this.stakingTransaction.amountToStake = amount;
98+
return this;
99+
}
100+
101+
/**
102+
* Sets the staking contract ABI for this staking tx.
103+
*
104+
* @param {ContractAbi} abi - The staking contract ABI
105+
* @returns {StakingBuilder} This transaction builder
106+
*/
107+
stakingContractABI(abi: ContractAbi): this {
108+
this.stakingTransaction.stakingContractABI = abi;
109+
return this;
110+
}
111+
112+
/**
113+
* Sets the transaction data for this staking tx.
114+
*
115+
* @param {string} data - The transaction data
116+
* @returns {StakingBuilder} This transaction builder
117+
*/
118+
transactionData(data: string): this {
119+
this.stakingTransaction.transactionData = data;
120+
return this;
121+
}
122+
123+
/** @inheritdoc */
124+
validateTransaction(transaction?: StakingTransaction): void {
125+
if (!transaction) {
126+
throw new Error('transaction not defined');
127+
}
128+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
129+
assert(transaction.amountToStake, 'Amount to stake is required');
130+
131+
this.validateAddress({ address: transaction.stakingContractAddress });
132+
}
133+
134+
/** @inheritdoc */
135+
protected async buildImplementation(): Promise<Transaction> {
136+
this.transaction.type = this.transactionType;
137+
await this.stakingTransaction.build();
138+
return this.transaction;
139+
}
140+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { ExitDelegationTransaction } from './transaction/exitDelegation';
1515
import { BurnNftTransaction } from './transaction/burnNftTransaction';
1616
import { TokenTransactionBuilder } from './transactionBuilder/tokenTransactionBuilder';
1717
import { TokenTransaction } from './transaction/tokenTransaction';
18+
import { StakingBuilder } from './transactionBuilder/stakingBuilder';
19+
import { StakingTransaction } from './transaction/stakingTransaction';
1820

1921
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2022
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -43,6 +45,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4345
const tokenTransferTx = new TokenTransaction(this._coinConfig);
4446
tokenTransferTx.fromDeserializedSignedTransaction(signedTx);
4547
return this.getTokenTransactionBuilder(tokenTransferTx);
48+
case TransactionType.ContractCall:
49+
const stakingTx = new StakingTransaction(this._coinConfig);
50+
stakingTx.fromDeserializedSignedTransaction(signedTx);
51+
return this.getStakingBuilder(stakingTx);
4652
case TransactionType.StakingUnlock:
4753
const exitDelegationTx = new ExitDelegationTransaction(this._coinConfig);
4854
exitDelegationTx.fromDeserializedSignedTransaction(signedTx);
@@ -76,6 +82,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
7682
return this.initializeBuilder(tx, new TokenTransactionBuilder(this._coinConfig));
7783
}
7884

85+
getStakingBuilder(tx?: StakingTransaction): StakingBuilder {
86+
return this.initializeBuilder(tx, new StakingBuilder(this._coinConfig));
87+
}
88+
7989
/**
8090
* Gets an exit delegation transaction builder.
8191
*

0 commit comments

Comments
 (0)