Skip to content

Commit 4962338

Browse files
feat(sdk-coin-trx): add support for reward claim
Ticket: SC-2333
1 parent 2ad9e03 commit 4962338

File tree

8 files changed

+545
-0
lines changed

8 files changed

+545
-0
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export enum ContractType {
3030
* This is the contract for withdrawing expired unfrozen balances
3131
*/
3232
WithdrawExpireUnfreeze,
33+
/**
34+
* This is the contract for withdrawing reward balances
35+
*/
36+
WithdrawBalance,
3337
/**
3438
* This is the contract for delegating resource
3539
*/

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface RawData {
5050
| VoteWitnessContract[]
5151
| UnfreezeBalanceV2Contract[]
5252
| WithdrawExpireUnfreezeContract[]
53+
| WithdrawBalanceContract[]
5354
| ResourceManagementContract[];
5455
}
5556

@@ -212,6 +213,12 @@ export interface WithdrawExpireUnfreezeContract {
212213
type?: string;
213214
}
214215

216+
export interface WithdrawBalanceContract {
217+
// same as withdraw expire unfreeze
218+
parameter: WithdrawExpireUnfreezeValue;
219+
type?: string;
220+
}
221+
215222
export interface UnfreezeBalanceContractParameter {
216223
parameter: {
217224
value: {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
UnfreezeBalanceV2Contract,
2929
WithdrawExpireUnfreezeContract,
3030
ResourceManagementContract,
31+
WithdrawBalanceContract,
3132
} from './iface';
3233

3334
/**
@@ -189,6 +190,18 @@ export class Transaction extends BaseTransaction {
189190
value: '0',
190191
};
191192
break;
193+
case ContractType.WithdrawBalance:
194+
this._type = TransactionType.StakingClaim;
195+
const withdrawBalanceValue = (rawData.contract[0] as WithdrawBalanceContract).parameter.value;
196+
output = {
197+
address: withdrawBalanceValue.owner_address,
198+
value: '0', // no value field
199+
};
200+
input = {
201+
address: withdrawBalanceValue.owner_address,
202+
value: '0',
203+
};
204+
break;
192205
case ContractType.DelegateResourceContract:
193206
this._type = TransactionType.DelegateResource;
194207
const delegateValue = (rawData.contract[0] as ResourceManagementContract).parameter.value;

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ export function decodeTransaction(hexString: string): RawData {
217217
contract = decodeWithdrawExpireUnfreezeContract(rawTransaction.contracts[0].parameter.value);
218218
contractType = ContractType.WithdrawExpireUnfreeze;
219219
break;
220+
case 'type.googleapis.com/protocol.WithdrawBalanceContract':
221+
contract = decodeWithdrawBalanceContract(rawTransaction.contracts[0].parameter.value);
222+
contractType = ContractType.WithdrawBalance;
223+
break;
220224
case 'type.googleapis.com/protocol.UnfreezeBalanceV2Contract':
221225
contract = decodeUnfreezeBalanceV2Contract(rawTransaction.contracts[0].parameter.value);
222226
contractType = ContractType.UnfreezeBalanceV2;
@@ -564,6 +568,41 @@ export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalance
564568
];
565569
}
566570

571+
/**
572+
* Deserialize the segment of the txHex corresponding with withdraw balance contract
573+
* Decoded contract is the same as withdraw expire unfreeze
574+
*
575+
* @param {string} base64 - The base64 encoded contract data
576+
* @returns {WithdrawExpireUnfreezeContractParameter[]} - Array containing the decoded withdraw contract
577+
*/
578+
export function decodeWithdrawBalanceContract(base64: string): WithdrawExpireUnfreezeContractParameter[] {
579+
let withdrawContract: WithdrawContractDecoded;
580+
try {
581+
withdrawContract = protocol.WithdrawBalanceContract.decode(Buffer.from(base64, 'base64')).toJSON();
582+
} catch (e) {
583+
throw new UtilsError('There was an error decoding the withdraw contract in the transaction.');
584+
}
585+
586+
if (!withdrawContract.ownerAddress) {
587+
throw new UtilsError('Owner address does not exist in this withdraw contract.');
588+
}
589+
590+
// deserialize attributes
591+
const owner_address = getBase58AddressFromByteArray(
592+
getByteArrayFromHexAddress(Buffer.from(withdrawContract.ownerAddress, 'base64').toString('hex'))
593+
);
594+
595+
return [
596+
{
597+
parameter: {
598+
value: {
599+
owner_address,
600+
},
601+
},
602+
},
603+
];
604+
}
605+
567606
/**
568607
* Deserialize the segment of the txHex corresponding with withdraw expire unfreeze contract
569608
*
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { createHash } from 'crypto';
2+
import { TransactionType, BaseKey, ExtendTransactionError, BuildTransactionError, SigningError } from '@bitgo/sdk-core';
3+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
4+
import { TransactionBuilder } from './transactionBuilder';
5+
import { Transaction } from './transaction';
6+
import { TransactionReceipt, WithdrawBalanceContract } from './iface';
7+
import { protocol } from '../../resources/protobuf/tron';
8+
import {
9+
decodeTransaction,
10+
getByteArrayFromHexAddress,
11+
getBase58AddressFromHex,
12+
TRANSACTION_MAX_EXPIRATION,
13+
TRANSACTION_DEFAULT_EXPIRATION,
14+
} from './utils';
15+
16+
import ContractType = protocol.Transaction.Contract.ContractType;
17+
18+
export class WithdrawBalanceTxBuilder extends TransactionBuilder {
19+
protected _signingKeys: BaseKey[];
20+
21+
constructor(_coinConfig: Readonly<CoinConfig>) {
22+
super(_coinConfig);
23+
this._signingKeys = [];
24+
this.transaction = new Transaction(_coinConfig);
25+
}
26+
27+
/** @inheritdoc */
28+
protected get transactionType(): TransactionType {
29+
return TransactionType.StakingClaim;
30+
}
31+
32+
/** @inheritdoc */
33+
extendValidTo(extensionMs: number): void {
34+
if (this.transaction.signature && this.transaction.signature.length > 0) {
35+
throw new ExtendTransactionError('Cannot extend a signed transaction');
36+
}
37+
38+
if (extensionMs <= 0) {
39+
throw new Error('Value cannot be below zero');
40+
}
41+
42+
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
43+
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
44+
}
45+
46+
if (this._expiration) {
47+
this._expiration = this._expiration + extensionMs;
48+
} else {
49+
throw new Error('There is not expiration to extend');
50+
}
51+
}
52+
53+
initBuilder(rawTransaction: TransactionReceipt | string): this {
54+
this.transaction = this.fromImplementation(rawTransaction);
55+
this.transaction.setTransactionType(this.transactionType);
56+
this.validateRawTransaction(rawTransaction);
57+
const tx = this.fromImplementation(rawTransaction);
58+
this.transaction = tx;
59+
this._signingKeys = [];
60+
const rawData = tx.toJson().raw_data;
61+
this._refBlockBytes = rawData.ref_block_bytes;
62+
this._refBlockHash = rawData.ref_block_hash;
63+
this._expiration = rawData.expiration;
64+
this._timestamp = rawData.timestamp;
65+
const contractCall = rawData.contract[0] as WithdrawBalanceContract;
66+
this.initWithdrawBalanceContractCall(contractCall);
67+
return this;
68+
}
69+
70+
/**
71+
* Initialize the withdraw balance contract call specific data
72+
*
73+
* @param {WithdrawBalanceContract} withdrawBalanceContractCall object with freeze txn data
74+
*/
75+
protected initWithdrawBalanceContractCall(withdrawBalanceContractCall: WithdrawBalanceContract): void {
76+
const { owner_address } = withdrawBalanceContractCall.parameter.value;
77+
if (owner_address) {
78+
this.source({ address: getBase58AddressFromHex(owner_address) });
79+
}
80+
}
81+
82+
protected async buildImplementation(): Promise<Transaction> {
83+
this.createWithdrawBalanceTransaction();
84+
/** @inheritdoccreateTransaction */
85+
// This method must be extended on child classes
86+
if (this._signingKeys.length > 0) {
87+
this.applySignatures();
88+
}
89+
90+
if (!this.transaction.id) {
91+
throw new BuildTransactionError('A valid transaction must have an id');
92+
}
93+
return Promise.resolve(this.transaction);
94+
}
95+
96+
/**
97+
* Helper method to create the withdraw balance transaction
98+
*/
99+
private createWithdrawBalanceTransaction(): void {
100+
const rawDataHex = this.getWithdrawBalanceRawDataHex();
101+
const rawData = decodeTransaction(rawDataHex);
102+
const contract = rawData.contract[0] as WithdrawBalanceContract;
103+
const contractParameter = contract.parameter;
104+
contractParameter.value.owner_address = this._ownerAddress.toLocaleLowerCase();
105+
contractParameter.type_url = 'type.googleapis.com/protocol.WithdrawBalanceContract';
106+
contract.type = 'WithdrawBalanceContract';
107+
const hexBuffer = Buffer.from(rawDataHex, 'hex');
108+
const id = createHash('sha256').update(hexBuffer).digest('hex');
109+
const txRecip: TransactionReceipt = {
110+
raw_data: rawData,
111+
raw_data_hex: rawDataHex,
112+
txID: id,
113+
signature: this.transaction.signature,
114+
};
115+
this.transaction = new Transaction(this._coinConfig, txRecip);
116+
}
117+
118+
/**
119+
* Helper method to get the withdraw expire unfreeze transaction raw data hex
120+
*
121+
* @returns {string} the freeze balance transaction raw data hex
122+
*/
123+
private getWithdrawBalanceRawDataHex(): string {
124+
const rawContract = {
125+
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
126+
};
127+
const withdrawBalanceContract = protocol.WithdrawBalanceContract.fromObject(rawContract);
128+
const withdrawBalanceContractBytes = protocol.WithdrawBalanceContract.encode(withdrawBalanceContract).finish();
129+
const txContract = {
130+
type: ContractType.WithdrawBalanceContract,
131+
parameter: {
132+
value: withdrawBalanceContractBytes,
133+
type_url: 'type.googleapis.com/protocol.WithdrawBalanceContract',
134+
},
135+
};
136+
const raw = {
137+
refBlockBytes: Buffer.from(this._refBlockBytes, 'hex'),
138+
refBlockHash: Buffer.from(this._refBlockHash, 'hex'),
139+
expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION,
140+
timestamp: this._timestamp || Date.now(),
141+
contract: [txContract],
142+
};
143+
const rawTx = protocol.Transaction.raw.create(raw);
144+
return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex');
145+
}
146+
147+
/** @inheritdoc */
148+
protected signImplementation(key: BaseKey): Transaction {
149+
if (this._signingKeys.some((signingKey) => signingKey.key === key.key)) {
150+
throw new SigningError('Duplicated key');
151+
}
152+
this._signingKeys.push(key);
153+
154+
// We keep this return for compatibility but is not meant to be use
155+
return this.transaction;
156+
}
157+
158+
private applySignatures(): void {
159+
if (!this.transaction.inputs) {
160+
throw new SigningError('Transaction has no inputs');
161+
}
162+
163+
this._signingKeys.forEach((key) => this.applySignature(key));
164+
}
165+
166+
/**
167+
* Validates the transaction
168+
*
169+
* @param {Transaction} transaction - The transaction to validate
170+
* @throws {BuildTransactionError} when the transaction is invalid
171+
*/
172+
validateTransaction(transaction: Transaction): void {
173+
this.validateWithdrawBalanceTransactionFields();
174+
}
175+
176+
/**
177+
* Validates if the transaction is a valid withdraw balance transaction
178+
*
179+
* @param {TransactionReceipt} transaction - The transaction to validate
180+
* @throws {BuildTransactionError} when the transaction is invalid
181+
*/
182+
private validateWithdrawBalanceTransactionFields(): void {
183+
if (!this._ownerAddress) {
184+
throw new BuildTransactionError('Missing parameter: source');
185+
}
186+
187+
if (!this._refBlockBytes || !this._refBlockHash) {
188+
throw new BuildTransactionError('Missing block reference information');
189+
}
190+
}
191+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { FreezeBalanceTxBuilder } from './freezeBalanceTxBuilder';
1414
import { VoteWitnessTxBuilder } from './voteWitnessTxBuilder';
1515
import { UnfreezeBalanceTxBuilder } from './unfreezeBalanceTxBuilder';
1616
import { WithdrawExpireUnfreezeTxBuilder } from './withdrawExpireUnfreezeTxBuilder';
17+
import { WithdrawBalanceTxBuilder } from './withdrawBuilder';
1718
import { DelegateResourceTxBuilder } from './delegateResourceTxBuilder';
1819
import { UndelegateResourceTxBuilder } from './undelegateResourceTxBuilder';
1920

@@ -89,6 +90,16 @@ export class WrappedBuilder extends TransactionBuilder {
8990
return this.initializeBuilder(tx, new WithdrawExpireUnfreezeTxBuilder(this._coinConfig));
9091
}
9192

93+
/**
94+
* Returns a specific builder to create a withdraw balance transaction
95+
*
96+
* @param {Transaction} [tx] The transaction to initialize builder
97+
* @returns {WithdrawBalanceTxBuilder} The specific withdraw balance builder
98+
*/
99+
getWithdrawBalanceTxBuilder(tx?: TransactionReceipt | string): WithdrawBalanceTxBuilder {
100+
return this.initializeBuilder(tx, new WithdrawBalanceTxBuilder(this._coinConfig));
101+
}
102+
92103
/**
93104
* Returns a specific builder to create a delegate resource transaction
94105
*
@@ -152,6 +163,8 @@ export class WrappedBuilder extends TransactionBuilder {
152163
return this.getUnfreezeBalanceV2TxBuilder(raw);
153164
case ContractType.WithdrawExpireUnfreeze:
154165
return this.getWithdrawExpireUnfreezeTxBuilder(raw);
166+
case ContractType.WithdrawBalance:
167+
return this.getWithdrawBalanceTxBuilder(raw);
155168
case ContractType.DelegateResourceContract:
156169
return this.getDelegateResourceTxBuilder(raw);
157170
case ContractType.UnDelegateResourceContract:

modules/sdk-coin-trx/test/resources.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ export const WITHDRAW_EXPIRE_UNFREEZE_CONTRACT = [
107107
},
108108
];
109109

110+
export const WITHDRAW_BALANCE_CONTRACT = [
111+
{
112+
parameter: {
113+
value: {
114+
owner_address: '4173a5993cd182ae152adad8203163f780c65a8aa5',
115+
},
116+
type_url: 'type.googleapis.com/protocol.WithdrawBalanceContract',
117+
},
118+
type: 'WithdrawBalanceContract',
119+
},
120+
];
121+
110122
export const VOTE_WITNESS_CONTRACT = [
111123
{
112124
parameter: {

0 commit comments

Comments
 (0)