增加一个获取转账价格的接口

This commit is contained in:
zhl 2023-05-24 16:27:13 +08:00
parent 06313311c3
commit e522aaa693
11 changed files with 414 additions and 22 deletions

View File

@ -10,6 +10,7 @@ import logger from 'logger/logger'
import BlocknumSchedule from 'schedule/blocknum.schedule' import BlocknumSchedule from 'schedule/blocknum.schedule'
import { RedisClient } from 'redis/RedisClient' import { RedisClient } from 'redis/RedisClient'
import { restartAllUnFinishedTask } from 'service/chain.service' import { restartAllUnFinishedTask } from 'service/chain.service'
import { PriceSvr } from 'service/price.service'
const zReqParserPlugin = require('plugins/zReqParser') const zReqParserPlugin = require('plugins/zReqParser')
@ -105,6 +106,7 @@ export class ApiServer {
} }
private initSchedules() { private initSchedules() {
new BlocknumSchedule().scheduleAll() new BlocknumSchedule().scheduleAll()
new PriceSvr().scheduleAll()
} }
private restoreChainQueue() {} private restoreChainQueue() {}
private setErrHandler() { private setErrHandler() {

View File

@ -8,13 +8,19 @@ import { ERC721Reactor } from './ERC721Reactor'
import { HttpRetryProvider } from './HttpRetryProvider' import { HttpRetryProvider } from './HttpRetryProvider'
import { WalletReactor } from './WalletReactor' import { WalletReactor } from './WalletReactor'
import { DistributorReactor } from './DistributorReactor' 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 @singleton
export class BlockChain { export class BlockChain {
private web3: Web3 private web3: Web3
instanceCacheMap: Map<string, any> instanceCacheMap: Map<string, any>
private accountMaster: AddedAccount private accountMaster: AddedAccount
private currentChain: IChainData
public erc20Reactor: ERC20Reactor public erc20Reactor: ERC20Reactor
public erc721Reactor: ERC721Reactor public erc721Reactor: ERC721Reactor
public walletReactor: WalletReactor public walletReactor: WalletReactor
@ -23,8 +29,12 @@ export class BlockChain {
public currentBlockNum: number = 0 public currentBlockNum: number = 0
constructor() { 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 = new Web3(provider)
this.web3.eth.handleRevert = true
this.confirmQueue = new ConfirmQueue(this.web3) this.confirmQueue = new ConfirmQueue(this.web3)
let key = process.env.CHAIN_MASTER_KEY let key = process.env.CHAIN_MASTER_KEY
this.accountMaster = this.web3.eth.accounts.wallet.add(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) { public async getContractInstance(address: string, abi: any) {
if (!this.instanceCacheMap.has(address)) { if (!this.instanceCacheMap.has(address)) {
const instance = new this.web3.eth.Contract(abi, address, { from: this.accountMaster.address }) const instance = new this.web3.eth.Contract(abi, address, { from: this.accountMaster.address })
@ -97,10 +111,15 @@ export class BlockChain {
return abi return abi
} }
public async generateGasShow(gas: any) { public async generateGasShow(gas: any): Promise<IPriceData> {
let price = await this.web3.eth.getGasPrice() 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) let eth = fromTokenMinimalUnit(ehtBN, 18)
return { gas, price, eth } return { gas, price, eth, leagel }
} }
} }

View File

@ -116,31 +116,31 @@ export class ERC20Reactor {
account, account,
gas, gas,
encodeABI = false, encodeABI = false,
estimate = false,
}: { }: {
address: string address: string
from: string from?: string
to: string to: string
account?: string account?: string
amount: number | string amount: number | string
gas?: number 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 contract = new this.web3.eth.Contract(abiFt, address, { from: account || this.account.address })
const amountBN = Web3.utils.toBN(Web3.utils.toWei(amount + '')) const amountBN = Web3.utils.toBN(Web3.utils.toWei(amount + ''))
if (encodeABI) { 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({ return contract.methods.transferFrom(from, to, amountBN).send({
gas: gas || 1000000, 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({ async mint({
address, address,
to, to,

291
src/chain/allchain.ts Normal file
View File

@ -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',
},
]

View File

@ -1,15 +1,13 @@
import BaseController from 'common/base.controller' import BaseController from 'common/base.controller'
import { role, router } from 'decorators/router' import { role, router } from 'decorators/router'
import { BlockChain } from 'chain/BlockChain' import { PriceSvr } from 'service/price.service'
import { ZError } from 'common/ZError'
class TokenController extends BaseController { class TokenController extends BaseController {
@role('anon') @role('anon')
@router('post /chain/estimate_transfer_gas') @router('post /chain/estimate_transfer_gas')
async calcGasPrice(req, res) { async calcGasPrice(req, res) {
const { address } = req.params const { address } = req.params
let gas = await new BlockChain().erc20Reactor.calcTransferGas({ address }) let data = await new PriceSvr().getTokenTransferPrice(address)
let data = await new BlockChain().generateGasShow(gas)
return data return data
} }
} }

View File

@ -1,15 +1,20 @@
import { BlockChain } from 'chain/BlockChain' import { BlockChain } from 'chain/BlockChain'
import { singleton } from 'decorators/singleton' import { singleton } from 'decorators/singleton'
import logger from 'logger/logger'
import * as schedule from 'node-schedule' import * as schedule from 'node-schedule'
@singleton @singleton
export default class BlocknumSchedule { export default class BlocknumSchedule {
parseAllRecord() { async parseAllRecord() {
new BlockChain().updateCurrenBlockNum() try {
await new BlockChain().updateCurrenBlockNum()
} catch (err) {
logger.info('updateCurrenBlockNum error', err.message || err)
}
} }
scheduleAll() { scheduleAll() {
const job = schedule.scheduleJob('*/5 * * * * *', async () => { const job = schedule.scheduleJob('*/5 * * * * *', async () => {
await this.parseAllRecord() this.parseAllRecord()
}) })
} }
} }

View File

@ -1,3 +1,4 @@
import axios from 'axios'
import logger from 'logger/logger' import logger from 'logger/logger'
import { ChainTask } from 'models/ChainTask' import { ChainTask } from 'models/ChainTask'
import { RequestTask } from 'models/RequestTask' 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)
}

View File

@ -1,3 +1,5 @@
import assert from 'assert'
import { AllChains } from 'chain/allchain'
import { HttpRetryProvider } from 'chain/HttpRetryProvider' import { HttpRetryProvider } from 'chain/HttpRetryProvider'
import logger from 'logger/logger' import logger from 'logger/logger'
import { NftTransferEvent } from 'models/NftTransferEvent' import { NftTransferEvent } from 'models/NftTransferEvent'
@ -30,7 +32,10 @@ export class EventSyncSvr {
fromBlock: number fromBlock: number
eventProcesser: any 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 // @ts-ignore
this.web3 = new Web3(this.provider) this.web3 = new Web3(this.provider)
this.contract = new this.web3.eth.Contract(abi, address) this.contract = new this.web3.eth.Contract(abi, address)

View File

@ -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<string, IPriceData> = 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()
})
}
}

8
src/structs/ChainData.ts Normal file
View File

@ -0,0 +1,8 @@
export interface IChainData {
name: string
type: string
rpc: string
id: number
symbol: string
explorerurl: string
}

7
src/structs/PriceData.ts Normal file
View File

@ -0,0 +1,7 @@
export interface IPriceData {
gas: number
price: string
eth: string
leagel?: string
expired?: number
}