Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions modules/sdk-coin-trx/src/lib/accountCreateTxBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { createHash } from 'crypto';
import { TransactionType, BaseKey, ExtendTransactionError, BuildTransactionError, SigningError } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction';
import { TransactionReceipt, AccountCreateContract } from './iface';
import { protocol } from '../../resources/protobuf/tron';
import {
decodeTransaction,
getByteArrayFromHexAddress,
getBase58AddressFromHex,
getHexAddressFromBase58Address,
TRANSACTION_MAX_EXPIRATION,
TRANSACTION_DEFAULT_EXPIRATION,
} from './utils';
import { ACCOUNT_CREATE_TYPE_URL } from './constants';

import ContractType = protocol.Transaction.Contract.ContractType;

export class AccountCreateTxBuilder extends TransactionBuilder {
protected _signingKeys: BaseKey[];
// Stored as hex address, consistent with _ownerAddress
protected _accountAddress: string;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._signingKeys = [];
this.transaction = new Transaction(_coinConfig);
}

/** @inheritdoc */
protected get transactionType(): TransactionType {
return TransactionType.AccountCreate;
}

/**
* Sets the account address (Base58) to be created/activated on-chain.
* Stored internally as hex for protobuf encoding.
*
* @param {object} address - object containing the Base58 address of the new account
* @returns {this}
*/
setAccountAddress(address: { address: string }): this {
this.validateAddress(address);
this._accountAddress = getHexAddressFromBase58Address(address.address);
return this;
}

/** @inheritdoc */
extendValidTo(extensionMs: number): void {
if (this.transaction.signature && this.transaction.signature.length > 0) {
throw new ExtendTransactionError('Cannot extend a signed transaction');
}

if (extensionMs <= 0) {
throw new Error('Value cannot be below zero');
}

if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
}

if (this._expiration) {
this._expiration = this._expiration + extensionMs;
} else {
throw new Error('There is not expiration to extend');
}
}

initBuilder(rawTransaction: TransactionReceipt | string): this {
this.validateRawTransaction(rawTransaction);
const tx = this.fromImplementation(rawTransaction);
this.transaction = tx;
this._signingKeys = [];
const rawData = tx.toJson().raw_data;
this._refBlockBytes = rawData.ref_block_bytes;
this._refBlockHash = rawData.ref_block_hash;
this._expiration = rawData.expiration;
this._timestamp = rawData.timestamp;
this.transaction.setTransactionType(this.transactionType);
const contractCall = rawData.contract[0] as AccountCreateContract;
this.initAccountCreateContractCall(contractCall);
return this;
}

/**
* Initialize the account create contract call specific data.
* Addresses stored in the receipt are hex (set by createAccountCreateTransaction).
*
* @param {AccountCreateContract} accountCreateContractCall object with account create contract data
*/
protected initAccountCreateContractCall(accountCreateContractCall: AccountCreateContract): void {
const { owner_address, account_address } = accountCreateContractCall.parameter.value;
if (owner_address) {
// owner_address stored in receipt is hex; source() expects Base58
this.source({ address: getBase58AddressFromHex(owner_address) });
}
if (account_address) {
// account_address stored in receipt is hex; store directly
this._accountAddress = account_address;
}
}

protected async buildImplementation(): Promise<Transaction> {
this.createAccountCreateTransaction();
if (this._signingKeys.length > 0) {
this.applySignatures();
}

if (!this.transaction.id) {
throw new BuildTransactionError('A valid transaction must have an id');
}
return Promise.resolve(this.transaction);
}

/**
* Helper method to create the account create transaction
*/
private createAccountCreateTransaction(): void {
const rawDataHex = this.getAccountCreateTxRawDataHex();
const rawData = decodeTransaction(rawDataHex);
const contract = rawData.contract[0] as AccountCreateContract;
const contractParameter = contract.parameter;
contractParameter.value.owner_address = this._ownerAddress.toLocaleLowerCase();
contractParameter.value.account_address = this._accountAddress.toLocaleLowerCase();
contractParameter.type_url = ACCOUNT_CREATE_TYPE_URL;
contract.type = 'AccountCreateContract';
const hexBuffer = Buffer.from(rawDataHex, 'hex');
const id = createHash('sha256').update(hexBuffer).digest('hex');
const txReceipt: TransactionReceipt = {
raw_data: rawData,
raw_data_hex: rawDataHex,
txID: id,
signature: this.transaction.signature,
};
this.transaction = new Transaction(this._coinConfig, txReceipt);
}

/**
* Helper method to get the account create transaction raw data hex
*
* @returns {string} the account create transaction raw data hex
*/
private getAccountCreateTxRawDataHex(): string {
const rawContract = {
ownerAddress: getByteArrayFromHexAddress(this._ownerAddress),
accountAddress: getByteArrayFromHexAddress(this._accountAddress),
};
const accountCreateContract = protocol.AccountCreateContract.fromObject(rawContract);
const accountCreateContractBytes = protocol.AccountCreateContract.encode(accountCreateContract).finish();
const txContract = {
type: ContractType.AccountCreateContract,
parameter: {
value: accountCreateContractBytes,
type_url: ACCOUNT_CREATE_TYPE_URL,
},
};
const raw = {
refBlockBytes: Buffer.from(this._refBlockBytes, 'hex'),
refBlockHash: Buffer.from(this._refBlockHash, 'hex'),
expiration: this._expiration || Date.now() + TRANSACTION_DEFAULT_EXPIRATION,
timestamp: this._timestamp || Date.now(),
contract: [txContract],
};
const rawTx = protocol.Transaction.raw.create(raw);
return Buffer.from(protocol.Transaction.raw.encode(rawTx).finish()).toString('hex');
}

