Skip to content

Commit 7713863

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

11 files changed

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

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

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,21 @@ export function getHexAddressFromBase58Address(base58: string): string {
9090
// pulled from: https://github.com/TRON-US/tronweb/blob/dcb8efa36a5ebb65c4dab3626e90256a453f3b0d/src/utils/help.js#L17
9191
// but they don't surface this call in index.js
9292
const bytes = tronweb.utils.crypto.decodeBase58Address(base58);
93-
return getHexAddressFromByteArray(bytes);
93+
return getHexAddressFromByteArray(bytes as any);
9494
}
9595

9696
/**
9797
* @param privateKey
9898
*/
9999
export function getPubKeyFromPriKey(privateKey: TronBinaryLike): ByteArray {
100-
return tronweb.utils.crypto.getPubKeyFromPriKey(privateKey);
100+
return tronweb.utils.crypto.getPubKeyFromPriKey(privateKey as any);
101101
}
102102

103103
/**
104104
* @param privateKey
105105
*/
106106
export function getAddressFromPriKey(privateKey: TronBinaryLike): ByteArray {
107-
return tronweb.utils.crypto.getAddressFromPriKey(privateKey);
107+
return tronweb.utils.crypto.getAddressFromPriKey(privateKey as any);
108108
}
109109

110110
/**
@@ -127,7 +127,7 @@ export function getBase58AddressFromHex(hex: string): string {
127127
* @param transaction
128128
*/
129129
export function signTransaction(privateKey: string | ByteArray, transaction: TransactionReceipt): TransactionReceipt {
130-
return tronweb.utils.crypto.signTransaction(privateKey, transaction);
130+
return tronweb.utils.crypto.signTransaction(privateKey, transaction) as unknown as TransactionReceipt;
131131
}
132132

133133
/**
@@ -136,14 +136,14 @@ export function signTransaction(privateKey: string | ByteArray, transaction: Tra
136136
* @param useTronHeader
137137
*/
138138
export function signString(message: string, privateKey: string | ByteArray, useTronHeader = true): string {
139-
return tronweb.Trx.signString(message, privateKey, useTronHeader);
139+
return tronweb.Trx.signString(message, privateKey as any, useTronHeader);
140140
}
141141

142142
/**
143143
* @param pubBytes
144144
*/
145145
export function getRawAddressFromPubKey(pubBytes: TronBinaryLike): ByteArray {
146-
return tronweb.utils.crypto.computeAddress(pubBytes);
146+
return tronweb.utils.crypto.computeAddress(pubBytes as any);
147147
}
148148

149149
/**
@@ -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)