Skip to content

Commit 41d53f9

Browse files
committed
feat(sdk-coin-vet): add unstake buidlers for validators
Ticket: SC-6238
1 parent 6754492 commit 41d53f9

File tree

11 files changed

+794
-1
lines changed

11 files changed

+794
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export const DELEGATE_CLAUSE_METHOD_ID = '0x08bbb824';
1010
export const ADD_VALIDATION_METHOD_ID = '0xc3c4b138';
1111
export const INCREASE_STAKE_METHOD_ID = '0x43b0de9a';
1212
export const DECREASE_STAKE_METHOD_ID = '0x1a73ba01';
13+
export const SIGNAL_EXIT_METHOD_ID = '0xcb652cef';
14+
export const WITHDRAW_STAKE_METHOD_ID = '0xc23a5cea';
1315
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
1416
export const BURN_NFT_METHOD_ID = '0x2e17de78';
1517
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export { NFTTransaction } from './transaction/nftTransaction';
1717
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
1818
export { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
1919
export { DecreaseStakeTransaction } from './transaction/decreaseStakeTransaction';
20+
export { SignalExitTransaction } from './transaction/signalExitTransaction';
21+
export { WithdrawStakeTransaction } from './transaction/withdrawStakeTransaction';
2022
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
2123
export { TransferBuilder } from './transactionBuilder/transferBuilder';
2224
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
@@ -31,5 +33,7 @@ export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
3133
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
3234
export { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
3335
export { DecreaseStakeBuilder } from './transactionBuilder/decreaseStakeBuilder';
36+
export { SignalExitBuilder } from './transactionBuilder/signalExitBuilder';
37+
export { WithdrawStakeBuilder } from './transactionBuilder/withdrawStakeBuilder';
3438
export { TransactionBuilderFactory } from './transactionBuilderFactory';
3539
export { Constants, Utils, Interface };
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 SignalExitTransaction extends Transaction {
13+
private _stakingContractAddress: string;
14+
private _validator: string;
15+
16+
constructor(_coinConfig: Readonly<CoinConfig>) {
17+
super(_coinConfig);
18+
this._type = TransactionType.StakingUnvote;
19+
}
20+
21+
get validator(): string {
22+
return this._validator;
23+
}
24+
25+
set validator(address: string) {
26+
this._validator = address;
27+
}
28+
29+
get stakingContractAddress(): string {
30+
return this._stakingContractAddress;
31+
}
32+
33+
set stakingContractAddress(address: string) {
34+
this._stakingContractAddress = address;
35+
}
36+
37+
buildClauses(): void {
38+
if (!this.stakingContractAddress) {
39+
throw new Error('Staking contract address is not set');
40+
}
41+
42+
if (!this.validator) {
43+
throw new Error('Validator address is not set');
44+
}
45+
46+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
47+
const signalExitData = this.getSignalExitClauseData(this.validator);
48+
this._transactionData = signalExitData;
49+
this._clauses = [
50+
{
51+
to: this.stakingContractAddress,
52+
value: ZERO_VALUE_AMOUNT,
53+
data: signalExitData,
54+
},
55+
];
56+
57+
this._recipients = [
58+
{
59+
address: this.stakingContractAddress,
60+
amount: ZERO_VALUE_AMOUNT,
61+
},
62+
];
63+
}
64+
65+
getSignalExitClauseData(validator: string): string {
66+
const methodName = 'signalExit';
67+
const types = ['address'];
68+
const params = [validator];
69+
70+
const method = EthereumAbi.methodID(methodName, types);
71+
const args = EthereumAbi.rawEncode(types, params);
72+
73+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
74+
}
75+
76+
toJson(): VetTransactionData {
77+
return {
78+
id: this.id,
79+
chainTag: this.chainTag,
80+
blockRef: this.blockRef,
81+
expiration: this.expiration,
82+
gasPriceCoef: this.gasPriceCoef,
83+
gas: this.gas,
84+
dependsOn: this.dependsOn,
85+
nonce: this.nonce,
86+
data: this.transactionData,
87+
value: ZERO_VALUE_AMOUNT,
88+
sender: this.sender,
89+
to: this.stakingContractAddress,
90+
stakingContractAddress: this.stakingContractAddress,
91+
validatorAddress: this.validator,
92+
};
93+
}
94+
95+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
96+
try {
97+
if (!signedTx || !signedTx.body) {
98+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
99+
}
100+
101+
this.rawTransaction = signedTx;
102+
103+
const body = signedTx.body;
104+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
105+
this.blockRef = body.blockRef || '0x0';
106+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
107+
this.clauses = body.clauses || [];
108+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
109+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
110+
this.dependsOn = body.dependsOn || null;
111+
this.nonce = String(body.nonce);
112+
113+
if (body.clauses.length > 0) {
114+
const clause = body.clauses[0];
115+
if (clause.to) {
116+
this.stakingContractAddress = clause.to;
117+
}
118+
119+
if (clause.data) {
120+
this.transactionData = clause.data;
121+
const decoded = utils.decodeSignalExitData(clause.data);
122+
this.validator = decoded.validator;
123+
}
124+
}
125+
126+
this.recipients = body.clauses.map((clause) => ({
127+
address: (clause.to || '0x0').toString().toLowerCase(),
128+
amount: new BigNumber(clause.value || 0).toString(),
129+
}));
130+
this.loadInputsAndOutputs();
131+
132+
if (signedTx.signature && signedTx.origin) {
133+
this.sender = signedTx.origin.toString().toLowerCase();
134+
}
135+
136+
if (signedTx.signature) {
137+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
138+
139+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
140+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
141+
}
142+
}
143+
} catch (e) {
144+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
145+
}
146+
}
147+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,9 @@ export class Transaction extends BaseTransaction {
396396
this.type === TransactionType.StakingClaim ||
397397
this.type === TransactionType.StakingLock ||
398398
this.type === TransactionType.StakingAdd ||
399-
this.type === TransactionType.StakingDeactivate
399+
this.type === TransactionType.StakingDeactivate ||
400+
this.type === TransactionType.StakingUnvote ||
401+
this.type === TransactionType.StakingPledge
400402
) {
401403
transactionBody.reserved = {
402404
features: 1, // mark transaction as delegated i.e. will use gas payer
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 WithdrawStakeTransaction extends Transaction {
13+
private _stakingContractAddress: string;
14+
private _validator: string;
15+
16+
constructor(_coinConfig: Readonly<CoinConfig>) {
17+
super(_coinConfig);
18+
this._type = TransactionType.StakingPledge;
19+
}
20+
21+
get validator(): string {
22+
return this._validator;
23+
}
24+
25+
set validator(address: string) {
26+
this._validator = address;
27+
}
28+
29+
get stakingContractAddress(): string {
30+
return this._stakingContractAddress;
31+
}
32+
33+
set stakingContractAddress(address: string) {
34+
this._stakingContractAddress = address;
35+
}
36+
37+
buildClauses(): void {
38+
if (!this.stakingContractAddress) {
39+
throw new Error('Staking contract address is not set');
40+
}
41+
42+
if (!this.validator) {
43+
throw new Error('Validator address is not set');
44+
}
45+
46+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
47+
const withdrawStakeData = this.getWithdrawStakeClauseData(this.validator);
48+
this._transactionData = withdrawStakeData;
49+
this._clauses = [
50+
{
51+
to: this.stakingContractAddress,
52+
value: ZERO_VALUE_AMOUNT,
53+
data: withdrawStakeData,
54+
},
55+
];
56+
57+
this._recipients = [
58+
{
59+
address: this.stakingContractAddress,
60+
amount: ZERO_VALUE_AMOUNT,
61+
},
62+
];
63+
}
64+
65+
getWithdrawStakeClauseData(validator: string): string {
66+
const methodName = 'withdrawStake';
67+
const types = ['address'];
68+
const params = [validator];
69+
70+
const method = EthereumAbi.methodID(methodName, types);
71+
const args = EthereumAbi.rawEncode(types, params);
72+
73+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
74+
}
75+
76+
toJson(): VetTransactionData {
77+
return {
78+
id: this.id,
79+
chainTag: this.chainTag,
80+
blockRef: this.blockRef,
81+
expiration: this.expiration,
82+
gasPriceCoef: this.gasPriceCoef,
83+
gas: this.gas,
84+
dependsOn: this.dependsOn,
85+
nonce: this.nonce,
86+
data: this.transactionData,
87+
value: ZERO_VALUE_AMOUNT,
88+
sender: this.sender,
89+
to: this.stakingContractAddress,
90+
stakingContractAddress: this.stakingContractAddress,
91+
validatorAddress: this.validator,
92+
};
93+
}
94+
95+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
96+
try {
97+
if (!signedTx || !signedTx.body) {
98+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
99+
}
100+
101+
this.rawTransaction = signedTx;
102+
103+
const body = signedTx.body;
104+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
105+
this.blockRef = body.blockRef || '0x0';
106+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
107+
this.clauses = body.clauses || [];
108+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
109+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
110+
this.dependsOn = body.dependsOn || null;
111+
this.nonce = String(body.nonce);
112+
113+
if (body.clauses.length > 0) {
114+
const clause = body.clauses[0];
115+
if (clause.to) {
116+
this.stakingContractAddress = clause.to;
117+
}
118+
119+
if (clause.data) {
120+
this.transactionData = clause.data;
121+
const decoded = utils.decodeWithdrawStakeData(clause.data);
122+
this.validator = decoded.validator;
123+
}
124+
}
125+
126+
this.recipients = body.clauses.map((clause) => ({
127+
address: (clause.to || '0x0').toString().toLowerCase(),
128+
amount: new BigNumber(clause.value || 0).toString(),
129+
}));
130+
this.loadInputsAndOutputs();
131+
132+
if (signedTx.signature && signedTx.origin) {
133+
this.sender = signedTx.origin.toString().toLowerCase();
134+
}
135+
136+
if (signedTx.signature) {
137+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
138+
139+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
140+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
141+
}
142+
}
143+
} catch (e) {
144+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)