import {utils} from '@womasoft/js-utils'; import * as TronWeb from 'tronweb'; import {mnemonicTools} from '../../mnemonic' import {defaultParams, erc20} from "./config"; import {error, IReturnResult, Return, txListStat, txListTronStat, txListType} from '../../config'; import { IBalance, IChainsParams, IChainsVerify, IContractDetails, IFromMnemonicParams, IGetBalanceParams, ISignParams, IToWallet, ITxDetails, ITxList, ITxListParams, ITxListToList, IVerifyBalanceParams } from "../chains"; import {IApi, ITron, ITronFunctionName, ITronSignTransactionParams} from "./tron"; import {ethers} from "ethers"; class Tron implements ITron { // 接收访问参数 private readonly _params: IChainsParams constructor(params: IChainsParams) { this._params = params ?? defaultParams; if (this._params.api && utils.tools.isStrLast(this._params.api, '/')) this._params.api = utils.tools.toStrFirst(this._params.api, this._params.api.length - 1); if (!this._params.keys && this._params.key) this._params.keys = [this._params.key]; } readonly functionName: ITronFunctionName = { // 交易 transfer: 'transfer(address,uint256)', // 授权 approve: 'approve(address,uint256)', } readonly verify: IChainsVerify = { address: (address: string): boolean => { const tronWeb = this.getWallet(''); return tronWeb.isAddress(address); }, privateKey(privateKey: string): boolean { privateKey = privateKey || ""; return privateKey.length === 64; }, publicKey(publicKey: string): boolean { publicKey = publicKey || ""; return true; }, balance: async (address: string, amount: string, params?: IVerifyBalanceParams): Promise> => { params = params || {}; const ReturnClass = new Return(); if (!params.fee) params.fee = '0'; if (!utils.tools.isAmount(amount)) return error.amount; const getBalance = await this.getAllBalance(address, [params.contract]); // 主币金额 let amountMain = getBalance.code > 0 ? 0 : Number(getBalance.rs[this._params.symbol]); // 对比金额 const amountValue = Number(amount); // 手续费 const feeValue = Number(params.fee); if (params.contract) { const amountToken = getBalance.code > 0 ? 0 : Number(getBalance.rs[params.contract]); if (amountToken < amountValue) return error.amountNotEnough; if (amountMain < feeValue) return error.feeNotEnough; } else { if (amountMain < amountValue) return error.amountNotEnough; if (feeValue > 0) amountMain = utils.math.minus(amountMain, feeValue); if (amountMain < amountValue) return error.feeNotEnough; } return ReturnClass.end(); }, } async fromMnemonicToWallet(mnemonic: string, params?: IFromMnemonicParams | undefined): Promise> { params = params || {}; if (!params.path) params.path = this.getPath(); const data = await mnemonicTools.toSeed(mnemonic, params.path); if (data.code > 0) return data; return this.fromPrivateKeyToWallet(data.privateKey) } fromPrivateKeyToWallet(privateKey: string): IReturnResult { const ReturnClass = new Return(); privateKey = privateKey.indexOf('0x') === 0 ? privateKey.substring(2, privateKey.length) : privateKey; if (!this.verify.privateKey(privateKey)) return error.privateKey const address = this.getAddress(privateKey); const data: IToWallet = { address, publicKey: '', privateKey, } ReturnClass.setPrivateKey(privateKey) ReturnClass.setPublicKey('') ReturnClass.setAddress(address) ReturnClass.setResult(data); return ReturnClass.end() } async fromRandomToWallet(): Promise> { const data = await this.getWallet().createAccount(); return this.fromPrivateKeyToWallet(data.privateKey); } getPath(index?: number): string { let path = "m/44'/195'/0'/0/{index}"; if (!utils.tools.isInteger(index)) index = 0; return utils.tools.replaceByJson(path, {index}); } getWallet(privateKey?: string): any { let key = ''; if (this._params.keys && this._params.keys.length > 0) key = utils.tools.toShuffle(this._params.keys)[0]; return new TronWeb({ fullHost: this._params.rpcUrl, headers: {"TRON-PRO-API-KEY": key}, privateKey: privateKey ?? '', }) } getAddress(value: string): string { return this.getWallet().address.fromPrivateKey(value); } getAddressFromHex(address: string): string { return this.getWallet().address.fromHex(address); } getAddressToHex(address: string): string { return this.getWallet().address.toHex(address); } getAddrToEvm(address: string) { let addr = this.getWallet().address.toHex(address); return ethers.utils.getAddress(`0x${addr.substring(2, addr.length)}`) } getEvmToAddr(address: string) { return this.getWallet().address.fromHex(address); } async getEncodeParams(params: any) { const AbiCoder = ethers.utils.AbiCoder; const ADDRESS_PREFIX_REGEX = /^(41)/; let typesValues = params let parameters = '' if (typesValues.length == 0) return parameters const abiCoder = new AbiCoder(); let types = []; const values = []; for (let i = 0; i < typesValues.length; i++) { let {type, value} = typesValues[i]; if (type == 'address') value = value.replace(ADDRESS_PREFIX_REGEX, '0x'); else if (type == 'address[]') value = value.map((v: any) => this.getAddressToHex(v).replace(ADDRESS_PREFIX_REGEX, '0x')); types.push(type); values.push(value); } try { parameters = abiCoder.encode(types, values).replace(/^(0x)/, ''); } catch (ex) { console.error('encodeParams', ex); } return parameters } getContract(address: string, abi: string, privateKey?: string): any { abi = abi ?? erc20; const tronWeb = this.getWallet(privateKey ?? ''); if (privateKey) { const wallet = this.fromPrivateKeyToWallet(privateKey); tronWeb.setAddress(wallet.address); } else { tronWeb.setAddress(address); } return tronWeb.contract(JSON.parse(abi), address); } async getAllBalance(address: string, params?: string[]): Promise> { params = params || []; const ReturnClass = new Return<{ [p: string]: string }>(); if (!this.verify.address(address)) return error.address; // const data = await this.api.getAccounts(address); // const result = data.tokens ?? []; const mainBalance = await this.getBalance(address); const list = {}; if (mainBalance.code === 0) list[this._params.symbol] = mainBalance.rs.usable; if (params.length) { const promiseList = []; params.map((item) => { if (utils.types.undefined(item)) return; promiseList.push(this.getBalance(address, {contract: item})) }); const all = await Promise.all(promiseList); if (all.length > 0) all.map((item) => { if (item.code > 0) return; list[item.contracts.address] = item.rs.usable; }) } // if (result.length > 0) { // const item = result.find(m => m.tokenId === '_'); // list[this._params.symbol] = utils.math.dividedBy(item.balance, Math.pow(10, item.tokenDecimal)).toString(); // } // params.map((item: any) => { // if (utils.types.undefined(item)) return; // const data = result.find(m => m.tokenId === item); // if (data) list[item] = utils.math.dividedBy(data.balance, Math.pow(10, data.tokenDecimal)).toString(); // }) ReturnClass.setResult(list); return ReturnClass.end(); } async getBalance(address: string, params?: IGetBalanceParams): Promise> { params = params || {}; const ReturnClass = new Return(); if (!this.verify.address(address)) return error.address; const tronWeb = this.getWallet(); tronWeb.setAddress(address); let data; if (params.contract) { if (Number(params.contract) > 0) { const accountData = await this.getWallet().trx.getAccount(address); const {assetV2} = accountData; if (assetV2.length) { const balance = assetV2.find(m => m.key === params.contract) data = balance ? balance.value : 0; } if (!params.decimals) { const contractData = await this.api.getTrc10Details(params.contract); params.decimals = contractData.precision ?? 0; } ReturnClass.setContracts({address: params.contract, decimals: params.decimals}); } else { const contractClass = await this.getContract(params.contract, erc20); if (!params.decimals) params.decimals = await contractClass.decimals().call(); data = await contractClass.balanceOf(address).call(); ReturnClass.setContracts({address: params.contract, decimals: params.decimals}); } } else { params.decimals = params.decimals || 6; data = await tronWeb.trx.getBalance(address); } const amount = utils.math.dividedBy(data.toString(), Math.pow(10, params.decimals)).toString(); ReturnClass.setResult({ balance: amount.toString(), usable: amount.toString(), lock: '0', unconfirmed: '0', }) return ReturnClass.end(); } getBlockHeight(): Promise> { return Promise.resolve(undefined); } async getContractDetails(address: string): Promise> { const ReturnClass = new Return(); if (!this.verify.address(address)) return error.contract; const contractClass = this.getContract(address, erc20); const all = await Promise.all([contractClass.name().call(), contractClass.symbol().call(), contractClass.decimals().call()]).catch(err => { const data = err.toString(); if (data.indexOf('CALL_EXCEPTION') >= 0 || data.indexOf('missing revert data in call exception') >= 0) return error.contract; if (data.indexOf('NETWORK_ERROR') >= 0) return error.networksTimeout; }) as IReturnResult if (all.code > 0) return all; ReturnClass.setResult({name: all[0], symbol: all[1], decimals: all[2], address}); return ReturnClass.end(); } getFee(gasLimit: string, gasPrice: string): string { return ""; } getPublicKey(privateKey: string): string { return ""; } async getTxDetailsByHash(hash: string): Promise> { const ReturnClass = new Return(); const data = await this.getWallet().trx.getConfirmedTransaction(hash).catch(err => { console.log(err.toString()) return null }); let params: ITxDetails = { type: txListType.init, stat: txListStat.init, hash: hash, blockHash: '', blockNumber: '0', from: '', to: '', fee: '', nonce: '0', value: '', time: '', timestamp: '', }; if (utils.types.null(data)) { params.stat = txListStat.unconfirmed; } else { if (data.ret[0].contractRet === 'SUCCESS') params.stat = txListStat.success; if (data.ret[0].contractRet === 'REVERT') params.stat = txListStat.fail; } ReturnClass.setResult(params); return ReturnClass.end(); } async getTxList(address: string, params?: ITxListParams | undefined): Promise> { params = params || {}; params.type = params.type || 'all'; params.size = params.size || 50 params.page = params.page || 1; const ReturnClass = new Return(); const tronWeb = this.getWallet(''); const addressLower = address.toLocaleLowerCase(); let resultList: ITxList[] = []; if (params.contract && !utils.tools.isNumber(params.contract)) { const main = await this.api.getTxList(params.type, address, params.size); const data = await this.api.getTxListTrc20(params.type, address, params.size); if (main.data.length) main.data.map(item => { const {confirmed, trigger_info, tokenInfo, cost} = item; if (confirmed === false && trigger_info && tokenInfo) { const fee = tronWeb.fromSun(cost.fee ?? 0); const value = utils.math.dividedBy(trigger_info.parameter._value, Math.pow(10, tokenInfo.tokenDecimal)).toString(); const to: ITxListToList[] = [{ address: trigger_info.parameter._to, amount: value, }]; const paramsJson: ITxList = { type: txListType.init, stat: txListStat.confirming, statTrx: txListTronStat.init, hash: item.hash, from: item.ownerAddress, to, value: value.toString(), time: utils.tools.formatDate(item.timestamp), timestamp: item.timestamp.toString(), fee: fee, contractAddress: '', contractDecimals: tokenInfo.tokenDecimal, contractSymbol: "", contractName: "", }; const fromLower = paramsJson.from.toLocaleLowerCase(); const toLower = paramsJson.to[0].address.toLocaleLowerCase(); if (fromLower === addressLower) paramsJson.type = txListType.out; else if (toLower === addressLower) paramsJson.type = txListType.in; resultList.push(paramsJson); } }) if (data.success && data.data.length > 0) data.data.map((item) => { const type = item.type; const token_info = item.token_info; const contractAddress = token_info.address; const decimals = token_info.decimals; if (params.contract !== contractAddress) return; const value = utils.math.dividedBy(item.value, Math.pow(10, decimals)).toString(); const to: ITxListToList[] = [{ address: tronWeb.address.fromHex(item.to), amount: value, }]; const paramsJson: ITxList = { type: txListType.init, stat: txListStat.success, statTrx: txListTronStat.init, hash: item.transaction_id, from: tronWeb.address.fromHex(item.from), to, value: value.toString(), time: utils.tools.formatDate(item.block_timestamp), timestamp: item.block_timestamp.toString(), fee: '0', contractAddress: contractAddress, contractDecimals: decimals, contractSymbol: "", contractName: "", }; // 授权交易 if (type === 'Approval') { paramsJson.statTrx = txListTronStat.approval; paramsJson.value = '0'; } const fromLower = paramsJson.from.toLocaleLowerCase(); const toLower = paramsJson.to[0].address.toLocaleLowerCase(); if (fromLower === addressLower) paramsJson.type = txListType.out; else if (toLower === addressLower) paramsJson.type = txListType.in; resultList.push(paramsJson); }) } else { const data = await this.api.getTxList(params.type, address, params.size); if (data.data.length > 0) data.data.map((item) => { const {cost} = item; const value = tronWeb.fromSun(item.amount); const fee = tronWeb.fromSun(cost.fee ?? 0); const to: ITxListToList[] = [{ address: item.toAddress ?? '', amount: value, }]; const paramsJson: ITxList = { type: txListType.init, stat: txListStat.init, statTrx: txListTronStat.init, hash: item.hash, from: item.ownerAddress, to, value: value.toString(), time: utils.tools.formatDate(item.timestamp), timestamp: item.timestamp.toString(), fee, contractAddress: '', contractDecimals: 6, contractSymbol: '', contractName: "", }; const fromLower = paramsJson.from.toLocaleLowerCase(); const toLower = paramsJson.to[0].address.toLocaleLowerCase(); if (fromLower === addressLower) paramsJson.type = txListType.out; else if (toLower === addressLower) paramsJson.type = txListType.in; if (item.contractType === 31) paramsJson.statTrx = txListTronStat.contract; else if (item.contractType === 11) paramsJson.statTrx = txListTronStat.freeze; if (item.confirmed === false) paramsJson.stat = txListStat.confirming; else if (item.result === 'SUCCESS') paramsJson.stat = txListStat.success; else if (item.result === 'FAIL') paramsJson.stat = txListStat.fail; resultList.push(paramsJson); }) } resultList = resultList.sort((a, b) => parseInt(b.timestamp) - parseInt(a.timestamp)); if (params.type === 'in') resultList = resultList.filter(m => m.type === txListType.in); if (params.type === 'out') resultList = resultList.filter(m => m.type === txListType.out); ReturnClass.setResult(resultList); return ReturnClass.end(); } getUtils(): any { } async getAllowance(address: string, exchangeAddress: string, contractAddress: string, abi: string): Promise> { const ReturnClass = new Return(); const contractClass = await this.getContract(contractAddress, abi); const transaction = await contractClass.allowance(address, exchangeAddress).call(); ReturnClass.setResult(transaction); return ReturnClass.end(); } async signApprove(privateKey: string, exchangeAddress: string, contractAddress: string, abi: string, value?: string): Promise> { const contractClass = await this.getContract(contractAddress, abi, privateKey); let decimals = await contractClass.decimals().call(); if (typeof decimals !== "number") decimals = decimals.toNumber(); const functionName = 'approve(address,uint256)'; const params: ITronSignTransactionParams[] = [ {type: 'address', value: exchangeAddress}, {type: 'uint256', value: Math.ceil(utils.math.multipliedBy(value ?? '1000000000', Math.pow(10, decimals)))} ]; return await this.signTransaction(privateKey, contractAddress, functionName, 50, params); } async sendTx(value: string): Promise> { const ReturnClass = new Return(); const tronWeb = this.getWallet(''); const res = await tronWeb.trx.sendRawTransaction(value).catch(err => { console.log('err', err.toString()) return undefined }); const {result, txid} = res; if (result) { ReturnClass.setResult(txid); } else { if (res.code === 'BANDWITH_ERROR') return error.feeNotEnough; console.error(res) return error.error; } return ReturnClass.end(); } async sign(from: string, to: string, amount: string, params?: ISignParams): Promise> { params = params || {}; if (params.contract) { if (params.contractType) params.contractType = 'trc-20'; if (Number(params.contract) > 0) return this._signTrc10(params.privateKey, params.contract, from, to, amount); return this._signTrc20(params.privateKey, params.contract, from, to, amount); } else { return this._signTrx(params.privateKey, from, to, amount); } } private async _signTrx(privateKey: string | undefined, from: string, to: string, amount: string) { const ReturnClass = new Return(); if (from === to) return error.addressNotSelf; if (privateKey) { const getAddress = this.getAddress(privateKey); if (from !== getAddress) return error.privateKey; } const tronWeb = this.getWallet(privateKey); const amountInit = Number(tronWeb['toSun'](amount)); const tradTrx = await tronWeb.transactionBuilder.sendTrx(to, amountInit, from).catch(err => { if (err.toString().indexOf('balance is not sufficient') >= 0) return error.amountNotEnough; }); if (tradTrx.code > 0) return tradTrx; // tradTrx.raw_data_hex.length * 1000 / 1000000 * 1.01 let fee = utils.math.multipliedBy(tradTrx.raw_data_hex.length, 0.00101).toString(); ReturnClass.setFee(fee.toString()); // 验证余额 const verifyBalance = await this.verify.balance(from, amount, {fee}); if (verifyBalance.code > 0) return verifyBalance; if (privateKey) { ReturnClass.setHash(tradTrx.txID); const raw = await tronWeb.trx['sign'](tradTrx); ReturnClass.setResult(raw); } else { ReturnClass.setResult(fee); } return ReturnClass.end(); } private async _signTrc10(privateKey: string | undefined, contract: string, from: string, to: string, amount: string) { const ReturnClass = new Return(); if (from === to) return error.addressNotSelf; if (privateKey) { const getAddress = this.getAddress(privateKey); if (from !== getAddress) return error.privateKey; } // 验证余额 const verifyBalance = await this.verify.balance(from, amount, {contract}); if (verifyBalance.code > 0) return verifyBalance; const tronWeb = this.getWallet(privateKey); const amountInit = tronWeb['toSun'](amount); const tradTrx = await tronWeb.transactionBuilder['sendToken'](to, amountInit, contract, from); // tradTrx.raw_data_hex.length * 1000 / 1000000 * 1.01 let fee = utils.math.multipliedBy(tradTrx.raw_data_hex.length, 0.00101).toString(); ReturnClass.setFee(fee); if (privateKey) { ReturnClass.setHash(tradTrx.txID); const raw = await tronWeb.trx['sign'](tradTrx); ReturnClass.setResult(raw); } else { ReturnClass.setResult(fee); } return ReturnClass.end(); } private async _signTrc20(privateKey: string | undefined, contract: string, from: string, to: string, amount: string) { if (from === to) return error.addressNotSelf; if (privateKey) { if (!this.verify.privateKey(privateKey)) return error.privateKey; const getAddress = this.getAddress(privateKey); if (from !== getAddress) return error.privateKey; } const token = this.getContract(contract, erc20); if (token.code > 0) return token; let decimals = await token['decimals']().call(); // 验证余额 const verifyBalance = await this.verify.balance(from, amount, {contract, fee: '7'}); if (verifyBalance.code > 0) return verifyBalance; decimals = typeof decimals === "number" ? decimals : decimals.toNumber(); const amountInit = utils.math.multipliedBy(amount, Math.pow(10, decimals)); //根据方法构造参数 let params = [ {type: 'address', value: to}, {type: 'uint256', value: Math.ceil(amountInit)}, ]; return await this.signTransaction(privateKey, contract, this.functionName.transfer, 50, params); } async signTransaction(privateKey: string | undefined, contract: string, functionName: string, feeLimit: number, params: ITronSignTransactionParams[]): Promise> { const ReturnClass = new Return(); let temporaryPrivateKey; if (!privateKey) { const walletData = await this.fromRandomToWallet(); temporaryPrivateKey = walletData.privateKey; } const tronWeb = this.getWallet(privateKey ?? temporaryPrivateKey); let options = { feeLimit: tronWeb['toSun'](feeLimit ?? 50), shouldPollResponse: false, }; let { transaction } = await tronWeb.transactionBuilder.triggerSmartContract(contract, functionName, options, params); let fee = utils.math.multipliedBy(transaction.raw_data_hex.length, 0.00101).toString(); ReturnClass.setFee(fee); if (privateKey) { const raw = await tronWeb.trx.sign(transaction); ReturnClass.setHash(transaction.txID); ReturnClass.setResult(raw); } else { ReturnClass.setResult(fee); } return ReturnClass.end(); } async transfer(from: string, to: string, amount: string, params?: ISignParams | undefined): Promise> { params = params || {}; if (!this.verify.privateKey(params.privateKey)) return error.privateKey; const data = await this.sign(from, to, amount, params); if (data.code > 0) return data; return this.sendTx(data.rs); } readonly api: IApi = { getAccounts: (address: string): Promise => { const url = `${this._params.api}/api/account?address={address}`; return new utils.Http().get(url, {address}); }, getTrc10Details: (contract: string): Promise => { const url = `${this._params.rpcUrl}/wallet/getassetissuebyid`; return new utils.Http().post(url, {value: contract}); }, getTxList: (type: string, address: string, size: number): Promise => { // let only_to = false, only_from = false; // if (type === 'in') only_to = true; // else if (type === 'out') only_from = true; // const url = `${this._params.rpcUrl}/v1/accounts/{address}/transactions?only_confirmed=true&only_unconfirmed=false&only_to={only_to}&only_from={only_from}&limit={size}`; const url = `${this._params.api}/api/transaction?sort=-timestamp&count=true&limit={size}&start=0&address={address}`; return new utils.Http().get(url, {address, size}); }, getTxListTrc20: (type: string, address: string, size: number): Promise => { let only_to = false, only_from = false; if (type === 'in') only_to = true; else if (type === 'out') only_from = true; const url = `${this._params.rpcUrl}/v1/accounts/{address}/transactions/trc20?only_confirmed=true&only_unconfirmed=false&only_to={only_to}&only_from={only_from}&limit={size}`; const http = new utils.Http(); return http.get(url, {address, size, only_to, only_from}); } }; } export { Tron } export type { ITron }