/** @inheritdoc */
protected signImplementation(key: BaseKey): Transaction {
if (this._signingKeys.some((signingKey) => signingKey.key === key.key)) {
throw new SigningError('Duplicated key');
}
this._signingKeys.push(key);

// We keep this return for compatibility but is not meant to be use
return this.transaction;
}

private applySignatures(): void {
if (!this.transaction.inputs) {
throw new SigningError('Transaction has no inputs');
}

this._signingKeys.forEach((key) => this.applySignature(key));
}

/**
* Validates the transaction
*
* @param {Transaction} transaction - The transaction to validate
* @throws {BuildTransactionError} when the transaction is invalid
*/
validateTransaction(transaction: Transaction): void {
this.validateAccountCreateTransactionFields();
}

/**
* Validates if the transaction is a valid account create transaction
*
* @throws {BuildTransactionError} when the transaction is invalid
*/
private validateAccountCreateTransactionFields(): void {
if (!this._ownerAddress) {
throw new BuildTransactionError('Missing parameter: source');
}

if (!this._accountAddress) {
throw new BuildTransactionError('Missing parameter: account address');
}

if (!this._refBlockBytes || !this._refBlockHash) {
throw new BuildTransactionError('Missing block reference information');
}
}
}
1 change: 1 addition & 0 deletions modules/sdk-coin-trx/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const DELEGATION_TYPE_URL = 'type.googleapis.com/protocol.DelegateResourceContract';
export const UNDELEGATION_TYPE_URL = 'type.googleapis.com/protocol.UnDelegateResourceContract';
export const ACCOUNT_CREATE_TYPE_URL = 'type.googleapis.com/protocol.AccountCreateContract';
2 changes: 1 addition & 1 deletion modules/sdk-coin-trx/src/lib/contractCallBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class ContractCallBuilder extends TransactionBuilder {
}

if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
}

if (this._expiration) {
Expand Down
4 changes: 4 additions & 0 deletions modules/sdk-coin-trx/src/lib/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export enum ContractType {
* This is the contract for un-delegating resource
*/
UnDelegateResourceContract,
/**
* This is the contract for creating/activating a new account
*/
AccountCreate,
}

export enum PermissionType {
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder {
}

if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
}

if (this._expiration) {
Expand Down
35 changes: 34 additions & 1 deletion modules/sdk-coin-trx/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export interface RawData {
| UnfreezeBalanceV2Contract[]
| WithdrawExpireUnfreezeContract[]
| WithdrawBalanceContract[]
| ResourceManagementContract[];
| ResourceManagementContract[]
| AccountCreateContract[];
}

export interface Value {
Expand Down Expand Up @@ -363,6 +364,38 @@ export interface ResourceManagementContractParameter {
};
}

/**
* AccountCreate contract value fields
*/
export interface AccountCreateValueFields {
owner_address: string;
account_address: string;
}

/**
* AccountCreate contract value interface
*/
export interface AccountCreateValue {
type_url?: string;
value: AccountCreateValueFields;
}

/**
* AccountCreate contract interface
*/
export interface AccountCreateContract {
parameter: AccountCreateValue;
type?: string;
}

/**
* AccountCreate contract decoded interface
*/
export interface AccountCreateContractDecoded {
ownerAddress?: string;
accountAddress?: string;
}

/**
* Delegate/Undelegate resource contract decoded interface
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export abstract class ResourceManagementTxBuilder extends TransactionBuilder {
}

if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
}

if (this._expiration) {
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-coin-trx/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
WithdrawExpireUnfreezeContract,
ResourceManagementContract,
WithdrawBalanceContract,
AccountCreateContract,
} from './iface';

/**
Expand Down Expand Up @@ -226,6 +227,13 @@ export class Transaction extends BaseTransaction {
value: undelegateValue.balance.toString(),
};
break;
case ContractType.AccountCreate: {
this._type = TransactionType.AccountCreate;
const createValue = (rawData.contract[0] as AccountCreateContract).parameter.value;
output = { address: createValue.account_address, value: '0' };
input = { address: createValue.owner_address, value: '0' };
break;
}
default:
throw new ParseTransactionError('Unsupported contract type');
}
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class UnfreezeBalanceTxBuilder extends TransactionBuilder {
}

if (extensionMs > TRANSACTION_MAX_EXPIRATION) {
throw new ExtendTransactionError('The expiration cannot be extended more than one year');
throw new ExtendTransactionError('The expiration cannot be extended more than one day');
}

if (this._expiration) {
Expand Down
Loading
Loading