Skip to content

Commit 86f1830

Browse files
committed
feat(sdk-coin-trx): add unfreeze and withdraw for tron unstaking
Ticket: SC-1670
1 parent 0f0826e commit 86f1830

11 files changed

+646
-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 unfreezing balances
19+
*/
20+
UnfreezeBalanceV2,
21+
/**
22+
* This is the contract for withdrawing expired unfrozen balances
23+
*/
24+
WithdrawExpireUnfreeze,
1725
}
1826

1927
export enum PermissionType {

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

Lines changed: 54 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+
| UnfreezeBalanceV2Contract[]
50+
| WithdrawExpireUnfreezeContract[];
4651
}
4752

4853
export interface Value {
@@ -117,3 +122,51 @@ export interface AccountInfo {
117122
active_permission: [{ keys: [PermissionKey] }];
118123
trc20: [Record<string, string>];
119124
}
125+
126+
/**
127+
* Unfreeze transaction value fields
128+
*/
129+
export interface UnfreezeBalanceValueFields {
130+
resource: string;
131+
unfreeze_balance: number;
132+
owner_address: string;
133+
}
134+
135+
/**
136+
* Unfreeze balance contract value interface
137+
*/
138+
export interface UnfreezeBalanceValue {
139+
type_url?: string;
140+
value: UnfreezeBalanceValueFields;
141+
}
142+
143+
/**
144+
* Unfreeze balance v2 contract interface
145+
*/
146+
export interface UnfreezeBalanceV2Contract {
147+
parameter: UnfreezeBalanceValue;
148+
type?: string;
149+
}
150+
151+
/**
152+
* Withdraw transaction value fields
153+
*/
154+
export interface WithdrawBalanceValueFields {
155+
owner_address: string;
156+
}
157+
158+
/**
159+
* Withdraw balance contract value interface
160+
*/
161+
export interface WithdrawBalanceValue {
162+
type_url?: string;
163+
value: WithdrawBalanceValueFields;
164+
}
165+
166+
/**
167+
* Withdraw expire unfreeze contract interface
168+
*/
169+
export interface WithdrawExpireUnfreezeContract {
170+
parameter: WithdrawBalanceValue;
171+
type?: string;
172+
}
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: 33 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+
RawData,
23+
TransactionReceipt,
24+
TransferContract,
25+
TriggerSmartContract,
26+
UnfreezeBalanceV2Contract,
27+
WithdrawExpireUnfreezeContract,
28+
} from './iface';
2129

