Skip to content

Commit 2339d7c

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

File tree

10 files changed

+685
-2
lines changed

10 files changed

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

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
}

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ export function decodeTransaction(hexString: string): RawData {
175175
contractType = ContractType.TriggerSmartContract;
176176
contract = exports.decodeTriggerSmartContract(rawTransaction.contracts[0].parameter.value);
177177
break;
178+
case 'type.googleapis.com/protocol.FreezeBalanceV2Contract':
179+
contractType = ContractType.FreezeBalanceV2;
180+
contract = decodeFreezeBalanceV2Contract(rawTransaction.contracts[0].parameter.value);
181+
break;
182+
case 'type.googleapis.com/protocol.VoteWitnessContract':
183+
contractType = ContractType.VoteWitness;
184+
contract = decodeVoteWitnessContract(rawTransaction.contracts[0].parameter.value);
185+
break;
178186
default:
179187
throw new UtilsError('Unsupported contract type');
180188
}
@@ -360,6 +368,97 @@ export function decodeAccountPermissionUpdateContract(base64: string): AccountPe
360368
};
361369
}
362370

371+
/**
372+
* Deserialize the segment of the txHex corresponding with freeze balance contract
373+
*
374+
* @param {string} base64 - The base64 encoded contract data
375+
* @returns {Array} - Array containing the decoded freeze contract
376+
*/
377+
export function decodeFreezeBalanceV2Contract(base64: string): any[] {
378+
let freezeContract;
379+
try {
380+
freezeContract = protocol.FreezeBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
381+
} catch (e) {
382+
throw new UtilsError('There was an error decoding the freeze contract in the transaction.');
383+
}
384+
385+
if (!freezeContract.ownerAddress) {
386+
throw new UtilsError('Owner address does not exist in this freeze contract.');
387+
}
388+
389+
if (!freezeContract.resource) {
390+
throw new UtilsError('Resource type does not exist in this freeze contract.');
391+
}
392+
393+
if (!freezeContract.hasOwnProperty('frozenBalance')) {
394+
throw new UtilsError('Frozen balance does not exist in this freeze contract.');
395+
}
396+
397+
// deserialize attributes
398+
const owner_address = getBase58AddressFromByteArray(
399+
getByteArrayFromHexAddress(Buffer.from(freezeContract.ownerAddress, 'base64').toString('hex'))
400+
);
401+
const resource = freezeContract.resource;
402+
const frozen_balance = freezeContract.frozenBalance;
403+
404+
return [
405+
{
406+
parameter: {
407+
value: {
408+
resource,
409+
frozen_balance: Number(frozen_balance),
410+
owner_address,
411+
},
412+
},
413+
},
414+
];
415+
}
416+
417+
/**
418+
* Deserialize the segment of the txHex corresponding with vote witness contract
419+
*
420+
* @param {string} base64 - The base64 encoded contract data
421+
* @returns {Array} - Array containing the decoded vote witness contract
422+
*/
423+
export function decodeVoteWitnessContract(base64: string): any[] {
424+
let voteContract;
425+
try {
426+
voteContract = protocol.VoteWitnessContract.decode(Buffer.from(base64, 'base64')).toJSON();
427+
} catch (e) {
428+
throw new UtilsError('There was an error decoding the vote contract in the transaction.');
429+
}
430+
431+
if (!voteContract.ownerAddress) {
432+
throw new UtilsError('Owner address does not exist in this vote contract.');
433+
}
434+
435+
if (!Array.isArray(voteContract.votes) || voteContract.votes.length === 0) {
436+
throw new UtilsError('Votes do not exist or are empty in this vote contract.');
437+
}
438+
439+
// deserialize attributes
440+
const owner_address = getBase58AddressFromByteArray(
441+
getByteArrayFromHexAddress(Buffer.from(voteContract.ownerAddress, 'base64').toString('hex'))
442+
);
443+
const votes = voteContract.votes.map((vote: any) => ({
444+
vote_address: getBase58AddressFromByteArray(
445+
getByteArrayFromHexAddress(Buffer.from(vote.voteAddress, 'base64').toString('hex'))
446+
),
447+
vote_count: Number(vote.voteCount),
448+
}));
449+
450+
return [
451+
{
452+
parameter: {
453+
value: {
454+
owner_address,
455+
votes,
456+
},
457+
},
458+
},
459+
];
460+
}
461+
363462
/**
364463
* @param raw
365464
*/

0 commit comments

Comments
 (0)