Skip to content

Commit 90d066b

Browse files
committed
feat(sdk-coin-trx): add stake support (freeze and vote)
Ticket: SC-1632
1 parent dac0f3c commit 90d066b

11 files changed

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

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Entry } from '@bitgo/sdk-core';
22
import { ContractType, PermissionType } from './enum';
3+
import { TronResource } from './resourcesTypes';
34

45
export interface Account {
56
publicKey: string;
@@ -42,7 +43,12 @@ export interface RawData {
4243
ref_block_hash: string;
4344
fee_limit?: number;
4445
contractType?: ContractType;
45-
contract: TransferContract[] | AccountPermissionUpdateContract[] | TriggerSmartContract[];
46+
contract:
47+
| TransferContract[]
48+
| AccountPermissionUpdateContract[]
49+
| TriggerSmartContract[]
50+
| FreezeBalanceV2Contract[]
51+
| VoteWitnessContract[];
4652
}
4753

4854
export interface Value {
@@ -117,3 +123,81 @@ export interface AccountInfo {
117123
active_permission: [{ keys: [PermissionKey] }];
118124
trc20: [Record<string, string>];
119125
}
126+
127+
/**
128+
* Freeze balance contract value fields
129+
*/
130+
export interface FreezeBalanceValueFields {
131+
resource: string;
132+
frozen_balance: number;
133+
owner_address: string;
134+
}
135+
136+
/**
137+
* Freeze balance contract value interface
138+
*/
139+
export interface FreezeBalanceValue {
140+
type_url?: string;
141+
value: FreezeBalanceValueFields;
142+
}
143+
144+
/**
145+
* Freeze balance v2 contract interface
146+
*/
147+
export interface FreezeBalanceV2Contract {
148+
parameter: FreezeBalanceValue;
149+
type?: string;
150+
}
151+
152+
export interface FreezeBalanceContractParameter {
153+
parameter: {
154+
value: {
155+
resource: TronResource;
156+
frozen_balance: number;
157+
owner_address: string;
158+
};
159+
};
160+
}
161+
162+
/**
163+
* Vote data in a vote transaction
164+
*/
165+
export interface VoteWitnessData {
166+
vote_address: string;
167+
vote_count: number;
168+
}
169+
170+
/**
171+
* Vote transaction value fields
172+
*/
173+
export interface VoteWitnessValueFields {
174+
owner_address: string;
175+
votes: VoteWitnessData[];
176+
}
177+
178+
/**
179+
* Vote contract value interface
180+
*/
181+
export interface VoteWitnessValue {
182+
type_url?: string;
183+
value: VoteWitnessValueFields;
184+
}
185+
186+
/**
187+
* Vote witness contract interface
188+
*/
189+
export interface VoteWitnessContract {
190+
parameter: VoteWitnessValue;
191+
type?: string;
192+
}
193+
export interface VoteWitnessContractParameter {
194+
parameter: {
195+
value: {
196+
owner_address: string;
197+
votes: Array<{
198+
vote_address: string;
199+
vote_count: number;
200+
}>;
201+
};
202+
};
203+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Valid resource types for Tron freezing and unfreezing
3+
*/
4+
export enum TronResource {
5+
BANDWIDTH = 'BANDWIDTH',
6+
ENERGY = 'ENERGY',
7+
}

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)