2230
/**
2331
* Tron transaction model.
@@ -126,6 +134,30 @@ export class Transaction extends BaseTransaction {
126134
value: '0',
127135
};
128136
break;
137+
case ContractType.UnfreezeBalanceV2:
138+
this._type = TransactionType.StakingUnlock;
139+
const unfreezeValues = (rawData.contract[0] as UnfreezeBalanceV2Contract).parameter.value;
140+
output = {
141+
address: unfreezeValues.owner_address,
142+
value: unfreezeValues.unfreeze_balance.toString(),
143+
};
144+
input = {
145+
address: unfreezeValues.owner_address,
146+
value: unfreezeValues.unfreeze_balance.toString(),
147+
};
148+
break;
149+
case ContractType.WithdrawExpireUnfreeze:
150+
this._type = TransactionType.StakingWithdraw;
151+
const withdrawValues = (rawData.contract[0] as WithdrawExpireUnfreezeContract).parameter.value;
152+
output = {
153+
address: withdrawValues.owner_address,
154+
value: '0', // no value field
155+
};
156+
input = {
157+
address: withdrawValues.owner_address,
158+
value: '0',
159+
};
160+
break;
129161
default:
130162
throw new ParseTransactionError('Unsupported contract type');
131163
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 './resourceTypes';
6+
7+
interface RawUnfreezeContract {
8+
parameter: {
9+
value: {
10+
resource?: string;
11+
unfreeze_balance?: number;
12+
owner_address?: string;
13+
};
14+
};
15+
type: string;
16+
}
17+
18+
export class UnfreezeBuilder extends TransactionBuilder {
19+
/** @inheritdoc */
20+
protected get transactionType(): TransactionType {
21+
return TransactionType.StakingUnlock;
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.validateUnfreezeTransaction(rawTx);
34+
} else {
35+
this.validateUnfreezeTransaction(transaction as TransactionReceipt);
36+
}
37+
}
38+
39+
/**
40+
* Validates if the transaction is a valid unfreeze transaction
41+
* @param transaction The transaction to validate
42+
* @throws {InvalidTransactionError} when the transaction is invalid
43+
*/
44+
private validateUnfreezeTransaction(transaction: TransactionReceipt): void {
45+
if (
46+
!transaction ||
47+
!transaction.raw_data ||
48+
!transaction.raw_data.contract ||
49+
transaction.raw_data.contract.length === 0
50+
) {
51+
throw new InvalidTransactionError('Invalid transaction: missing or empty contract array');
52+
}
53+
54+
const contract = transaction.raw_data.contract[0] as RawUnfreezeContract;
55+
56+
// Validate contract type
57+
if (contract.type !== 'UnfreezeBalanceV2Contract') {
58+
throw new InvalidTransactionError(
59+
`Invalid unfreeze transaction: expected contract type UnfreezeBalanceV2Contract but got ${contract.type}`
60+
);
61+
}
62+
63+
// Validate parameter value
64+
if (!contract.parameter || !contract.parameter.value) {
65+
throw new InvalidTransactionError('Invalid unfreeze transaction: missing parameter value');
66+
}
67+
68+
const value = contract.parameter.value;
69+
70+
// Validate resource
71+
if (!Object.values(TronResource).includes(value.resource as TronResource)) {
72+
throw new InvalidTransactionError(
73+
`Invalid unfreeze transaction: resource must be ${Object.values(TronResource).join(' or ')}, got ${
74+
value.resource
75+
}`
76+
);
77+
}
78+
79+
// Validate unfreeze_balance
80+
if (!value.unfreeze_balance || value.unfreeze_balance <= 0) {
81+
throw new InvalidTransactionError('Invalid unfreeze transaction: unfreeze_balance must be positive');
82+
}
83+
84+
// Validate owner_address
85+
if (!value.owner_address || typeof value.owner_address !== 'string' || value.owner_address.length === 0) {
86+
throw new InvalidTransactionError('Invalid unfreeze transaction: missing or invalid owner_address');
87+
}
88+
}
89+
90+
/**
91+
* Check if the transaction is a valid unfreeze transaction
92+
* @param transaction Transaction to check
93+
* @returns True if the transaction is a valid unfreeze transaction
94+
*/
95+
canSign(transaction: TransactionReceipt): boolean {
96+
try {
97+
this.validateUnfreezeTransaction(transaction);
98+
return true;
99+
} catch (e) {
100+
return false;
101+
}
102+
}
103+
}

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.WithdrawExpireUnfreezeContract':
179+
contract = decodeWithdrawExpireUnfreezeContract(rawTransaction.contracts[0].parameter.value);
180+
contractType = ContractType.WithdrawExpireUnfreeze;
181+
break;
182+
case 'type.googleapis.com/protocol.UnfreezeBalanceV2Contract':
183+
contract = decodeUnfreezeBalanceV2Contract(rawTransaction.contracts[0].parameter.value);
184+
contractType = ContractType.UnfreezeBalanceV2;
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 unfreeze balance contract
373+
*
374+
* @param {string} base64 - The base64 encoded contract data
375+
* @returns {Array} - Array containing the decoded unfreeze contract
376+
*/
377+
export function decodeUnfreezeBalanceV2Contract(base64: string): any[] {
378+
let unfreezeContract;
379+
try {
380+
unfreezeContract = protocol.UnfreezeBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
381+
} catch (e) {
382+
throw new UtilsError('There was an error decoding the unfreeze contract in the transaction.');
383+
}
384+
385+
if (!unfreezeContract.ownerAddress) {
386+
throw new UtilsError('Owner address does not exist in this unfreeze contract.');
387+
}
388+
389+
if (!unfreezeContract.resource) {
390+
throw new UtilsError('Resource type does not exist in this unfreeze contract.');
391+
}
392+
393+
if (!unfreezeContract.hasOwnProperty('unfrozenBalance')) {
394+
throw new UtilsError('Unfreeze balance does not exist in this unfreeze contract.');
395+
}
396+
397+
// deserialize attributes
398+
const owner_address = getBase58AddressFromByteArray(
399+
getByteArrayFromHexAddress(Buffer.from(unfreezeContract.ownerAddress, 'base64').toString('hex'))
400+
);
401+
402+
// Convert ResourceCode enum value to string resource name
403+
const resourceValue = unfreezeContract.resource;
404+
let resource: string;
405+
if (resourceValue === protocol.ResourceCode.BANDWIDTH) {
406+
resource = 'BANDWIDTH';
407+
} else if (resourceValue === protocol.ResourceCode.ENERGY) {
408+
resource = 'ENERGY';
409+
} else {
410+
throw new UtilsError(`Unknown resource type: ${resourceValue}`);
411+
}
412+
413+
const unfreeze_balance = unfreezeContract.unfrozenBalance;
414+
415+
return [
416+
{
417+
parameter: {
418+
value: {
419+
resource,
420+
unfreeze_balance: Number(unfreeze_balance),
421+
owner_address,
422+
},
423+
},
424+
},
425+
];
426+
}
427+
428+
/**
429+
* Deserialize the segment of the txHex corresponding with withdraw expire unfreeze contract
430+
*
431+
* @param {string} base64 - The base64 encoded contract data
432+
* @returns {Array} - Array containing the decoded withdraw contract
433+
*/
434+
export function decodeWithdrawExpireUnfreezeContract(base64: string): any[] {
435+
let withdrawContract;
436+
try {
437+
withdrawContract = protocol.WithdrawBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
438+
} catch (e) {
439+
throw new UtilsError('There was an error decoding the withdraw contract in the transaction.');
440+
}
441+
442+
if (!withdrawContract.ownerAddress) {
443+
throw new UtilsError('Owner address does not exist in this withdraw contract.');
444+
}
445+
446+
// deserialize attributes
447+
const owner_address = getBase58AddressFromByteArray(
448+
getByteArrayFromHexAddress(Buffer.from(withdrawContract.ownerAddress, 'base64').toString('hex'))
449+
);
450+
451+
return [
452+
{
453+
parameter: {
454+
value: {
455+
owner_address,
456+
},
457+
},
458+
},
459+
];
460+
}
461+
363462
/**
364463
* @param raw
365464
*/

0 commit comments

Comments
 (0)