Skip to content

Commit f226c24

Browse files
authored
Merge pull request #8773 from BitGo/bhavidhingra/chalo-457-feattrx-add-accountcreatecontract-support
feat(trx): add AccountCreateContract tx builder
2 parents 38e258b + f827d69 commit f226c24

25 files changed

Lines changed: 639 additions & 17 deletions
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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, AccountCreateContract } from './iface';
7+
import { protocol } from '../../resources/protobuf/tron';
8+
import {
9+
decodeTransaction,
10+
getByteArrayFromHexAddress,
11+
getBase58AddressFromHex,
12+
getHexAddressFromBase58Address,
13+
TRANSACTION_MAX_EXPIRATION,
14+
TRANSACTION_DEFAULT_EXPIRATION,
15+
} from './utils';
16+
import { ACCOUNT_CREATE_TYPE_URL } from './constants';
17+
18+
import ContractType = protocol.Transaction.Contract.ContractType;
19+
20+
export class AccountCreateTxBuilder extends TransactionBuilder {
21+
protected _signingKeys: BaseKey[];
22+
// Stored as hex address, consistent with _ownerAddress
23+
protected _accountAddress: string;
24+
25+
constructor(_coinConfig: Readonly<CoinConfig>) {
26+
super(_coinConfig);
27+
this._signingKeys = [];
28+
this.transaction = new Transaction(_coinConfig);
29+
}
30+
31+
/** @inheritdoc */
32+
protected get transactionType(): TransactionType {
33+
return TransactionType.AccountCreate;
34+
}
35+
36+
/**
37+
* Sets the account address (Base58) to be created/activated on-chain.
38+
* Stored internally as hex for protobuf encoding.
39+
*
40+
* @param {object} address - object containing the Base58 address of the new account
41+
* @returns {this}
42+
*/
43+
setAccountAddress(address: { address: string }): this {
44+
this.validateAddress(address);
45+
this._accountAddress = getHexAddressFromBase58Address(address.address);
46+
return this;
47+
}
48+
49+
/** @inheritdoc */
50+
extendValidTo(extensionMs: number): void {
51+
if (this.transaction.signature && this.transaction.signature.length > 0) {
52+
throw new ExtendTransactionError('Cannot extend a signed transaction');
53+
}
54+
55+
if (extensionMs <= 0) {
56+
throw new Error('Value cannot be below zero');
57+
}
58+
59+
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
60+
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
61+
}
62+
63+
if (this._expiration) {
64+
this._expiration = this._expiration + extensionMs;
65+
} else {
66+
throw new Error('There is not expiration to extend');
67+
}
68+
}
69+
70+
initBuilder(rawTransaction: TransactionReceipt | string): this {
71+
this.validateRawTransaction(rawTransaction);
72+
const tx = this.fromImplementation(rawTransaction);
73+
this.transaction = tx;
74+
this._signingKeys = [];
75+
const rawData = tx.toJson().raw_data;
76+
this._refBlockBytes = rawData.ref_block_bytes;
77+
this._refBlockHash = rawData.ref_block_hash;
78+
this._expiration = rawData.expiration;
79+
this._timestamp = rawData.timestamp;
80+
this.transaction.setTransactionType(this.transactionType);
81+
const contractCall = rawData.contract[0] as AccountCreateContract;
82+
this.initAccountCreateContractCall(contractCall);
83+
return this;
84+
}
85+
86+
/**
87+
* Initialize the account create contract call specific data.
88+
* Addresses stored in the receipt are hex (set by createAccountCreateTransaction).
89+
*
90+
* @param {AccountCreateContract} accountCreateContractCall object with account create contract data
91+
*/
92+
protected initAccountCreateContractCall(accountCreateContractCall: AccountCreateContract): void {
93+
const { owner_address, account_address } = accountCreateContractCall.parameter.value;
94+
if (owner_address) {
95+
// owner_address stored in receipt is hex; source() expects Base58
96+
this.source({ address: getBase58AddressFromHex(owner_address) });
97+
}
98+
if (account_address) {
99+
// account_address stored in receipt is hex; store directly
100+
this._accountAddress = account_address;
101+
}
102+
}
103+
104+
protected async buildImplementation(): Promise<Transaction> {
105+
this.createAccountCreateTransaction();
106+
if (this._signingKeys.length > 0) {
107+
this.applySignatures();
108+
}
109+
110+
if (!this.transaction.id) {
111+
throw new BuildTransactionError('A valid transaction must have an id');
112+
}
113+
return Promise.resolve(this.transaction);
114+
}
115+
116+
/**
117+
* Helper method to create the account create transaction
118+
*/
119+
private createAccountCreateTransaction(): void {
120+
const rawDataHex = this.getAccountCreateTxRawDataHex();
121+
const rawData = decodeTransaction(rawDataHex);
122+
const contract = rawData.contract[0] as AccountCreateContract;
123+
const contractParameter = contract.parameter;
124+
contractParameter.value.owner_address = this._ownerAddress.toLocaleLowerCase();
125+
contractParameter.value.account_address = this._accountAddress.toLocaleLowerCase();
126+
contractParameter.type_url = ACCOUNT_CREATE_TYPE_URL;
127+
contract.type = 'AccountCreateContract';
128+
const hexBuffer = Buffer.from(rawDataHex, 'hex');
129+
const id = createHash('sha256').update(hexBuffer).digest('hex');
130+
const txReceipt: TransactionReceipt = {
131+
raw_data: rawData,
132+
raw_data_hex: rawDataHex,
133+
txID: id,
134+
signature: this.transaction.signature,
135+
};
136+
this.transaction = new Transaction(this._coinConfig, txReceipt);
137+
}
138+
139+
/**
140+
* Helper method to get the account create transaction raw data hex
141+
*
142+
* @returns {string} the account create transaction raw data hex
143+
*/
144+
private getAccountCreateTxRawDataHex(): string {
145+
const rawContract = {
146+
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
147+
accountAddress: getByteArrayFromHexAddress(this._accountAddress),
148+
};
149+
const accountCreateContract = protocol.AccountCreateContract.fromObject(rawContract);
150+
const accountCreateContractBytes = protocol.AccountCreateContract.encode(accountCreateContract).finish();
151+
const txContract = {
152+
type: ContractType.AccountCreateContract,
153+
parameter: {
154+
value: accountCreateContractBytes,
155+
type_url: ACCOUNT_CREATE_TYPE_URL,
156+
},
157+
};
158+
const raw = {
159+
refBlockBytes: Buffer.from(this._refBlockBytes, 'hex'),
160+
refBlockHash: Buffer.from(this._refBlockHash, 'hex'),
161+
expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION,
162+
timestamp: this._timestamp || Date.now(),
163+
contract: [txContract],
164+
};
165+
const rawTx = protocol.Transaction.raw.create(raw);
166+
return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex');
167+
}
168+
169+
/** @inheritdoc */
170+
protected signImplementation(key: BaseKey): Transaction {
171+
if (this._signingKeys.some((signingKey) => signingKey.key === key.key)) {
172+
throw new SigningError('Duplicated key');
173+
}
174+
this._signingKeys.push(key);
175+
176+
// We keep this return for compatibility but is not meant to be use
177+
return this.transaction;
178+
}
179+
180+
private applySignatures(): void {
181+
if (!this.transaction.inputs) {
182+
throw new SigningError('Transaction has no inputs');
183+
}
184+
185+
this._signingKeys.forEach((key) => this.applySignature(key));
186+
}
187+
188+
/**
189+
* Validates the transaction
190+
*
191+
* @param {Transaction} transaction - The transaction to validate
192+
* @throws {BuildTransactionError} when the transaction is invalid
193+
*/
194+
validateTransaction(transaction: Transaction): void {
195+
this.validateAccountCreateTransactionFields();
196+
}
197+
198+
/**
199+
* Validates if the transaction is a valid account create transaction
200+
*
201+
* @throws {BuildTransactionError} when the transaction is invalid
202+
*/
203+
private validateAccountCreateTransactionFields(): void {
204+
if (!this._ownerAddress) {
205+
throw new BuildTransactionError('Missing parameter: source');
206+
}
207+
208+
if (!this._accountAddress) {
209+
throw new BuildTransactionError('Missing parameter: account address');
210+
}
211+
212+
if (!this._refBlockBytes || !this._refBlockHash) {
213+
throw new BuildTransactionError('Missing block reference information');
214+
}
215+
}
216+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const DELEGATION_TYPE_URL = 'type.googleapis.com/protocol.DelegateResourceContract';
22
export const UNDELEGATION_TYPE_URL = 'type.googleapis.com/protocol.UnDelegateResourceContract';
3+
export const ACCOUNT_CREATE_TYPE_URL = 'type.googleapis.com/protocol.AccountCreateContract';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class ContractCallBuilder extends TransactionBuilder {
143143
}
144144

