From e522aaa69356b09aa92b5fc00afc8e7d95c976b8 Mon Sep 17 00:00:00 2001 From: zhl Date: Wed, 24 May 2023 16:27:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AA=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=BD=AC=E8=B4=A6=E4=BB=B7=E6=A0=BC=E7=9A=84=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.server.ts | 2 + src/chain/BlockChain.ts | 29 ++- src/chain/ERC20Reactor.ts | 18 +- src/chain/allchain.ts | 291 +++++++++++++++++++++++++++ src/controllers/token.controllers.ts | 6 +- src/schedule/blocknum.schedule.ts | 11 +- src/service/chain.service.ts | 6 + src/service/event.sync.service.ts | 7 +- src/service/price.service.ts | 51 +++++ src/structs/ChainData.ts | 8 + src/structs/PriceData.ts | 7 + 11 files changed, 414 insertions(+), 22 deletions(-) create mode 100644 src/chain/allchain.ts create mode 100644 src/service/price.service.ts create mode 100644 src/structs/ChainData.ts create mode 100644 src/structs/PriceData.ts diff --git a/src/api.server.ts b/src/api.server.ts index c2afbc5..6eeaf62 100644 --- a/src/api.server.ts +++ b/src/api.server.ts @@ -10,6 +10,7 @@ import logger from 'logger/logger' import BlocknumSchedule from 'schedule/blocknum.schedule' import { RedisClient } from 'redis/RedisClient' import { restartAllUnFinishedTask } from 'service/chain.service' +import { PriceSvr } from 'service/price.service' const zReqParserPlugin = require('plugins/zReqParser') @@ -105,6 +106,7 @@ export class ApiServer { } private initSchedules() { new BlocknumSchedule().scheduleAll() + new PriceSvr().scheduleAll() } private restoreChainQueue() {} private setErrHandler() { diff --git a/src/chain/BlockChain.ts b/src/chain/BlockChain.ts index 2c0db70..ec14a17 100644 --- a/src/chain/BlockChain.ts +++ b/src/chain/BlockChain.ts @@ -8,13 +8,19 @@ import { ERC721Reactor } from './ERC721Reactor' import { HttpRetryProvider } from './HttpRetryProvider' import { WalletReactor } from './WalletReactor' import { DistributorReactor } from './DistributorReactor' -import { fromTokenMinimalUnit } from 'utils/number.util' +import { fromTokenMinimalUnit, safeNumberToBN, toBN } from 'utils/number.util' +import { AllChains } from './allchain' +import assert from 'assert' +import { IPriceData } from 'structs/PriceData' +import { IChainData } from 'structs/ChainData' +import { queryEthPrice } from 'service/chain.service' @singleton export class BlockChain { private web3: Web3 instanceCacheMap: Map private accountMaster: AddedAccount + private currentChain: IChainData public erc20Reactor: ERC20Reactor public erc721Reactor: ERC721Reactor public walletReactor: WalletReactor @@ -23,8 +29,12 @@ export class BlockChain { public currentBlockNum: number = 0 constructor() { - const provider = new HttpRetryProvider(process.env.CHAIN_RPC_URL.split('|')) + const defaultChain = parseInt(process.env.CHAIN_DEFAULT) + this.currentChain = AllChains.find(o => o.id === defaultChain) + assert(this.currentChain, `chain data with ${defaultChain} not found`) + const provider = new HttpRetryProvider(this.currentChain.rpc.split('|')) this.web3 = new Web3(provider) + this.web3.eth.handleRevert = true this.confirmQueue = new ConfirmQueue(this.web3) let key = process.env.CHAIN_MASTER_KEY this.accountMaster = this.web3.eth.accounts.wallet.add(key) @@ -47,6 +57,10 @@ export class BlockChain { }) } + public get currentAccount() { + return this.accountMaster.address + } + public async getContractInstance(address: string, abi: any) { if (!this.instanceCacheMap.has(address)) { const instance = new this.web3.eth.Contract(abi, address, { from: this.accountMaster.address }) @@ -97,10 +111,15 @@ export class BlockChain { return abi } - public async generateGasShow(gas: any) { + public async generateGasShow(gas: any): Promise { let price = await this.web3.eth.getGasPrice() - let ehtBN = this.web3.utils.toBN(price).mul(this.web3.utils.toBN(gas)) + let ehtBN = safeNumberToBN(price).mul(safeNumberToBN(gas)) + let leagelSymbol = 'USD' + let ethSymbol = this.currentChain.type !== 'Testnet' ? this.currentChain.symbol : 'ETH' + let leagelPriceData = await queryEthPrice(ethSymbol, leagelSymbol) + let leagelPriceBN = safeNumberToBN(leagelPriceData[leagelSymbol] * 100) + let leagel = fromTokenMinimalUnit(ehtBN.mul(leagelPriceBN), 20) let eth = fromTokenMinimalUnit(ehtBN, 18) - return { gas, price, eth } + return { gas, price, eth, leagel } } } diff --git a/src/chain/ERC20Reactor.ts b/src/chain/ERC20Reactor.ts index 30b123b..1edd5cd 100644 --- a/src/chain/ERC20Reactor.ts +++ b/src/chain/ERC20Reactor.ts @@ -116,31 +116,31 @@ export class ERC20Reactor { account, gas, encodeABI = false, + estimate = false, }: { address: string - from: string + from?: string to: string account?: string amount: number | string gas?: number - encodeABI: boolean + encodeABI?: boolean + estimate?: boolean }) { + from = from || account || this.account.address const contract = new this.web3.eth.Contract(abiFt, address, { from: account || this.account.address }) const amountBN = Web3.utils.toBN(Web3.utils.toWei(amount + '')) if (encodeABI) { - return contract.methods.transfer(to, amountBN).encodeABI() + return contract.methods.transferFrom(from, to, amountBN).encodeABI() + } + if (estimate) { + return contract.methods.transferFrom(from, to, 0).estimateGas() } return contract.methods.transferFrom(from, to, amountBN).send({ gas: gas || 1000000, }) } - async calcTransferGas({ address }: { address: string }) { - const contract = new this.web3.eth.Contract(abiFt, address, { from: this.account.address }) - let gas = await contract.methods.transferFrom(this.account.address, this.account.address, 0).estimateGas() - return gas - } - async mint({ address, to, diff --git a/src/chain/allchain.ts b/src/chain/allchain.ts new file mode 100644 index 0000000..bdea6da --- /dev/null +++ b/src/chain/allchain.ts @@ -0,0 +1,291 @@ +export const AllChains = [ + { + name: 'Ethereum Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.ankr.com/eth', + id: 1, + symbol: 'ETH', + explorerurl: 'https://etherscan.io', + }, + { + name: 'Ethereum Ropsten Testnet RPC', + type: 'Testnet', + rpc: 'https://ropsten.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', + id: 3, + symbol: 'ETH', + explorerurl: 'https://ropsten.etherscan.io', + }, + { + name: 'Ethereum Rinkeby Testnet RPC', + type: 'Testnet', + rpc: 'https://rinkey.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', + id: 4, + symbol: 'ETH', + explorerurl: 'https://rinkey.etherscan.io', + }, + { + name: 'Ethereum Goerli Testnet RPC', + type: 'Testnet', + rpc: 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', + id: 5, + symbol: 'ETH', + explorerurl: 'https://goerli.etherscan.io', + }, + { + name: 'Ethereum Kovan Testnet RPC', + type: 'Testnet', + rpc: 'https://kovan.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', + id: 6, + symbol: 'ETH', + explorerurl: 'https://kovan.etherscan.io', + }, + { + name: 'Ubiq Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.octano.dev/', + id: 8, + symbol: 'UBQ', + explorerurl: 'https://ubiqscan.io/', + }, + { + name: 'Elastos ETH Mainnet RPC', + type: 'Mainnet', + rpc: 'https://api.elastos.io/eth', + id: 20, + symbol: 'ELA', + explorerurl: 'https://explorer.elaeth.io/', + }, + { + name: 'Cronos Mainnet RPC', + type: 'Mainnet', + rpc: 'https://evm-cronos.crypto.org', + id: 25, + symbol: 'CRO', + explorerurl: 'https://cronos.crypto.org/explorer/', + }, + { + name: 'Telos EVM Mainnet RPC', + type: 'Mainnet', + rpc: 'https://mainnet.telos.net/evm', + id: 40, + symbol: 'TLOS', + explorerurl: 'https://telos.net/', + }, + { + name: 'Binance Smart Chain', + type: 'Mainnet', + rpc: 'https://rpc.ankr.com/bsc', + id: 56, + symbol: 'BNB', + explorerurl: 'https://bscscan.com', + }, + { + name: 'OKExChain Mainnet RPC', + type: 'Mainnet', + rpc: 'https://exchainrpc.okex.org', + id: 66, + symbol: 'OKT', + explorerurl: 'https://www.oklink.com/okexchain', + }, + { + name: 'Hoo Mainnet RPC', + type: 'Mainnet', + rpc: 'https://http-mainnet.hoosmartchain.com', + id: 70, + symbol: 'HOO', + explorerurl: 'https://hooscan.com', + }, + { + name: 'Binance Testnet', + type: 'Testnet', + rpc: 'https://data-seed-prebsc-1-s1.binance.org:8545/', + id: 97, + symbol: 'BNB', + explorerurl: 'https://testnet.bscscan.com', + }, + { + name: 'xDai Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.xdaichain.com/', + id: 100, + symbol: 'XDAI', + explorerurl: 'https://blockscout.com/xdai/mainnet/', + }, + { + name: 'Fuse Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.fuse.io', + id: 122, + symbol: 'FUSE', + explorerurl: 'https://explorer.fuse.io/', + }, + { + name: 'HECO Mainnet RPC', + type: 'Mainnet', + rpc: 'https://http-mainnet-node.huobichain.com/', + id: 128, + symbol: 'HT', + explorerurl: 'https://hecoinfo.com/', + }, + { + name: 'Matic Mainnet RPC', + type: 'Mainnet', + rpc: 'https://polygon-rpc.com', + id: 137, + symbol: 'MATIC', + explorerurl: 'https://explorer.matic.network/', + }, + { + name: 'Fantom Opera Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.ftm.tools/', + id: 250, + symbol: 'FTM', + explorerurl: 'https://ftmscan.com', + }, + { + name: 'HECO Testnet RPC', + type: 'Testnet', + rpc: 'https://http-testnet.hecochain.com', + id: 256, + symbol: 'HT', + explorerurl: 'https://testnet.hecoinfo.com/', + }, + { + name: 'KCC Mainnet', + type: 'Mainnet', + rpc: 'https://rpc-mainnet.kcc.network', + id: 321, + symbol: 'KCS', + explorerurl: 'https://scan.kcc.network', + }, + { + name: 'KCC Testnet', + type: 'Testnet', + rpc: 'https://rpc-testnet.kcc.network', + id: 322, + symbol: 'tKCS', + explorerurl: 'https://scan-testnet.kcc.network', + }, + { + name: 'Moonriver Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.moonriver.moonbeam.network', + id: 1285, + symbol: 'MOVR', + explorerurl: 'https://blockscout.moonriver.moonbeam.network/', + }, + { + name: 'Fantom Testnet RPC', + type: 'Testnet', + rpc: 'https://rpc.testnet.fantom.network/', + id: 4002, + symbol: 'FTM', + explorerurl: 'https://testnet.ftmscan.com', + }, + { + name: 'IoTeX Mainnet RPC', + type: 'Mainnet', + rpc: 'https://babel-api.mainnet.iotex.io', + id: 4689, + symbol: 'IOTEX', + explorerurl: 'https://iotexscan.io/', + }, + { + name: 'Nahmii Mainnet RPC', + type: 'Mainnet', + rpc: 'https://l2.nahmii.io/', + id: 5551, + symbol: 'ETH', + explorerurl: 'https://explorer.nahmii.io/', + }, + { + name: 'Nahmii Testnet RPC', + type: 'Testnet', + rpc: 'https://l2.testnet.nahmii.io/', + id: 5553, + symbol: 'ETH', + explorerurl: 'https://explorer.testnet.nahmii.io/', + }, + { + name: 'Arbitrum One', + type: 'Mainnet', + rpc: 'https://rpc.ankr.com/arbitrum', + id: 42161, + symbol: 'ETH', + explorerurl: 'https://arbiscan.io/', + }, + { + name: 'Celo Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.ankr.com/celo', + id: 42220, + symbol: 'CELO', + explorerurl: 'https://celoscan.com', + }, + { + name: 'Avalanche C Chain Local RPC', + type: 'Testnet', + rpc: 'https://localhost:9650/ext/bc/C/rpc', + id: 43112, + symbol: 'AVAX', + explorerurl: 'https://snowtrace.io', + }, + { + name: 'Avalanche FUJI Testnet RPC', + type: 'Testnet', + rpc: 'https://api.avax-test.network/ext/bc/C/rpc', + id: 43113, + symbol: 'AVAX', + explorerurl: 'https://testnet.explorer.avax.network/', + }, + { + name: 'Avalanche C Chain Mainnet RPC', + type: 'Mainnet', + rpc: 'https://rpc.ankr.com/avalanche', + id: 43114, + symbol: 'AVAX', + explorerurl: 'https://snowtrace.io', + }, + { + name: 'Matic Testnet RPC', + type: 'Testnet', + rpc: 'https://rpc-mumbai.maticvigil.com|https://matic-mumbai.chainstacklabs.com|https://polygon-testnet.public.blastapi.io|https://rpc.ankr.com/polygon_mumbai', + id: 80001, + symbol: 'MATIC', + explorerurl: 'https://mumbai.polygonscan.com/', + }, + { + name: 'Arbitrum Goerli', + type: 'Testnet', + rpc: 'https://goerli-rollup.arbitrum.io/rpc|https://endpoints.omniatech.io/v1/arbitrum/goerli/public', + id: 421613, + symbol: 'AGOR', + symbol2: 'ETH', + explorerurl: 'https://goerli-rollup-explorer.arbitrum.io', + }, + { + name: 'Harmony Mainnet RPC', + type: 'Mainnet', + rpc: 'https://api.harmony.one/', + id: 1666600000, + symbol: 'ONE', + explorerurl: 'https://explorer.harmony.one', + }, + { + name: 'Harmony Testnet RPC', + type: 'Testnet', + rpc: 'https://api.s0.b.hmny.io/', + id: 1666700000, + symbol: 'ONE', + explorerurl: 'https://explorer.harmony.one', + }, + { + name: 'Local Testnet', + type: 'Local', + rpc: 'https://login-test.kingsome.cn/rpc', + id: 1338, + symbol: 'ETH', + explorerurl: 'https://explorer.harmony.one', + }, +] diff --git a/src/controllers/token.controllers.ts b/src/controllers/token.controllers.ts index 34a73c4..e153253 100644 --- a/src/controllers/token.controllers.ts +++ b/src/controllers/token.controllers.ts @@ -1,15 +1,13 @@ import BaseController from 'common/base.controller' import { role, router } from 'decorators/router' -import { BlockChain } from 'chain/BlockChain' -import { ZError } from 'common/ZError' +import { PriceSvr } from 'service/price.service' class TokenController extends BaseController { @role('anon') @router('post /chain/estimate_transfer_gas') async calcGasPrice(req, res) { const { address } = req.params - let gas = await new BlockChain().erc20Reactor.calcTransferGas({ address }) - let data = await new BlockChain().generateGasShow(gas) + let data = await new PriceSvr().getTokenTransferPrice(address) return data } } diff --git a/src/schedule/blocknum.schedule.ts b/src/schedule/blocknum.schedule.ts index 4f2656e..c3f38ef 100644 --- a/src/schedule/blocknum.schedule.ts +++ b/src/schedule/blocknum.schedule.ts @@ -1,15 +1,20 @@ import { BlockChain } from 'chain/BlockChain' import { singleton } from 'decorators/singleton' +import logger from 'logger/logger' import * as schedule from 'node-schedule' @singleton export default class BlocknumSchedule { - parseAllRecord() { - new BlockChain().updateCurrenBlockNum() + async parseAllRecord() { + try { + await new BlockChain().updateCurrenBlockNum() + } catch (err) { + logger.info('updateCurrenBlockNum error', err.message || err) + } } scheduleAll() { const job = schedule.scheduleJob('*/5 * * * * *', async () => { - await this.parseAllRecord() + this.parseAllRecord() }) } } diff --git a/src/service/chain.service.ts b/src/service/chain.service.ts index a9ce27b..e61800c 100644 --- a/src/service/chain.service.ts +++ b/src/service/chain.service.ts @@ -1,3 +1,4 @@ +import axios from 'axios' import logger from 'logger/logger' import { ChainTask } from 'models/ChainTask' import { RequestTask } from 'models/RequestTask' @@ -15,3 +16,8 @@ export async function restartAllUnFinishedTask() { } } } + +export async function queryEthPrice(eth: string, usd: string = 'USD') { + const url = `https://min-api.cryptocompare.com/data/price?fsym=${eth}&tsyms=${usd}` + return axios.get(url).then(res => res.data) +} diff --git a/src/service/event.sync.service.ts b/src/service/event.sync.service.ts index fd179b1..8dc0cd2 100644 --- a/src/service/event.sync.service.ts +++ b/src/service/event.sync.service.ts @@ -1,3 +1,5 @@ +import assert from 'assert' +import { AllChains } from 'chain/allchain' import { HttpRetryProvider } from 'chain/HttpRetryProvider' import logger from 'logger/logger' import { NftTransferEvent } from 'models/NftTransferEvent' @@ -30,7 +32,10 @@ export class EventSyncSvr { fromBlock: number eventProcesser: any }) { - this.provider = new HttpRetryProvider(process.env.CHAIN_RPC_URL.split('|')) + const defaultChain = parseInt(process.env.CHAIN_DEFAULT) + const chainData = AllChains.find(o => o.id === defaultChain) + assert(chainData, `chain data with ${defaultChain} not found`) + this.provider = new HttpRetryProvider(chainData.rpc.split('|')) // @ts-ignore this.web3 = new Web3(this.provider) this.contract = new this.web3.eth.Contract(abi, address) diff --git a/src/service/price.service.ts b/src/service/price.service.ts new file mode 100644 index 0000000..965a4ce --- /dev/null +++ b/src/service/price.service.ts @@ -0,0 +1,51 @@ +import { BlockChain } from 'chain/BlockChain' +import { singleton } from 'decorators/singleton' +import { IPriceData } from 'structs/PriceData' +import * as schedule from 'node-schedule' +import logger from 'logger/logger' + +@singleton +export class PriceSvr { + private priceMap: Map = new Map() + + public async getTokenTransferPrice(token: string, refresh: boolean = false) { + logger.debug(`get price for ${token}, refresh: ${refresh}`) + const key = `token_transfer_price|${token}` + let data = this.priceMap.get(key) + if (!data || data.expired < Date.now() || refresh) { + logger.debug(`need update price for ${token}`) + let account = new BlockChain().currentAccount + try { + let gas = await new BlockChain().erc20Reactor.transfer({ + address: token, + to: account, + amount: 0, + estimate: true, + }) + let data = await new BlockChain().generateGasShow(gas) + data = { ...data, ...{ expired: Date.now() + 1000 * 60 } } + this.priceMap.set(key, data) + } catch (e) { + logger.log(e) + } + } + return this.priceMap.get(key) + } + private async refreshAll() { + for (let key of this.priceMap.keys()) { + let [type, token] = key.split('|') + if (type == 'token_transfer_price') { + try { + this.getTokenTransferPrice(token, true) + } catch (e) { + logger.info(`error refresh price of token: ${token}`, e.message || e) + } + } + } + } + scheduleAll() { + const job = schedule.scheduleJob('*/30 * * * * *', async () => { + this.refreshAll() + }) + } +} diff --git a/src/structs/ChainData.ts b/src/structs/ChainData.ts new file mode 100644 index 0000000..95b5d6d --- /dev/null +++ b/src/structs/ChainData.ts @@ -0,0 +1,8 @@ +export interface IChainData { + name: string + type: string + rpc: string + id: number + symbol: string + explorerurl: string +} diff --git a/src/structs/PriceData.ts b/src/structs/PriceData.ts new file mode 100644 index 0000000..1128b7f --- /dev/null +++ b/src/structs/PriceData.ts @@ -0,0 +1,7 @@ +export interface IPriceData { + gas: number + price: string + eth: string + leagel?: string + expired?: number +}