Skip to content

Commit f32b13c

Browse files
committed
feat(tron): add stake support (freeze and vote)
TICKET: SC-1632
1 parent dac0f3c commit f32b13c

File tree

10 files changed

+660
-8
lines changed

10 files changed

+660
-8
lines changed

modules/sdk-coin-trx/src/lib/enum.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export enum ContractType {
1414
* This is a smart contract type.
1515
*/
1616
TriggerSmartContract,
17+
/**
18+
* This is the contract for freezeBuilder
19+
*/
20+
FreezeBalanceV2,
21+
/**
22+
* This is the contract for voting for witnesses
23+
*/
24+
VoteWitness,
1725
}
1826

1927
export enum PermissionType {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { TransactionBuilder } from './transactionBuilder';
3+
4+
/**
5+
* Valid resource types for Tron freezing
6+
*/
7+
export enum FreezeResource {
8+
BANDWIDTH = 'BANDWIDTH',
9+
ENERGY = 'ENERGY',
10+
}
11+
12+
export class FreezeBuilder extends TransactionBuilder {
13+
/** @inheritdoc */
14+
protected get transactionType(): TransactionType {
15+
return TransactionType.StakingActivate;
16+
}
17+
18+
/** Override to initialize this builder from a raw transaction */
19+
initBuilder(rawTransaction: string | any): void {
20+
this.transaction = this.fromImplementation(rawTransaction);
21+
// Explicitly set the transaction type after initialization
22+
this.transaction.setTransactionType(this.transactionType);
23+
}
24+
25+
validateTransaction(transaction: any): void {
26+
if (transaction && typeof transaction.toJson === 'function') {
27+
super.validateTransaction(transaction);
28+
// Get the raw transaction data from the Transaction object
29+
const rawTx = transaction.toJson();
30+
this.validateFreezeTransaction(rawTx);
31+
} else {
32+
// If it's already a raw transaction object, validate it directly
33+
this.validateFreezeTransaction(transaction);
34+
}
35+
}
36+
37+
/**
38+
* Validates if the transaction is a valid freeze transaction
39+
* @param transaction The transaction to validate
40+
* @throws {InvalidTransactionError} when the transaction is invalid
41+
*/
42+
private validateFreezeTransaction(transaction: any): void {
43+
if (
44+
!transaction ||
45+
!transaction.raw_data ||
46+
!transaction.raw_data.contract ||
47+
transaction.raw_data.contract.length === 0
48+
) {
49+
throw new InvalidTransactionError('Invalid transaction: missing or empty contract array');
50+
}
51+
52+
const contract = transaction.raw_data.contract[0];
53+
54+
// Validate contract type
55+
if (contract.type !== 'FreezeBalanceV2Contract') {
56+
throw new InvalidTransactionError(
57+
`Invalid freeze transaction: expected contract type FreezeBalanceV2Contract but got ${contract.type}`
58+
);
59+
}
60+
61+
// Validate parameter value
62+
if (!contract.parameter || !contract.parameter.value) {
63+
throw new InvalidTransactionError('Invalid freeze transaction: missing parameter value');
64+
}
65+
66+
const value = contract.parameter.value;
67+
68+
// Validate resource
69+
if (!Object.values(FreezeResource).includes(value.resource)) {
70+
throw new InvalidTransactionError(
71+
`Invalid freeze transaction: resource must be ${Object.values(FreezeResource).join(' or ')}, got ${
72+
value.resource
73+
}`
74+
);
75+
}
76+
77+
// Validate frozen_balance
78+
if (!value.frozen_balance || value.frozen_balance <= 0) {
79+
throw new InvalidTransactionError('Invalid freeze transaction: frozen_balance must be positive');
80+
}
81+
82+
// Validate owner_address
83+
if (!value.owner_address || typeof value.owner_address !== 'string' || value.owner_address.length === 0) {
84+
throw new InvalidTransactionError('Invalid freeze transaction: missing or invalid owner_address');
85+
}
86+
}
87+
88+
/**
89+
* Check if the transaction is a valid freeze transaction
90+
* @param transaction Transaction to check
91+
* @returns True if the transaction is a valid freeze transaction
92+
*/
93+
canSign(transaction: any): boolean {
94+
try {
95+
this.validateFreezeTransaction(transaction);
96+
return true;
97+
} catch (e) {
98+
return false;
99+
}
100+
}
101+
}

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ export interface RawData {
4242
ref_block_hash: string;
4343
fee_limit?: number;
4444
contractType?: ContractType;
45-
contract: TransferContract[] | AccountPermissionUpdateContract[] | TriggerSmartContract[];
45+
contract:
46+
| TransferContract[]
47+
| AccountPermissionUpdateContract[]
48+
| TriggerSmartContract[]
49+
| FreezeBalanceV2Contract[]
50+
| VoteWitnessContract[];
4651
}
4752

4853
export interface Value {
@@ -117,3 +122,60 @@ export interface AccountInfo {
117122
active_permission: [{ keys: [PermissionKey] }];
118123
trc20: [Record<string, string>];
119124
}
125+
126+
/**
127+
* Freeze balance contract value fields
128+
*/
129+
export interface FreezeBalanceValueFields {
130+
resource: string;
131+
frozen_balance: number;
132+
owner_address: string;
133+
}
134+
135+
/**
136+
* Freeze balance contract value interface
137+
*/
138+
export interface FreezeBalanceValue {
139+
type_url?: string;
140+
value: FreezeBalanceValueFields;
141+
}
142+
143+
/**
144+
* Freeze balance v2 contract interface
145+
*/
146+
export interface FreezeBalanceV2Contract {
147+
parameter: FreezeBalanceValue;
148+
type?: string;
149+
}
150+
151+
/**
152+
* Vote data in a vote transaction
153+
*/
154+
export interface VoteData {
155+
vote_address: string;
156+
vote_count: number;
157+
}
158+
159+
/**
160+
* Vote transaction value fields
161+
*/
162+
export interface VoteValueFields {
163+
owner_address: string;
164+
votes: VoteData[];
165+
}
166+
167+
/**
168+
* Vote contract value interface
169+
*/
170+
export interface VoteValue {
171+
type_url?: string;
172+
value: VoteValueFields;
173+
}
174+
175+
/**
176+
* Vote witness contract interface
177+
*/
178+
export interface VoteWitnessContract {
179+
parameter: VoteValue;
180+
type?: string;
181+
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ import {
1717
tokenMainnetContractAddresses,
1818
tokenTestnetContractAddresses,
1919
} from './utils';
20-
import { ContractEntry, RawData, TransactionReceipt, TransferContract, TriggerSmartContract } from './iface';
20+
import {
21+
ContractEntry,
22+
FreezeBalanceV2Contract,
23+
RawData,
24+
TransactionReceipt,
25+
TransferContract,
26+
TriggerSmartContract,
27+
VoteWitnessContract,
28+
} from './iface';
2129

2230
/**
2331
* Tron transaction model.
@@ -126,6 +134,34 @@ export class Transaction extends BaseTransaction {
126134
value: '0',
127135
};
128136
break;
137+
case ContractType.FreezeBalanceV2:
138+
this._type = TransactionType.StakingActivate;
139+
const freezeValue = (rawData.contract[0] as FreezeBalanceV2Contract).parameter.value;
140+
output = {
141+
address: freezeValue.owner_address,
142+
value: freezeValue.frozen_balance.toString(),
143+
};
144+
input = {
145+
address: freezeValue.owner_address,
146+
value: freezeValue.frozen_balance.toString(),
147+
};
148+
break;
149+
case ContractType.VoteWitness:
150+
this._type = TransactionType.StakingVote;
151+
const voteValues = (rawData.contract[0] as VoteWitnessContract).parameter.value;
152+
153+
// Calculate total vote count
154+
const totalVoteCount = voteValues.votes.reduce((sum, vote) => sum + vote.vote_count, 0);
155+
156+
output = {
157+
address: voteValues.owner_address,
158+
value: totalVoteCount.toString(),
159+
};
160+
input = {
161+
address: voteValues.owner_address,
162+
value: totalVoteCount.toString(),
163+
};
164+
break;
129165
default:
130166
throw new ParseTransactionError('Unsupported contract type');
131167
}

0 commit comments

Comments
 (0)