145145
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
146-
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
146+
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
147147
}
148148

149149
if (this._expiration) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export enum ContractType {
4242
* This is the contract for un-delegating resource
4343
*/
4444
UnDelegateResourceContract,
45+
/**
46+
* This is the contract for creating/activating a new account
47+
*/
48+
AccountCreate,
4549
}
4650

4751
export enum PermissionType {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder {
6565
}
6666

6767
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
68-
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
68+
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
6969
}
7070

7171
if (this._expiration) {

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export interface RawData {
5151
| UnfreezeBalanceV2Contract[]
5252
| WithdrawExpireUnfreezeContract[]
5353
| WithdrawBalanceContract[]
54-
| ResourceManagementContract[];
54+
| ResourceManagementContract[]
55+
| AccountCreateContract[];
5556
}
5657

5758
export interface Value {
@@ -363,6 +364,38 @@ export interface ResourceManagementContractParameter {
363364
};
364365
}
365366

367+
/**
368+
* AccountCreate contract value fields
369+
*/
370+
export interface AccountCreateValueFields {
371+
owner_address: string;
372+
account_address: string;
373+
}
374+
375+
/**
376+
* AccountCreate contract value interface
377+
*/
378+
export interface AccountCreateValue {
379+
type_url?: string;
380+
value: AccountCreateValueFields;
381+
}
382+
383+
/**
384+
* AccountCreate contract interface
385+
*/
386+
export interface AccountCreateContract {
387+
parameter: AccountCreateValue;
388+
type?: string;
389+
}
390+
391+
/**
392+
* AccountCreate contract decoded interface
393+
*/
394+
export interface AccountCreateContractDecoded {
395+
ownerAddress?: string;
396+
accountAddress?: string;
397+
}
398+
366399
/**
367400
* Delegate/Undelegate resource contract decoded interface
368401
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export abstract class ResourceManagementTxBuilder extends TransactionBuilder {
7070
}
7171

7272
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
73-
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
73+
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
7474
}
7575

7676
if (this._expiration) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
WithdrawExpireUnfreezeContract,
3030
ResourceManagementContract,
3131
WithdrawBalanceContract,
32+
AccountCreateContract,
3233
} from './iface';
3334

3435
/**
@@ -226,6 +227,13 @@ export class Transaction extends BaseTransaction {
226227
value: undelegateValue.balance.toString(),
227228
};
228229
break;
230+
case ContractType.AccountCreate: {
231+
this._type = TransactionType.AccountCreate;
232+
const createValue = (rawData.contract[0] as AccountCreateContract).parameter.value;
233+
output = { address: createValue.account_address, value: '0' };
234+
input = { address: createValue.owner_address, value: '0' };
235+
break;
236+
}
229237
default:
230238
throw new ParseTransactionError('Unsupported contract type');
231239
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class UnfreezeBalanceTxBuilder extends TransactionBuilder {
6565
}
6666

6767
if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
68-
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
68+
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
6969
}
7070

7171
if (this._expiration) {

0 commit comments

Comments
 (0)