Skip to content

Commit 6b12add

Browse files
feat(sdk-coin-vet): add delegate txn builder
Ticket: SC-3643
1 parent b9fc852 commit 6b12add

File tree

9 files changed

+506
-0
lines changed

9 files changed

+506
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
export const VET_TRANSACTION_ID_LENGTH = 64;
22
export const VET_ADDRESS_LENGTH = 40;
33
export const VET_BLOCK_ID_LENGTH = 64;
4+
export const ZERO_VALUE_AMOUNT = '0';
45

56
export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
67
export const STAKING_METHOD_ID = '0xd8da3bbf';
78
export const STAKE_CLAUSE_METHOD_ID = '0x604f2177';
9+
export const DELEGATE_CLAUSE_METHOD_ID = '0x3207555d';
810
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
911
export const BURN_NFT_METHOD_ID = '0x2e17de78';
1012
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { FlushTokenTransaction } from './transaction/flushTokenTransaction';
99
export { TokenTransaction } from './transaction/tokenTransaction';
1010
export { StakingTransaction } from './transaction/stakingTransaction';
1111
export { StakeClauseTransaction } from './transaction/stakeClauseTransaction';
12+
export { DelegateClauseTransaction } from './transaction/delegateClauseTransaction';
1213
export { ExitDelegationTransaction } from './transaction/exitDelegation';
1314
export { BurnNftTransaction } from './transaction/burnNftTransaction';
1415
export { ClaimRewardsTransaction } from './transaction/claimRewards';
@@ -19,6 +20,7 @@ export { AddressInitializationBuilder } from './transactionBuilder/addressInitia
1920
export { FlushTokenTransactionBuilder } from './transactionBuilder/flushTokenTransactionBuilder';
2021
export { StakingBuilder } from './transactionBuilder/stakingBuilder';
2122
export { StakeClauseTxnBuilder } from './transactionBuilder/stakeClauseTxnBuilder';
23+
export { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
2224
export { NFTTransactionBuilder } from './transactionBuilder/nftTransactionBuilder';
2325
export { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
2426
export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 } from '../iface';
6+
import EthereumAbi from 'ethereumjs-abi';
7+
import utils from '../utils';
8+
import BigNumber from 'bignumber.js';
9+
import { addHexPrefix } from 'ethereumjs-util';
10+
import { ZERO_VALUE_AMOUNT } from '../constants';
11+
12+
export class DelegateClauseTransaction extends Transaction {
13+
private _stakingContractAddress: string;
14+
private _tokenId: number;
15+
private _delegateForever = true;
16+
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
this._type = TransactionType.StakingDelegate;
20+
}
21+
22+
get stakingContractAddress(): string {
23+
return this._stakingContractAddress;
24+
}
25+
26+
set stakingContractAddress(address: string) {
27+
this._stakingContractAddress = address;
28+
}
29+
30+
get tokenId(): number {
31+
return this._tokenId;
32+
}
33+
34+
set tokenId(tokenId: number) {
35+
this._tokenId = tokenId;
36+
}
37+
38+
get delegateForever(): boolean {
39+
return this._delegateForever;
40+
}
41+
42+
set delegateForever(delegateForever: boolean) {
43+
this._delegateForever = delegateForever;
44+
}
45+
46+
buildClauses(): void {
47+
if (!this.stakingContractAddress) {
48+
throw new Error('Staking contract address is not set');
49+
}
50+
51+
utils.validateDelegationContractAddress(this.stakingContractAddress, this._coinConfig);
52+
53+
if (this.tokenId === undefined || this.tokenId === null) {
54+
throw new Error('Token ID is not set');
55+
}
56+
57+
const data = this.getDelegateData(this.tokenId, this.delegateForever);
58+
this._transactionData = data;
59+
60+
// Create the clause for delegation
61+
this._clauses = [
62+
{
63+
to: this.stakingContractAddress,
64+
value: ZERO_VALUE_AMOUNT,
65+
data: this._transactionData,
66+
},
67+
];
68+
69+
// Set recipients based on the clauses
70+
this._recipients = [
71+
{
72+
address: this.stakingContractAddress,
73+
amount: ZERO_VALUE_AMOUNT,
74+
},
75+
];
76+
}
77+
/**
78+
* Encodes delegation transaction data using ethereumjs-abi for delegate method
79+
*
80+
* @param {number} tokenId - The Token ID for delegation
81+
* @returns {string} - The encoded transaction data
82+
*/
83+
getDelegateData(levelId: number, delegateForever = true): string {
84+
const methodName = 'delegate';
85+
const types = ['uint256', 'bool'];
86+
const params = [levelId, delegateForever];
87+
88+
const method = EthereumAbi.methodID(methodName, types);
89+
const args = EthereumAbi.rawEncode(types, params);
90+
91+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
92+
}
93+
94+
toJson(): VetTransactionData {
95+
const json: VetTransactionData = {
96+
id: this.id,
97+
chainTag: this.chainTag,
98+
blockRef: this.blockRef,
99+
expiration: this.expiration,
100+
gasPriceCoef: this.gasPriceCoef,
101+
gas: this.gas,
102+
dependsOn: this.dependsOn,
103+
nonce: this.nonce,
104+
data: this.transactionData,
105+
value: ZERO_VALUE_AMOUNT,
106+
sender: this.sender,
107+
to: this.stakingContractAddress,
108+
stakingContractAddress: this.stakingContractAddress,
109+
amountToStake: ZERO_VALUE_AMOUNT,
110+
nftTokenId: this.tokenId,
111+
autorenew: this.delegateForever,
112+
};
113+
114+
return json;
115+
}
116+
117+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
118+
try {
119+
if (!signedTx || !signedTx.body) {
120+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
121+
}
122+
123+
// Store the raw transaction
124+
this.rawTransaction = signedTx;
125+
126+
// Set transaction body properties
127+
const body = signedTx.body;
128+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
129+
this.blockRef = body.blockRef || '0x0';
130+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
131+
this.clauses = body.clauses || [];
132+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
133+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
134+
this.dependsOn = body.dependsOn || null;
135+
this.nonce = String(body.nonce);
136+
137+
// Set delegation-specific properties
138+
if (body.clauses.length > 0) {
139+
const clause = body.clauses[0];
140+
if (clause.to) {
141+
this.stakingContractAddress = clause.to;
142+
}
143+
if (clause.data) {
144+
this.transactionData = clause.data;
145+
const decoded = utils.decodeDelegateClauseData(clause.data);
146+
this.tokenId = decoded.tokenId;
147+
this.delegateForever = decoded.delegateForever;
148+
}
149+
}
150+
151+
// Set recipients from clauses
152+
this.recipients = body.clauses.map((clause) => ({
153+
address: (clause.to || '0x0').toString().toLowerCase(),
154+
amount: new BigNumber(clause.value || 0).toString(),
155+
}));
156+
this.loadInputsAndOutputs();
157+
158+
// Set sender address
159+
if (signedTx.signature && signedTx.origin) {
160+
this.sender = signedTx.origin.toString().toLowerCase();
161+
}
162+
163+
// Set signatures if present
164+
if (signedTx.signature) {
165+
// First signature is sender's signature
166+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
167+
168+
// If there's additional signature data, it's the fee payer's signature
169+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
170+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
171+
}
172+
}
173+
} catch (e) {
174+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
175+
}
176+
}
177+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export class Transaction extends BaseTransaction {
390390
this.type === TransactionType.SendNFT ||
391391
this.type === TransactionType.ContractCall ||
392392
this.type === TransactionType.StakingActivate ||
393+
this.type === TransactionType.StakingDelegate ||
393394
this.type === TransactionType.StakingUnlock ||
394395
this.type === TransactionType.StakingWithdraw ||
395396
this.type === TransactionType.StakingClaim
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 { DelegateClauseTransaction } from '../transaction/delegateClauseTransaction';
9+
import utils from '../utils';
10+
11+
export class DelegateTxnBuilder extends TransactionBuilder {
12+
/**
13+
* Creates a new Delegate Clause txn instance.
14+
*
15+
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
16+
*/
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
this._transaction = new DelegateClauseTransaction(_coinConfig);
20+
}
21+
22+
/**
23+
* Initializes the builder with an existing Delegate txn.
24+
*
25+
* @param {DelegateClauseTransaction} tx - The transaction to initialize the builder with
26+
*/
27+
initBuilder(tx: DelegateClauseTransaction): void {
28+
this._transaction = tx;
29+
}
30+
31+
/**
32+
* Gets the staking transaction instance.
33+
*
34+
* @returns {DelegateClauseTransaction} The delegate transaction
35+
*/
36+
get delegateTransaction(): DelegateClauseTransaction {
37+
return this._transaction as DelegateClauseTransaction;
38+
}
39+
40+
/**
41+
* Gets the transaction type for delegate.
42+
*
43+
* @returns {TransactionType} The transaction type
44+
*/
45+
protected get transactionType(): TransactionType {
46+
return TransactionType.StakingDelegate;
47+
}
48+
49+
/**
50+
* Validates the transaction clauses for delegate 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+
if (!clause.to || !utils.isValidAddress(clause.to)) {
62+
return false;
63+
}
64+
65+
return true;
66+
} catch (e) {
67+
return false;
68+
}
69+
}
70+
71+
/**
72+
* Sets the staking contract address for this delegate tx.
73+
* The address must be explicitly provided to ensure the correct contract is used.
74+
*
75+
* @param {string} address - The staking contract address (required)
76+
* @returns {DelegateTxnBuilder} This transaction builder
77+
* @throws {Error} If no address is provided
78+
*/
79+
stakingContractAddress(address: string): this {
80+
if (!address) {
81+
throw new Error('Staking contract address is required');
82+
}
83+
this.validateAddress({ address });
84+
this.delegateTransaction.stakingContractAddress = address;
85+
return this;
86+
}
87+
88+
/**
89+
* Sets the token ID for this delegate tx.
90+
*
91+
* @param {number} levelId - The level ID for staking
92+
* @returns {DelegateTxnBuilder} This transaction builder
93+
*/
94+
tokenId(tokenId: number): this {
95+
this.delegateTransaction.tokenId = tokenId;
96+
return this;
97+
}
98+
99+
/**
100+
* Sets the transaction data for this delegate tx.
101+
*
102+
* @param {string} data - The transaction data
103+
* @returns {DelegateTxnBuilder} This transaction builder
104+
*/
105+
transactionData(data: string): this {
106+
this.delegateTransaction.transactionData = data;
107+
return this;
108+
}
109+
110+
/** @inheritdoc */
111+
validateTransaction(transaction?: DelegateClauseTransaction): void {
112+
if (!transaction) {
113+
throw new Error('transaction not defined');
114+
}
115+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
116+
117+
assert(transaction.tokenId, 'Token ID is required');
118+
assert(transaction.delegateForever, 'delegate forever flag is required');
119+
this.validateAddress({ address: transaction.stakingContractAddress });
120+
}
121+
122+
/** @inheritdoc */
123+
protected async buildImplementation(): Promise<Transaction> {
124+
this.transaction.type = this.transactionType;
125+
await this.delegateTransaction.build();
126+
return this.transaction;
127+
}
128+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { NFTTransactionBuilder } from './transactionBuilder/nftTransactionBuilde
2323
import { NFTTransaction } from './transaction/nftTransaction';
2424
import { StakeClauseTransaction } from './transaction/stakeClauseTransaction';
2525
import { StakeClauseTxnBuilder } from './transactionBuilder/stakeClauseTxnBuilder';
26+
import { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
27+
import { DelegateClauseTransaction } from './transaction/delegateClauseTransaction';
2628

2729
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2830
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -63,6 +65,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
6365
const stakeClauseTx = new StakeClauseTransaction(this._coinConfig);
6466
stakeClauseTx.fromDeserializedSignedTransaction(signedTx);
6567
return this.getStakingActivateBuilder(stakeClauseTx);
68+
case TransactionType.StakingDelegate:
69+
const delegateClauseTx = new DelegateClauseTransaction(this._coinConfig);
70+
delegateClauseTx.fromDeserializedSignedTransaction(signedTx);
71+
return this.getStakingDelegateBuilder(delegateClauseTx);
6672
case TransactionType.StakingUnlock:
6773
const exitDelegationTx = new ExitDelegationTransaction(this._coinConfig);
6874
exitDelegationTx.fromDeserializedSignedTransaction(signedTx);
@@ -104,6 +110,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
104110
return this.initializeBuilder(tx, new StakingBuilder(this._coinConfig));
105111
}
106112

113+
getStakingDelegateBuilder(tx?: DelegateClauseTransaction): DelegateTxnBuilder {
114+
return this.initializeBuilder(tx, new DelegateTxnBuilder(this._coinConfig));
115+
}
116+
107117
getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
108118
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
109119
}

0 commit comments

Comments
 (0)