Skip to content

Commit 07bbc63

Browse files
committed
feat(sdk-coin-trx): add AccountCreateContract tx builder and support
Add AccountCreateTxBuilder, ACCOUNT_CREATE_TYPE_URL constant, AccountCreate ContractType enum value, iface types, decode helper, transaction.ts case, wrappedBuilder factory, and unit tests for the new builder. TICKET: CHALO-457
1 parent 8b304dd commit 07bbc63

10 files changed

Lines changed: 625 additions & 2 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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 year');
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.transaction = this.fromImplementation(rawTransaction);
72+
this.transaction.setTransactionType(this.transactionType);
73+
this.validateRawTransaction(rawTransaction);
74+
const tx = this.fromImplementation(rawTransaction);
75+
this.transaction = tx;
76+
this._signingKeys = [];
77+
const rawData = tx.toJson().raw_data;
78+
this._refBlockBytes = rawData.ref_block_bytes;
79+
this._refBlockHash = rawData.ref_block_hash;
80+
this._expiration = rawData.expiration;
81+
this._timestamp = rawData.timestamp;
82+
const contractCall = rawData.contract[0] as AccountCreateContract;
83+
this.initAccountCreateContractCall(contractCall);
84+
return this;
85+
}
86+
87+
/**
88+
* Initialize the account create contract call specific data.
89+
* Addresses stored in the receipt are hex (set by createAccountCreateTransaction).
90+
*
91+
* @param {AccountCreateContract} accountCreateContractCall object with account create contract data
92+
*/
93+
protected initAccountCreateContractCall(accountCreateContractCall: AccountCreateContract): void {
94+
const { owner_address, account_address } = accountCreateContractCall.parameter.value;
95+
if (owner_address) {
96+
// owner_address stored in receipt is hex; source() expects Base58
97+
this.source({ address: getBase58AddressFromHex(owner_address) });
98+
}
99+
if (account_address) {
100+
// account_address stored in receipt is hex; store directly
101+
this._accountAddress = account_address;
102+
}
103+
}
104+
105+
protected async buildImplementation(): Promise<Transaction> {
106+
this.createAccountCreateTransaction();
107+
if (this._signingKeys.length > 0) {
108+
this.applySignatures();
109+
}
110+
111+
if (!this.transaction.id) {
112+
throw new BuildTransactionError('A valid transaction must have an id');
113+
}
114+
return Promise.resolve(this.transaction);
115+
}
116+
117+
/**
118+
* Helper method to create the account create transaction
119+
*/
120+
private createAccountCreateTransaction(): void {
121+
const rawDataHex = this.getAccountCreateTxRawDataHex();
122+
const rawData = decodeTransaction(rawDataHex);
123+
const contract = rawData.contract[0] as AccountCreateContract;
124+
const contractParameter = contract.parameter;
125+
contractParameter.value.owner_address = this._ownerAddress.toLocaleLowerCase();
126+
contractParameter.value.account_address = this._accountAddress.toLocaleLowerCase();
127+
contractParameter.type_url = ACCOUNT_CREATE_TYPE_URL;
128+
contract.type = 'AccountCreateContract';
129+
const hexBuffer = Buffer.from(rawDataHex, 'hex');
130+
const id = createHash('sha256').update(hexBuffer).digest('hex');
131+
const txReceipt: TransactionReceipt = {
132+
raw_data: rawData,
133+
raw_data_hex: rawDataHex,
134+
txID: id,
135+
signature: this.transaction.signature,
136+
};
137+
this.transaction = new Transaction(this._coinConfig, txReceipt);
138+
}
139+
140+
/**
141+
* Helper method to get the account create transaction raw data hex
142+
*
143+
* @returns {string} the account create transaction raw data hex
144+
*/
145+
private getAccountCreateTxRawDataHex(): string {
146+
const rawContract = {
147+
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
148+
accountAddress: getByteArrayFromHexAddress(this._accountAddress),
149+
};
150+
const accountCreateContract = protocol.AccountCreateContract.fromObject(rawContract);
151+
const accountCreateContractBytes = protocol.AccountCreateContract.encode(accountCreateContract).finish();
152+
const txContract = {
153+
type: ContractType.AccountCreateContract,
154+
parameter: {
155+
value: accountCreateContractBytes,
156+
type_url: ACCOUNT_CREATE_TYPE_URL,
157+
},
158+
};
159+
const raw = {
160+
refBlockBytes: Buffer.from(this._refBlockBytes, 'hex'),
161+
refBlockHash: Buffer.from(this._refBlockHash, 'hex'),
162+
expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION,
163+
timestamp: this._timestamp || Date.now(),
164+
contract: [txContract],
165+
};
166+
const rawTx = protocol.Transaction.raw.create(raw);
167+
return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex');
168+
}
169+
170+
/** @inheritdoc */
171+
protected signImplementation(key: BaseKey): Transaction {
172+
if (this._signingKeys.some((signingKey) => signingKey.key === key.key)) {
173+
throw new SigningError('Duplicated key');
174+
}
175+
this._signingKeys.push(key);
176+
177+
// We keep this return for compatibility but is not meant to be use
178+
return this.transaction;
179+
}
180+
181+
private applySignatures(): void {
182+
if (!this.transaction.inputs) {
183+
throw new SigningError('Transaction has no inputs');
184+
}
185+
186+
this._signingKeys.forEach((key) => this.applySignature(key));
187+
}
188+
189+
/**
190+
* Validates the transaction
191+
*
192+
* @param {Transaction} transaction - The transaction to validate
193+
* @throws {BuildTransactionError} when the transaction is invalid
194+
*/
195+
validateTransaction(transaction: Transaction): void {
196+
this.validateAccountCreateTransactionFields();
197+
}
198+
199+
/**
200+
* Validates if the transaction is a valid account create transaction
201+
*
202+
* @throws {BuildTransactionError} when the transaction is invalid
203+
*/
204+
private validateAccountCreateTransactionFields(): void {
205+
if (!this._ownerAddress) {
206+
throw new BuildTransactionError('Missing parameter: source');
207+
}
208+
209+
if (!this._accountAddress) {
210+
throw new BuildTransactionError('Missing parameter: account address');
211+
}
212+
213+
if (!this._refBlockBytes || !this._refBlockHash) {
214+
throw new BuildTransactionError('Missing block reference information');
215+
}
216+
}
217+
}
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/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/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/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/utils.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import {
2222
WithdrawContractDecoded,
2323
ResourceManagementContractParameter,
2424
ResourceManagementContractDecoded,
25+
AccountCreateContract,
26+
AccountCreateContractDecoded,
2527
} from './iface';
2628
import { ContractType, PermissionType, TronResource } from './enum';
2729
import { AbiCoder, hexConcat } from 'ethers/lib/utils';
28-
import { DELEGATION_TYPE_URL } from './constants';
30+
import { DELEGATION_TYPE_URL, ACCOUNT_CREATE_TYPE_URL } from './constants';
2931

