Skip to content
Draft
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
217 changes: 217 additions & 0 deletions modules/sdk-coin-trx/src/lib/accountCreateTxBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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 year');
}

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

initBuilder(rawTransaction: TransactionReceipt | string): this {
this.transaction = this.fromImplementation(rawTransaction);
this.transaction.setTransactionType(this.transactionType);
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;
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';
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
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
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
49 changes: 48 additions & 1 deletion modules/sdk-coin-trx/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import {
WithdrawContractDecoded,
ResourceManagementContractParameter,
ResourceManagementContractDecoded,
AccountCreateContract,
AccountCreateContractDecoded,
} from './iface';
import { ContractType, PermissionType, TronResource } from './enum';
import { AbiCoder, hexConcat } from 'ethers/lib/utils';
import { DELEGATION_TYPE_URL } from './constants';
import { DELEGATION_TYPE_URL, ACCOUNT_CREATE_TYPE_URL } from './constants';

export const TRANSACTION_MAX_EXPIRATION = 86400000; // one day
export const TRANSACTION_DEFAULT_EXPIRATION = 10800000; // three hours
Expand Down Expand Up @@ -233,6 +235,10 @@ export function decodeTransaction(hexString: string): RawData {
contractType = ContractType.UnDelegateResourceContract;
contract = decodeUnDelegateResourceContract(rawTransaction.contracts[0].parameter.value);
break;
case ACCOUNT_CREATE_TYPE_URL:
contractType = ContractType.AccountCreate;
contract = decodeAccountCreateContract(rawTransaction.contracts[0].parameter.value);
break;
default:
throw new UtilsError('Unsupported contract type');
}
Expand Down Expand Up @@ -753,6 +759,47 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage
];
}

/**
* Deserialize the segment of the txHex corresponding with the account create contract
*
* @param {Uint8Array} value - The raw protobuf bytes from the contract parameter
* @returns {AccountCreateContract[]} - Array containing the decoded account create contract
*/
export function decodeAccountCreateContract(base64: string): AccountCreateContract[] {
let decoded: AccountCreateContractDecoded;
try {
decoded = protocol.AccountCreateContract.decode(Buffer.from(base64, 'base64')).toJSON();
} catch (e) {
throw new UtilsError('There was an error decoding the account create contract in the transaction.');
}

if (!decoded.ownerAddress) {
throw new UtilsError('Owner address does not exist in this account create contract.');
}

if (!decoded.accountAddress) {
throw new UtilsError('Account address does not exist in this account create contract.');
}

const owner_address = getBase58AddressFromByteArray(
getByteArrayFromHexAddress(Buffer.from(decoded.ownerAddress, 'base64').toString('hex'))
);
const account_address = getBase58AddressFromByteArray(
getByteArrayFromHexAddress(Buffer.from(decoded.accountAddress, 'base64').toString('hex'))
);

return [
{
parameter: {
value: {
owner_address,
account_address,
},
},
},
];
}

/**
* @param raw
*/
Expand Down
Loading