PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-trx/src
Просмотр файла: trx.ts
/**
* @prettier
*/
import * as secp256k1 from 'secp256k1';
import { randomBytes } from 'crypto';
import { CoinFamily, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { bip32 } from '@bitgo/secp256k1';
import * as request from 'superagent';
import {
BaseCoin,
BitGoBase,
common,
getBip32Keys,
getIsKrsRecovery,
getIsUnsignedSweep,
KeyPair,
MethodNotImplementedError,
ParsedTransaction,
ParseTransactionOptions,
SignedTransaction,
SignTransactionOptions,
TransactionExplanation,
TransactionFee,
TransactionPrebuild as BaseTransactionPrebuild,
TransactionRecipient as Recipient,
VerifyAddressOptions,
VerifyTransactionOptions,
BaseTransaction,
MultisigType,
multisigTypes,
} from '@bitgo/sdk-core';
import { Interface, Utils, WrappedBuilder } from './lib';
import { getBuilder } from './lib/builder';
import { TransactionReceipt } from './lib/iface';
import { isInteger, isUndefined } from 'lodash';
export const MINIMUM_TRON_MSIG_TRANSACTION_FEE = 1e6;
export const SAFE_TRON_TRANSACTION_FEE = 2.1 * 1e6; // TRON foundation recommends 2.1 TRX as fees for guaranteed transaction
export const SAFE_TRON_TOKEN_TRANSACTION_FEE = 100 * 1e6; // TRON foundation recommends 100 TRX as fees for guaranteed transaction
export const RECOVER_TRANSACTION_EXPIRY = 86400000; // 24 hour
export const DEFAULT_SCAN_FACTOR = 20; // default number of receive addresses to scan for funds
export interface TronSignTransactionOptions extends SignTransactionOptions {
txPrebuild: TransactionPrebuild;
prv: string;
}
export interface TxInfo {
recipients: Recipient[];
from: string;
txid: string;
}
export interface AddressInfo {
address: string;
chain: number;
index: number;
}
export interface TronTransactionExplanation extends TransactionExplanation {
expiration: number;
timestamp: number;
}
export interface TransactionPrebuild extends BaseTransactionPrebuild {
txHex: string;
txInfo: TxInfo;
addressInfo?: AddressInfo;
feeInfo: TransactionFee;
}
export interface ExplainTransactionOptions {
txHex?: string; // txHex is poorly named here; it is just a wrapped JSON object
halfSigned?: {
txHex: string; // txHex is poorly named here; it is just a wrapped JSON object
};
feeInfo: TransactionFee;
}
export interface RecoveryOptions {
userKey: string; // Box A
backupKey: string; // Box B
bitgoKey: string; // Box C - this is bitgo's xpub and will be used to derive their root address
recoveryDestination: string; // base58 address
krsProvider?: string;
tokenContractAddress?: string;
walletPassphrase?: string;
startingScanIndex?: number;
scan?: number;
}
export interface ConsolidationRecoveryOptions {
userKey: string;
backupKey: string;
bitgoKey: string;
tokenContractAddress?: string;
startingScanIndex?: number; // default to 1 (inclusive)
endingScanIndex?: number; // default to startingScanIndex + 20 (exclusive)
}
export interface ConsolidationRecoveryBatch {
transactions: RecoveryTransaction[];
}
export interface FeeInfo {
fee: string;
}
export interface RecoveryTransaction {
txHex?: string;
feeInfo?: FeeInfo;
coin?: string;
tx?: TransactionPrebuild;
recoveryAmount?: number;
tokenTxs?: TransactionReceipt[];
addressInfo?: AddressInfo;
}
export enum NodeTypes {
Full,
Solidity,
}
/**
* This structure is not a complete model of the AccountResponse from a node.
*/
export interface AccountResponse {
data: [Interface.AccountInfo];
}
export class Trx extends BaseCoin {
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
super(bitgo);
if (!staticsCoin) {
throw new Error('missing required constructor parameter staticsCoin');
}
this._staticsCoin = staticsCoin;
}
getChain() {
return this._staticsCoin.name;
}
getFamily(): CoinFamily {
return this._staticsCoin.family;
}
getFullName() {
return this._staticsCoin.fullName;
}
getBaseFactor() {
return Math.pow(10, this._staticsCoin.decimalPlaces);
}
/** @inheritdoc */
transactionDataAllowed() {
return true;
}
/** {@inheritDoc } **/
supportsMultisig(): boolean {
return true;
}
/** inherited doc */
getDefaultMultisigType(): MultisigType {
return multisigTypes.onchain;
}
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
return new Trx(bitgo, staticsCoin);
}
/**
* Flag for sending value of 0
* @returns {boolean} True if okay to send 0 value, false otherwise
*/
valuelessTransferAllowed(): boolean {
return true;
}
/** @inheritDoc */
allowsAccountConsolidations(): boolean {
return true;
}
/**
* Checks if this is a valid base58
* @param address
*/
isValidAddress(address: string): boolean {
if (!address) {
return false;
}
return Utils.isBase58Address(address);
}
/**
* Checks if this is a valid hex address
* @param address hex address
*/
isValidHexAddress(address: string): boolean {
return /^41[0-9a-f]{40}$/i.test(address);
}
/**
* Generate ed25519 key pair
*
* @param seed
* @returns {Object} object with generated pub, prv
*/
generateKeyPair(seed?: Buffer): KeyPair {
// TODO: move this and address creation logic to account-lib
if (!seed) {
// An extended private key has both a normal 256 bit private key and a 256 bit chain code, both of which must be
// random. 512 bits is therefore the maximum entropy and gives us maximum security against cracking.
seed = randomBytes(512 / 8);
}
const hd = bip32.fromSeed(seed);
return {
pub: hd.neutered().toBase58(),
prv: hd.toBase58(),
};
}
isValidXpub(xpub: string): boolean {
try {
return bip32.fromBase58(xpub).isNeutered();
} catch (e) {
return false;
}
}
isValidPub(pub: string): boolean {
if (this.isValidXpub(pub)) {
// xpubs can be converted into regular pubs, so technically it is a valid pub
return true;
}
return new RegExp('^04[a-zA-Z0-9]{128}$').test(pub);
}
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
return {};
}
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
throw new MethodNotImplementedError();
}
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
return true;
}
/**
* Derive a user key using the chain path of the address
* @param key
* @param path
* @returns {string} derived private key
*/
deriveKeyWithPath({ key, path }: { key: string; path: string }): string {
const keychain = bip32.fromBase58(key);
const derivedKeyNode = keychain.derivePath(path);
return derivedKeyNode.toBase58();
}
/**
* Assemble keychain and half-sign prebuilt transaction
*
* @param params
* @param params.txPrebuild {Object} prebuild object returned by platform
* @param params.prv {String} user prv
* @returns Bluebird<SignedTransaction>
*/
async signTransaction(params: TronSignTransactionOptions): Promise<SignedTransaction> {
const txBuilder = getBuilder(this.getChain()).from(params.txPrebuild.txHex);
let key;
const { chain, index } = params.txPrebuild?.addressInfo ?? { chain: 0, index: 0 };
if (chain === 0 && index === 0) {
key = params.prv;
} else {
const derivationPath = `0/0/${chain}/${index}`;
key = this.deriveKeyWithPath({ key: params.prv, path: derivationPath });
}
txBuilder.sign({ key });
const transaction = await txBuilder.build();
const response = {
txHex: JSON.stringify(transaction.toJson()),
};
if (transaction.toJson().signature.length >= 2) {
return response;
}
// Half signed transaction
return {
halfSigned: response,
};
}
/**
* Return boolean indicating whether input is valid seed for the coin
*
* @param prv - the prv to be checked
*/
isValidXprv(prv: string): boolean {
try {
return !bip32.fromBase58(prv).isNeutered();
} catch {
return false;
}
}
/**
* Convert a message to string in hexadecimal format.
*
* @param message {Buffer|String} message to sign
* @return the message as a hexadecimal string
*/
toHexString(message: string | Buffer): string {
if (typeof message === 'string') {
return Buffer.from(message).toString('hex');
} else if (Buffer.isBuffer(message)) {
return message.toString('hex');
} else {
throw new Error('Invalid messaged passed to signMessage');
}
}
/**
* Sign message with private key
*
* @param key
* @param message
*/
async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
const toSign = this.toHexString(message);
let prv: string | undefined = key.prv;
if (this.isValidXprv(prv)) {
prv = bip32.fromBase58(prv).privateKey?.toString('hex');
}
if (!prv) {
throw new Error('no privateKey');
}
let sig = Utils.signString(toSign, prv, true);
// remove the preceding 0x
sig = sig.replace(/^0x/, '');
return Buffer.from(sig, 'hex');
}
/**
* Converts an xpub to a uncompressed pub
* @param xpub
*/
xpubToUncompressedPub(xpub: string): string {
if (!this.isValidXpub(xpub)) {
throw new Error('invalid xpub');
}
const publicKey = bip32.fromBase58(xpub).publicKey;
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false /* compressed */)).toString('hex');
}
/**
* Modify prebuild before sending it to the server.
* @param buildParams The whitelisted parameters for this prebuild
*/
async getExtraPrebuildParams(buildParams: any): Promise<any> {
if (buildParams.recipients[0].data && buildParams.feeLimit) {
buildParams.recipients[0].feeLimit = buildParams.feeLimit;
}
}
pubToHexAddress(pub: string): string {
const byteArrayAddr = Utils.getByteArrayFromHexAddress(pub);
const rawAddress = Utils.getRawAddressFromPubKey(byteArrayAddr);
return Utils.getHexAddressFromByteArray(rawAddress);
}
xprvToCompressedPrv(xprv: string): string {
if (!this.isValidXprv(xprv)) {
throw new Error('invalid xprv');
}
const hdNode = bip32.fromBase58(xprv);
if (!hdNode.privateKey) {
throw new Error('no privateKey');
}
return hdNode.privateKey.toString('hex');
}
private getNodeUrl(node: NodeTypes): string {
switch (node) {
case NodeTypes.Full:
return common.Environments[this.bitgo.getEnv()].tronNodes.full;
case NodeTypes.Solidity:
return common.Environments[this.bitgo.getEnv()].tronNodes.solidity;
default:
throw new Error('node type not found');
}
}
/**
* Make a query to Trongrid for information such as balance, token balance, solidity calls
* @param query {Object} key-value pairs of parameters to append after /api
* @returns {Object} response from Trongrid
*/
private async recoveryPost(query: { path: string; jsonObj: any; node: NodeTypes }): Promise<any> {
const nodeUri = this.getNodeUrl(query.node);
const response = await request
.post(nodeUri + query.path)
.type('json')
.send(query.jsonObj);
if (!response.ok) {
throw new Error('could not reach Tron node');
}
// unfortunately, it doesn't look like most TRON nodes return valid json as body
return JSON.parse(response.text);
}
/**
* Make a query to Trongrid for information such as balance, token balance, solidity calls
* @param query {Object} key-value pairs of parameters to append after /api
* @returns {Object} response from Trongrid
*/
private async recoveryGet(query: { path: string; jsonObj: any; node: NodeTypes }): Promise<any> {
const nodeUri = this.getNodeUrl(query.node);
const response = await request
.get(nodeUri + query.path)
.type('json')
.send(query.jsonObj);
if (!response.ok) {
throw new Error('could not reach Tron node');
}
// unfortunately, it doesn't look like most TRON nodes return valid json as body
return JSON.parse(response.text);
}
/**
* Query our explorer for the balance of an address
* @param address {String} the address encoded in hex
* @returns {BigNumber} address balance
*/
private async getAccountBalancesFromNode(address: string): Promise<AccountResponse> {
return await this.recoveryGet({
path: '/v1/accounts/' + address,
jsonObj: {},
node: NodeTypes.Full,
});
}
/**
* Retrieves our build transaction from a node.
* @param toAddr hex-encoded address
* @param fromAddr hex-encoded address
* @param amount
*/
private async getBuildTransaction(
toAddr: string,
fromAddr: string,
amount: number
): Promise<Interface.TransactionReceipt> {
// our addresses should be base58, we'll have to encode to hex
return await this.recoveryPost({
path: '/wallet/createtransaction',
jsonObj: {
to_address: toAddr,
owner_address: fromAddr,
amount,
},
node: NodeTypes.Full,
});
}
/**
* Retrieves our build transaction from a node.
* @param toAddr hex-encoded address
* @param fromAddr hex-encoded address
* @param amount
*/
private async getTriggerSmartContractTransaction(
toAddr: string,
fromAddr: string,
amount: string,
contractAddr: string
): Promise<{ transaction: Interface.TransactionReceipt }> {
const functionSelector = 'transfer(address,uint256)';
const types = ['address', 'uint256'];
const values = [toAddr, amount];
const parameter = Utils.encodeDataParams(types, values, '');
return await this.recoveryPost({
path: '/wallet/triggersmartcontract',
jsonObj: {
owner_address: fromAddr,
contract_address: contractAddr,
function_selector: functionSelector,
parameter: parameter,
fee_limit: 100000000,
},
node: NodeTypes.Full,
});
}
/**
* Throws an error if any keys in the ownerKeys collection don't match the keys array we pass
* @param ownerKeys
* @param keys
*/
checkPermissions(ownerKeys: { address: string; weight: number }[], keys: string[]) {
keys = keys.map((k) => k.toUpperCase());
ownerKeys.map((key) => {
const hexKey = key.address.toUpperCase();
if (!keys.includes(hexKey)) {
throw new Error(`pub address ${hexKey} not found in account`);
}
if (key.weight !== 1) {
throw new Error('owner permission is invalid for this structure');
}
});
}
/**
* Format for offline vault signing
* @param {BaseTransaction} tx
* @param {number} fee
* @param {number} recoveryAmount
* @returns {RecoveryTransaction}
*/
formatForOfflineVault(
tx: BaseTransaction,
fee: number,
recoveryAmount: number,
addressInfo?: AddressInfo
): RecoveryTransaction {
const txJSON = tx.toJson();
const format = {
txHex: JSON.stringify(txJSON),
recoveryAmount,
feeInfo: {
fee: `${fee}`,
},
tx: txJSON, // Leaving it as txJSON for backwards compatibility
coin: this.getChain(),
};
return addressInfo ? { ...format, addressInfo } : format;
}
/**
* Builds a funds recovery transaction without BitGo.
* We need to do three queries during this:
* 1) Node query - how much money is in the account
* 2) Build transaction - build our transaction for the amount
* 3) Send signed build - send our signed build to a public node
*
* Note 1: for base address recoveries, fund will be recovered to recovery destination if base address balance is
* more than 2.1 TRX for native TRX recovery and 100 TRX for token recover. For receive addresses, fund will be
* recovered to base address first then swept to base address(decided as the universal pattern in team meeting).
*
* Note 2: the function supports token sweep from base address.
* TODO: support token sweep from receive address.
*
* @param params
*/
async recover(params: RecoveryOptions): Promise<RecoveryTransaction> {
const isKrsRecovery = getIsKrsRecovery(params);
const isUnsignedSweep = getIsUnsignedSweep(params);
if (!this.isValidAddress(params.recoveryDestination)) {
throw new Error('Invalid destination address!');
}
let startIdx = params.startingScanIndex;
if (isUndefined(startIdx)) {
startIdx = 1;
} else if (!isInteger(startIdx) || startIdx < 0) {
throw new Error('Invalid starting index to scan for addresses');
}
let numIteration = params.scan;
if (isUndefined(numIteration)) {
numIteration = 20;
} else if (!isInteger(numIteration) || numIteration <= 0) {
throw new Error('Invalid scanning factor');
}
// get our user, backup keys
const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false });
// we need to decode our bitgoKey to a base58 address
const bitgoHexAddr = this.pubToHexAddress(this.xpubToUncompressedPub(params.bitgoKey));
let recoveryFromAddrHex = bitgoHexAddr;
let recoveryToAddressHex = Utils.getHexAddressFromBase58Address(params.recoveryDestination);
// call the node to get our account balance for base address
let account = await this.getAccountBalancesFromNode(Utils.getBase58AddressFromHex(recoveryFromAddrHex));
let recoveryAmount = account.data[0].balance;
let userXPrv = keys[0].toBase58();
let isReceiveAddress = false;
let addressInfo: AddressInfo | undefined;
const tokenContractAddr = params.tokenContractAddress;
// check for possible token recovery, recover the token provide by user
if (tokenContractAddr) {
let rawTokenTxn: any | undefined;
for (const token of account.data[0].trc20) {
if (token[tokenContractAddr]) {
const amount = token[tokenContractAddr];
const tokenContractAddrHex = Utils.getHexAddressFromBase58Address(tokenContractAddr);
rawTokenTxn = (
await this.getTriggerSmartContractTransaction(
recoveryToAddressHex,
recoveryFromAddrHex,
amount,
tokenContractAddrHex
)
).transaction;
recoveryAmount = parseInt(amount, 10);
break;
}
}
// build and sign token txns
if (rawTokenTxn) {
// Check there is sufficient of the native asset to cover fees
const trxBalance = account.data[0].balance;
if (trxBalance < SAFE_TRON_TOKEN_TRANSACTION_FEE) {
throw new Error(
`Amount of funds to recover ${trxBalance} is less than ${SAFE_TRON_TOKEN_TRANSACTION_FEE} and wouldn't be able to fund a trc20 send`
);
}
const txBuilder = getBuilder(this.getChain()).from(rawTokenTxn);
// Default expiry is 1 minute which is too short for recovery purposes
// extend the expiry to 1 day
txBuilder.extendValidTo(RECOVER_TRANSACTION_EXPIRY);
// this tx should be enough to drop into a node
if (isUnsignedSweep) {
return this.formatForOfflineVault(await txBuilder.build(), SAFE_TRON_TOKEN_TRANSACTION_FEE, recoveryAmount);
}
const userPrv = this.xprvToCompressedPrv(userXPrv);
txBuilder.sign({ key: userPrv });
// krs recoveries don't get signed
if (!isKrsRecovery && !isReceiveAddress) {
const backupXPrv = keys[1].toBase58();
const backupPrv = this.xprvToCompressedPrv(backupXPrv);
txBuilder.sign({ key: backupPrv });
}
return this.formatForOfflineVault(await txBuilder.build(), SAFE_TRON_TOKEN_TRANSACTION_FEE, recoveryAmount);
} else {
throw Error('Not found token to recover, please check token balance');
}
}
// let us recover the native Tron
if (recoveryAmount > SAFE_TRON_TRANSACTION_FEE) {
const userXPub = keys[0].neutered().toBase58();
const backupXPub = keys[1].neutered().toBase58();
// check multisig permissions
const keyHexAddresses = [
this.pubToHexAddress(this.xpubToUncompressedPub(userXPub)),
this.pubToHexAddress(this.xpubToUncompressedPub(backupXPub)),
bitgoHexAddr,
];
// run checks to ensure this is a valid tx - permissions match our signer keys
const ownerKeys: { address: string; weight: number }[] = [];
for (const key of account.data[0].owner_permission.keys) {
const address = Utils.getHexAddressFromBase58Address(key.address);
const weight = key.weight;
ownerKeys.push({ address, weight });
}
const activePermissionKeys: { address: string; weight: number }[] = [];
for (const key of account.data[0].active_permission[0].keys) {
const address = Utils.getHexAddressFromBase58Address(key.address);
const weight = key.weight;
activePermissionKeys.push({ address, weight });
}
this.checkPermissions(ownerKeys, keyHexAddresses);
this.checkPermissions(activePermissionKeys, keyHexAddresses);
} else {
// Check receive addresses for funds
// Check for first derived wallet with funds
// Receive addresses are derived from the user key
for (let i = startIdx; i < numIteration + startIdx; i++) {
const derivationPath = `0/0/0/${i}`;
const userKey = keys[0].derivePath(derivationPath);
const xpub = userKey.neutered();
const receiveAddress = this.pubToHexAddress(this.xpubToUncompressedPub(xpub.toBase58()));
const address = Utils.getBase58AddressFromHex(receiveAddress);
// call the node to get our account balance
const accountInfo = await this.getAccountBalancesFromNode(address);
if (accountInfo.data[0] && accountInfo.data[0].balance > SAFE_TRON_TRANSACTION_FEE) {
account = accountInfo;
recoveryAmount = accountInfo.data[0].balance;
userXPrv = userKey.toBase58(); // assign derived userXPrx
isReceiveAddress = true;
recoveryFromAddrHex = receiveAddress;
recoveryToAddressHex = bitgoHexAddr;
addressInfo = {
address,
chain: 0,
index: i,
};
break;
}
}
}
// a sweep potentially needs to pay for multi-sig transfer, destination account activation and bandwidth
// TRON foundation recommends 2.1 TRX for guaranteed confirmation
if (!recoveryAmount || SAFE_TRON_TRANSACTION_FEE >= recoveryAmount) {
throw new Error(
`Amount of funds to recover ${recoveryAmount} is less than ${SAFE_TRON_TRANSACTION_FEE} and wouldn't be able to fund a send`
);
}
const recoveryAmountMinusFees = recoveryAmount - SAFE_TRON_TRANSACTION_FEE;
const buildTx = await this.getBuildTransaction(recoveryToAddressHex, recoveryFromAddrHex, recoveryAmountMinusFees);
// construct our tx
const txBuilder = (getBuilder(this.getChain()) as WrappedBuilder).from(buildTx);
// Default expiry is 1 minute which is too short for recovery purposes
// extend the expiry to 1 day
txBuilder.extendValidTo(RECOVER_TRANSACTION_EXPIRY);
const tx = await txBuilder.build();
// this tx should be enough to drop into a node
if (isUnsignedSweep) {
return this.formatForOfflineVault(tx, SAFE_TRON_TRANSACTION_FEE, recoveryAmountMinusFees, addressInfo);
}
const userPrv = this.xprvToCompressedPrv(userXPrv);
txBuilder.sign({ key: userPrv });
// krs recoveries don't get signed
if (!isKrsRecovery && !isReceiveAddress) {
const backupXPrv = keys[1].toBase58();
const backupPrv = this.xprvToCompressedPrv(backupXPrv);
txBuilder.sign({ key: backupPrv });
}
const txSigned = await txBuilder.build();
return this.formatForOfflineVault(txSigned, SAFE_TRON_TRANSACTION_FEE, recoveryAmountMinusFees, addressInfo);
}
/**
* Builds native TRX recoveries of receive addresses in batch without BitGo.
* Funds will be recovered to base address first. You need to initiate another sweep txn after that.
* Note: there will be another recoverTokenConsolidations function to support token recover from receive addresses.
*
* @param {ConsolidationRecoveryOptions} params - options for consolidation recovery.
* @param {string} [params.startingScanIndex] - receive address index to start scanning from. default to 1 (inclusive).
* @param {string} [params.endingScanIndex] - receive address index to end scanning at. default to startingScanIndex + 20 (exclusive).
*/
async recoverConsolidations(params: ConsolidationRecoveryOptions): Promise<ConsolidationRecoveryBatch> {
const isUnsignedConsolidations = getIsUnsignedSweep(params);
const startIdx = params.startingScanIndex || 1;
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;
if (startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR) {
throw new Error(
`Invalid starting or ending index to scan for addresses. startingScanIndex: ${startIdx}, endingScanIndex: ${endIdx}.`
);
}
const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false });
const baseAddrHex = this.pubToHexAddress(this.xpubToUncompressedPub(params.bitgoKey));
const txnsBatch: RecoveryTransaction[] = [];
for (let i = startIdx; i < endIdx; i++) {
const derivationPath = `0/0/0/${i}`;
const userKey = keys[0].derivePath(derivationPath);
const userKeyXPub = userKey.neutered();
const receiveAddressHex = this.pubToHexAddress(this.xpubToUncompressedPub(userKeyXPub.toBase58()));
const receiveAddress = Utils.getBase58AddressFromHex(receiveAddressHex);
// call the node to get our account balance
const accountInfo = await this.getAccountBalancesFromNode(receiveAddress);
if (accountInfo.data[0] && accountInfo.data[0].balance > SAFE_TRON_TRANSACTION_FEE) {
let recoveryAmount = 0;
// Tokens must be consolidate before the native asset. First construct token txns
let rawTokenTxn: any | undefined;
// check for possible token recovery, recover the token provide by user
if (params.tokenContractAddress) {
if (accountInfo.data[0].balance > SAFE_TRON_TOKEN_TRANSACTION_FEE && accountInfo.data[0].trc20[0]) {
const tokenDataArray = accountInfo.data[0].trc20;
for (const tokenData of tokenDataArray) {
const contractAddress = Object.keys(tokenData) as Array<string>;
if (params.tokenContractAddress === contractAddress[0]) {
const amount = tokenData[contractAddress[0]];
const tokenContractAddrHex = Utils.getHexAddressFromBase58Address(contractAddress[0]);
rawTokenTxn = (
await this.getTriggerSmartContractTransaction(
baseAddrHex,
receiveAddressHex,
amount,
tokenContractAddrHex
)
).transaction;
recoveryAmount = parseInt(amount, 10);
break;
}
}
}
// build and sign token txns
if (rawTokenTxn) {
const addressInfo = {
address: receiveAddress,
chain: 0,
index: i,
};
const txBuilder = getBuilder(this.getChain()).from(rawTokenTxn);
// Default expiry is 1 minute which is too short for recovery purposes
// extend the expiry to 1 day
txBuilder.extendValidTo(RECOVER_TRANSACTION_EXPIRY);
// this tx should be enough to drop into a node
if (!isUnsignedConsolidations) {
const userPrv = this.xprvToCompressedPrv(userKey.toBase58());
// receive address only needs to be signed by user key
txBuilder.sign({ key: userPrv });
}
const tx = await txBuilder.build();
txnsBatch.push(
this.formatForOfflineVault(tx, SAFE_TRON_TOKEN_TRANSACTION_FEE, recoveryAmount, addressInfo)
);
}
} else {
const addressBalance = accountInfo.data[0].balance;
const addressInfo = {
address: receiveAddress,
chain: 0,
index: i,
};
const recoveryAmount = addressBalance - SAFE_TRON_TRANSACTION_FEE;
const buildTx = await this.getBuildTransaction(baseAddrHex, receiveAddressHex, recoveryAmount);
// construct our tx
const txBuilder = (getBuilder(this.getChain()) as WrappedBuilder).from(buildTx);
// Default expiry is 1 minute which is too short for recovery purposes
// extend the expiry to 1 day
txBuilder.extendValidTo(RECOVER_TRANSACTION_EXPIRY);
if (!isUnsignedConsolidations) {
const userPrv = this.xprvToCompressedPrv(userKey.toBase58());
// receive address only needs to be signed by user key
txBuilder.sign({ key: userPrv });
}
const tx = await txBuilder.build();
txnsBatch.push(this.formatForOfflineVault(tx, SAFE_TRON_TRANSACTION_FEE, recoveryAmount, addressInfo));
}
}
}
return {
transactions: txnsBatch,
};
}
/**
* Explain a Tron transaction from txHex
* @param params
*/
async explainTransaction(params: ExplainTransactionOptions): Promise<TronTransactionExplanation> {
const txHex = params.txHex || (params.halfSigned && params.halfSigned.txHex);
if (!txHex || !params.feeInfo) {
throw new Error('missing explain tx parameters');
}
const txBuilder = getBuilder(this.getChain()).from(txHex);
const tx = await txBuilder.build();
const outputs = [
{
amount: tx.outputs[0].value.toString(),
address: tx.outputs[0].address, // Should turn it into a readable format, aka base58
},
];
const displayOrder = [
'id',
'outputAmount',
'changeAmount',
'outputs',
'changeOutputs',
'fee',
'timestamp',
'expiration',
];
return {
displayOrder,
id: tx.id,
outputs,
outputAmount: outputs[0].amount,
changeOutputs: [], // account based does not use change outputs
changeAmount: '0', // account base does not make change
fee: params.feeInfo,
timestamp: tx.validFrom,
expiration: tx.validTo,
};
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!