3032
export const TRANSACTION_MAX_EXPIRATION = 86400000; // one day
3133
export const TRANSACTION_DEFAULT_EXPIRATION = 10800000; // three hours
@@ -233,6 +235,10 @@ export function decodeTransaction(hexString: string): RawData {
233235
contractType = ContractType.UnDelegateResourceContract;
234236
contract = decodeUnDelegateResourceContract(rawTransaction.contracts[0].parameter.value);
235237
break;
238+
case ACCOUNT_CREATE_TYPE_URL:
239+
contractType = ContractType.AccountCreate;
240+
contract = decodeAccountCreateContract(rawTransaction.contracts[0].parameter.value);
241+
break;
236242
default:
237243
throw new UtilsError('Unsupported contract type');
238244
}
@@ -753,6 +759,47 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage
753759
];
754760
}
755761

762+
/**
763+
* Deserialize the segment of the txHex corresponding with the account create contract
764+
*
765+
* @param {Uint8Array} value - The raw protobuf bytes from the contract parameter
766+
* @returns {AccountCreateContract[]} - Array containing the decoded account create contract
767+
*/
768+
export function decodeAccountCreateContract(base64: string): AccountCreateContract[] {
769+
let decoded: AccountCreateContractDecoded;
770+
try {
771+
decoded = protocol.AccountCreateContract.decode(Buffer.from(base64, 'base64')).toJSON();
772+
} catch (e) {
773+
throw new UtilsError('There was an error decoding the account create contract in the transaction.');
774+
}
775+
776+
if (!decoded.ownerAddress) {
777+
throw new UtilsError('Owner address does not exist in this account create contract.');
778+
}
779+
780+
if (!decoded.accountAddress) {
781+
throw new UtilsError('Account address does not exist in this account create contract.');
782+
}
783+
784+
const owner_address = getBase58AddressFromByteArray(
785+
getByteArrayFromHexAddress(Buffer.from(decoded.ownerAddress, 'base64').toString('hex'))
786+
);
787+
const account_address = getBase58AddressFromByteArray(
788+
getByteArrayFromHexAddress(Buffer.from(decoded.accountAddress, 'base64').toString('hex'))
789+
);
790+
791+
return [
792+
{
793+
parameter: {
794+
value: {
795+
owner_address,
796+
account_address,
797+
},
798+
},
799+
},
800+
];
801+
}
802+
756803
/**
757804
* @param raw
758805
*/

0 commit comments

Comments
 (0)