Skip to content

Commit 91af3f8

Browse files
abhijit0943Vijay-Jagannathan
authored andcommitted
feat(sdk-coin-trx): add unfreeze and withdraw for tron unstaking
Ticket: SC-1670
1 parent 39cae57 commit 91af3f8

11 files changed

+637
-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
@@ -22,6 +22,14 @@ export enum ContractType {
2222
* This is the contract for voting for witnesses
2323
*/
2424
VoteWitness,
25+
/**
26+
* This is the contract for unfreezing balances
27+
*/
28+
UnfreezeBalanceV2,
29+
/**
30+
* This is the contract for withdrawing expired unfrozen balances
31+
*/
32+
WithdrawExpireUnfreeze,
2533
}
2634

2735
export enum PermissionType {

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

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export interface RawData {
4747
| AccountPermissionUpdateContract[]
4848
| TriggerSmartContract[]
4949
| FreezeBalanceV2Contract[]
50-
| VoteWitnessContract[];
50+
| VoteWitnessContract[]
51+
| UnfreezeBalanceV2Contract[]
52+
| WithdrawExpireUnfreezeContract[];
5153
}
5254

5355
export interface Value {
@@ -132,6 +134,15 @@ export interface FreezeBalanceValueFields {
132134
owner_address: string;
133135
}
134136

137+
/**
138+
* Unfreeze transaction value fields
139+
*/
140+
export interface UnfreezeBalanceValueFields {
141+
resource: string;
142+
unfreeze_balance: number;
143+
owner_address: string;
144+
}
145+
135146
/**
136147
* Freeze balance contract value interface
137148
*/
@@ -148,6 +159,22 @@ export interface FreezeBalanceV2Contract {
148159
type?: string;
149160
}
150161

162+
/**
163+
* Unfreeze balance contract value interface
164+
*/
165+
export interface UnfreezeBalanceValue {
166+
type_url?: string;
167+
value: UnfreezeBalanceValueFields;
168+
}
169+
170+
/**
171+
* Unfreeze balance v2 contract interface
172+
*/
173+
export interface UnfreezeBalanceV2Contract {
174+
parameter: UnfreezeBalanceValue;
175+
type?: string;
176+
}
177+
151178
/**
152179
* Freeze balance contract parameter interface
153180
*/
@@ -161,6 +188,39 @@ export interface FreezeBalanceContractParameter {
161188
};
162189
}
163190

191+
/**
192+
* Withdraw transaction value fields
193+
*/
194+
export interface WithdrawExpireUnfreezeValueFields {
195+
owner_address: string;
196+
}
197+
198+
/**
199+
* Withdraw balance contract value interface
200+
*/
201+
export interface WithdrawExpireUnfreezeValue {
202+
type_url?: string;
203+
value: WithdrawExpireUnfreezeValueFields;
204+
}
205+
206+
/**
207+
* Withdraw expire unfreeze contract interface
208+
*/
209+
export interface WithdrawExpireUnfreezeContract {
210+
parameter: WithdrawExpireUnfreezeValue;
211+
type?: string;
212+
}
213+
214+
export interface UnfreezeBalanceContractParameter {
215+
parameter: {
216+
value: {
217+
resource: TronResource;
218+
unfreeze_balance: number;
219+
owner_address: string;
220+
};
221+
};
222+
}
223+
164224
/**
165225
* Freeze balance contract decoded interface
166226
*/
@@ -227,3 +287,14 @@ export interface VoteWitnessContractParameter {
227287
};
228288
};
229289
}
290+
291+
/**
292+
* Withdraw expire unfreeze contract parameter interface
293+
*/
294+
export interface WithdrawExpireUnfreezeContractParameter {
295+
parameter: {
296+
value: {
297+
owner_address: string;
298+
};
299+
};
300+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum TronResource {
2+
BANDWIDTH = 'BANDWIDTH',
3+
ENERGY = 'ENERGY',
4+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
TransferContract,
2626
TriggerSmartContract,
2727
VoteWitnessContract,
28+
UnfreezeBalanceV2Contract,
29+
WithdrawExpireUnfreezeContract,
2830
} from './iface';
2931

3032
/**
@@ -162,6 +164,30 @@ export class Transaction extends BaseTransaction {
162164
value: totalVoteCount.toString(),
163165
};
164166
break;
167+
case ContractType.UnfreezeBalanceV2:
168+
this._type = TransactionType.StakingUnlock;
169+
const unfreezeValues = (rawData.contract[0] as UnfreezeBalanceV2Contract).parameter.value;
170+
output = {
171+
address: unfreezeValues.owner_address,
172+
value: unfreezeValues.unfreeze_balance.toString(),
173+
};
174+
input = {
175+
address: unfreezeValues.owner_address,
176+
value: unfreezeValues.unfreeze_balance.toString(),
177+
};
178+
break;
179+
case ContractType.WithdrawExpireUnfreeze:
180+
this._type = TransactionType.StakingWithdraw;
181+
const withdrawValues = (rawData.contract[0] as WithdrawExpireUnfreezeContract).parameter.value;
182+
output = {
183+
address: withdrawValues.owner_address,
184+
value: '0', // no value field
185+
};
186+
input = {
187+
address: withdrawValues.owner_address,
188+
value: '0',
189+
};
190+
break;
165191
default:
166192
throw new ParseTransactionError('Unsupported contract type');
167193
}
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 './resourceTypes';
6+
7+
interface RawUnfreezeBalanceContract {
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 UnfreezeBalanceTxBuilder 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 {TransactionReceipt} transaction - The transaction to validate
42+
* @throws {InvalidTransactionError} when the transaction is invalid
43+
*/
44+
private validateUnfreezeTransaction(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 RawUnfreezeBalanceContract;
50+
51+
// Validate contract type
52+
if (contract.type !== 'UnfreezeBalanceV2Contract') {
53+
throw new InvalidTransactionError(
54+
`Invalid unfreeze transaction: expected contract type UnfreezeBalanceV2Contract but got ${contract.type}`
55+
);
56+
}
57+
58+
// Validate parameter value
59+
if (!contract?.parameter?.value) {
60+
throw new InvalidTransactionError('Invalid unfreeze transaction: missing parameter value');
61+
}
62+
63+
const value = contract.parameter.value;
64+
65+
// Validate resource
66+
if (!Object.values(TronResource).includes(value.resource as TronResource)) {
67+
throw new InvalidTransactionError(
68+
`Invalid unfreeze transaction: resource must be ${Object.values(TronResource).join(' or ')}, got ${
69+
value.resource
70+
}`
71+
);
72+
}
73+
74+
// Validate unfreeze_balance
75+
if (!value.unfreeze_balance || value.unfreeze_balance <= 0) {
76+
throw new InvalidTransactionError('Invalid unfreeze transaction: unfreeze_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 unfreeze transaction: missing or invalid owner_address');
82+
}
83+
}
84+
85+
/**
86+
* Check if the transaction is a valid unfreeze transaction
87+
* @param {TransactionReceipt} transaction - Transaction to check
88+
* @returns True if the transaction is a valid unfreeze transaction
89+
*/
90+
canSign(transaction: TransactionReceipt): boolean {
91+
try {
92+
this.validateUnfreezeTransaction(transaction);
93+
return true;
94+
} catch (e) {
95+
return false;
96+
}
97+
}
98+
}

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

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
VoteWitnessContractParameter,
1717
FreezeContractDecoded,
1818
VoteContractDecoded,
19+
UnfreezeBalanceContractParameter,
20+
WithdrawExpireUnfreezeContractParameter,
1921
} from './iface';
2022
import { ContractType, PermissionType, TronResource } from './enum';
2123
import { AbiCoder, hexConcat } from 'ethers/lib/utils';
@@ -177,7 +179,9 @@ export function decodeTransaction(hexString: string): RawData {
177179
| AccountPermissionUpdateContract[]
178180
| TriggerSmartContract[]
179181
| FreezeBalanceContractParameter[]
180-
| VoteWitnessContractParameter[];
182+
| VoteWitnessContractParameter[]
183+
| UnfreezeBalanceContractParameter[]
184+
| WithdrawExpireUnfreezeContractParameter[];
181185

182186
let contractType: ContractType;
183187

@@ -203,6 +207,14 @@ export function decodeTransaction(hexString: string): RawData {
203207
contractType = ContractType.VoteWitness;
204208
contract = decodeVoteWitnessContract(rawTransaction.contracts[0].parameter.value);
205209
break;
210+
case 'type.googleapis.com/protocol.WithdrawExpireUnfreezeContract':
211+
contract = decodeWithdrawExpireUnfreezeContract(rawTransaction.contracts[0].parameter.value);
212+
contractType = ContractType.WithdrawExpireUnfreeze;
213+
break;
214+
case 'type.googleapis.com/protocol.UnfreezeBalanceV2Contract':
215+
contract = decodeUnfreezeBalanceV2Contract(rawTransaction.contracts[0].parameter.value);
216+
contractType = ContractType.UnfreezeBalanceV2;
217+
break;
206218
default:
207219
throw new UtilsError('Unsupported contract type');
208220
}
@@ -488,6 +500,99 @@ export function decodeVoteWitnessContract(base64: string): VoteWitnessContractPa
488500
},
489501
},
490502
];
503+
}
504+
505+
/**
506+
* Deserialize the segment of the txHex corresponding with unfreeze balance contract
507+
*
508+
* @param {string} base64 - The base64 encoded contract data
509+
* @returns {UnfreezeBalanceContractParameter[]} - Array containing the decoded unfreeze contract
510+
*/
511+
export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalanceContractParameter[] {
512+
interface UnfreezeContractDecoded {
513+
ownerAddress?: string;
514+
resource?: number;
515+
unfrozenBalance?: string | number;
516+
}
517+
518+
let unfreezeContract: UnfreezeContractDecoded;
519+
try {
520+
unfreezeContract = protocol.UnfreezeBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
521+
} catch (e) {
522+
throw new UtilsError('There was an error decoding the unfreeze contract in the transaction.');
523+
}
524+
525+
if (!unfreezeContract.ownerAddress) {
526+
throw new UtilsError('Owner address does not exist in this unfreeze contract.');
527+
}
528+
529+
if (unfreezeContract.resource === undefined) {
530+
throw new UtilsError('Resource type does not exist in this unfreeze contract.');
531+
}
532+
533+
if (unfreezeContract.unfrozenBalance === undefined) {
534+
throw new UtilsError('Unfreeze balance does not exist in this unfreeze contract.');
535+
}
536+
537+
538+
// deserialize attributes
539+
const owner_address = getBase58AddressFromByteArray(
540+
getByteArrayFromHexAddress(Buffer.from(unfreezeContract.ownerAddress, 'base64').toString('hex'))
541+
);
542+
543+
// Convert ResourceCode enum value to string resource name
544+
const resourceValue = unfreezeContract.resource;
545+
const resourceEnum = resourceValue === protocol.ResourceCode.BANDWIDTH ? TronResource.BANDWIDTH : TronResource.ENERGY;
546+
547+
return [
548+
{
549+
parameter: {
550+
value: {
551+
resource: resourceEnum,
552+
unfreeze_balance: Number(unfreezeContract.unfrozenBalance),
553+
owner_address,
554+
},
555+
},
556+
},
557+
];
558+
}
559+
560+
/**
561+
* Deserialize the segment of the txHex corresponding with withdraw expire unfreeze contract
562+
*
563+
* @param {string} base64 - The base64 encoded contract data
564+
* @returns {WithdrawExpireUnfreezeContractParameter[]} - Array containing the decoded withdraw contract
565+
*/
566+
export function decodeWithdrawExpireUnfreezeContract(base64: string): WithdrawExpireUnfreezeContractParameter[] {
567+
interface WithdrawContractDecoded {
568+
ownerAddress?: string;
569+
}
570+
571+
let withdrawContract: WithdrawContractDecoded;
572+
try {
573+
withdrawContract = protocol.WithdrawBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
574+
} catch (e) {
575+
throw new UtilsError('There was an error decoding the withdraw contract in the transaction.');
576+
}
577+
578+
if (!withdrawContract.ownerAddress) {
579+
throw new UtilsError('Owner address does not exist in this withdraw contract.');
580+
}
581+
582+
// deserialize attributes
583+
const owner_address = getBase58AddressFromByteArray(
584+
getByteArrayFromHexAddress(Buffer.from(withdrawContract.ownerAddress, 'base64').toString('hex'))
585+
);
586+
587+
return [
588+
{
589+
parameter: {
590+
value: {
591+
owner_address,
592+
},
593+
},
594+
},
595+
];
491596
}
492597

493598
/**

0 commit comments

Comments
 (0)