project init
This commit is contained in:
commit
ded16ac57b
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
dist/*.js
|
14
.eslintrc.js
Normal file
14
.eslintrc.js
Normal file
@ -0,0 +1,14 @@
|
||||
/** @format */
|
||||
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser', //定义ESLint的解析器
|
||||
extends: [
|
||||
'plugin:prettier/recommended', // 使用prettier中的样式规范,且如果使得ESLint会检测prettier的格式问题,同样将格式问题以error的形式抛出
|
||||
],
|
||||
parserOptions: {ecmaVersion: 2019, sourceType: 'module'},
|
||||
env: {
|
||||
//指定代码的运行环境
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
}
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.idea
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
.env.development
|
13
.prettierrc.js
Normal file
13
.prettierrc.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
"printWidth": 120,
|
||||
"semi": false, // 在语句末尾添加分号
|
||||
"singleQuote": true, // 使用单引号而非双引号
|
||||
"trailingComma": "all", // 在任何可能的多行中输入尾逗号
|
||||
"bracketSpacing": true, // 在对象字面量声明所使用的的花括号前后({})输出空格
|
||||
"jsxBracketSameLine": true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
|
||||
"arrowParens": "avoid", // 为单行箭头函数的参数添加圆括号。
|
||||
"requirePragma": false, // Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码
|
||||
"insertPragma": false, // 顶部插入一个 @format
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
};
|
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Api",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"dev:api"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
]
|
||||
}
|
35
README.md
Normal file
35
README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 工作流系统使用的链交互程序
|
||||
|
||||
简明流程:
|
||||
|
||||
1. 使用企业微信的审判功能作为工作流
|
||||
2. 审判通过后, 获取审批表单信息和附件, 处理后入库
|
||||
3. 根据不同的操作生成对应操作的 abi, 添加到多签钱包的 schedule
|
||||
4. 发送邮件给确认者
|
||||
5. 收到 ScheduleConfirm 事件后, 上链执行对应的 schedule
|
||||
6. 执行结束后, 调用企业微信的 `发起审批` 流程, 通知申请人
|
||||
|
||||
系统时序图:
|
||||
|
||||
```mermaid
|
||||
%% NFT资产管理时序图
|
||||
sequenceDiagram
|
||||
participant user
|
||||
participant qywx
|
||||
participant chain_client
|
||||
participant admin
|
||||
participant chain_wallet
|
||||
|
||||
user->>qywx: 发起审批流程
|
||||
loop 审批流程
|
||||
qywx->qywx: 审核
|
||||
end
|
||||
qywx->>chain_client: 审批通过后通知到链客户端
|
||||
chain_client->>chain_wallet: 根据规则发起一系列的schedule
|
||||
chain_client->>admin: 发送邮件通知拥有确认权限的人
|
||||
admin->>chain_wallet: 确认schedule
|
||||
chain_wallet-->>chain_client: 通知已确认
|
||||
chain_client->>chain_wallet: 执行对应的schedule
|
||||
chain_client->>qywx: 发起通知的审批流程
|
||||
qywx->>user: 通知申请人, 整个流程结果
|
||||
```
|
71
package.json
Normal file
71
package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "web-chain-clien",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prod:api": "NODE_PATH=./dist node dist/api.js",
|
||||
"run:prod": "NODE_ENV=production ts-node -r tsconfig-paths/register src/api.ts",
|
||||
"dev:api": "npx ts-node src/api.ts",
|
||||
"lint": "eslint --ext .ts src/**",
|
||||
"format": "eslint --ext .ts src/** --fix",
|
||||
"dev:monitor": "NODE_ENV=development ts-node -r tsconfig-paths/register src/monitor.ts",
|
||||
"prod:monitor": "NODE_PATH=./dist node dist/monitor.js"
|
||||
},
|
||||
"author": "z",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.2.1",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
"@fastify/helmet": "^10.1.0",
|
||||
"@fastify/jwt": "^6.7.1",
|
||||
"@metamask/eth-sig-util": "^4.0.1",
|
||||
"@typegoose/auto-increment": "^0.4.1",
|
||||
"@typegoose/typegoose": "^7.4.6",
|
||||
"axios": "^0.21.1",
|
||||
"bson": "^4.0.4",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"fast-rbac": "^1.3.0",
|
||||
"fast-xml-parser": "^4.1.3",
|
||||
"fastify": "^4.15.0",
|
||||
"fastify-file-upload": "^3.0.0",
|
||||
"fastify-plugin": "^3.0.0",
|
||||
"fastify-xml-body-parser": "^2.2.0",
|
||||
"graphql": "^16.7.1",
|
||||
"mercurius": "^13.0.0",
|
||||
"mongoose": "5.10.3",
|
||||
"mongoose-findorcreate": "^3.0.0",
|
||||
"node-schedule": "^2.0.0",
|
||||
"node-xlsx": "^0.21.0",
|
||||
"nodemailer": "^6.9.1",
|
||||
"redis": "^3.1.2",
|
||||
"tracer": "^1.1.6",
|
||||
"web3": "^1.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/node-schedule": "^2.1.0",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/redis": "^2.8.28",
|
||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"prettier": "^2.3.0",
|
||||
"node": "18.14.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
2
pm2_dev.sh
Executable file
2
pm2_dev.sh
Executable file
@ -0,0 +1,2 @@
|
||||
pm2 start npm --name "chain-graphql" --log-date-format "YYYY-MM-DD HH:mm:ss" -- run "dev:api"
|
||||
# pm2 start npm --name "chain-monitor" --log-date-format "YYYY-MM-DD HH:mm:ss" -- run "dev:monitor"
|
31536
src/abis/BEBadge.json
Normal file
31536
src/abis/BEBadge.json
Normal file
File diff suppressed because one or more lines are too long
34905
src/abis/BEMultiSigWallet.json
Normal file
34905
src/abis/BEMultiSigWallet.json
Normal file
File diff suppressed because one or more lines are too long
46694
src/abis/ERC1155.json
Normal file
46694
src/abis/ERC1155.json
Normal file
File diff suppressed because one or more lines are too long
16866
src/abis/ERC20.json
Normal file
16866
src/abis/ERC20.json
Normal file
File diff suppressed because one or more lines are too long
37741
src/abis/ERC721.json
Normal file
37741
src/abis/ERC721.json
Normal file
File diff suppressed because one or more lines are too long
18228
src/abis/NftDistributor.json
Normal file
18228
src/abis/NftDistributor.json
Normal file
File diff suppressed because one or more lines are too long
185
src/api.server.ts
Normal file
185
src/api.server.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import fastify, { FastifyError, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import helmet from '@fastify/helmet'
|
||||
import mercurius from 'mercurius'
|
||||
import * as dotenv from 'dotenv'
|
||||
const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development'
|
||||
dotenv.config({ path: envFile })
|
||||
import { IncomingMessage, Server, ServerResponse } from 'http'
|
||||
import { RouterMap } from 'decorators/router'
|
||||
import { mongoose } from '@typegoose/typegoose'
|
||||
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 schema = require('./schema')
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
const zTokenParserPlugin = require('plugins/zTokenParser')
|
||||
|
||||
const apiAuthPlugin = require('plugins/apiauth')
|
||||
|
||||
const fs = require('fs')
|
||||
const join = require('path').join
|
||||
|
||||
require('./common/Extend')
|
||||
|
||||
export class ApiServer {
|
||||
server: FastifyInstance<Server, IncomingMessage, ServerResponse>
|
||||
|
||||
public constructor() {
|
||||
this.server = fastify({ logger: true, trustProxy: true, bodyLimit: 100 * 1024 * 1024 })
|
||||
this.registerPlugins()
|
||||
}
|
||||
private registerPlugins() {
|
||||
this.server.register(require('@fastify/formbody'))
|
||||
this.server.register(require('fastify-xml-body-parser'))
|
||||
this.server.register(zReqParserPlugin)
|
||||
this.server.register(helmet, { hidePoweredBy: false })
|
||||
this.server.register(zTokenParserPlugin)
|
||||
|
||||
this.server.register(apiAuthPlugin, {
|
||||
secret: process.env.API_TOKEN_SECRET,
|
||||
expiresIn: process.env.API_TOKEN_EXPIRESIN,
|
||||
})
|
||||
|
||||
this.server.register(require('@fastify/cors'), {})
|
||||
}
|
||||
|
||||
private registerRouter() {
|
||||
logger.log('register api routers')
|
||||
let self = this
|
||||
for (let [controller, config] of RouterMap.decoratedRouters) {
|
||||
for (let data of config.data) {
|
||||
logger.info(
|
||||
'add router',
|
||||
data.method || 'all',
|
||||
data.path,
|
||||
`${data.target.constructor.name}.${controller.name}()`,
|
||||
)
|
||||
// @ts-ignore
|
||||
self.server[data.method || 'all'](
|
||||
data.path,
|
||||
{
|
||||
preValidation: async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
request.roles = config.roles
|
||||
await this.apiAuth(request, reply)
|
||||
},
|
||||
},
|
||||
controller,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
private initGraphql() {
|
||||
this.server.register(mercurius, {
|
||||
schema,
|
||||
graphiql: true,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 加载所有的controller
|
||||
*/
|
||||
initControllers() {
|
||||
logger.info('Bootstrap controllers...')
|
||||
const controllers = join(__dirname, './controllers')
|
||||
fs.readdirSync(controllers)
|
||||
.filter((file: string) => ~file.search(/^[^.].*\.(ts|js)$/))
|
||||
.forEach((file: any) => {
|
||||
// logger.log(file);
|
||||
return require(join(controllers, file))
|
||||
})
|
||||
}
|
||||
async connectDB() {
|
||||
const options = {
|
||||
useNewUrlParser: true,
|
||||
poolSize: 5,
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelay: 300000,
|
||||
useUnifiedTopology: true,
|
||||
}
|
||||
const uri = process.env.DB_MAIN
|
||||
logger.info(`connect to ${uri} ...`)
|
||||
try {
|
||||
// await mongoose.createConnection(uri, options)
|
||||
await mongoose.connect(uri, options)
|
||||
logger.log('DB Connected')
|
||||
} catch (err) {
|
||||
logger.log(`DB Connection Error: ${err.message}`)
|
||||
}
|
||||
let opts = { url: process.env.REDIS }
|
||||
new RedisClient(opts)
|
||||
logger.log('REDIS Connected')
|
||||
}
|
||||
private initSchedules() {
|
||||
new BlocknumSchedule().scheduleAll()
|
||||
new PriceSvr().scheduleAll()
|
||||
}
|
||||
private restoreChainQueue() {}
|
||||
private setErrHandler() {
|
||||
this.server.setNotFoundHandler(function (
|
||||
request: any,
|
||||
reply: { send: (arg0: { errcode: number; errmsg: string }) => void },
|
||||
) {
|
||||
reply.send({ errcode: 404, errmsg: 'page not found' })
|
||||
})
|
||||
this.server.setErrorHandler(function (error: FastifyError, request: FastifyRequest, reply: FastifyReply) {
|
||||
let statusCode = (error && error.statusCode) || 100
|
||||
if (statusCode >= 500) {
|
||||
logger.error(error)
|
||||
} else if (statusCode >= 400) {
|
||||
logger.info(error)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
reply.code(200).send({ errcode: statusCode, errmsg: error ? error.message : 'unknown error' })
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 格式化接口返回数据, 统一封装成如下格式
|
||||
* {
|
||||
* code: 0,
|
||||
* msg?: '',
|
||||
* data: any
|
||||
* }
|
||||
* @private
|
||||
*/
|
||||
private setFormatSend() {
|
||||
this.server.addHook('preSerialization', async (request: FastifyRequest, reply: FastifyReply, payload) => {
|
||||
reply.header('X-Powered-By', 'PHP/5.4.16')
|
||||
// @ts-ignore
|
||||
if (!payload.errcode) {
|
||||
payload = {
|
||||
errcode: 0,
|
||||
data: payload,
|
||||
}
|
||||
}
|
||||
return payload
|
||||
})
|
||||
}
|
||||
public async start() {
|
||||
let self = this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// await self.connectDB()
|
||||
// self.initControllers()
|
||||
// self.registerRouter()
|
||||
// self.setErrHandler()
|
||||
// self.setFormatSend()
|
||||
// self.initSchedules()
|
||||
self.initGraphql()
|
||||
// restartAllUnFinishedTask()
|
||||
this.server.listen(
|
||||
{ port: parseInt(process.env.API_PORT), host: process.env.API_HOST },
|
||||
(err: any, address: any) => {
|
||||
if (err) {
|
||||
logger.log(err)
|
||||
process.exit(0)
|
||||
}
|
||||
resolve && resolve(address)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
12
src/api.ts
Normal file
12
src/api.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiServer } from './api.server'
|
||||
import logger from './logger/logger'
|
||||
|
||||
class Server extends ApiServer {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
||||
let server = new Server()
|
||||
server.start().then(address => {
|
||||
logger.log(`Api Server listening at ${address}`)
|
||||
})
|
139
src/chain/BlockChain.ts
Normal file
139
src/chain/BlockChain.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import { TaskType } from 'models/RequestTask'
|
||||
import { ConfirmQueue } from 'queue/confirm.queue'
|
||||
import Web3 from 'web3'
|
||||
import { TransactionReceipt, AddedAccount } from 'web3-core'
|
||||
import { ERC20Reactor } from './ERC20Reactor'
|
||||
import { ERC721Reactor } from './ERC721Reactor'
|
||||
import { HttpRetryProvider } from './HttpRetryProvider'
|
||||
import { WalletReactor } from './WalletReactor'
|
||||
import { DistributorReactor } from './DistributorReactor'
|
||||
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 { PriceSvr } from 'service/price.service'
|
||||
|
||||
@singleton
|
||||
export class BlockChain {
|
||||
public web3: Web3
|
||||
instanceCacheMap: Map<string, any>
|
||||
private accountMaster: AddedAccount
|
||||
private currentChain: IChainData
|
||||
public erc20Reactor: ERC20Reactor
|
||||
public erc721Reactor: ERC721Reactor
|
||||
public walletReactor: WalletReactor
|
||||
public distributorReactor: DistributorReactor
|
||||
public confirmQueue: ConfirmQueue
|
||||
public currentBlockNum: number = 0
|
||||
|
||||
constructor() {
|
||||
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)
|
||||
this.instanceCacheMap = new Map()
|
||||
this.erc20Reactor = new ERC20Reactor({
|
||||
web3: this.web3,
|
||||
address: process.env.CHAIN_FT_ADDRESS,
|
||||
})
|
||||
this.erc721Reactor = new ERC721Reactor({
|
||||
web3: this.web3,
|
||||
address: process.env.CHAIN_NFT_ADDRESS,
|
||||
})
|
||||
this.walletReactor = new WalletReactor({
|
||||
web3: this.web3,
|
||||
address: process.env.CHAIN_WALLET_ADDRESS,
|
||||
})
|
||||
this.distributorReactor = new DistributorReactor({
|
||||
web3: this.web3,
|
||||
address: process.env.CHAIN_DISTRIBUTOR_ADDRESS,
|
||||
})
|
||||
}
|
||||
|
||||
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 })
|
||||
this.instanceCacheMap.set(address, instance)
|
||||
}
|
||||
return this.instanceCacheMap.get(address)
|
||||
}
|
||||
|
||||
public async getTransactionReceipt(txHash: string) {
|
||||
return this.web3.eth.getTransactionReceipt(txHash)
|
||||
}
|
||||
|
||||
public async getTxConfirms(txhash: string) {
|
||||
const receipt: TransactionReceipt = await this.getTransactionReceipt(txhash)
|
||||
const latest = await this.web3.eth.getBlockNumber()
|
||||
return latest - receipt.blockNumber + 1
|
||||
}
|
||||
|
||||
public async updateCurrenBlockNum() {
|
||||
this.currentBlockNum = await this.web3.eth.getBlockNumber()
|
||||
// logger.debug(`update block num: ${this.currentBlockNum}`)
|
||||
}
|
||||
|
||||
public async generateFunAbi(reqData: any) {
|
||||
let taskType = parseInt(reqData.type)
|
||||
reqData.encodeABI = true
|
||||
let abi
|
||||
switch (taskType) {
|
||||
case TaskType.MINT_FT:
|
||||
abi = await this.erc20Reactor.mint(reqData)
|
||||
break
|
||||
case TaskType.MINT_NFT:
|
||||
reqData.tokenId = reqData.tokenId || reqData.tokenid
|
||||
abi = await this.erc721Reactor.mint(reqData)
|
||||
break
|
||||
case TaskType.TRANSFER_FT:
|
||||
abi = await this.erc20Reactor.transfer(reqData)
|
||||
break
|
||||
case TaskType.TRANSFER_NFT:
|
||||
reqData.tokenId = reqData.tokenId || reqData.tokenid
|
||||
abi = await this.erc721Reactor.transfer(reqData)
|
||||
break
|
||||
case TaskType.PUBLISH_AIRDROP_LIST:
|
||||
reqData.nftList = [reqData.tokenId || reqData.tokenid]
|
||||
abi = await this.distributorReactor.publishAirdropList(reqData)
|
||||
break
|
||||
}
|
||||
return abi
|
||||
}
|
||||
|
||||
public async sendEth({ from, to, amount }: { from: string; to: string; amount: number }) {
|
||||
const amountToSend = this.web3.utils.toWei(amount + '', 'ether')
|
||||
let gas = await this.web3.eth.estimateGas({
|
||||
from,
|
||||
to,
|
||||
value: '0',
|
||||
})
|
||||
return this.web3.eth.sendTransaction({
|
||||
from,
|
||||
to,
|
||||
gas: 21000,
|
||||
value: amountToSend,
|
||||
})
|
||||
}
|
||||
|
||||
public async generateGasShow(gas: any): Promise<IPriceData> {
|
||||
let price = await new PriceSvr().refreshGasPrice()
|
||||
let ehtBN = safeNumberToBN(price).mul(safeNumberToBN(gas))
|
||||
let ethSymbol = this.currentChain.type !== 'Testnet' ? this.currentChain.symbol : 'ETH'
|
||||
let leagelPrice = await new PriceSvr().queryEthPrice(ethSymbol)
|
||||
let leagelPriceBN = safeNumberToBN(leagelPrice)
|
||||
let leagel = fromTokenMinimalUnit(ehtBN.mul(leagelPriceBN), 20)
|
||||
let eth = fromTokenMinimalUnit(ehtBN, 18)
|
||||
return { gas, price, eth, leagel }
|
||||
}
|
||||
}
|
4553
src/chain/Contracts.ts
Normal file
4553
src/chain/Contracts.ts
Normal file
File diff suppressed because it is too large
Load Diff
67
src/chain/DistributorReactor.ts
Normal file
67
src/chain/DistributorReactor.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { Account } from 'web3-core'
|
||||
const abi = require('abis/NftDistributor.json').abi
|
||||
|
||||
export class DistributorReactor {
|
||||
private web3: Web3
|
||||
private contract: Contract
|
||||
private account: Account
|
||||
constructor({ web3, address }: { web3: Web3; address: string }) {
|
||||
this.web3 = web3
|
||||
this.account = this.web3.eth.accounts.wallet[0]
|
||||
this.contract = new this.web3.eth.Contract(abi, address, { from: this.account.address })
|
||||
}
|
||||
/**
|
||||
* 发布NFT列表
|
||||
*/
|
||||
async publishAirdropList({
|
||||
address,
|
||||
to,
|
||||
nftList,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
address?: string
|
||||
to: string
|
||||
nftList: string[]
|
||||
encodeABI?: boolean
|
||||
}) {
|
||||
const contract = address ? new this.web3.eth.Contract(abi, address, { from: this.account.address }) : this.contract
|
||||
if (encodeABI) {
|
||||
return contract.methods.addNFTData(to, nftList).encodeABI()
|
||||
}
|
||||
let gas = await contract.methods.addNFTData(to, nftList).estimateGas({ from: this.account.address })
|
||||
let res = await contract.methods.addNFTData(to, nftList).send({ gas: (gas * 1.5) | 0 })
|
||||
return res
|
||||
}
|
||||
/**
|
||||
* mint nft to user
|
||||
*/
|
||||
async mintNft({
|
||||
address,
|
||||
to,
|
||||
count,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
address?: string
|
||||
to: string
|
||||
count: number
|
||||
encodeABI?: boolean
|
||||
}) {
|
||||
const contract = address ? new this.web3.eth.Contract(abi, address, { from: this.account.address }) : this.contract
|
||||
const countStr = count + ''
|
||||
if (encodeABI) {
|
||||
return contract.methods.mintToUser(to, countStr).encodeABI()
|
||||
}
|
||||
let gas = await contract.methods.mintToUser(to, countStr).estimateGas({ from: this.account.address })
|
||||
let res = await contract.methods.mintToUser(to, countStr).send({ gas: (gas * 1.5) | 0 })
|
||||
return res
|
||||
}
|
||||
/**
|
||||
* 查询用户可mint的数量
|
||||
*/
|
||||
async getMintableCount({ address, user }: { address?: string; user: string }) {
|
||||
const contract = address ? new this.web3.eth.Contract(abi, address, { from: this.account.address }) : this.contract
|
||||
return await contract.methods.getMintableCount(user).call()
|
||||
}
|
||||
}
|
205
src/chain/ERC20Reactor.ts
Normal file
205
src/chain/ERC20Reactor.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { BN } from 'ethereumjs-util'
|
||||
import Web3 from 'web3'
|
||||
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { Account } from 'web3-core'
|
||||
import { AllChains } from './allchain'
|
||||
import { HttpRetryProvider } from './HttpRetryProvider'
|
||||
|
||||
const abiFt = require('abis/ERC20.json').abi
|
||||
export class ERC20Reactor {
|
||||
private web3: Web3
|
||||
private contract: Contract
|
||||
private account: Account
|
||||
|
||||
constructor({ web3, address }: { web3: Web3; address: string }) {
|
||||
this.web3 = web3
|
||||
this.account = this.web3.eth.accounts.wallet[0]
|
||||
this.contract = new this.web3.eth.Contract(abiFt, address, { from: this.account.address })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get balance or count for current account on specific asset contract.
|
||||
*
|
||||
* @param address - Asset ERC20 contract address.
|
||||
* @param selectedAddress - Current account public address.
|
||||
* @returns Promise resolving to BN object containing balance for current account on specific asset contract.
|
||||
*/
|
||||
async getBalanceOf({ address, selectedAddress }: { address?: string; selectedAddress: string }): Promise<BN> {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiFt, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<BN>((resolve, reject) => {
|
||||
contract.methods.balanceOf(selectedAddress).call({ from: selectedAddress }, (error: Error, result: BN) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for the decimals for a given ERC20 asset.
|
||||
*
|
||||
* @param address - ERC20 asset contract string.
|
||||
* @returns Promise resolving to the 'decimals'.
|
||||
*/
|
||||
async getTokenDecimals(address?: string): Promise<string> {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiFt, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.decimals().call((error: Error, result: BN | string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for symbol for a given ERC20 asset.
|
||||
*
|
||||
* @param address - ERC20 asset contract address.
|
||||
* @returns Promise resolving to the 'symbol'.
|
||||
*/
|
||||
async getTokenSymbol(address?: string): Promise<string> {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiFt, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.symbol().call((error: Error, result: BN | string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Query if a contract implements an interface.
|
||||
*
|
||||
* @param address - Asset contract address.
|
||||
* @param userAddress - The public address for the currently active user's account.
|
||||
* @returns Promise resolving an object containing the standard, decimals, symbol and balance of the given contract/userAddress pair.
|
||||
*/
|
||||
async getDetails({ address, userAddress }: { address?: string; userAddress: string }): Promise<{
|
||||
standard: string
|
||||
symbol: string | undefined
|
||||
decimals: string | undefined
|
||||
balance: BN | undefined
|
||||
}> {
|
||||
const [decimals, symbol] = await Promise.all([this.getTokenDecimals(address), this.getTokenSymbol(address)])
|
||||
let balance
|
||||
if (userAddress) {
|
||||
balance = await this.getBalanceOf({ address, selectedAddress: userAddress })
|
||||
}
|
||||
return {
|
||||
decimals,
|
||||
symbol,
|
||||
balance,
|
||||
standard: 'ERC20',
|
||||
}
|
||||
}
|
||||
|
||||
async transfer({
|
||||
address,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
account,
|
||||
chain,
|
||||
gas,
|
||||
encodeABI = false,
|
||||
estimate = false,
|
||||
}: {
|
||||
address: string
|
||||
from?: string
|
||||
to: string
|
||||
account?: string
|
||||
chain?: number
|
||||
amount: number | string
|
||||
gas?: number
|
||||
encodeABI?: boolean
|
||||
estimate?: boolean
|
||||
}) {
|
||||
from = from || account || this.account.address
|
||||
let w3 = this.web3
|
||||
if (chain) {
|
||||
let chainData = AllChains.find(o => o.id === chain)
|
||||
if (chainData) {
|
||||
const provider = new HttpRetryProvider(chainData.rpc.split('|'))
|
||||
w3 = new Web3(provider)
|
||||
}
|
||||
}
|
||||
const contract = new w3.eth.Contract(abiFt, address, { from: account || this.account.address })
|
||||
const amountBN = Web3.utils.toBN(Web3.utils.toWei(amount + ''))
|
||||
if (encodeABI) {
|
||||
if (from === this.account.address) return contract.methods.transfer(to, amountBN).encodeABI()
|
||||
return contract.methods.transferFrom(from, to, amountBN).encodeABI()
|
||||
}
|
||||
let gasEstimate =
|
||||
from === this.account.address
|
||||
? await contract.methods.transfer(to, amountBN).estimateGas()
|
||||
: await contract.methods.transferFrom(from, to, amountBN).estimateGas()
|
||||
if (estimate) {
|
||||
return gasEstimate
|
||||
}
|
||||
if (from === this.account.address) return contract.methods.transfer(to, amountBN).send({ gas: gas || gasEstimate })
|
||||
return contract.methods.transferFrom(from, to, amountBN).send({
|
||||
gas: gas || gasEstimate,
|
||||
})
|
||||
}
|
||||
|
||||
async mint({
|
||||
address,
|
||||
to,
|
||||
amount,
|
||||
account,
|
||||
chain,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
account?: string
|
||||
address?: string
|
||||
to: string
|
||||
amount: string
|
||||
chain?: number
|
||||
encodeABI: boolean
|
||||
}) {
|
||||
let w3 = this.web3
|
||||
if (chain) {
|
||||
let chainData = AllChains.find(o => o.id === chain)
|
||||
if (chainData) {
|
||||
const provider = new HttpRetryProvider(chainData.rpc.split('|'))
|
||||
w3 = new Web3(provider)
|
||||
}
|
||||
}
|
||||
const contract = address
|
||||
? new w3.eth.Contract(abiFt, address, { from: account || this.account.address })
|
||||
: this.contract
|
||||
let amountBN = Web3.utils.toBN(Web3.utils.toWei(amount + ''))
|
||||
if (encodeABI) {
|
||||
return contract.methods.mint(to, amountBN).encodeABI()
|
||||
}
|
||||
let gas = await contract.methods.mint(to, amountBN).estimateGas({ gas: 1000000 })
|
||||
return contract.methods.mint(to, amountBN).send({ gas: (gas * 1.1) | 0 })
|
||||
}
|
||||
|
||||
async getPastEvents({ address, fromBlock }: { address?: string; fromBlock: number }) {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiFt, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return contract.getPastEvents('Transfer', {
|
||||
fromBlock,
|
||||
toBlock: fromBlock + 50000,
|
||||
})
|
||||
}
|
||||
}
|
408
src/chain/ERC721Reactor.ts
Normal file
408
src/chain/ERC721Reactor.ts
Normal file
@ -0,0 +1,408 @@
|
||||
import { timeoutFetch } from 'utils/net.util'
|
||||
import { getFormattedIpfsUrl } from 'utils/wallet.util'
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { Account } from 'web3-core'
|
||||
import { AllChains } from './allchain'
|
||||
import { HttpRetryProvider } from './HttpRetryProvider'
|
||||
|
||||
export const ERC721 = 'ERC721'
|
||||
export const ERC721_INTERFACE_ID = '0x80ac58cd'
|
||||
export const ERC721_METADATA_INTERFACE_ID = '0x5b5e139f'
|
||||
export const ERC721_ENUMERABLE_INTERFACE_ID = '0x780e9d63'
|
||||
|
||||
const abiNft = require('abis/BEBadge.json').abi
|
||||
|
||||
export class ERC721Reactor {
|
||||
private web3: Web3
|
||||
private contract: Contract
|
||||
private account: Account
|
||||
|
||||
constructor({ web3, address }: { web3: Web3; address: string }) {
|
||||
this.web3 = web3
|
||||
this.account = this.web3.eth.accounts.wallet[0]
|
||||
this.contract = new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if contract implements ERC721Metadata interface.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @returns Promise resolving to whether the contract implements ERC721Metadata interface.
|
||||
*/
|
||||
contractSupportsMetadataInterface = async (address?: string): Promise<boolean> => {
|
||||
return this.contractSupportsInterface({ address, interfaceId: ERC721_METADATA_INTERFACE_ID })
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if contract implements ERC721Enumerable interface.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @returns Promise resolving to whether the contract implements ERC721Enumerable interface.
|
||||
*/
|
||||
contractSupportsEnumerableInterface = async (address?: string): Promise<boolean> => {
|
||||
return this.contractSupportsInterface({ address, interfaceId: ERC721_ENUMERABLE_INTERFACE_ID })
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if contract implements ERC721 interface.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @returns Promise resolving to whether the contract implements ERC721 interface.
|
||||
*/
|
||||
contractSupportsBase721Interface = async (address?: string): Promise<boolean> => {
|
||||
return this.contractSupportsInterface({ address, interfaceId: ERC721_INTERFACE_ID })
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate assets assigned to an owner.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @param selectedAddress - Current account public address.
|
||||
* @param index - A collectible counter less than `balanceOf(selectedAddress)`.
|
||||
* @returns Promise resolving to token identifier for the 'index'th asset assigned to 'selectedAddress'.
|
||||
*/
|
||||
getCollectibleTokenId = async ({
|
||||
address,
|
||||
selectedAddress,
|
||||
index,
|
||||
}: {
|
||||
address?: string
|
||||
selectedAddress: string
|
||||
index: number
|
||||
}): Promise<string> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.tokenOfOwnerByIndex(selectedAddress, index).call((error: Error, result: string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getBalance = async ({ address, selectedAddress }: { address?: string; selectedAddress: string }): Promise<number> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
contract.methods.balanceOf(selectedAddress).call((error: Error, result: number) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for tokenURI for a given asset.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @param tokenId - ERC721 asset identifier.
|
||||
* @returns Promise resolving to the 'tokenURI'.
|
||||
*/
|
||||
getTokenURI = async ({ address, tokenId }: { address?: string; tokenId: string }): Promise<string> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
const supportsMetadata = await this.contractSupportsMetadataInterface(address)
|
||||
if (!supportsMetadata) {
|
||||
throw new Error('Contract does not support ERC721 metadata interface.')
|
||||
}
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.tokenURI(tokenId).call((error: Error, result: string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for name for a given asset.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @returns Promise resolving to the 'name'.
|
||||
*/
|
||||
getAssetName = async (address?: string): Promise<string> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.name().call((error: Error, result: string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for symbol for a given asset.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @returns Promise resolving to the 'symbol'.
|
||||
*/
|
||||
getAssetSymbol = async (address?: string): Promise<string> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.symbol().call((error: Error, result: string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for owner for a given ERC721 asset.
|
||||
*
|
||||
* @param address - ERC721 asset contract address.
|
||||
* @param tokenId - ERC721 asset identifier.
|
||||
* @returns Promise resolving to the owner address.
|
||||
*/
|
||||
async getOwnerOf({ address, tokenId }: { address?: string; tokenId: string }): Promise<string> {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
contract.methods.ownerOf(tokenId).call((error: Error, result: string) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if a contract implements an interface.
|
||||
*
|
||||
* @param address - Asset contract address.
|
||||
* @param interfaceId - Interface identifier.
|
||||
* @returns Promise resolving to whether the contract implements `interfaceID`.
|
||||
*/
|
||||
private contractSupportsInterface = async ({
|
||||
address,
|
||||
interfaceId,
|
||||
}: {
|
||||
address?: string
|
||||
interfaceId: string
|
||||
}): Promise<boolean> => {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
contract.methods.supportsInterface(interfaceId).call((error: Error, result: boolean) => {
|
||||
/* istanbul ignore if */
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query if a contract implements an interface.
|
||||
*
|
||||
* @param address - Asset contract address.
|
||||
* @param ipfsGateway - The user's preferred IPFS gateway.
|
||||
* @param tokenId - tokenId of a given token in the contract.
|
||||
* @returns Promise resolving an object containing the standard, tokenURI, symbol and name of the given contract/tokenId pair.
|
||||
*/
|
||||
getDetails = async ({
|
||||
address,
|
||||
ipfsGateway,
|
||||
tokenId,
|
||||
}: {
|
||||
address?: string
|
||||
ipfsGateway: string
|
||||
tokenId?: string
|
||||
}): Promise<{
|
||||
standard: string
|
||||
tokenURI: string | undefined
|
||||
symbol: string | undefined
|
||||
name: string | undefined
|
||||
image: string | undefined
|
||||
}> => {
|
||||
const isERC721 = await this.contractSupportsBase721Interface(address)
|
||||
if (!isERC721) {
|
||||
throw new Error("This isn't a valid ERC721 contract")
|
||||
}
|
||||
|
||||
let tokenURI, image, symbol, name
|
||||
|
||||
// TODO upgrade to use Promise.allSettled for name/symbol when we can refactor to use es2020 in tsconfig
|
||||
try {
|
||||
symbol = await this.getAssetSymbol(address)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
name = await this.getAssetName(address)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (tokenId) {
|
||||
try {
|
||||
tokenURI = await this.getTokenURI({ address, tokenId })
|
||||
if (tokenURI.startsWith('ipfs://')) {
|
||||
tokenURI = getFormattedIpfsUrl(ipfsGateway, tokenURI, true)
|
||||
}
|
||||
|
||||
const response = await timeoutFetch(tokenURI)
|
||||
const object = await response.json()
|
||||
image = object ? object.image : ''
|
||||
if (image.startsWith('ipfs://')) {
|
||||
image = getFormattedIpfsUrl(ipfsGateway, image, true)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
standard: ERC721,
|
||||
tokenURI,
|
||||
symbol,
|
||||
name,
|
||||
image,
|
||||
}
|
||||
}
|
||||
|
||||
async transfer({
|
||||
address,
|
||||
from,
|
||||
to,
|
||||
tokenId,
|
||||
account,
|
||||
chain,
|
||||
gas,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
address?: string
|
||||
from: string
|
||||
to: string
|
||||
tokenId: string
|
||||
account?: string
|
||||
chain?: number
|
||||
gas?: number
|
||||
encodeABI?: boolean
|
||||
}) {
|
||||
let w3 = this.web3
|
||||
if (chain) {
|
||||
let chainData = AllChains.find(o => o.id === chain)
|
||||
if (chainData) {
|
||||
const provider = new HttpRetryProvider(chainData.rpc.split('|'))
|
||||
w3 = new Web3(provider)
|
||||
}
|
||||
}
|
||||
const contract = address
|
||||
? new w3.eth.Contract(abiNft, address, { from: account || this.account.address })
|
||||
: this.contract
|
||||
if (encodeABI) {
|
||||
return contract.methods.safeTransferFrom(from, to, tokenId).encodeABI()
|
||||
}
|
||||
return contract.methods.safeTransferFrom(from, to, tokenId).send({
|
||||
from,
|
||||
gas: gas || 1000000,
|
||||
})
|
||||
}
|
||||
|
||||
async mint({
|
||||
address,
|
||||
to,
|
||||
tokenId,
|
||||
chain,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
address?: string
|
||||
to: string
|
||||
tokenId: string
|
||||
chain?: number
|
||||
encodeABI?: boolean
|
||||
}) {
|
||||
let w3 = this.web3
|
||||
if (chain) {
|
||||
let chainData = AllChains.find(o => o.id === chain)
|
||||
if (chainData) {
|
||||
const provider = new HttpRetryProvider(chainData.rpc.split('|'))
|
||||
w3 = new Web3(provider)
|
||||
}
|
||||
}
|
||||
const contract = address ? new w3.eth.Contract(abiNft, address, { from: this.account.address }) : this.contract
|
||||
if (encodeABI) {
|
||||
return contract.methods.mint(to, tokenId).encodeABI()
|
||||
}
|
||||
let gas = await contract.methods.mint(to, tokenId).estimateGas({ gas: 1000000 })
|
||||
return contract.methods.mint(to, tokenId).send({ gas: (gas * 1.1) | 0 })
|
||||
}
|
||||
|
||||
async batchMint({
|
||||
account,
|
||||
address,
|
||||
to,
|
||||
tokenIds,
|
||||
chain,
|
||||
encodeABI = false,
|
||||
}: {
|
||||
account?: string
|
||||
address?: string
|
||||
to: string
|
||||
tokenIds: string[]
|
||||
chain?: number
|
||||
encodeABI?: boolean
|
||||
}) {
|
||||
let w3 = this.web3
|
||||
if (chain) {
|
||||
let chainData = AllChains.find(o => o.id === chain)
|
||||
if (chainData) {
|
||||
const provider = new HttpRetryProvider(chainData.rpc.split('|'))
|
||||
w3 = new Web3(provider)
|
||||
}
|
||||
}
|
||||
const contract = address
|
||||
? new w3.eth.Contract(abiNft, address, { from: account || this.account.address })
|
||||
: this.contract
|
||||
// let gas = await contract.methods.batchMint(to, tokenIds, configIds).estimateGas({ gas: 1000000 })
|
||||
if (encodeABI) {
|
||||
return contract.methods.batchMint(to, tokenIds).encodeABI()
|
||||
}
|
||||
return contract.methods.batchMint(to, tokenIds).send({ gas: 1000000 })
|
||||
}
|
||||
|
||||
async getPastEvents({ address, fromBlock }: { address?: string; fromBlock: number }) {
|
||||
const contract = address
|
||||
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
|
||||
: this.contract
|
||||
return contract.getPastEvents('BatchMint', {
|
||||
fromBlock,
|
||||
toBlock: fromBlock + 50000,
|
||||
})
|
||||
}
|
||||
}
|
87
src/chain/HttpRetryProvider.ts
Normal file
87
src/chain/HttpRetryProvider.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import axios from 'axios'
|
||||
import deepmerge from 'deepmerge'
|
||||
import logger from 'logger/logger'
|
||||
import { generateHeader } from 'utils/net.util'
|
||||
import { retry } from 'utils/promise.util'
|
||||
|
||||
const defaultOptions = {
|
||||
retry: {
|
||||
retries: 5,
|
||||
},
|
||||
}
|
||||
|
||||
export class HttpRetryProvider {
|
||||
public currentIndex: number
|
||||
public options: any
|
||||
public urls: string[]
|
||||
|
||||
constructor(urls: string[], options: any = {}) {
|
||||
if (!urls || !urls.length) {
|
||||
throw new TypeError(`Invalid URLs: '${urls}'`)
|
||||
}
|
||||
this.urls = urls
|
||||
this.options = deepmerge(defaultOptions, options)
|
||||
this.currentIndex = 0
|
||||
}
|
||||
|
||||
public async send(payload, callback) {
|
||||
try {
|
||||
const [result, index] = await retry(() => trySend(payload, this.urls, this.currentIndex), this.options.retry)
|
||||
this.currentIndex = index
|
||||
callback(null, result)
|
||||
} catch (e) {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
public async sendAsync(payload, callback) {
|
||||
try {
|
||||
const [result, index] = await retry(() => trySend(payload, this.urls, this.currentIndex), this.options.retry)
|
||||
this.currentIndex = index
|
||||
callback(null, result)
|
||||
} catch (e) {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpRetryProviderError extends Error {
|
||||
public errors: any
|
||||
constructor(message, errors) {
|
||||
super(message)
|
||||
this.errors = errors
|
||||
}
|
||||
}
|
||||
|
||||
async function trySend(payload, urls, initialIndex) {
|
||||
const errors = []
|
||||
|
||||
let index = initialIndex
|
||||
for (let count = 0; count < urls.length; count++) {
|
||||
const url = urls[index]
|
||||
const headers = generateHeader()
|
||||
try {
|
||||
const config: any = {
|
||||
method: 'post',
|
||||
url,
|
||||
headers,
|
||||
data: JSON.stringify(payload),
|
||||
timeout: 30 * 1000,
|
||||
}
|
||||
const response = await axios(config)
|
||||
if (response.status !== 200) {
|
||||
const text = response.data
|
||||
throw new Error(`[${response.status}: ${url}]: ${text}`)
|
||||
}
|
||||
const result = response.data
|
||||
return [result, index]
|
||||
} catch (e) {
|
||||
// log error here
|
||||
logger.error({ error: e.message }, 'fetch fail')
|
||||
errors.push(e)
|
||||
}
|
||||
index = (index + 1) % urls.length
|
||||
}
|
||||
|
||||
throw new HttpRetryProviderError('Request failed for all urls', errors)
|
||||
}
|
89
src/chain/TransactionConfirm.ts
Normal file
89
src/chain/TransactionConfirm.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import Web3 from 'web3'
|
||||
|
||||
const DEFAULT_INTERVAL = 500
|
||||
|
||||
const DEFAULT_BLOCKS_TO_WAIT = 6
|
||||
|
||||
interface Options {
|
||||
interval: number
|
||||
blocksToWait: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for one or multiple transactions to confirm.
|
||||
*
|
||||
* @param web3
|
||||
* @param txnHash A transaction hash or list of those
|
||||
* @param options Wait timers
|
||||
* @return Transaction receipt
|
||||
*/
|
||||
export function waitTransaction(web3: Web3, txnHash: string | string[], options: Options = null): Promise<any> {
|
||||
const interval = options && options.interval ? options.interval : DEFAULT_INTERVAL
|
||||
const blocksToWait = options && options.blocksToWait ? options.blocksToWait : DEFAULT_BLOCKS_TO_WAIT
|
||||
var transactionReceiptAsync = async function (txnHash, resolve, reject) {
|
||||
try {
|
||||
var receipt = web3.eth.getTransactionReceipt(txnHash)
|
||||
if (!receipt) {
|
||||
setTimeout(function () {
|
||||
transactionReceiptAsync(txnHash, resolve, reject)
|
||||
}, interval)
|
||||
} else {
|
||||
if (blocksToWait > 0) {
|
||||
var resolvedReceipt = await receipt
|
||||
if (!resolvedReceipt || !resolvedReceipt.blockNumber)
|
||||
setTimeout(function () {
|
||||
transactionReceiptAsync(txnHash, resolve, reject)
|
||||
}, interval)
|
||||
else {
|
||||
try {
|
||||
var block = await web3.eth.getBlock(resolvedReceipt.blockNumber)
|
||||
var current = await web3.eth.getBlock('latest')
|
||||
if (current.number - block.number >= blocksToWait) {
|
||||
var txn = await web3.eth.getTransaction(txnHash)
|
||||
if (txn.blockNumber != null) resolve(resolvedReceipt)
|
||||
else reject(new Error('Transaction with hash: ' + txnHash + ' ended up in an uncle block.'))
|
||||
} else
|
||||
setTimeout(function () {
|
||||
transactionReceiptAsync(txnHash, resolve, reject)
|
||||
}, interval)
|
||||
} catch (e) {
|
||||
setTimeout(function () {
|
||||
transactionReceiptAsync(txnHash, resolve, reject)
|
||||
}, interval)
|
||||
}
|
||||
}
|
||||
} else resolve(receipt)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve multiple transactions once
|
||||
if (Array.isArray(txnHash)) {
|
||||
var promises = []
|
||||
txnHash.forEach(function (oneTxHash) {
|
||||
promises.push(waitTransaction(web3, oneTxHash, options))
|
||||
})
|
||||
return Promise.all(promises)
|
||||
} else {
|
||||
return new Promise(function (resolve, reject) {
|
||||
transactionReceiptAsync(txnHash, resolve, reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the transaction was success based on the receipt.
|
||||
*
|
||||
* https://ethereum.stackexchange.com/a/45967/620
|
||||
*
|
||||
* @param receipt Transaction receipt
|
||||
*/
|
||||
export function isSuccessfulTransaction(receipt: any): boolean {
|
||||
if (receipt.status == '0x1' || receipt.status == 1) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
150
src/chain/WalletReactor.ts
Normal file
150
src/chain/WalletReactor.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { Account } from 'web3-core'
|
||||
import { ZERO_BYTES32 } from 'common/Constants'
|
||||
import { generateRandomBytes32 } from 'utils/wallet.util'
|
||||
const abi = require('abis/BEMultiSigWallet.json').abi
|
||||
|
||||
/**
|
||||
* ["address", "uint256", "bytes", "uint256", "bytes32"],
|
||||
* [target, value, data, predecessor, salt]
|
||||
* data: method.encodeABI
|
||||
* salt: generateRandomBytes32();
|
||||
*/
|
||||
export interface IOperationData {
|
||||
scheduleId?: string
|
||||
targets: string[]
|
||||
values: string[]
|
||||
datas: string[]
|
||||
predecessor: string
|
||||
salt: string
|
||||
transactionHash?: string
|
||||
}
|
||||
export class WalletReactor {
|
||||
private web3: Web3
|
||||
private contract: Contract
|
||||
private account: Account
|
||||
|
||||
constructor({ web3, address }: { web3: Web3; address: string }) {
|
||||
this.web3 = web3
|
||||
this.account = this.web3.eth.accounts.wallet[0]
|
||||
this.contract = new this.web3.eth.Contract(abi, address, { from: this.account.address })
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启执行一个timelock的任务, 该方法需要有proposer角色
|
||||
* @param {*} seconds delay的秒数
|
||||
* @returns
|
||||
*/
|
||||
async beginSchedule(operation: IOperationData, seconds: number) {
|
||||
// let operation: any = this.genOperation(contractAddress, 0, data, ZERO_BYTES32, salt)
|
||||
let gas = await this.contract.methods
|
||||
.schedule(operation.targets, operation.values, operation.datas, operation.predecessor, operation.salt, seconds)
|
||||
.estimateGas({ from: this.account.address })
|
||||
let res = await this.contract.methods
|
||||
.schedule(operation.targets, operation.values, operation.datas, operation.predecessor, operation.salt, seconds)
|
||||
.send({ gas: gas | 0 })
|
||||
operation.transactionHash = res.transactionHash
|
||||
return operation
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消定时, 该方法需要有proposer角色
|
||||
* @param {bytes32} id beginSchedule返回的id
|
||||
* @returns
|
||||
*/
|
||||
async cancelSchedule(id) {
|
||||
let gas = await this.contract.methods.cancel(id).estimateGas({ from: this.account.address })
|
||||
let res = await this.contract.methods.cancel(id).send({ gas: gas | 0 })
|
||||
return res
|
||||
}
|
||||
/**
|
||||
* 查询定时
|
||||
* @param {bytes32} scheduleId beginSchedule返回的id
|
||||
* @returns
|
||||
*/
|
||||
async querySchedule(scheduleId: string) {
|
||||
let instance = this.contract
|
||||
return this.makeBatchRequest([
|
||||
// 查询的scheduleid是否在合约中, 包含所有状态
|
||||
instance.methods.isOperation(scheduleId).call,
|
||||
// 查询的scheduleid是否在pending状态
|
||||
instance.methods.isOperationPending(scheduleId).call,
|
||||
// 查询的scheduleid是否已经到了执行时间
|
||||
instance.methods.isOperationReady(scheduleId).call,
|
||||
// 查询的scheduleid是否已经执行完成
|
||||
instance.methods.isOperationDone(scheduleId).call,
|
||||
// 查询的scheduleid是否已经满足可执行的confirm数量
|
||||
instance.methods.isConfirmed(scheduleId).call,
|
||||
// 查询的scheduleid的可执行时间
|
||||
instance.methods.getTimestamp(scheduleId).call,
|
||||
])
|
||||
}
|
||||
/**
|
||||
* 执行schedule的任务, 该方法需要有executor的role
|
||||
* @returns
|
||||
*/
|
||||
async executeSchedule(operation: IOperationData) {
|
||||
let gas = await this.contract.methods
|
||||
.execute(operation.targets, operation.values, operation.datas, operation.predecessor, operation.salt)
|
||||
.estimateGas({ from: this.account.address })
|
||||
gas = gas | 0
|
||||
let res = await this.contract.methods
|
||||
.execute(operation.targets, operation.values, operation.datas, operation.predecessor, operation.salt)
|
||||
.send({ gas })
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装schedule的请求数据
|
||||
* @param {address} targets 方法所在contract的address
|
||||
* @param {uint256} values 如果调用的方法涉及到转账, 这里表示转账金额
|
||||
* @param {bytes} datas 通过contract.methods.methodName(params).encodeABI()获取
|
||||
* @param {bytes32} predecessor that specifies a dependency between operations, 如果没有, 则传bytes32(0)
|
||||
* @param {bytes32} salt 随机串, 用于区分不同的执行任务
|
||||
* @returns
|
||||
*/
|
||||
genOperation({ targets, values, datas, predecessor, salt }: IOperationData): IOperationData {
|
||||
const scheduleId = this.web3.utils.keccak256(
|
||||
this.web3.eth.abi.encodeParameters(
|
||||
['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'],
|
||||
[targets, values, datas, predecessor, salt],
|
||||
),
|
||||
)
|
||||
return { scheduleId, targets, values, datas, predecessor, salt }
|
||||
}
|
||||
|
||||
makeBatchRequest(calls: any[], callFrom?: any) {
|
||||
let batch = new this.web3.BatchRequest()
|
||||
let promises = calls.map(call => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = call.request({ from: callFrom }, (error, data) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
batch.add(request)
|
||||
})
|
||||
})
|
||||
batch.execute()
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
async updateRequired(num: number) {
|
||||
let contractAddress = [process.env.CHAIN_WALLET_ADDRESS]
|
||||
let values = ['0']
|
||||
let abi = await this.contract.methods.changeRequirement(num + '').encodeABI()
|
||||
let salt = generateRandomBytes32()
|
||||
let operation: any = this.genOperation({
|
||||
targets: contractAddress,
|
||||
values,
|
||||
datas: [abi],
|
||||
predecessor: ZERO_BYTES32,
|
||||
salt,
|
||||
})
|
||||
operation = await this.beginSchedule(operation, 60)
|
||||
return operation
|
||||
}
|
||||
}
|
292
src/chain/allchain.ts
Normal file
292
src/chain/allchain.ts
Normal file
@ -0,0 +1,292 @@
|
||||
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://endpoints.omniatech.io/v1/arbitrum/one/public|https://rpc.ankr.com/arbitrum',
|
||||
id: 42161,
|
||||
network: 'ARBITRUM',
|
||||
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,
|
||||
network: 'AGOR',
|
||||
symbol: 'AGOR',
|
||||
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',
|
||||
},
|
||||
]
|
56
src/clock/Clock.ts
Normal file
56
src/clock/Clock.ts
Normal file
@ -0,0 +1,56 @@
|
||||
export class Clock {
|
||||
public running: boolean = false
|
||||
public paused: boolean = false
|
||||
|
||||
public deltaTime: number
|
||||
public currentTime: number
|
||||
public elapsedTime: number
|
||||
|
||||
protected now: Function =
|
||||
(typeof window !== 'undefined' &&
|
||||
window.performance &&
|
||||
window.performance.now &&
|
||||
window.performance.now.bind(window.performance)) ||
|
||||
Date.now
|
||||
protected _interval
|
||||
|
||||
constructor(useInterval: boolean = true) {
|
||||
this.start(useInterval)
|
||||
}
|
||||
|
||||
start(useInterval: boolean = true) {
|
||||
this.deltaTime = 0
|
||||
this.currentTime = this.now()
|
||||
this.elapsedTime = 0
|
||||
this.running = true
|
||||
|
||||
if (useInterval) {
|
||||
// auto set interval to 60 ticks per second
|
||||
this._interval = setInterval(this.tick.bind(this), 1000 / 60)
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false
|
||||
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval)
|
||||
}
|
||||
}
|
||||
|
||||
tick(newTime = this.now()) {
|
||||
if (!this.paused) {
|
||||
this.deltaTime = newTime - this.currentTime
|
||||
this.elapsedTime += this.deltaTime
|
||||
}
|
||||
this.currentTime = newTime
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused = true
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.paused = false
|
||||
}
|
||||
}
|
50
src/clock/ClockTimer.ts
Normal file
50
src/clock/ClockTimer.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Delayed, Type } from './Delayed'
|
||||
import { Clock } from './Clock'
|
||||
|
||||
export default class ClockTimer extends Clock {
|
||||
delayed: Delayed[] = []
|
||||
|
||||
constructor(autoStart: boolean = false) {
|
||||
super(autoStart)
|
||||
}
|
||||
|
||||
tick() {
|
||||
super.tick()
|
||||
if (this.paused) {
|
||||
return
|
||||
}
|
||||
let delayedList = this.delayed
|
||||
let i = delayedList.length
|
||||
|
||||
while (i--) {
|
||||
const delayed = delayedList[i]
|
||||
|
||||
if (delayed.active) {
|
||||
delayed.tick(this.deltaTime)
|
||||
} else {
|
||||
delayedList.splice(i, 1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(handler: Function, time: number, ...args: any[]) {
|
||||
let delayed = new Delayed(handler, args, time, Type.Interval)
|
||||
this.delayed.push(delayed)
|
||||
return delayed
|
||||
}
|
||||
|
||||
setTimeout(handler: Function, time: number, ...args: any[]) {
|
||||
let delayed = new Delayed(handler, args, time, Type.Timeout)
|
||||
this.delayed.push(delayed)
|
||||
return delayed
|
||||
}
|
||||
|
||||
clear() {
|
||||
let i = this.delayed.length
|
||||
while (i--) {
|
||||
this.delayed[i].clear()
|
||||
}
|
||||
this.delayed = []
|
||||
}
|
||||
}
|
61
src/clock/Delayed.ts
Normal file
61
src/clock/Delayed.ts
Normal file
@ -0,0 +1,61 @@
|
||||
export enum Type {
|
||||
Interval,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
export class Delayed {
|
||||
public active: boolean = true
|
||||
public paused: boolean = false
|
||||
|
||||
public time: number
|
||||
public elapsedTime: number = 0
|
||||
|
||||
protected handler: Function
|
||||
protected args: any
|
||||
protected type: number
|
||||
|
||||
constructor(handler: Function, args: any, time: number, type: number) {
|
||||
this.handler = handler
|
||||
this.args = args
|
||||
this.time = time
|
||||
this.type = type
|
||||
}
|
||||
|
||||
tick(deltaTime: number) {
|
||||
if (this.paused) {
|
||||
return
|
||||
}
|
||||
|
||||
this.elapsedTime += deltaTime
|
||||
|
||||
if (this.elapsedTime >= this.time) {
|
||||
this.execute()
|
||||
}
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.handler.apply(this, this.args)
|
||||
|
||||
if (this.type === Type.Timeout) {
|
||||
this.active = false
|
||||
} else {
|
||||
this.elapsedTime -= this.time
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.elapsedTime = 0
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused = true
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.paused = false
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.active = false
|
||||
}
|
||||
}
|
73
src/clock/Schedule.ts
Normal file
73
src/clock/Schedule.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { singleton } from '../decorators/singleton'
|
||||
import Clock from './ClockTimer'
|
||||
import { Delayed } from './Delayed'
|
||||
|
||||
/**
|
||||
* 全局定时器
|
||||
*/
|
||||
@singleton
|
||||
export class Schedule {
|
||||
clock: Clock = new Clock()
|
||||
gameClock: Map<string, Delayed> = new Map()
|
||||
|
||||
public start() {
|
||||
this.clock.start()
|
||||
}
|
||||
|
||||
beginSchedule(millisecond: number, handler: Function, name: string): void {
|
||||
if (this.gameClock.has(name) && this.gameClock.get(name)?.active) {
|
||||
console.log(`当前已存在进行中的clock: ${name}`)
|
||||
this.gameClock.get(name).clear()
|
||||
this.gameClock.delete(name)
|
||||
}
|
||||
let timeOverFun = function () {
|
||||
handler && handler()
|
||||
}
|
||||
this.gameClock.set(name, this.clock.setTimeout(timeOverFun, millisecond, name))
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消某个计时器
|
||||
*/
|
||||
stopSchedule(name: string): number {
|
||||
console.log(`manual stop schedule: ${name}`)
|
||||
if (!this.gameClock.has(name)) {
|
||||
return -1
|
||||
}
|
||||
let clock = this.gameClock.get(name)
|
||||
if (!clock.active) {
|
||||
this.gameClock.delete(name)
|
||||
return -1
|
||||
}
|
||||
let time = clock.elapsedTime
|
||||
clock.clear()
|
||||
this.gameClock.delete(name)
|
||||
return time
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个定时器是否active
|
||||
* @param {string} name
|
||||
* @return {boolean}
|
||||
*/
|
||||
scheduleActive(name: string): boolean {
|
||||
return this.gameClock.has(name) && this.gameClock.get(name).active
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个计时器剩余时间
|
||||
* @param {string} name
|
||||
* @return {number}
|
||||
*/
|
||||
getLeftTime(name: string) {
|
||||
if (!this.gameClock.has(name)) {
|
||||
return 0
|
||||
}
|
||||
let clock = this.gameClock.get(name)
|
||||
if (!clock.active) {
|
||||
this.gameClock.delete(name)
|
||||
return 0
|
||||
}
|
||||
return clock.time - clock.elapsedTime
|
||||
}
|
||||
}
|
107
src/common/AsyncQueue.ts
Normal file
107
src/common/AsyncQueue.ts
Normal file
@ -0,0 +1,107 @@
|
||||
type Callback<T> = () => Promise<T>
|
||||
|
||||
export type AsyncQueue<T = void> = {
|
||||
push: (task: Callback<T>) => Promise<T>
|
||||
flush: () => Promise<void>
|
||||
size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that each callback pushed onto the queue is executed in series.
|
||||
* Such a quetie 😻
|
||||
* @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple
|
||||
* tasks are pushed onto the queue while there is an active task, only the
|
||||
* last one will be executed, once the active task has completed.
|
||||
* e.g. in the below example, only 0 and 3 will be executed.
|
||||
* ```
|
||||
* const queue = createAsyncQueue({ dedupeConcurrent: true })
|
||||
* queue.push(async () => console.log(0)) // returns 0
|
||||
* queue.push(async () => console.log(1)) // returns 3
|
||||
* queue.push(async () => console.log(2)) // returns 3
|
||||
* queue.push(async () => console.log(3)) // returns 3
|
||||
* ```
|
||||
* */
|
||||
export function createAsyncQueue<T = void>(opts = { dedupeConcurrent: false }): AsyncQueue<T> {
|
||||
const { dedupeConcurrent } = opts
|
||||
let queue: Callback<T>[] = []
|
||||
let running: Promise<void> | undefined
|
||||
let nextPromise = new DeferredPromise<T>()
|
||||
const push = (task: Callback<T>) => {
|
||||
let taskPromise = new DeferredPromise<T>()
|
||||
if (dedupeConcurrent) {
|
||||
queue = []
|
||||
if (nextPromise.started) nextPromise = new DeferredPromise<T>()
|
||||
taskPromise = nextPromise
|
||||
}
|
||||
queue.push(() => {
|
||||
taskPromise.started = true
|
||||
task().then(taskPromise.resolve).catch(taskPromise.reject)
|
||||
return taskPromise.promise
|
||||
})
|
||||
if (!running) running = start()
|
||||
return taskPromise.promise
|
||||
}
|
||||
const start = async () => {
|
||||
while (queue.length) {
|
||||
const task = queue.shift()!
|
||||
await task().catch(() => {})
|
||||
}
|
||||
running = undefined
|
||||
}
|
||||
return {
|
||||
push,
|
||||
flush: () => running || Promise.resolve(),
|
||||
get size() {
|
||||
return queue.length
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const createAsyncQueues = <T = void>(opts = { dedupeConcurrent: false }) => {
|
||||
const queues: { [queueId: string]: AsyncQueue<T> } = {}
|
||||
const push = (queueId: string, task: Callback<T>) => {
|
||||
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
|
||||
return queues[queueId].push(task)
|
||||
}
|
||||
const flush = (queueId: string) => {
|
||||
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
|
||||
return queues[queueId].flush()
|
||||
}
|
||||
return { push, flush }
|
||||
}
|
||||
|
||||
class DeferredPromise<T = void, E = any> {
|
||||
started = false
|
||||
resolve: (x: T | PromiseLike<T>) => void = () => {}
|
||||
reject: (x: E) => void = () => {}
|
||||
promise: Promise<T>
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((res, rej) => {
|
||||
this.resolve = res
|
||||
this.reject = rej
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// function main() {
|
||||
// const queue = createAsyncQueue()
|
||||
// queue.push(async () => {
|
||||
// console.log(0)
|
||||
// }) // returns 0
|
||||
// queue.push(async () => {
|
||||
// console.log(1)
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// setTimeout(() => {
|
||||
// console.log('12')
|
||||
// resolve()
|
||||
// }, 1000)
|
||||
// })
|
||||
// }) // returns 3
|
||||
// queue.push(async () => console.log(2)) // returns 3
|
||||
// queue.push(async () => console.log(3)) // returns 3
|
||||
// console.log('hi')
|
||||
// }
|
||||
|
||||
// main()
|
13
src/common/Constants.ts
Normal file
13
src/common/Constants.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
export const MAX_BATCH_REQ_COUNT = 50
|
||||
|
||||
export const CONFIRM_MAIL_HTML = `
|
||||
<h1>有东西需要你确认<h1>
|
||||
<p>{{title}}</p>
|
||||
<p>{{desc}}</p>
|
||||
<p>点击链接进入确认页面, 如果无法跳转, 就复制链接, 电脑上直接用浏览器打开, 手机上使用MetaMask的浏览器打开</p>
|
||||
<p><a href="{{link}}" target="_blank">{{link2}}</a></p>
|
||||
`
|
6
src/common/Debug.ts
Normal file
6
src/common/Debug.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import debug from 'debug'
|
||||
|
||||
debug.log = console.info.bind(console)
|
||||
|
||||
export const error = debug('chain:error')
|
||||
error.log = console.error.bind(console)
|
966
src/common/Extend.ts
Normal file
966
src/common/Extend.ts
Normal file
@ -0,0 +1,966 @@
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param value 要补0的数值
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
function zeroize(value: number | string, length: number = 2): string {
|
||||
let str = '' + value
|
||||
let zeros = ''
|
||||
for (let i = 0, len = length - str.length; i < len; i++) {
|
||||
zeros += '0'
|
||||
}
|
||||
return zeros + str
|
||||
}
|
||||
|
||||
/****************************************扩展Object****************************************/
|
||||
interface Object {
|
||||
/**
|
||||
* 返回一个浅副本的对象
|
||||
* 此对象会拷贝key value
|
||||
*
|
||||
* @memberOf Object
|
||||
*/
|
||||
clone?(): Object
|
||||
|
||||
/**
|
||||
* 将数据拷贝到 to
|
||||
* @param to 目标
|
||||
*/
|
||||
copyto?(to: Object): void
|
||||
|
||||
/**
|
||||
* 获取指定属性的描述,会查找当前数据和原型数据
|
||||
* @param property 指定的属性名字
|
||||
*/
|
||||
getPropertyDescriptor?(property: string): PropertyDescriptor
|
||||
|
||||
zssign?(target: any): any
|
||||
}
|
||||
|
||||
Object.defineProperties(Object.prototype, {
|
||||
clone: {
|
||||
value: function () {
|
||||
let o = {}
|
||||
for (let n in this) {
|
||||
// @ts-ignore
|
||||
o[n] = this[n]
|
||||
}
|
||||
return o
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
getPropertyDescriptor: {
|
||||
value: function (property: string): any {
|
||||
let data = Object.getOwnPropertyDescriptor(this, property)
|
||||
if (data) {
|
||||
return data
|
||||
}
|
||||
let prototype = Object.getPrototypeOf(this)
|
||||
if (prototype) {
|
||||
return prototype.getPropertyDescriptor(property)
|
||||
}
|
||||
return
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
copyto: {
|
||||
value: function (to: Object) {
|
||||
for (let p in this) {
|
||||
if (!(p in to)) {
|
||||
// 本身没有这个属性
|
||||
// @ts-ignore
|
||||
to[p] = this[p]
|
||||
} else {
|
||||
let data: PropertyDescriptor = to.getPropertyDescriptor(p)
|
||||
if (data) {
|
||||
if (data.set || data.writable) {
|
||||
// 可进行赋值
|
||||
// @ts-ignore
|
||||
to[p] = this[p]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
zssign: {
|
||||
value: function (target: Object) {
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert undefined or null to object')
|
||||
}
|
||||
|
||||
let output = Object(target)
|
||||
for (let nextKey in this) {
|
||||
if (!(nextKey in output)) {
|
||||
// 本身没有这个属性
|
||||
output[nextKey] = this[nextKey]
|
||||
}
|
||||
// else {
|
||||
// let data: PropertyDescriptor = output.getPropertyDescriptor(nextKey);
|
||||
// if (data) {
|
||||
// if (data.set || data.writable) {// 可进行赋值
|
||||
// output[nextKey] = this[nextKey];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
return output
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展Math****************************************/
|
||||
interface Math {
|
||||
/**
|
||||
* 角度转弧度的乘数
|
||||
* Math.PI / 180
|
||||
* @type {number}
|
||||
* @memberOf Math
|
||||
*/
|
||||
DEG_TO_RAD: number
|
||||
/**
|
||||
* 弧度转角度的乘数
|
||||
* 180 / Math.PI
|
||||
*/
|
||||
RAD_TO_DEG: number
|
||||
/**
|
||||
* 整圆的弧度
|
||||
*/
|
||||
PI2: number
|
||||
/**
|
||||
* 90°的弧度
|
||||
*
|
||||
* @type {number}
|
||||
* @memberOf Math
|
||||
*/
|
||||
PI_1_2: number
|
||||
|
||||
/**
|
||||
* 让数值处于指定的最大值和最小值之间,低于最小值取最小值,高于最大值取最大值
|
||||
* @param value 要处理的数值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
clamp?(value: number, min: number, max: number): number
|
||||
|
||||
/**
|
||||
* 从最小值到最大值之间随机[min,max)
|
||||
*/
|
||||
random2?(min: number, max: number): number
|
||||
}
|
||||
|
||||
Math.DEG_TO_RAD = Math.PI / 180
|
||||
|
||||
Math.RAD_TO_DEG = 180 / Math.PI
|
||||
|
||||
Math.PI2 = 2 * Math.PI
|
||||
|
||||
Math.PI_1_2 = Math.PI * 0.5
|
||||
|
||||
Math.clamp = (value, min, max) => {
|
||||
if (value < min) {
|
||||
value = min
|
||||
}
|
||||
if (value > max) {
|
||||
value = max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
Math.random2 = (min, max) => {
|
||||
return min + Math.random() * (max - min)
|
||||
}
|
||||
|
||||
/****************************************扩展Number********************************************/
|
||||
interface Number {
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?(length: number): string
|
||||
|
||||
/**
|
||||
* 数值介于,`min` `max`直接,包含min,max
|
||||
* 即:[min,max]
|
||||
*
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {boolean}
|
||||
*/
|
||||
between?(min: number, max: number): boolean
|
||||
}
|
||||
|
||||
Object.defineProperties(Number.prototype, {
|
||||
zeroize: {
|
||||
value: function (this: number, length: number) {
|
||||
return zeroize(this, length)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
between: {
|
||||
value: function (this: number, min: number, max: number) {
|
||||
return min <= this && max >= this
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展String****************************************/
|
||||
interface String {
|
||||
/**
|
||||
* 替换字符串中{0}{1}{2}{a} {b}这样的数据,用obj对应key替换,或者是数组中对应key的数据替换
|
||||
*/
|
||||
substitute?(args: any[]): string
|
||||
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?(length: number): string
|
||||
|
||||
/**
|
||||
* 将一个字符串转换成一个很小几率重复的数值
|
||||
* <font color="#ff0000">此方法hash的字符串并不一定唯一,慎用</font>
|
||||
*/
|
||||
hash?(): number
|
||||
|
||||
/**
|
||||
* 获取字符串长度,中文方块字符算两个长度
|
||||
*/
|
||||
trueLength?(): number
|
||||
|
||||
/**
|
||||
* 中文字符个数
|
||||
* */
|
||||
cnLength?(): number
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
* */
|
||||
versionCompare?(target: string): number
|
||||
}
|
||||
|
||||
Object.defineProperties(String.prototype, {
|
||||
zeroize: {
|
||||
value: function (length: number) {
|
||||
return zeroize(this, length)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
substitute: {
|
||||
value: function (this: string) {
|
||||
let len = arguments.length
|
||||
if (len > 0) {
|
||||
let obj: IArguments
|
||||
if (len == 1) {
|
||||
obj = arguments[0]
|
||||
if (typeof obj !== 'object') {
|
||||
obj = arguments
|
||||
}
|
||||
} else {
|
||||
obj = arguments
|
||||
}
|
||||
if (obj instanceof Object && !(obj instanceof RegExp)) {
|
||||
return this.replace(/\{(?:%([^{}]+)%)?([^{}]+)\}/g, function (match: string, handler: string, key: string) {
|
||||
//检查key中,是否为%开头,如果是,则尝试按方法处理
|
||||
// @ts-ignore
|
||||
let value = obj[key]
|
||||
if (handler) {
|
||||
//如果有处理器,拆分处理器
|
||||
let func = String.subHandler[handler]
|
||||
if (func) {
|
||||
value = func(value)
|
||||
}
|
||||
}
|
||||
return value !== undefined ? '' + value : match
|
||||
})
|
||||
}
|
||||
}
|
||||
return this.toString() //防止生成String对象,ios反射String对象会当成一个NSDictionary处理
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
hash: {
|
||||
value: function () {
|
||||
let len = this.length
|
||||
let hash = 5381
|
||||
for (let i = 0; i < len; i++) {
|
||||
hash += (hash << 5) + this.charCodeAt(i)
|
||||
}
|
||||
return hash & 0x7fffffff
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
trueLength: {
|
||||
value: function () {
|
||||
let arr: string[] = this.match(/[^x00-xff]/gi)
|
||||
return this.length + (arr ? arr.length : 0)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
cnLength: {
|
||||
value: function () {
|
||||
// /[\u2E80-\u9FBF]
|
||||
let arr: string[] = this.match(/[^x00-xff]/gi)
|
||||
return arr ? arr.length : 0
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
versionCompare: {
|
||||
value: function (target: string): number {
|
||||
const GTR = 1 //大于
|
||||
const LSS = -1 //小于
|
||||
const EQU = 0 //等于
|
||||
if (!target) {
|
||||
return GTR
|
||||
}
|
||||
let v1arr = String(this)
|
||||
.split('.')
|
||||
.map(function (a) {
|
||||
return parseInt(a)
|
||||
})
|
||||
let v2arr = String(target)
|
||||
.split('.')
|
||||
.map(function (a) {
|
||||
return parseInt(a)
|
||||
})
|
||||
let arrLen = Math.max(v1arr.length, v2arr.length)
|
||||
let result
|
||||
|
||||
//排除错误调用
|
||||
if (this == undefined || target == undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
//检查空字符串,任何非空字符串都大于空字符串
|
||||
if (this.length == 0 && target.length == 0) {
|
||||
return EQU
|
||||
} else if (this.length == 0) {
|
||||
return LSS
|
||||
} else if (target.length == 0) {
|
||||
return GTR
|
||||
}
|
||||
//循环比较版本号
|
||||
for (let i = 0; i < arrLen; i++) {
|
||||
result = versionComp(v1arr[i], v2arr[i])
|
||||
if (result == EQU) {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
function versionComp(n1: number, n2: number) {
|
||||
if (typeof n1 != 'number') {
|
||||
n1 = 0
|
||||
}
|
||||
if (typeof n2 != 'number') {
|
||||
n2 = 0
|
||||
}
|
||||
if (n1 > n2) {
|
||||
return GTR
|
||||
} else if (n1 < n2) {
|
||||
return LSS
|
||||
} else {
|
||||
return EQU
|
||||
}
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface StringConstructor {
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param value 要补0的数值
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?: (value: number, length: number) => string
|
||||
|
||||
/**
|
||||
* substitute的回调函数
|
||||
*
|
||||
* @type {Readonly<{ [index: string]: { (input: any): string } }>}
|
||||
* @memberOf StringConstructor
|
||||
*/
|
||||
subHandler?: Readonly<{ [index: string]: { (input: any): string } }>
|
||||
}
|
||||
|
||||
String.zeroize = zeroize
|
||||
|
||||
/****************************************扩展Date****************************************/
|
||||
|
||||
interface Date {
|
||||
/**
|
||||
* 格式化日期
|
||||
*
|
||||
* @param {string} mask 时间字符串
|
||||
* @param {boolean} [local] 是否基于本地时间显示,目前项目,除了报错信息,其他时间都用UTC时间显示
|
||||
* @returns {string} 格式化后的时间
|
||||
*/
|
||||
format(mask: string, local?: boolean): string
|
||||
|
||||
/**
|
||||
* 增加n天
|
||||
* @param {number} days
|
||||
* @return {Date}
|
||||
*/
|
||||
addDays(days: number): Date
|
||||
}
|
||||
|
||||
Object.defineProperties(Date.prototype, {
|
||||
format: {
|
||||
value: function (mask: string, local?: boolean) {
|
||||
let d: Date = this
|
||||
// @ts-ignore
|
||||
return mask.replace(/"[^"]*"|'[^']*'|(?:d{1,2}|m{1,2}|yy(?:yy)?|([hHMs])\1?)/g, function ($0: string) {
|
||||
switch ($0) {
|
||||
case 'd':
|
||||
return gd()
|
||||
case 'dd':
|
||||
return zeroize(gd())
|
||||
case 'M':
|
||||
return gM() + 1
|
||||
case 'MM':
|
||||
return zeroize(gM() + 1)
|
||||
case 'yy':
|
||||
return (gy() + '').substr(2)
|
||||
case 'yyyy':
|
||||
return gy()
|
||||
case 'h':
|
||||
return gH() % 12 || 12
|
||||
case 'hh':
|
||||
return zeroize(gH() % 12 || 12)
|
||||
case 'H':
|
||||
return gH()
|
||||
case 'HH':
|
||||
return zeroize(gH())
|
||||
case 'm':
|
||||
return gm()
|
||||
case 'mm':
|
||||
return zeroize(gm())
|
||||
case 's':
|
||||
return gs()
|
||||
case 'ss':
|
||||
return zeroize(gs())
|
||||
default:
|
||||
return $0.substr(1, $0.length - 2)
|
||||
}
|
||||
})
|
||||
|
||||
function gd() {
|
||||
return local ? d.getDate() : d.getUTCDate()
|
||||
}
|
||||
|
||||
function gM() {
|
||||
return local ? d.getMonth() : d.getUTCMonth()
|
||||
}
|
||||
|
||||
function gy() {
|
||||
return local ? d.getFullYear() : d.getUTCFullYear()
|
||||
}
|
||||
|
||||
function gH() {
|
||||
return local ? d.getHours() : d.getUTCHours()
|
||||
}
|
||||
|
||||
function gm() {
|
||||
return local ? d.getMinutes() : d.getUTCMinutes()
|
||||
}
|
||||
|
||||
function gs() {
|
||||
return local ? d.getSeconds() : d.getUTCSeconds()
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
addDays: {
|
||||
value: function (days: number) {
|
||||
this.setDate(this.getDate() + days)
|
||||
return this
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展Array****************************************/
|
||||
const enum ArraySort {
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
ASC = 0,
|
||||
/**
|
||||
* 降序
|
||||
*/
|
||||
DESC = 1,
|
||||
}
|
||||
|
||||
interface ArrayConstructor {
|
||||
// binaryInsert<T>(partArr: T[], item: T, filter: { (tester: T, ...args): boolean }, ...args);
|
||||
SORT_DEFAULT: { number: 0; string: ''; boolean: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于对Array排序时,处理undefined
|
||||
*/
|
||||
Array.SORT_DEFAULT = {
|
||||
number: 0,
|
||||
string: '',
|
||||
boolean: false,
|
||||
}
|
||||
Object.freeze(Array.SORT_DEFAULT)
|
||||
|
||||
interface Array<T> {
|
||||
/**
|
||||
* 如果数组中没有要放入的对象,则将对象放入数组
|
||||
*
|
||||
* @param {T} t 要放入的对象
|
||||
* @returns {number} 放入的对象,在数组中的索引
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
pushOnce?(t: T): number
|
||||
|
||||
/**
|
||||
*
|
||||
* 删除某个数据
|
||||
* @param {T} t
|
||||
* @returns {boolean} true 有这个数据并且删除成功
|
||||
* false 没有这个数据
|
||||
*/
|
||||
zremove?(t: T): boolean
|
||||
|
||||
/**
|
||||
* 排序 支持多重排序
|
||||
* 降序, 升序
|
||||
* @param {(keyof T)[]} kArr 参数属性列表
|
||||
* @param {(boolean[] | ArraySort[])} [dArr] 是否降序,默认升序
|
||||
* @returns {this}
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
multiSort?(kArr: (keyof T)[], dArr?: boolean[] | ArraySort[]): this
|
||||
|
||||
/**
|
||||
* 默认排序
|
||||
*
|
||||
* @param {string} [key]
|
||||
* @param {boolean} [descend]
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
doSort?(key?: keyof T, descend?: boolean | ArraySort): this
|
||||
|
||||
doSort?(descend?: boolean | ArraySort, key?: keyof T): this
|
||||
|
||||
/**
|
||||
* 将数组克隆到to
|
||||
* to的数组长度会和当前数组一致
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<T>} to
|
||||
*/
|
||||
cloneTo?<T>(to: Array<T>): void
|
||||
|
||||
/**
|
||||
* 将数组附加到to中
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<T>} to
|
||||
*
|
||||
* @member ArrayConstructor
|
||||
*/
|
||||
appendTo?<T>(to: Array<T>): void
|
||||
|
||||
/**
|
||||
* 移除数组index位置的元素, 比slice效率高
|
||||
* @param index
|
||||
*/
|
||||
spliceOne?(index: number): boolean
|
||||
|
||||
/**
|
||||
* 随机排序
|
||||
*/
|
||||
randomSort?(): void
|
||||
|
||||
/**
|
||||
* 检查数组中是否含有另外一个object
|
||||
* @param obj 与数组同类型的obj | 同类型的数组 | 指定child字段的值 | 指定child字段的数组
|
||||
* @param child 比较字段
|
||||
*/
|
||||
contains?<T>(obj: T | T[] | {} | {}[], child?: string): boolean
|
||||
|
||||
/**
|
||||
* 将数组随机插入当前数组中
|
||||
* @param arr
|
||||
*/
|
||||
randomInsert?<T>(arr: Array<T>): void
|
||||
|
||||
/**
|
||||
* 随机获取n个元素
|
||||
* @param count
|
||||
*/
|
||||
randomGet?<T>(count?: number): T[]
|
||||
|
||||
/**
|
||||
* 随机获取1个元素
|
||||
*/
|
||||
randomOne?<T>(): T
|
||||
|
||||
/**
|
||||
* 随机移除n个元素
|
||||
* @param count
|
||||
*/
|
||||
randomRemove?<T>(count?: number): T[]
|
||||
|
||||
/**
|
||||
* 数组移动n位
|
||||
* @param n n > 0 右移, n<0 左移
|
||||
*/
|
||||
moveElement?<T>(n: number): T[]
|
||||
|
||||
/**
|
||||
* 两个数组并集
|
||||
* @param arr
|
||||
*/
|
||||
union?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 两个数组交集
|
||||
* @param arr
|
||||
*/
|
||||
intersect?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 相对于arr的差集
|
||||
* @param arr
|
||||
*/
|
||||
difference?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 转换成Map
|
||||
* @param {string} key 用于生成map的key字段名
|
||||
* @return {Map<any, T>}
|
||||
*/
|
||||
toMap?<T>(key: string): Map<any, T>
|
||||
}
|
||||
|
||||
Object.defineProperties(Array.prototype, {
|
||||
cloneTo: {
|
||||
value: function <T>(this: T[], b: any[]) {
|
||||
b.length = this.length
|
||||
let len = this.length
|
||||
b.length = len
|
||||
for (let i = 0; i < len; i++) {
|
||||
b[i] = this[i]
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
appendTo: {
|
||||
value: function <T>(this: T[], b: any[]) {
|
||||
let len = this.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
b.push(this[i])
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
pushOnce: {
|
||||
value: function <T>(this: T[], t: T) {
|
||||
let idx = this.indexOf(t)
|
||||
if (!~idx) {
|
||||
idx = this.length
|
||||
this.push(t)
|
||||
}
|
||||
return idx
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
zremove: {
|
||||
value: function <T>(this: T[], t: T) {
|
||||
let idx = this.indexOf(t)
|
||||
if (~idx) {
|
||||
this.splice(idx, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
doSort: {
|
||||
value: function () {
|
||||
let key: string, descend: boolean
|
||||
let len = arguments.length
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
let arg = arguments[i]
|
||||
let t = typeof arg
|
||||
if (t === 'string') {
|
||||
key = arg
|
||||
} else {
|
||||
descend = !!arg
|
||||
}
|
||||
}
|
||||
if (key) {
|
||||
return this.sort((a: any, b: any) => (descend ? b[key] - a[key] : a[key] - b[key]))
|
||||
} else {
|
||||
return this.sort((a: any, b: any) => (descend ? b - a : a - b))
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
multiSort: {
|
||||
value: function (kArr: string[], dArr?: boolean[] | boolean) {
|
||||
let isArr = Array.isArray(dArr)
|
||||
return this.sort((a: any, b: any): number => {
|
||||
const def = Array.SORT_DEFAULT
|
||||
for (let idx = 0, len = kArr.length; idx < len; idx++) {
|
||||
let key = kArr[idx]
|
||||
// @ts-ignore
|
||||
let mode = isArr ? !!dArr[idx] : !!dArr
|
||||
let av = a[key]
|
||||
let bv = b[key]
|
||||
let typea = typeof av
|
||||
let typeb = typeof bv
|
||||
if (typea == 'object' || typeb == 'object') {
|
||||
return 0
|
||||
} else if (typea != typeb) {
|
||||
if (typea == 'undefined') {
|
||||
// @ts-ignore
|
||||
bv = def[typeb]
|
||||
} else if (typeb == 'undefined') {
|
||||
// @ts-ignore
|
||||
av = def[typea]
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if (av < bv) {
|
||||
return mode ? 1 : -1
|
||||
} else if (av > bv) {
|
||||
return mode ? -1 : 1
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
spliceOne: {
|
||||
value: function (index: number): boolean {
|
||||
if (index === -1 || index >= this.length) {
|
||||
return false
|
||||
}
|
||||
const len = this.length - 1
|
||||
for (let i = index; i < len; i++) {
|
||||
this[i] = this[i + 1]
|
||||
}
|
||||
this.length = len
|
||||
return true
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
randomSort: {
|
||||
value: function <T>() {
|
||||
for (let j, x, i = this.length; i; j = (Math.random() * i) | 0, x = this[--i], this[i] = this[j], this[j] = x) {}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
contains: {
|
||||
value: function <T>(obj: T | T[] | {} | {}[], child?: string): boolean {
|
||||
let result = false
|
||||
if (child) {
|
||||
const isArr = Array.isArray(obj)
|
||||
if (isArr) {
|
||||
// @ts-ignore
|
||||
if (obj[0].hasOwnProperty(child)) {
|
||||
let set0 = new Set()
|
||||
// @ts-ignore
|
||||
for (let s of obj) {
|
||||
// @ts-ignore
|
||||
set0.add(s[child])
|
||||
}
|
||||
// @ts-ignore
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return set0.size === set1.size
|
||||
} else {
|
||||
// @ts-ignore
|
||||
let set0 = new Set(obj)
|
||||
let set1 = new Set(this.filter((x: {}) => set0.has(x)))
|
||||
return set1.size === set0.size
|
||||
}
|
||||
} else {
|
||||
if (obj.hasOwnProperty(child)) {
|
||||
for (let sub of this) {
|
||||
if (sub.hasOwnProperty(child)) {
|
||||
// @ts-ignore
|
||||
if (sub[child] === obj[child]) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let sub of this) {
|
||||
if (sub.hasOwnProperty(child)) {
|
||||
// @ts-ignore
|
||||
if (sub[child] === obj) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不指定 比较字段 的话, 只处理2种情况
|
||||
// 1: obj 为数组
|
||||
// 2: obj 不是数组
|
||||
if (Array.isArray(obj)) {
|
||||
let set0 = new Set(obj)
|
||||
// @ts-ignore
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return set1.size === set0.size
|
||||
} else {
|
||||
let idx = this.indexOf(obj)
|
||||
return !!~idx
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomInsert: {
|
||||
value: function <T>(arr: Array<T>) {
|
||||
const length = this.length
|
||||
arr.forEach(value => {
|
||||
this.splice(Math.random() * length, 0, value)
|
||||
})
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomGet: {
|
||||
value: function <T>(count: number = 1): T[] {
|
||||
let shuffled: T[] = this.slice(0),
|
||||
i = this.length,
|
||||
min = i - count,
|
||||
temp,
|
||||
index
|
||||
if (min < 0) {
|
||||
return shuffled
|
||||
}
|
||||
while (i-- > min) {
|
||||
index = Math.floor((i + 1) * Math.random())
|
||||
temp = shuffled[index]
|
||||
shuffled[index] = shuffled[i]
|
||||
shuffled[i] = temp
|
||||
}
|
||||
return shuffled.slice(min)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomOne: {
|
||||
value: function <T>(): T {
|
||||
let results = this.randomGet(1)
|
||||
if (results.length > 0) {
|
||||
return results[0]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomRemove: {
|
||||
value: function <T>(count: number = 1): T[] {
|
||||
let result = []
|
||||
while (count-- > 0 && this.length > 0) {
|
||||
let index = (Math.random() * this.length) | 0
|
||||
result.push(...this.splice(index, 1))
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
moveElement: {
|
||||
value: function <T>(n: number): T[] {
|
||||
if (Math.abs(n) > this.length) n = n % this.length
|
||||
return this.slice(-n).concat(this.slice(0, -n))
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
union: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let a = this.concat(b)
|
||||
return [...new Set(a)]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
intersect: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let set0 = new Set(b)
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return [...set1]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
difference: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let set0 = new Set(b)
|
||||
let set1 = new Set(this.filter(x => !set0.has(x)))
|
||||
return [...set1]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
toMap: {
|
||||
value: function <T>(this: T[], key: string) {
|
||||
let result: Map<any, T> = new Map()
|
||||
for (const o of this) {
|
||||
// @ts-ignore
|
||||
result.set(o[key], o)
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface Map<K, V> {
|
||||
/**
|
||||
* 只针对V为number的Map, 有值的话, 加上V, 没值则直接set
|
||||
* V为其他类型时, 直接set
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
inc?(key: K, value: V): number
|
||||
}
|
||||
|
||||
Object.defineProperties(Map.prototype, {
|
||||
inc: {
|
||||
value: function <K, V>(key: K, value: V) {
|
||||
if (typeof value == 'number') {
|
||||
this.set(key, (this.get(key) || 0) + value)
|
||||
} else {
|
||||
this.set(key, value)
|
||||
}
|
||||
return this.get(key)
|
||||
},
|
||||
},
|
||||
})
|
13
src/common/ZError.ts
Normal file
13
src/common/ZError.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { FastifyError } from 'fastify'
|
||||
|
||||
export class ZError implements FastifyError {
|
||||
code: string
|
||||
statusCode?: number
|
||||
message: string
|
||||
name: string
|
||||
|
||||
constructor(statusCode: number, message: string) {
|
||||
this.statusCode = statusCode
|
||||
this.message = message
|
||||
}
|
||||
}
|
7
src/common/base.controller.ts
Normal file
7
src/common/base.controller.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import fastify = require('fastify')
|
||||
|
||||
export const ROLE_ANON = 'anon'
|
||||
class BaseController {
|
||||
aotoRoute(req: fastify.FastifyRequest, res) {}
|
||||
}
|
||||
export default BaseController
|
9
src/config/events.json
Normal file
9
src/config/events.json
Normal file
@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"address": "0x8135D4F16A7AAA269cbf61CE9659D3A272BF541f",
|
||||
"event": "Confirmation",
|
||||
"abi": "BEMultiSigWallet",
|
||||
"fromBlock": 34353697,
|
||||
"eventProcesser": "ScheduleConfirmEvent"
|
||||
}
|
||||
]
|
55
src/controllers/chain.controllers.ts
Normal file
55
src/controllers/chain.controllers.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import BaseController from 'common/base.controller'
|
||||
import { role, router } from 'decorators/router'
|
||||
import { ChainTask, ChainTaskClass } from 'models/ChainTask'
|
||||
import { RequestTask } from 'models/RequestTask'
|
||||
import { ChainQueue } from 'queue/chain.queue'
|
||||
import { DocumentType } from '@typegoose/typegoose'
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { ZError } from 'common/ZError'
|
||||
|
||||
class ChainController extends BaseController {
|
||||
@role('anon')
|
||||
@router('post /chain/req')
|
||||
async addChainRequest(req, res) {
|
||||
// data是一个数组!!
|
||||
const { data, source, cb, taskId, force, max } = req.params
|
||||
console.log(`income chain request:: taskId: ${taskId}, source: ${source}, cb: ${cb}`)
|
||||
console.log(JSON.stringify(data))
|
||||
let maxTryCount = max !== undefined ? parseInt(max) : parseInt(process.env.CHAIN_MAX_TRY)
|
||||
let chainTask: DocumentType<ChainTaskClass> = await ChainTask.insertOrUpdate(
|
||||
{ taskId },
|
||||
{ source, taskData: data, cb },
|
||||
)
|
||||
|
||||
if ((!chainTask.newRecord && force) || chainTask.newRecord) {
|
||||
for (let sub of data) {
|
||||
let subType = sub.type
|
||||
let subTask = new RequestTask({
|
||||
taskId,
|
||||
chainTaskId: chainTask.id,
|
||||
taskType: subType,
|
||||
reqData: sub,
|
||||
maxTryCount,
|
||||
})
|
||||
await subTask.save()
|
||||
chainTask.tasks.pushOnce(subTask.id)
|
||||
new ChainQueue().addTaskToQueue(subTask)
|
||||
}
|
||||
}
|
||||
chainTask.newRecord = false
|
||||
await chainTask.save()
|
||||
return chainTask.toJson()
|
||||
}
|
||||
|
||||
@role('anon')
|
||||
@router('post /chain/query_info')
|
||||
async queryUserInfo(req, res) {
|
||||
let { address } = req.params
|
||||
if (!address) {
|
||||
throw new ZError(10, 'address is required')
|
||||
}
|
||||
let info = await new BlockChain().distributorReactor.getMintableCount({ user: address })
|
||||
return { count: info }
|
||||
}
|
||||
}
|
||||
export default ChainController
|
34
src/controllers/token.controllers.ts
Normal file
34
src/controllers/token.controllers.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { ZError } from 'common/ZError'
|
||||
import BaseController from 'common/base.controller'
|
||||
import { role, router } from 'decorators/router'
|
||||
|
||||
class TokenController extends BaseController {
|
||||
@role('anon')
|
||||
@router('post /chain/estimate_transfer_gas')
|
||||
async calcTransPrice(req, res) {
|
||||
const { address } = req.params
|
||||
const bc = new BlockChain()
|
||||
const account = bc.currentAccount
|
||||
let gas = await bc.erc20Reactor.transfer({
|
||||
address: address + '',
|
||||
to: account,
|
||||
amount: 0,
|
||||
estimate: true,
|
||||
})
|
||||
let data = bc.generateGasShow(gas)
|
||||
return data
|
||||
}
|
||||
|
||||
@role('anon')
|
||||
@router('post /chain/estimate_gas')
|
||||
async calcGasPrice(req, res) {
|
||||
let { gas } = req.params
|
||||
if (!gas) {
|
||||
throw new ZError(10, 'gas is required')
|
||||
}
|
||||
let data = new BlockChain().generateGasShow(gas)
|
||||
return data
|
||||
}
|
||||
}
|
||||
export default TokenController
|
19
src/decorators/dbconn.ts
Normal file
19
src/decorators/dbconn.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { mongoose } from '@typegoose/typegoose'
|
||||
|
||||
/**
|
||||
* 为model指定数据库连接
|
||||
* @param {string} name 数据库连接名字, 在config中必须要有对应的配置, 比如main, 则必须要有 db_main
|
||||
* */
|
||||
export function dbconn(name?: string) {
|
||||
return target => {
|
||||
name = name || 'main'
|
||||
const dbName = ('db_' + name).toUpperCase()
|
||||
const url = process.env[dbName]
|
||||
target['db'] = mongoose.createConnection(url, {
|
||||
useNewUrlParser: true,
|
||||
useCreateIndex: true,
|
||||
useFindAndModify: false,
|
||||
useUnifiedTopology: true,
|
||||
})
|
||||
}
|
||||
}
|
29
src/decorators/nojson.ts
Normal file
29
src/decorators/nojson.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
import { singleton } from './singleton'
|
||||
|
||||
@singleton
|
||||
export class NoJsonClass {
|
||||
private noJsonPropSet: Set<string> = new Set()
|
||||
|
||||
public addKey(className: string, propertyKey: string) {
|
||||
this.noJsonPropSet.add(className + '_' + propertyKey)
|
||||
}
|
||||
|
||||
public checkExist(className: string, propertyKey: string) {
|
||||
return this.noJsonPropSet.has(className + '_' + propertyKey)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 在不需要toJson方法输出的字段上加上 @noJson
|
||||
* @return {{(target: Function): void, (target: Object, propertyKey: (string | symbol)): void}}
|
||||
*/
|
||||
export function noJson() {
|
||||
// return Reflect.metadata(noJsonMetadataKey, !0)
|
||||
return function (target: Object, propertyKey: string) {
|
||||
new NoJsonClass().addKey(target.constructor.name, propertyKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function checkJson(target: any, propertyKey: string) {
|
||||
return !new NoJsonClass().checkExist(target.constructor.modelName, propertyKey)
|
||||
}
|
142
src/decorators/router.ts
Normal file
142
src/decorators/router.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import BaseController from '../common/base.controller'
|
||||
|
||||
export class RouterData {
|
||||
target?: any
|
||||
method?: string
|
||||
path?: string
|
||||
fun?: Function
|
||||
}
|
||||
|
||||
export class RouterMap {
|
||||
static decoratedRouters: Map<
|
||||
Function,
|
||||
{
|
||||
roles?: string[]
|
||||
permissions?: string[][]
|
||||
data?: RouterData[]
|
||||
depts?: string[]
|
||||
}
|
||||
> = new Map()
|
||||
}
|
||||
|
||||
export function router(route?: string) {
|
||||
return (target: BaseController, name: string, value: PropertyDescriptor) => {
|
||||
if (!route) {
|
||||
const controller = target.constructor.name
|
||||
const controllerName = controller.toLowerCase().replace('.controller', '')
|
||||
route = 'all ' + ['', controllerName, name].join('/')
|
||||
}
|
||||
const split = route.split(' ')
|
||||
if (split.length > 2) {
|
||||
throw new Error('路由中只允许一个空格')
|
||||
}
|
||||
const [method, path] = split
|
||||
// @ts-ignore
|
||||
const key = target[name]
|
||||
let routerData = new RouterData()
|
||||
routerData.target = target
|
||||
routerData.method = method
|
||||
routerData.path = path
|
||||
// @ts-ignore
|
||||
routerData.fun = target[name]
|
||||
|
||||
if (RouterMap.decoratedRouters.has(key)) {
|
||||
let objCurrent = RouterMap.decoratedRouters.get(key)
|
||||
if (!objCurrent.data) {
|
||||
objCurrent.data = [routerData]
|
||||
} else {
|
||||
objCurrent.data.push(routerData)
|
||||
}
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], objCurrent)
|
||||
} else {
|
||||
let routerObj = {
|
||||
data: [routerData],
|
||||
}
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], routerObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function role(roles?: string | string[]) {
|
||||
return (target: BaseController, name: string, value: PropertyDescriptor) => {
|
||||
let roleList: string[] = []
|
||||
if (roles) {
|
||||
if (Array.isArray(roles)) {
|
||||
roleList = roles
|
||||
} else {
|
||||
roleList = [roles]
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const key = target[name]
|
||||
let roleObj = { roles: roleList }
|
||||
if (RouterMap.decoratedRouters.has(key)) {
|
||||
let objCurrent = RouterMap.decoratedRouters.get(key)
|
||||
Object.assign(objCurrent, roleObj)
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], objCurrent)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], roleObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function permission(permissions?: string | string[]) {
|
||||
return (target: BaseController, name: string, value: PropertyDescriptor) => {
|
||||
let permissionList: string[][] = [[]]
|
||||
if (permissions) {
|
||||
if (Array.isArray(permissions)) {
|
||||
let arr = []
|
||||
for (let sub of permissions) {
|
||||
arr.push(sub.split(':'))
|
||||
}
|
||||
permissionList = arr
|
||||
} else {
|
||||
permissionList = [permissions.split(':')]
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const key = target[name]
|
||||
let permissionObj = { permissions: permissionList }
|
||||
if (RouterMap.decoratedRouters.has(key)) {
|
||||
let objCurrent = RouterMap.decoratedRouters.get(key)
|
||||
Object.assign(objCurrent, permissionObj)
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], objCurrent)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], permissionObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有dept修饰器的, 需要验证部门id是否存在
|
||||
*/
|
||||
export function dept(depts?: string | string[]) {
|
||||
return (target: BaseController, name: string, value: PropertyDescriptor) => {
|
||||
let deptList: string[] = []
|
||||
if (depts) {
|
||||
if (Array.isArray(depts)) {
|
||||
deptList = depts
|
||||
} else {
|
||||
deptList = [depts]
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const key = target[name]
|
||||
let deptObj = { depts: deptList }
|
||||
if (RouterMap.decoratedRouters.has(key)) {
|
||||
let objCurrent = RouterMap.decoratedRouters.get(key)
|
||||
Object.assign(objCurrent, deptObj)
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], objCurrent)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
RouterMap.decoratedRouters.set(target[name], deptObj)
|
||||
}
|
||||
}
|
||||
}
|
29
src/decorators/singleton.ts
Normal file
29
src/decorators/singleton.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 单例化一个class
|
||||
* 使用方法:
|
||||
* @singleton
|
||||
* class Test {}
|
||||
* new Test() === new Test() // returns `true`
|
||||
* 也可以不使用 decorator
|
||||
* const TestSingleton = singleton(Test)
|
||||
* new TestSingleton() === new TestSingleton() //returns 'true'
|
||||
*/
|
||||
|
||||
export const SINGLETON_KEY = Symbol()
|
||||
|
||||
export type Singleton<T extends new (...args: any[]) => any> = T & {
|
||||
[SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
|
||||
}
|
||||
export const singleton = <T extends new (...args: any[]) => any>(classTarget: T) =>
|
||||
new Proxy(classTarget, {
|
||||
construct(target: Singleton<T>, argumentsList, newTarget) {
|
||||
// Skip proxy for children
|
||||
if (target.prototype !== newTarget.prototype) {
|
||||
return Reflect.construct(target, argumentsList, newTarget)
|
||||
}
|
||||
if (!target[SINGLETON_KEY]) {
|
||||
target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)
|
||||
}
|
||||
return target[SINGLETON_KEY]
|
||||
},
|
||||
})
|
3
src/logger/logger.ts
Normal file
3
src/logger/logger.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const level = process.env.NODE_ENV === 'production' ? 'info' : 'log'
|
||||
const logger = require('tracer').colorConsole({ dateformat: 'yyyy-mm-dd HH:MM:ss.L', level })
|
||||
export default logger
|
201
src/models/Base.ts
Normal file
201
src/models/Base.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses'
|
||||
import { checkJson } from '../decorators/nojson'
|
||||
import { plugin, ReturnModelType } from '@typegoose/typegoose'
|
||||
|
||||
// @ts-ignore
|
||||
import findOrCreate from 'mongoose-findorcreate'
|
||||
import { Connection } from 'mongoose'
|
||||
import { ObjectId } from 'bson'
|
||||
import { isTrue } from '../utils/string.util'
|
||||
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types'
|
||||
|
||||
const jsonExcludeKeys = ['updatedAt', '__v']
|
||||
const saveExcludeKeys = ['createdAt', 'updatedAt', '__v', '_id']
|
||||
|
||||
@plugin(findOrCreate)
|
||||
export abstract class BaseModule extends FindOrCreate {
|
||||
static db: Connection
|
||||
|
||||
public updateFromReq(data: any) {
|
||||
for (let key in data) {
|
||||
if (saveExcludeKeys.indexOf(key) == -1) {
|
||||
this[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新
|
||||
* @param condition
|
||||
* @param data
|
||||
*/
|
||||
public static insertOrUpdate<T extends BaseModule>(
|
||||
this: ReturnModelType<AnyParamConstructor<T>>,
|
||||
condition: any,
|
||||
data: any,
|
||||
) {
|
||||
return this.findOneAndUpdate(condition, data, { upsert: true, new: true, setDefaultsOnInsert: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟删除
|
||||
* @param {string[]} ids
|
||||
*/
|
||||
public static deleteVirtual<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, ids: string[]) {
|
||||
return this.updateMany(
|
||||
// @ts-ignore
|
||||
{
|
||||
_id: { $in: ids },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
deleted: true,
|
||||
deleteTime: new Date(),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义分页查询
|
||||
* @param data
|
||||
* @param {boolean} json
|
||||
*/
|
||||
public static async pageQuery<T extends BaseModule>(
|
||||
this: ReturnModelType<AnyParamConstructor<T>>,
|
||||
data: any,
|
||||
json: boolean = false,
|
||||
) {
|
||||
let { start, limit, page } = data
|
||||
limit = +limit || 10
|
||||
start = +start || (+page - 1) * limit || 0
|
||||
// @ts-ignore
|
||||
let { opt, sort } = this.parseQueryParam(data)
|
||||
let records = await this.find(opt).sort(sort).skip(start).limit(limit)
|
||||
let total = await this.countDocuments(opt)
|
||||
if (json) {
|
||||
records.map((o: T) => o.toJson())
|
||||
}
|
||||
return { records, total, start, limit }
|
||||
}
|
||||
|
||||
public toJson() {
|
||||
let result: any = {}
|
||||
// @ts-ignore
|
||||
for (let key in this._doc) {
|
||||
if (checkJson(this, key + '') && jsonExcludeKeys.indexOf(key) == -1) {
|
||||
result[key] = this[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的查询条件拼接方法
|
||||
* @param {{}} params req.params
|
||||
* @param options
|
||||
* sort: 排序 比如: {createdAt: 1} 默认是 {_id: 1}
|
||||
* opt: 设置一些特殊的过滤条件, 比如{deleted: 0}
|
||||
* timeKey: 如果需要查询创建时间, 而且创建时间不为 createdAt, 可以用此字段设置
|
||||
* matchKey: 指定关键字查询的匹配字段, 可为string或[string]
|
||||
*
|
||||
* @return {{opt: any, sort: {_id: number}}}
|
||||
*/
|
||||
public static parseQueryParam(params: {}, options?: any) {
|
||||
const opt: any = { deleted: false }
|
||||
// @ts-ignore
|
||||
let obj = this.schema.paths
|
||||
for (let key in params) {
|
||||
if (key !== 'sort' && obj.hasOwnProperty(key)) {
|
||||
switch (obj[key].instance) {
|
||||
case 'String':
|
||||
opt[key] = { $regex: params[key], $options: 'i' }
|
||||
break
|
||||
case 'Number':
|
||||
opt[key] = params[key]
|
||||
break
|
||||
case 'Array':
|
||||
if (Array.isArray(params[key])) {
|
||||
opt[key] = { $in: params[key] }
|
||||
} else {
|
||||
opt[key] = params[key]
|
||||
}
|
||||
break
|
||||
case 'Date':
|
||||
// TODO:
|
||||
break
|
||||
case 'Boolean':
|
||||
opt[key] = isTrue(params[key])
|
||||
break
|
||||
case 'ObjectID':
|
||||
if (/^[0-9a-fA-F]{24}$/.test(params[key])) {
|
||||
opt[key] = new ObjectId(params[key])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.hasOwnProperty('key') && params['key']) {
|
||||
let orArr = []
|
||||
if (options?.matchKey) {
|
||||
if (Array.isArray(options?.matchKey)) {
|
||||
for (let key in options?.matchKey) {
|
||||
let _tmp = {}
|
||||
_tmp[key] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
} else {
|
||||
let _tmp = {}
|
||||
_tmp[options.matchKey] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
} else {
|
||||
for (let key in obj) {
|
||||
if (obj[key].instance === 'String') {
|
||||
let _tmp = {}
|
||||
_tmp[key] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(opt, { $or: orArr })
|
||||
}
|
||||
let timeKey = options?.timeKey ? options.timeKey : 'createdAt'
|
||||
if (params.hasOwnProperty('timeBegin') && !params.hasOwnProperty('timeEnd')) {
|
||||
let timeBegin = params['timeBegin']
|
||||
if (!(timeBegin instanceof Date)) {
|
||||
timeBegin = new Date(parseInt(timeBegin))
|
||||
}
|
||||
opt[timeKey] = { $gte: timeBegin }
|
||||
} else if (params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) {
|
||||
let timeBegin = params['timeBegin']
|
||||
if (!(timeBegin instanceof Date)) {
|
||||
timeBegin = new Date(parseInt(timeBegin))
|
||||
}
|
||||
let timeEnd = params['timeEnd']
|
||||
if (!(timeEnd instanceof Date)) {
|
||||
timeEnd = new Date(parseInt(timeEnd))
|
||||
}
|
||||
let tmpB = {}
|
||||
tmpB[timeKey] = { $gte: timeBegin }
|
||||
let tmpE = {}
|
||||
tmpE[timeKey] = { $lte: timeEnd }
|
||||
opt['$and'] = [tmpB, tmpE]
|
||||
} else if (!params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) {
|
||||
let timeEnd = params['timeEnd']
|
||||
if (!(timeEnd instanceof Date)) {
|
||||
timeEnd = new Date(parseInt(timeEnd))
|
||||
}
|
||||
opt[timeKey] = { $lte: timeEnd }
|
||||
}
|
||||
if (options?.opt) {
|
||||
Object.assign(opt, options.opt)
|
||||
}
|
||||
let sort = { _id: 1 }
|
||||
if (params.hasOwnProperty('sort')) {
|
||||
sort = params['sort']
|
||||
}
|
||||
return { opt, sort }
|
||||
}
|
||||
}
|
132
src/models/ChainTask.ts
Normal file
132
src/models/ChainTask.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { getModelForClass, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import logger from 'logger/logger'
|
||||
import { InfoSvr } from 'service/info.service'
|
||||
|
||||
import { BaseModule } from './Base'
|
||||
import { ReqTaskStatus, RequestTask } from './RequestTask'
|
||||
import { PriceSvr } from 'service/price.service'
|
||||
|
||||
export enum TaskStatus {
|
||||
NOTSTART = 0,
|
||||
PEDING = 1,
|
||||
SUCCESS = 2,
|
||||
PART_ERROR = 8,
|
||||
ERROR = 9,
|
||||
}
|
||||
|
||||
@dbconn()
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'chain_task', timestamps: true },
|
||||
options: { allowMixed: Severity.ALLOW },
|
||||
})
|
||||
export class ChainTaskClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public taskId!: string
|
||||
|
||||
@prop({ type: mongoose.Schema.Types.Mixed })
|
||||
public taskData: any
|
||||
|
||||
@prop()
|
||||
public source: string
|
||||
|
||||
@prop({ default: 0 })
|
||||
public gas: number
|
||||
|
||||
@prop()
|
||||
public cb: string
|
||||
|
||||
@prop({ enum: TaskStatus, default: TaskStatus.NOTSTART })
|
||||
public status: TaskStatus
|
||||
|
||||
@prop()
|
||||
public startTime: number
|
||||
|
||||
@prop()
|
||||
public endTime: number
|
||||
|
||||
@prop({ required: true, default: true })
|
||||
public newRecord: boolean
|
||||
|
||||
@prop({ required: true, default: false })
|
||||
public allEnd: boolean
|
||||
|
||||
@prop({ required: true, default: 0 })
|
||||
public successCount: number
|
||||
|
||||
@prop({ required: true, default: 0 })
|
||||
public errorCount: number
|
||||
|
||||
@prop({ type: String, required: true, default: [] })
|
||||
public tasks!: Array<string>
|
||||
|
||||
/**
|
||||
* 检查是否所有的任务都已完成(成功或重试次数达到上限)
|
||||
* 调用时机
|
||||
* 1. 每个 request task 成功
|
||||
* 2. 每个 request task 重试达到预设次数
|
||||
* @param chainTaskId
|
||||
*/
|
||||
public static async checkStatus(chainTaskId: string) {
|
||||
let record = await ChainTask.findById(chainTaskId)
|
||||
let sCount = 0
|
||||
let errCount = 0
|
||||
let hashList: string[] = []
|
||||
for (let subId of record.tasks) {
|
||||
let subData = await RequestTask.findById(subId)
|
||||
if (subData.status === ReqTaskStatus.SUCCESS) {
|
||||
sCount += 1
|
||||
} else if (subData.status === ReqTaskStatus.ERROR) {
|
||||
errCount += 1
|
||||
}
|
||||
if (subData.gasUsed) {
|
||||
record.gas += subData.gasUsed
|
||||
}
|
||||
hashList.push(subData.txHash)
|
||||
}
|
||||
record.successCount = sCount
|
||||
record.errorCount = errCount
|
||||
if (sCount === record.tasks.length) {
|
||||
record.status = TaskStatus.SUCCESS
|
||||
record.allEnd = true
|
||||
} else {
|
||||
record.allEnd = false
|
||||
if (record.status === TaskStatus.NOTSTART && sCount > 0) {
|
||||
record.status = TaskStatus.PEDING
|
||||
} else if (errCount === record.tasks.length) {
|
||||
record.status = TaskStatus.ERROR
|
||||
record.allEnd = true
|
||||
} else if (errCount + sCount === record.tasks.length) {
|
||||
record.status = TaskStatus.PART_ERROR
|
||||
record.allEnd = true
|
||||
}
|
||||
}
|
||||
await record.save()
|
||||
if (record.allEnd) {
|
||||
setImmediate(async function () {
|
||||
try {
|
||||
let gasPrice = await new PriceSvr().refreshGasPrice()
|
||||
let result = await new InfoSvr().reportTaskResult({
|
||||
id: record.taskId,
|
||||
result: record.status,
|
||||
successCount: record.successCount,
|
||||
errorCount: record.errorCount,
|
||||
gas: record.gas,
|
||||
gasPrice,
|
||||
hashList,
|
||||
cb: record.cb,
|
||||
})
|
||||
// logger.log(result)
|
||||
} catch (err) {
|
||||
logger.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public static async allUnFinishedTask() {
|
||||
return ChainTask.find({ allEnd: false })
|
||||
}
|
||||
}
|
||||
|
||||
export const ChainTask = getModelForClass(ChainTaskClass, { existingConnection: ChainTaskClass['db'] })
|
58
src/models/FtTransferEvent.ts
Normal file
58
src/models/FtTransferEvent.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
@dbconn()
|
||||
@index({ address: 1 }, { unique: false })
|
||||
@index({ transactionHash: 1, from: 1, to: 1 }, { unique: true })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'ft_transfer_event', timestamps: true },
|
||||
})
|
||||
export class FtTransferEventClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public address!: string
|
||||
@prop()
|
||||
public event: string
|
||||
@prop({ required: true })
|
||||
public transactionHash: string
|
||||
@prop()
|
||||
public blockNumber: number
|
||||
@prop()
|
||||
public blockHash: string
|
||||
@prop()
|
||||
public removed: boolean
|
||||
@prop()
|
||||
public from: string
|
||||
@prop()
|
||||
public to: string
|
||||
@prop()
|
||||
public amount: string
|
||||
@prop()
|
||||
public blockTime: number
|
||||
|
||||
public static async saveEvent(event: any) {
|
||||
const amount = event.returnValues?.value
|
||||
if (amount == undefined) {
|
||||
return
|
||||
}
|
||||
const from = event.returnValues?.from
|
||||
const to = event.returnValues?.to
|
||||
const data = {
|
||||
address: event.address,
|
||||
blockNumber: event.blockNumber,
|
||||
blockHash: event.blockHash,
|
||||
removed: event.removed,
|
||||
event: event.event,
|
||||
from,
|
||||
to,
|
||||
transactionHash: event.transactionHash,
|
||||
amount,
|
||||
blockTime: event.timestamp * 1000
|
||||
}
|
||||
return FtTransferEvent.insertOrUpdate({ transactionHash: event.transactionHash, amount, from, to }, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const FtTransferEvent = getModelForClass(FtTransferEventClass, {
|
||||
existingConnection: FtTransferEventClass['db'],
|
||||
})
|
63
src/models/NftTransferEvent.ts
Normal file
63
src/models/NftTransferEvent.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
@dbconn()
|
||||
@index({ tokenId: 1 }, { unique: false })
|
||||
@index({ transactionHash: 1, tokenId: 1, from: 1, to: 1 }, { unique: true })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'nft_transfer_event', timestamps: true },
|
||||
})
|
||||
export class NftTransferEventClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public address!: string
|
||||
@prop()
|
||||
public event: string
|
||||
@prop({ required: true })
|
||||
public transactionHash: string
|
||||
@prop()
|
||||
public blockNumber: number
|
||||
@prop()
|
||||
public blockHash: string
|
||||
@prop()
|
||||
public removed: boolean
|
||||
@prop()
|
||||
public from: string
|
||||
@prop()
|
||||
public to: string
|
||||
@prop()
|
||||
public tokenId: string
|
||||
@prop()
|
||||
public blockTime: number
|
||||
@prop({ default: 0 })
|
||||
public version: number
|
||||
|
||||
public static async saveEvent(event: any) {
|
||||
if (!event.success) {
|
||||
return
|
||||
}
|
||||
const tokenId = event.tokenId
|
||||
if (!tokenId) {
|
||||
return
|
||||
}
|
||||
const from = event.source
|
||||
const to = event.target
|
||||
const data = {
|
||||
address: event.tokenAddress,
|
||||
blockNumber: event.blockHeight,
|
||||
removed: event.removed,
|
||||
from,
|
||||
to,
|
||||
transactionHash: event.hash,
|
||||
tokenId,
|
||||
blockTime: new Date(event.time).getTime(),
|
||||
$inc: { version: 1 },
|
||||
}
|
||||
|
||||
return NftTransferEvent.insertOrUpdate({ transactionHash: event.hash, tokenId, from, to }, data)
|
||||
}
|
||||
}
|
||||
|
||||
export const NftTransferEvent = getModelForClass(NftTransferEventClass, {
|
||||
existingConnection: NftTransferEventClass['db'],
|
||||
})
|
132
src/models/RequestTask.ts
Normal file
132
src/models/RequestTask.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { getModelForClass, DocumentType, modelOptions, mongoose, prop, Severity, index } from '@typegoose/typegoose'
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import logger from 'logger/logger'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
export enum TaskType {
|
||||
UNKNOW = 0,
|
||||
MINT_FT = 1,
|
||||
MINT_NFT = 2,
|
||||
TRANSFER_FT = 3,
|
||||
TRANSFER_NFT = 4,
|
||||
PUBLISH_AIRDROP_LIST = 5,
|
||||
TRANSFER_ETH = 6,
|
||||
}
|
||||
|
||||
export const TaskTypeMap = new Map([
|
||||
[TaskType.UNKNOW, 'Unknow'],
|
||||
[TaskType.MINT_FT, 'Mint Ft'],
|
||||
[TaskType.MINT_NFT, 'Mint NFT'],
|
||||
[TaskType.TRANSFER_FT, 'Ft转账'],
|
||||
[TaskType.TRANSFER_NFT, 'NFT转账'],
|
||||
[TaskType.PUBLISH_AIRDROP_LIST, '公布空投名单'],
|
||||
[TaskType.TRANSFER_ETH, 'ETH转账'],
|
||||
])
|
||||
|
||||
export enum ReqTaskStatus {
|
||||
NOTSTART = 0,
|
||||
PEDING = 1,
|
||||
WAIT_CONFIRM = 2,
|
||||
SUCCESS = 3,
|
||||
REVERT = 98,
|
||||
ERROR = 99,
|
||||
}
|
||||
|
||||
@dbconn()
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'chain_request_task', timestamps: true },
|
||||
options: { allowMixed: Severity.ALLOW },
|
||||
})
|
||||
export class RequestTaskClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public taskId!: string
|
||||
|
||||
@prop({ required: true })
|
||||
public chainTaskId!: string
|
||||
|
||||
@prop({ enum: TaskType, default: TaskType.UNKNOW })
|
||||
public taskType: TaskType
|
||||
|
||||
@prop({ required: true, default: 0 })
|
||||
public tryCount: number
|
||||
|
||||
@prop({ required: true, default: 0 })
|
||||
public maxTryCount: number
|
||||
/**
|
||||
* address: string
|
||||
*/
|
||||
@prop({ type: mongoose.Schema.Types.Mixed })
|
||||
public reqData: any
|
||||
|
||||
@prop({ enum: ReqTaskStatus, default: ReqTaskStatus.NOTSTART })
|
||||
public status: ReqTaskStatus
|
||||
|
||||
@prop({ required: true, default: true })
|
||||
public newRecord: boolean
|
||||
|
||||
@prop()
|
||||
public startTime: number
|
||||
|
||||
@prop()
|
||||
public endTime: number
|
||||
|
||||
@prop()
|
||||
public txHash: string
|
||||
|
||||
@prop()
|
||||
public gasUsed: number
|
||||
/**
|
||||
* 添加时的block num
|
||||
*/
|
||||
@prop()
|
||||
public blockAdd: number
|
||||
/**
|
||||
* 请求时的block num
|
||||
*/
|
||||
@prop()
|
||||
public blockReq: number
|
||||
|
||||
@prop({ type: mongoose.Schema.Types.Mixed, default: [] })
|
||||
public errMsg: any[]
|
||||
|
||||
public async requestChain(this: DocumentType<RequestTaskClass>) {
|
||||
let result
|
||||
let self = this
|
||||
self.blockReq = new BlockChain().currentBlockNum
|
||||
await self.save()
|
||||
|
||||
switch (self.taskType) {
|
||||
case TaskType.MINT_FT:
|
||||
result = await new BlockChain().erc20Reactor.mint(self.reqData)
|
||||
break
|
||||
case TaskType.MINT_NFT:
|
||||
result = await new BlockChain().erc721Reactor.batchMint(self.reqData)
|
||||
break
|
||||
case TaskType.TRANSFER_FT:
|
||||
result = await new BlockChain().erc20Reactor.transfer(self.reqData)
|
||||
break
|
||||
case TaskType.TRANSFER_NFT:
|
||||
result = await new BlockChain().erc721Reactor.transfer(self.reqData)
|
||||
break
|
||||
case TaskType.PUBLISH_AIRDROP_LIST:
|
||||
result = await new BlockChain().distributorReactor.mintNft(self.reqData)
|
||||
break
|
||||
case TaskType.TRANSFER_ETH:
|
||||
result = await new BlockChain().sendEth(self.reqData)
|
||||
break
|
||||
}
|
||||
logger.info(result)
|
||||
let { transactionHash, gasUsed } = result
|
||||
self.txHash = transactionHash
|
||||
self.gasUsed = gasUsed
|
||||
self.status = ReqTaskStatus.WAIT_CONFIRM
|
||||
await self.save()
|
||||
}
|
||||
|
||||
public static async allUnFinishedTask(chainTaskId: string) {
|
||||
return RequestTask.find({ chainTaskId, status: { $ne: ReqTaskStatus.SUCCESS } })
|
||||
}
|
||||
}
|
||||
|
||||
export const RequestTask = getModelForClass(RequestTaskClass, { existingConnection: RequestTaskClass['db'] })
|
64
src/models/ScheduleConfirmEvent.ts
Normal file
64
src/models/ScheduleConfirmEvent.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import logger from 'logger/logger'
|
||||
import { TaskSvr } from 'service/task.service'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
@dbconn()
|
||||
@index({ transactionHash: 1 }, { unique: true })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'schedule_confirm_event', timestamps: true },
|
||||
})
|
||||
export class ScheduleConfirmEventClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public address!: string
|
||||
@prop()
|
||||
public event: string
|
||||
@prop({ required: true })
|
||||
public transactionHash: string
|
||||
@prop()
|
||||
public blockNumber: number
|
||||
@prop()
|
||||
public blockHash: string
|
||||
@prop()
|
||||
public removed: boolean
|
||||
@prop()
|
||||
public operater: string
|
||||
@prop({ type: () => [String] })
|
||||
public scheduleIds: string[]
|
||||
@prop()
|
||||
public blockTime: number
|
||||
@prop({ default: 0 })
|
||||
public version: number
|
||||
|
||||
public static async saveEvent(event: any) {
|
||||
logger.info(JSON.stringify(event))
|
||||
if (event.removed) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
address: event.address,
|
||||
blockNumber: event.blockNumber,
|
||||
removed: event.removed,
|
||||
operater: event.returnValues.sender,
|
||||
scheduleIds: event.returnValues.ids,
|
||||
transactionHash: event.transactionHash,
|
||||
blockTime: new Date(event.timestamp).getTime(),
|
||||
$inc: { version: 1 },
|
||||
}
|
||||
|
||||
let record = await ScheduleConfirmEvent.insertOrUpdate({ transactionHash: event.transactionHash }, data)
|
||||
if (record.version === 1) {
|
||||
logger.log('receive events: ' + JSON.stringify(record.scheduleIds))
|
||||
for (let id of record.scheduleIds) {
|
||||
await new TaskSvr().parseOneSchedule(id)
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
export const ScheduleConfirmEvent = getModelForClass(ScheduleConfirmEventClass, {
|
||||
existingConnection: ScheduleConfirmEventClass['db'],
|
||||
})
|
59
src/models/ScheduleExecutedEvent.ts
Normal file
59
src/models/ScheduleExecutedEvent.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
@dbconn()
|
||||
@index({ transactionHash: 1, scheduleId: 1 }, { unique: true })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'schedule_executed_event', timestamps: true },
|
||||
})
|
||||
export class ScheduleExecutedEventClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public address!: string
|
||||
@prop()
|
||||
public event: string
|
||||
@prop({ required: true })
|
||||
public transactionHash: string
|
||||
@prop()
|
||||
public blockNumber: number
|
||||
@prop()
|
||||
public blockHash: string
|
||||
@prop()
|
||||
public removed: boolean
|
||||
@prop()
|
||||
public operater: string
|
||||
@prop()
|
||||
public scheduleId: string
|
||||
@prop()
|
||||
public blockTime: number
|
||||
@prop({ default: 0 })
|
||||
public version: number
|
||||
|
||||
public static async saveEvent(event: any) {
|
||||
if (event.removed) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
address: event.address,
|
||||
blockNumber: event.blockNumber,
|
||||
removed: event.removed,
|
||||
operater: event.returnValues.sender,
|
||||
transactionHash: event.transactionHash,
|
||||
blockTime: new Date(event.timestamp).getTime(),
|
||||
$inc: { version: 1 },
|
||||
}
|
||||
|
||||
let record = await ScheduleExecutedEvent.insertOrUpdate(
|
||||
{ transactionHash: event.transactionHash, scheduleId: event.returnValues.id },
|
||||
data,
|
||||
)
|
||||
if (record.version === 1) {
|
||||
}
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
export const ScheduleExecutedEvent = getModelForClass(ScheduleExecutedEventClass, {
|
||||
existingConnection: ScheduleExecutedEventClass['db'],
|
||||
})
|
56
src/models/ScheduledAddedEvent.ts
Normal file
56
src/models/ScheduledAddedEvent.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
@dbconn()
|
||||
@index({ transactionHash: 1, scheduleId: 1 }, { unique: true })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'schedule_added_event', timestamps: true },
|
||||
})
|
||||
export class ScheduledAddedEventClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public address!: string
|
||||
@prop()
|
||||
public event: string
|
||||
@prop({ required: true })
|
||||
public transactionHash: string
|
||||
@prop()
|
||||
public blockNumber: number
|
||||
@prop()
|
||||
public blockHash: string
|
||||
@prop()
|
||||
public removed: boolean
|
||||
@prop()
|
||||
public operater: string
|
||||
@prop()
|
||||
public scheduleId: string
|
||||
@prop()
|
||||
public blockTime: number
|
||||
@prop({ default: 0 })
|
||||
public version: number
|
||||
|
||||
public static async saveEvent(event: any) {
|
||||
if (event.removed) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
address: event.address,
|
||||
blockNumber: event.blockNumber,
|
||||
removed: event.removed,
|
||||
operater: event.returnValues.sender,
|
||||
transactionHash: event.transactionHash,
|
||||
blockTime: new Date(event.timestamp).getTime(),
|
||||
$inc: { version: 1 },
|
||||
}
|
||||
|
||||
return ScheduledAddedEvent.insertOrUpdate(
|
||||
{ transactionHash: event.transactionHash, scheduleId: event.returnValues.id },
|
||||
data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const ScheduledAddedEvent = getModelForClass(ScheduledAddedEventClass, {
|
||||
existingConnection: ScheduledAddedEventClass['db'],
|
||||
})
|
69
src/monitor.ts
Normal file
69
src/monitor.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import logger from 'logger/logger'
|
||||
import { RedisClient } from 'redis/RedisClient'
|
||||
const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development'
|
||||
dotenv.config({ path: envFile })
|
||||
import { EventSyncSvr } from 'service/event.sync.service'
|
||||
import { NftTransferEvent } from 'models/NftTransferEvent'
|
||||
import { FtTransferEvent } from 'models/FtTransferEvent'
|
||||
import { ScheduleConfirmEvent } from 'models/ScheduleConfirmEvent'
|
||||
import { ScheduleExecutedEvent } from 'models/ScheduleExecutedEvent'
|
||||
|
||||
import 'common/Extend'
|
||||
|
||||
let svrs: any[] = []
|
||||
let lock = false
|
||||
|
||||
let eventProcessers = {
|
||||
NftTransferEvent: NftTransferEvent,
|
||||
FtTransferEvent: FtTransferEvent,
|
||||
ScheduleConfirmEvent: ScheduleConfirmEvent,
|
||||
ScheduleExecutedEvent: ScheduleExecutedEvent,
|
||||
}
|
||||
|
||||
const events = require('config/events.json')
|
||||
|
||||
async function initEventSvrs() {
|
||||
// let nfts = [{ address: '0x37c30a2945799a53c5358636a721b442458fa691' }]
|
||||
for (let event of events) {
|
||||
let eventSvr = new EventSyncSvr({
|
||||
address: event.address,
|
||||
event: event.event,
|
||||
abi: require('abis/' + event.abi + '.json').abi,
|
||||
fromBlock: event.fromBlock,
|
||||
eventProcesser: eventProcessers[event.eventProcesser],
|
||||
})
|
||||
svrs.push(eventSvr)
|
||||
}
|
||||
}
|
||||
|
||||
async function parseAllEvents() {
|
||||
if (lock) {
|
||||
logger.info('sync in process, cancel.')
|
||||
return
|
||||
}
|
||||
lock = true
|
||||
logger.info('begin sync nft events: ' + svrs.length)
|
||||
for (let svr of svrs) {
|
||||
try {
|
||||
await svr.parseEvents()
|
||||
} catch (err) {
|
||||
logger.info('parse event with error:: address: ' + svr.address + ' event: ' + svr.event)
|
||||
logger.info(err)
|
||||
}
|
||||
}
|
||||
lock = false
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let opts = { url: process.env.REDIS }
|
||||
new RedisClient(opts)
|
||||
logger.info('REDIS Connected')
|
||||
await initEventSvrs()
|
||||
setInterval(function () {
|
||||
parseAllEvents()
|
||||
}, 60000)
|
||||
parseAllEvents()
|
||||
}
|
||||
|
||||
main()
|
49
src/plugins/apiauth.ts
Normal file
49
src/plugins/apiauth.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
roles?: string[]
|
||||
user?: any
|
||||
token?: string
|
||||
}
|
||||
interface FastifyInstance {
|
||||
apiAuth: (request: FastifyRequest, reply: FastifyReply) => {}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiAuthOptions {
|
||||
secret: string
|
||||
expiresIn: string
|
||||
}
|
||||
|
||||
const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastify, opts) {
|
||||
fastify.register(require('@fastify/jwt'), {
|
||||
secret: opts.secret,
|
||||
sign: { expiresIn: opts.expiresIn },
|
||||
})
|
||||
// 只有路由配置的role为anon才不需要过滤
|
||||
fastify.decorate('apiAuth', async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
if (!request.roles || request.roles.indexOf('anon') == -1) {
|
||||
try {
|
||||
if (!request.token) {
|
||||
return reply.send({ errcode: 11, errmsg: 'need login' })
|
||||
}
|
||||
//@ts-ignore
|
||||
const data = this.jwt.verify(request.token)
|
||||
if (!data || !data.id) {
|
||||
return reply.send({ errcode: 10, errmsg: 'need login' })
|
||||
}
|
||||
// let account = await Account.findById(data.id)
|
||||
// if (!account) {
|
||||
// return reply.send({ code: 10, msg: 'need login' })
|
||||
// }
|
||||
// request.user = account
|
||||
} catch (err) {
|
||||
return reply.send({ errcode: 401, errmsg: 'need auth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default fastifyPlugin(apiAuthPlugin, '4.x')
|
26
src/plugins/zReqParser.ts
Normal file
26
src/plugins/zReqParser.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
|
||||
/**
|
||||
* 将post 和 get 的参数统一到 req.params
|
||||
*/
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
zReqParser: (request: FastifyRequest, reply: FastifyReply) => {}
|
||||
}
|
||||
}
|
||||
const zReqParserPlugin: FastifyPluginAsync = async function (fastify: FastifyInstance, options?: any) {
|
||||
fastify.addHook('preValidation', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
let params = request.params || {}
|
||||
if (request.query) {
|
||||
Object.assign(params, request.query)
|
||||
}
|
||||
if (request.body) {
|
||||
Object.assign(params, request.body)
|
||||
}
|
||||
request.params = params
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
export default fastifyPlugin(zReqParserPlugin, '4.x')
|
62
src/plugins/zTokenParser.ts
Normal file
62
src/plugins/zTokenParser.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
|
||||
const getTokenFromHeader = function (request) {
|
||||
let token: string | undefined
|
||||
if (request.headers && request.headers.authorization) {
|
||||
const parts = request.headers.authorization.split(' ')
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0]
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
token = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return token
|
||||
}
|
||||
const getTokenFromCookie = function (request) {
|
||||
let token: string | undefined
|
||||
if (request.cookies) {
|
||||
if (request.cookies['token']) {
|
||||
token = request.cookies['token']
|
||||
}
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
const getTokenFromParams = function (request) {
|
||||
let token: string | undefined
|
||||
token = request.params && request.params.token
|
||||
return token
|
||||
}
|
||||
|
||||
const getTokenFromQuery = function (request) {
|
||||
let token: string | undefined
|
||||
token = request.query && request.query.token
|
||||
return token
|
||||
}
|
||||
|
||||
const getTokenFromBody = function (request) {
|
||||
let token: string | undefined
|
||||
token = request.body && request.body.token
|
||||
return token
|
||||
}
|
||||
|
||||
const zTokenParserPlugin: FastifyPluginAsync = async function (fastify: FastifyInstance, options?: any) {
|
||||
fastify.addHook('preValidation', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
request['token'] =
|
||||
getTokenFromHeader(request) ||
|
||||
getTokenFromCookie(request) ||
|
||||
getTokenFromParams(request) ||
|
||||
getTokenFromQuery(request) ||
|
||||
getTokenFromBody(request)
|
||||
})
|
||||
return
|
||||
}
|
||||
/**
|
||||
* 依次从request的header, cookie, params, query和body中获取token, 加入到request.token中
|
||||
* header中的字段key为authorization, 格式为 Bearer xxxx
|
||||
* 其他位置的key都为 token
|
||||
*/
|
||||
|
||||
export default fastifyPlugin(zTokenParserPlugin, '4.x')
|
26
src/plugins/zrbac.ts
Normal file
26
src/plugins/zrbac.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
import RBAC from 'fast-rbac'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
/**
|
||||
* RBAC interface
|
||||
*/
|
||||
rbac: RBAC
|
||||
}
|
||||
}
|
||||
|
||||
const zRBACPlugin: FastifyPluginAsync = async function fastifyMetrics(
|
||||
fastify: FastifyInstance,
|
||||
options?: RBAC.Options,
|
||||
) {
|
||||
const rbac = new RBAC(options)
|
||||
fastify.decorate('rbac', rbac)
|
||||
return
|
||||
}
|
||||
|
||||
export = fastifyPlugin(zRBACPlugin, {
|
||||
fastify: '>=3.0.0',
|
||||
name: 'zrbac',
|
||||
})
|
61
src/queue/chain.queue.ts
Normal file
61
src/queue/chain.queue.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import { DocumentType } from '@typegoose/typegoose'
|
||||
import { ReqTaskStatus, RequestTaskClass } from 'models/RequestTask'
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { ChainTask } from 'models/ChainTask'
|
||||
import logger from 'logger/logger'
|
||||
|
||||
@singleton
|
||||
export class ChainQueue {
|
||||
private queue: AsyncQueue
|
||||
private blockChain: BlockChain
|
||||
|
||||
constructor() {
|
||||
this.queue = createAsyncQueue()
|
||||
this.blockChain = new BlockChain()
|
||||
}
|
||||
|
||||
public async addTaskToQueue(subTask: DocumentType<RequestTaskClass>) {
|
||||
if (subTask.maxTryCount && subTask.tryCount > subTask.maxTryCount) {
|
||||
subTask.status = ReqTaskStatus.ERROR
|
||||
await subTask.save()
|
||||
await ChainTask.checkStatus(subTask.chainTaskId)
|
||||
return
|
||||
}
|
||||
if (subTask.status === ReqTaskStatus.NOTSTART) {
|
||||
subTask.blockAdd = this.blockChain.currentBlockNum
|
||||
await subTask.save()
|
||||
}
|
||||
if (subTask.status === ReqTaskStatus.WAIT_CONFIRM) {
|
||||
this.blockChain.confirmQueue.addTaskToQueue(subTask)
|
||||
return
|
||||
}
|
||||
this.queue.push(async () => {
|
||||
try {
|
||||
subTask.tryCount += 1
|
||||
if (subTask.status === ReqTaskStatus.NOTSTART) {
|
||||
subTask.status = ReqTaskStatus.PEDING
|
||||
}
|
||||
if (!subTask.startTime) {
|
||||
subTask.startTime = Date.now()
|
||||
}
|
||||
await subTask.save()
|
||||
try {
|
||||
await subTask.requestChain()
|
||||
} catch (reqerr) {
|
||||
logger.info(reqerr)
|
||||
subTask.errMsg.push(JSON.stringify(reqerr))
|
||||
await subTask.save()
|
||||
// TODO:: 要排除数据已经提交到链上, 返回过程中的网络错误
|
||||
this.addTaskToQueue(subTask)
|
||||
return
|
||||
}
|
||||
this.blockChain.confirmQueue.addTaskToQueue(subTask)
|
||||
} catch (err) {
|
||||
subTask.errMsg.push(err)
|
||||
await subTask.save()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
44
src/queue/confirm.queue.ts
Normal file
44
src/queue/confirm.queue.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import { DocumentType } from '@typegoose/typegoose'
|
||||
import { ReqTaskStatus, RequestTaskClass } from 'models/RequestTask'
|
||||
import Web3 from 'web3'
|
||||
import { isSuccessfulTransaction, waitTransaction } from 'chain/TransactionConfirm'
|
||||
import { ChainTask } from 'models/ChainTask'
|
||||
import { ChainQueue } from './chain.queue'
|
||||
import logger from 'logger/logger'
|
||||
|
||||
@singleton
|
||||
export class ConfirmQueue {
|
||||
private queue: AsyncQueue
|
||||
private web3: Web3
|
||||
|
||||
constructor(web3: Web3) {
|
||||
this.queue = createAsyncQueue()
|
||||
this.web3 = web3
|
||||
}
|
||||
|
||||
public addTaskToQueue(task: DocumentType<RequestTaskClass>) {
|
||||
this.queue.push(async () => {
|
||||
try {
|
||||
let receipt = await waitTransaction(this.web3, task.txHash)
|
||||
logger.info(`receipt confirmed: ${task.txHash}`)
|
||||
if (isSuccessfulTransaction(receipt)) {
|
||||
task.status = ReqTaskStatus.SUCCESS
|
||||
task.endTime = Date.now()
|
||||
await task.save()
|
||||
await ChainTask.checkStatus(task.chainTaskId)
|
||||
} else {
|
||||
task.status = ReqTaskStatus.REVERT
|
||||
await task.save()
|
||||
new ChainQueue().addTaskToQueue(task)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
task.errMsg.push(err)
|
||||
await task.save()
|
||||
new ChainQueue().addTaskToQueue(task)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
306
src/redis/RedisClient.ts
Normal file
306
src/redis/RedisClient.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { resolveCname } from 'dns'
|
||||
import redis from 'redis'
|
||||
import { promisify } from 'util'
|
||||
import { singleton } from '../decorators/singleton'
|
||||
|
||||
type Callback = (...args: any[]) => void
|
||||
|
||||
@singleton
|
||||
export class RedisClient {
|
||||
public pub: redis.RedisClient
|
||||
public sub: redis.RedisClient
|
||||
|
||||
protected subscribeAsync: any
|
||||
protected unsubscribeAsync: any
|
||||
protected publishAsync: any
|
||||
|
||||
protected subscriptions: { [channel: string]: Callback[] } = {}
|
||||
|
||||
protected smembersAsync: any
|
||||
protected sismemberAsync: any
|
||||
protected hgetAsync: any
|
||||
protected hlenAsync: any
|
||||
protected pubsubAsync: any
|
||||
protected incrAsync: any
|
||||
protected decrAsync: any
|
||||
|
||||
constructor(opts?: redis.ClientOpts) {
|
||||
this.sub = redis.createClient(opts)
|
||||
this.pub = redis.createClient(opts)
|
||||
|
||||
// no listener limit
|
||||
this.sub.setMaxListeners(0)
|
||||
|
||||
// create promisified pub/sub methods.
|
||||
this.subscribeAsync = promisify(this.sub.subscribe).bind(this.sub)
|
||||
this.unsubscribeAsync = promisify(this.sub.unsubscribe).bind(this.sub)
|
||||
|
||||
this.publishAsync = promisify(this.pub.publish).bind(this.pub)
|
||||
|
||||
// create promisified redis methods.
|
||||
this.smembersAsync = promisify(this.pub.smembers).bind(this.pub)
|
||||
this.sismemberAsync = promisify(this.pub.sismember).bind(this.pub)
|
||||
this.hlenAsync = promisify(this.pub.hlen).bind(this.pub)
|
||||
this.hgetAsync = promisify(this.pub.hget).bind(this.pub)
|
||||
this.pubsubAsync = promisify(this.pub.pubsub).bind(this.pub)
|
||||
this.decrAsync = promisify(this.pub.decr).bind(this.pub)
|
||||
this.incrAsync = promisify(this.pub.incr).bind(this.pub)
|
||||
}
|
||||
|
||||
public async subscribe(topic: string, callback: Callback) {
|
||||
if (!this.subscriptions[topic]) {
|
||||
this.subscriptions[topic] = []
|
||||
}
|
||||
|
||||
this.subscriptions[topic].push(callback)
|
||||
|
||||
if (this.sub.listeners('message').length === 0) {
|
||||
this.sub.addListener('message', this.handleSubscription)
|
||||
}
|
||||
|
||||
await this.subscribeAsync(topic)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async unsubscribe(topic: string, callback?: Callback) {
|
||||
if (callback) {
|
||||
const index = this.subscriptions[topic].indexOf(callback)
|
||||
this.subscriptions[topic].splice(index, 1)
|
||||
} else {
|
||||
this.subscriptions[topic] = []
|
||||
}
|
||||
|
||||
if (this.subscriptions[topic].length === 0) {
|
||||
await this.unsubscribeAsync(topic)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async publish(topic: string, data: any) {
|
||||
if (data === undefined) {
|
||||
data = false
|
||||
}
|
||||
|
||||
await this.publishAsync(topic, JSON.stringify(data))
|
||||
}
|
||||
|
||||
public async exists(roomId: string): Promise<boolean> {
|
||||
return (await this.pubsubAsync('channels', roomId)).length > 0
|
||||
}
|
||||
|
||||
public async setex(key: string, value: string, seconds: number) {
|
||||
return new Promise(resolve => this.pub.setex(key, seconds, value, resolve))
|
||||
}
|
||||
|
||||
public async expire(key: string, seconds: number) {
|
||||
return new Promise(resolve => this.pub.expire(key, seconds, resolve))
|
||||
}
|
||||
|
||||
public async get(key: string): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.get(key, (err, data: string | null) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async set(key: string, val: string) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.set(key, val, () => {
|
||||
resolve && resolve('')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async del(roomId: string) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.del(roomId, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public async sadd(key: string, value: any) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.sadd(key, value, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public async smembers(key: string): Promise<string[]> {
|
||||
return await this.smembersAsync(key)
|
||||
}
|
||||
|
||||
public async sismember(key: string, field: string): Promise<number> {
|
||||
return await this.sismemberAsync(key, field)
|
||||
}
|
||||
|
||||
public async srem(key: string, value: any) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.srem(key, value, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public async scard(key: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.scard(key, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
public async srandmember(key: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.srandmember(key, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async sinter(...keys: string[]) {
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
this.pub.sinter(...keys, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zadd(key: string, value: any, member: string) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.zadd(key, value, member, resolve)
|
||||
})
|
||||
}
|
||||
public async zrangebyscore(key: string, min: number, max: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zrangebyscore(key, min, max, 'withscores', (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zcard(key: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zcard(key, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zcount(key: string, min: number, max: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zcount(key, min, max, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zrevrank(key: string, member: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zrevrank(key, member, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zscore(key: string, member: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zscore(key, member, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async zrevrange(key: string, start: number, end: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.zrevrange(key, start, end, 'withscores', (err, data) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async hset(key: string, field: string, value: string) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.hset(key, field, value, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public async hincrby(key: string, field: string, value: number) {
|
||||
return new Promise(resolve => {
|
||||
this.pub.hincrby(key, field, value, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public async hget(key: string, field: string) {
|
||||
return await this.hgetAsync(key, field)
|
||||
}
|
||||
|
||||
public async hgetall(key: string) {
|
||||
return new Promise<{ [key: string]: string }>((resolve, reject) => {
|
||||
this.pub.hgetall(key, (err, values) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(values)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async hdel(key: string, field: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pub.hdel(key, field, (err, ok) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(ok)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async hlen(key: string): Promise<number> {
|
||||
return await this.hlenAsync(key)
|
||||
}
|
||||
|
||||
public async incr(key: string): Promise<number> {
|
||||
return await this.incrAsync(key)
|
||||
}
|
||||
|
||||
public async decr(key: string): Promise<number> {
|
||||
return await this.decrAsync(key)
|
||||
}
|
||||
|
||||
protected handleSubscription = (channel: string, message: string) => {
|
||||
if (this.subscriptions[channel]) {
|
||||
for (let i = 0, l = this.subscriptions[channel].length; i < l; i++) {
|
||||
this.subscriptions[channel][i](JSON.parse(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/schedule/blocknum.schedule.ts
Normal file
20
src/schedule/blocknum.schedule.ts
Normal file
@ -0,0 +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 {
|
||||
async parseAllRecord() {
|
||||
try {
|
||||
await new BlockChain().updateCurrenBlockNum()
|
||||
} catch (err) {
|
||||
logger.info('updateCurrenBlockNum error', err.message || err)
|
||||
}
|
||||
}
|
||||
scheduleAll() {
|
||||
const job = schedule.scheduleJob('*/5 * * * * *', async () => {
|
||||
this.parseAllRecord()
|
||||
})
|
||||
}
|
||||
}
|
54
src/schema/index.ts
Normal file
54
src/schema/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { RequestTask } from 'models/RequestTask'
|
||||
|
||||
const graphql = require('graphql')
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { getFirstToken } from 'service/chain.service'
|
||||
|
||||
const { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLID, GraphQLList, GraphQLNonNull } = graphql
|
||||
|
||||
const nftType = new GraphQLObjectType({
|
||||
name: 'erc721',
|
||||
fields: () => ({
|
||||
address: { type: GraphQLString },
|
||||
contract: { type: GraphQLString },
|
||||
id: { type: GraphQLString },
|
||||
}),
|
||||
})
|
||||
|
||||
// Define Root Query
|
||||
const RootQuery = new GraphQLObjectType({
|
||||
name: 'RootQueryType',
|
||||
fields: {
|
||||
nft: {
|
||||
type: nftType,
|
||||
args: { address: { type: GraphQLID }, contract: { type: GraphQLString } },
|
||||
async resolve(parent, args) {
|
||||
return getFirstToken(args.address, args.contract)
|
||||
},
|
||||
},
|
||||
nfts: {
|
||||
type: new GraphQLList(nftType),
|
||||
async resolve(parent, args) {
|
||||
return await RequestTask.find(args)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// const Mutations = new GraphQLObjectType({
|
||||
// name: 'Mutations',
|
||||
// fields: {
|
||||
// addCar: {
|
||||
// type: nftType,
|
||||
// args: {},
|
||||
// async resolve(parent, args) {
|
||||
// const data = await RequestTask.addCar(args)
|
||||
// return data
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
module.exports = new GraphQLSchema({
|
||||
query: RootQuery,
|
||||
})
|
38
src/service/chain.service.ts
Normal file
38
src/service/chain.service.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import logger from 'logger/logger'
|
||||
import { ChainTask } from 'models/ChainTask'
|
||||
import { RequestTask } from 'models/RequestTask'
|
||||
import { ChainQueue } from 'queue/chain.queue'
|
||||
|
||||
export async function restartAllUnFinishedTask() {
|
||||
let chainTasks = await ChainTask.allUnFinishedTask()
|
||||
logger.info(`restore ${chainTasks.length} chain tasks`)
|
||||
let chainQueue = new ChainQueue()
|
||||
for (let cTask of chainTasks) {
|
||||
let subTasks = await RequestTask.allUnFinishedTask(cTask.id)
|
||||
logger.info(`restore ${subTasks.length} req tasks fro ${cTask.id}`)
|
||||
for (let subTask of subTasks) {
|
||||
chainQueue.addTaskToQueue(subTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFirstToken(address: string, contract?: string) {
|
||||
let tokenId = ''
|
||||
contract = contract || process.env.CHAIN_NFT_ADDRESS
|
||||
try {
|
||||
tokenId = await new BlockChain().erc721Reactor.getCollectibleTokenId({
|
||||
address: contract,
|
||||
selectedAddress: address,
|
||||
index: 0,
|
||||
})
|
||||
} catch (err) {
|
||||
logger.info(err.message || err)
|
||||
}
|
||||
|
||||
return {
|
||||
address,
|
||||
contract,
|
||||
id: tokenId,
|
||||
}
|
||||
}
|
71
src/service/event.sync.service.ts
Normal file
71
src/service/event.sync.service.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import assert from 'assert'
|
||||
import { AllChains } from 'chain/allchain'
|
||||
import { HttpRetryProvider } from 'chain/HttpRetryProvider'
|
||||
import logger from 'logger/logger'
|
||||
import { NftTransferEvent } from 'models/NftTransferEvent'
|
||||
import { RedisClient } from 'redis/RedisClient'
|
||||
|
||||
import { clearTimeCache, getPastEventsIter, processEvents } from 'utils/contract.util'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export class EventSyncSvr {
|
||||
web3: Web3
|
||||
provider: HttpRetryProvider
|
||||
fromBlock: number = 27599018
|
||||
toBlock: number = 10000
|
||||
contract: any
|
||||
event: string
|
||||
blockKey = ''
|
||||
address: string
|
||||
eventProcesser: any
|
||||
|
||||
constructor({
|
||||
address,
|
||||
event,
|
||||
abi,
|
||||
fromBlock,
|
||||
eventProcesser,
|
||||
}: {
|
||||
address: string
|
||||
event: string
|
||||
abi: any
|
||||
fromBlock: number
|
||||
eventProcesser: any
|
||||
}) {
|
||||
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)
|
||||
this.address = this.contract.options.address
|
||||
this.event = event
|
||||
this.fromBlock = fromBlock
|
||||
this.blockKey = `${address.toLowerCase()}_${event}`
|
||||
this.eventProcesser = eventProcesser
|
||||
}
|
||||
|
||||
async parseEvents() {
|
||||
let currentBlock = await this.web3.eth.getBlockNumber()
|
||||
let blockStr = await new RedisClient().get(this.blockKey)
|
||||
if (blockStr) {
|
||||
this.fromBlock = Math.max(parseInt(blockStr), this.fromBlock)
|
||||
}
|
||||
this.toBlock = currentBlock
|
||||
if (this.fromBlock > this.toBlock) {
|
||||
return
|
||||
}
|
||||
logger.log(`query events:: ${this.event} address: ${this.address} from: ${this.fromBlock} to: ${this.toBlock}`)
|
||||
let events = getPastEventsIter({
|
||||
contract: this.contract,
|
||||
event: this.event,
|
||||
fromBlock: this.fromBlock,
|
||||
toBlock: this.toBlock,
|
||||
})
|
||||
// this.fromBlock = this.toBlock
|
||||
await processEvents(this.web3, events, this.eventProcesser.saveEvent)
|
||||
// 处理完一种nft后, 清楚block的timestamp缓存
|
||||
clearTimeCache()
|
||||
}
|
||||
}
|
121
src/service/explore.service.ts
Normal file
121
src/service/explore.service.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import axios from 'axios'
|
||||
import logger from 'logger/logger'
|
||||
import { RedisClient } from 'redis/RedisClient'
|
||||
|
||||
export function queryNftTxCount(address: string) {
|
||||
let data = {
|
||||
method: 'Rocket_queryTxCount',
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
params: ['token_transfer_tx', address],
|
||||
}
|
||||
let url = `${process.env.EXPLOR_RPC_URL}`
|
||||
let reqConfig: any = {
|
||||
method: 'post',
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify(data),
|
||||
}
|
||||
return axios(reqConfig).then(res => res.data)
|
||||
}
|
||||
|
||||
export function queryNftTx(address: string, page: number, limit: number = 100) {
|
||||
let data = {
|
||||
method: 'Rocket_pageQueryTx',
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
params: ['token_transfer_tx', page, limit, address],
|
||||
}
|
||||
let url = `${process.env.EXPLOR_RPC_URL}`
|
||||
let reqConfig: any = {
|
||||
method: 'post',
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify(data),
|
||||
}
|
||||
return axios(reqConfig).then(res => res.data)
|
||||
}
|
||||
|
||||
export class ExploreNftTxSvr {
|
||||
address: string
|
||||
lastBlock: number
|
||||
blockKey: string
|
||||
eventProcesser: any
|
||||
|
||||
constructor({
|
||||
address,
|
||||
event,
|
||||
abi,
|
||||
fromBlock,
|
||||
eventProcesser,
|
||||
}: {
|
||||
address: string
|
||||
event: string
|
||||
abi: any
|
||||
fromBlock: number
|
||||
eventProcesser: any
|
||||
}) {
|
||||
this.address = address.toLowerCase()
|
||||
this.lastBlock = 0
|
||||
this.blockKey = `${address.toLowerCase()}_Transfer`
|
||||
this.eventProcesser = eventProcesser
|
||||
}
|
||||
|
||||
async parseEvents() {
|
||||
logger.info(`query nft tx list:: address: ${this.address}`)
|
||||
let blockStr = await new RedisClient().get(this.blockKey)
|
||||
if (blockStr) {
|
||||
this.lastBlock = Math.max(parseInt(blockStr), this.lastBlock)
|
||||
}
|
||||
|
||||
const pageSize = this.lastBlock > 0 ? 25 : 100
|
||||
let page = 0
|
||||
let minBlock = this.lastBlock
|
||||
let maxBlock = 0
|
||||
let next = true
|
||||
while (next) {
|
||||
logger.info(`parse tx event for : ${this.address} page: ${page}`)
|
||||
let result
|
||||
try {
|
||||
result = await queryNftTx(this.address, page, pageSize)
|
||||
} catch (err) {
|
||||
logger.info(`error parse tx event for : ${this.address} page: ${page}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
continue
|
||||
}
|
||||
|
||||
const { status, data } = result.result
|
||||
if (!!status) {
|
||||
// 请求出错, 暂停一秒重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
continue
|
||||
}
|
||||
if (data.length === 0) {
|
||||
// 没数据了
|
||||
next = false
|
||||
continue
|
||||
}
|
||||
for (let event of data) {
|
||||
maxBlock = Math.max(maxBlock, event.blockHeight)
|
||||
if (minBlock > 0) {
|
||||
minBlock = Math.min(event.blockHeight, minBlock)
|
||||
} else {
|
||||
minBlock = event.blockHeight
|
||||
}
|
||||
await this.eventProcesser.saveEvent(event)
|
||||
}
|
||||
// 如果当前批次最小的blockHeight 小于上次处理的最大blockHeight
|
||||
// 那么说明这批次里已经有上次已处理的事件, 那么可以跳出循环,结束当前查询了
|
||||
if (minBlock <= this.lastBlock) {
|
||||
next = false
|
||||
continue
|
||||
}
|
||||
page += 1
|
||||
}
|
||||
new RedisClient().set(this.blockKey, maxBlock + '')
|
||||
}
|
||||
}
|
31
src/service/info.service.ts
Normal file
31
src/service/info.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import axios from 'axios'
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import logger from 'logger/logger'
|
||||
import { hmacSha256 } from 'utils/security.util'
|
||||
|
||||
const REPORT_TASK_URI = '/api/internal/update_task'
|
||||
|
||||
const calcHash = function (data: any) {
|
||||
return hmacSha256(JSON.stringify(data), process.env.HASH_SALT)
|
||||
}
|
||||
@singleton
|
||||
export class InfoSvr {
|
||||
reportTaskResult(data: any) {
|
||||
let url = data.cb
|
||||
delete data.cb
|
||||
data.sign = calcHash(data)
|
||||
logger.info('report to info svr: ' + JSON.stringify(data))
|
||||
if (!url) {
|
||||
url = `${process.env.INFO_SVR_HOST}${REPORT_TASK_URI}`
|
||||
}
|
||||
let reqConfig: any = {
|
||||
method: 'post',
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify(data),
|
||||
}
|
||||
return axios(reqConfig)
|
||||
}
|
||||
}
|
30
src/service/mail.service.ts
Normal file
30
src/service/mail.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import logger from 'logger/logger'
|
||||
import { createTransport, Transporter } from 'nodemailer'
|
||||
import Mail from 'nodemailer/lib/mailer'
|
||||
|
||||
@singleton
|
||||
export class MailService {
|
||||
private transporter: Transporter
|
||||
constructor() {
|
||||
const options = {
|
||||
host: process.env.MAIL_SMTP_HOST,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.MAIL_SMTP_USER,
|
||||
pass: process.env.MAIL_SMTP_PASS,
|
||||
},
|
||||
logger: true,
|
||||
debug: false,
|
||||
}
|
||||
// @ts-ignore
|
||||
this.transporter = createTransport(options, {})
|
||||
}
|
||||
|
||||
public async send(message: Mail.Options) {
|
||||
logger.info('begin send mail: ')
|
||||
logger.info(JSON.stringify(message))
|
||||
await this.transporter.verify()
|
||||
return this.transporter.sendMail(message)
|
||||
}
|
||||
}
|
62
src/service/price.service.ts
Normal file
62
src/service/price.service.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { BlockChain } from 'chain/BlockChain'
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import * as schedule from 'node-schedule'
|
||||
import logger from 'logger/logger'
|
||||
import axios from 'axios'
|
||||
|
||||
export interface IPriceCache {
|
||||
price: string
|
||||
expired: number
|
||||
}
|
||||
@singleton
|
||||
export class PriceSvr {
|
||||
private priceMap: Map<string, IPriceCache> = new Map()
|
||||
|
||||
public async refreshGasPrice(refresh: boolean = false) {
|
||||
const key = `gasprice`
|
||||
let data = this.priceMap.get(key)
|
||||
if (!data || data.expired < Date.now() || refresh) {
|
||||
let price = await new BlockChain().web3.eth.getGasPrice()
|
||||
this.priceMap.set(key, { price, expired: Date.now() + 1000 * 60 })
|
||||
}
|
||||
return this.priceMap.get(key).price
|
||||
}
|
||||
|
||||
public async queryEthPrice(eth: string, refresh: boolean = false) {
|
||||
const usd = 'USD'
|
||||
const key = `crypto_usd|${eth}`
|
||||
let data = this.priceMap.get(key)
|
||||
if (!data || data.expired < Date.now() || refresh) {
|
||||
const key = process.env.CRYPTOCOMPARE_API_KEY
|
||||
const url = `https://min-api.cryptocompare.com/data/price?fsym=${eth}&tsyms=${usd}&api_key=${key}`
|
||||
let priceData = await axios.get(url).then(res => res.data)
|
||||
let price = priceData[usd] * 100 + ''
|
||||
this.priceMap.set(key, { price, expired: Date.now() + 1000 * 60 })
|
||||
}
|
||||
return this.priceMap.get(key).price
|
||||
}
|
||||
|
||||
private async refreshAll() {
|
||||
for (let key of this.priceMap.keys()) {
|
||||
let [type, token] = key.split('|')
|
||||
if (type == 'gasprice') {
|
||||
try {
|
||||
await this.refreshGasPrice(true)
|
||||
} catch (e) {
|
||||
logger.info(`error refresh gas price `, e.message || e)
|
||||
}
|
||||
} else if (type == 'crypto_usd') {
|
||||
try {
|
||||
await this.queryEthPrice(token, true)
|
||||
} catch (e) {
|
||||
logger.info(`error refresh crypto price: ${token}`, e.message || e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleAll() {
|
||||
const job = schedule.scheduleJob('*/30 * * * * *', async () => {
|
||||
this.refreshAll()
|
||||
})
|
||||
}
|
||||
}
|
8
src/structs/ChainData.ts
Normal file
8
src/structs/ChainData.ts
Normal 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
7
src/structs/PriceData.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface IPriceData {
|
||||
gas: number
|
||||
price: string
|
||||
eth: string
|
||||
leagel?: string
|
||||
expired?: number
|
||||
}
|
221
src/utils/contract.util.ts
Normal file
221
src/utils/contract.util.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import logger from 'logger/logger'
|
||||
import { toBN } from './number.util'
|
||||
import { BN } from 'ethereumjs-util'
|
||||
import { RedisClient } from 'redis/RedisClient'
|
||||
|
||||
const ONE = toBN(1)
|
||||
const TWO = toBN(2)
|
||||
const queryRange = toBN(1000)
|
||||
// 返回数据如果达到这个数值, 需要拆分块的区间, 重新获取
|
||||
const RESULT_LIMIT_COUNT = 99
|
||||
// 单个块event数量超过该值, 需要独立请求
|
||||
const SPLIT_LIMIT_COUNT = 40
|
||||
|
||||
const blockTimeMap: Map<number, number> = new Map()
|
||||
|
||||
async function divQueryPassEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
}: {
|
||||
contract: any
|
||||
event: string
|
||||
fromBlock: BN
|
||||
toBlock: BN
|
||||
options?: any
|
||||
}) {
|
||||
const middle = fromBlock.add(toBlock).divRound(TWO)
|
||||
const middlePlusOne = middle.add(ONE)
|
||||
|
||||
const firstHalfEvents = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock: middle,
|
||||
options,
|
||||
})
|
||||
const secondHalfEvents = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock: middlePlusOne,
|
||||
toBlock,
|
||||
options,
|
||||
})
|
||||
return [...firstHalfEvents, ...secondHalfEvents]
|
||||
}
|
||||
/**
|
||||
* 某些链最多返回99条数据, 针对这种情况, 如下方式处理
|
||||
* 1. 分析现有事件, 对于同一block返回数量超过设定值的, 单独拿出来
|
||||
* 2. 比如 fromBlock: 0, toBlock: 100, 51和54号块事件数量超标
|
||||
* 3. 那么 分别查询 0-50, 51-51, 52-53, 54-54, 54-100
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
async function splitQueryEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
events,
|
||||
}: {
|
||||
contract: any
|
||||
event: string
|
||||
fromBlock: BN
|
||||
toBlock: BN
|
||||
options?: any
|
||||
events: any[]
|
||||
}) {
|
||||
let countMap: Map<string, number> = new Map()
|
||||
for (let event of events) {
|
||||
countMap.inc(event.blockNumber, 1)
|
||||
}
|
||||
let blockArr: number[] = []
|
||||
for (let [key, val] of countMap.entries()) {
|
||||
if (val >= SPLIT_LIMIT_COUNT) {
|
||||
blockArr.push(parseInt(key))
|
||||
}
|
||||
}
|
||||
blockArr.sort((a, b) => a - b)
|
||||
let results: any[] = []
|
||||
let preBlock = fromBlock
|
||||
for (let i = 0; i < blockArr.length; i++) {
|
||||
const block = toBN(blockArr[i])
|
||||
let subFromBlock = preBlock
|
||||
let subToBlock = block
|
||||
if (!preBlock.eq(block)) {
|
||||
const partEvents0 = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock: subFromBlock,
|
||||
toBlock: subToBlock.sub(ONE),
|
||||
options,
|
||||
})
|
||||
results = results.concat(partEvents0)
|
||||
}
|
||||
|
||||
let partEvents1 = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock: subToBlock,
|
||||
toBlock: subToBlock,
|
||||
options,
|
||||
})
|
||||
results = results.concat(partEvents1)
|
||||
|
||||
if (i === blockArr.length - 1) {
|
||||
if (!subToBlock.eq(toBlock)) {
|
||||
let partEvents2 = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock: subToBlock.add(ONE),
|
||||
toBlock: toBlock,
|
||||
options,
|
||||
})
|
||||
results = results.concat(partEvents2)
|
||||
}
|
||||
}
|
||||
preBlock = block.add(ONE)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export async function getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
}: {
|
||||
contract: any
|
||||
event: string
|
||||
fromBlock: BN
|
||||
toBlock: BN
|
||||
options?: any
|
||||
}) {
|
||||
logger.debug(`${contract.options.address}: ${event} from: ${fromBlock} to: ${toBlock}`)
|
||||
let events
|
||||
try {
|
||||
events = await contract.getPastEvents(event, {
|
||||
...options,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
})
|
||||
if (events.length >= RESULT_LIMIT_COUNT) {
|
||||
events = splitQueryEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
events,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && /query returned more than \d+ results/.test(e.message)) {
|
||||
events = divQueryPassEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
export function* getPastEventsIter({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options,
|
||||
}: {
|
||||
contract: any
|
||||
event: string
|
||||
fromBlock: number
|
||||
toBlock: number
|
||||
options?: any
|
||||
}) {
|
||||
const address = contract.options.address
|
||||
const redisKey = `${address.toLowerCase()}_${event}`
|
||||
logger.debug(`*getPastEventsIter: ${event} from: ${fromBlock} to: ${toBlock}`)
|
||||
let from = toBN(fromBlock)
|
||||
let to = toBN(fromBlock).add(queryRange)
|
||||
const toBlockBN = toBN(toBlock)
|
||||
while (to.lt(toBlockBN)) {
|
||||
yield getPastEvents({ contract, event, fromBlock: from, toBlock: to, options })
|
||||
from = to.add(ONE)
|
||||
to = to.add(queryRange)
|
||||
yield new RedisClient().set(redisKey, from + '')
|
||||
}
|
||||
yield getPastEvents({ contract, event, fromBlock: from, toBlock: toBlockBN, options })
|
||||
yield new RedisClient().set(redisKey, toBlockBN.add(ONE) + '')
|
||||
}
|
||||
|
||||
export async function processEvents(web3, iterator, processedEvent) {
|
||||
for (const getPastEventPromise of iterator) {
|
||||
const events = await getPastEventPromise
|
||||
for (const event of events) {
|
||||
if (event?.blockNumber) {
|
||||
if (blockTimeMap.has(event.blockNumber)) {
|
||||
event.timestamp = blockTimeMap.get(event.blockNumber)
|
||||
} else {
|
||||
const blockData = await web3.eth.getBlock(event.blockNumber)
|
||||
event.timestamp = blockData.timestamp
|
||||
blockTimeMap.set(event.blockNumber, blockData.timestamp)
|
||||
}
|
||||
}
|
||||
await processedEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearTimeCache() {
|
||||
blockTimeMap.clear()
|
||||
}
|
138
src/utils/net.util.ts
Normal file
138
src/utils/net.util.ts
Normal file
@ -0,0 +1,138 @@
|
||||
const TIMEOUT_ERROR = new Error('timeout')
|
||||
|
||||
const hexRe = /^[0-9A-Fa-f]+$/gu
|
||||
|
||||
/**
|
||||
* Execute fetch and verify that the response was successful.
|
||||
*
|
||||
* @param request - Request information.
|
||||
* @param options - Fetch options.
|
||||
* @returns The fetch response.
|
||||
*/
|
||||
export async function successfulFetch(request: string, options?: RequestInit) {
|
||||
const response = await fetch(request, options)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed with status '${response.status}' for request '${request}'`)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute fetch and return object response.
|
||||
*
|
||||
* @param request - The request information.
|
||||
* @param options - The fetch options.
|
||||
* @returns The fetch response JSON data.
|
||||
*/
|
||||
export async function handleFetch(request: string, options?: RequestInit) {
|
||||
const response = await successfulFetch(request, options)
|
||||
const object = await response.json()
|
||||
return object
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute fetch and return object response, log if known error thrown, otherwise rethrow error.
|
||||
*
|
||||
* @param request - the request options object
|
||||
* @param request.url - The request url to query.
|
||||
* @param request.options - The fetch options.
|
||||
* @param request.timeout - Timeout to fail request
|
||||
* @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context
|
||||
* @returns The fetch response JSON data or undefined (if error occurs).
|
||||
*/
|
||||
export async function fetchWithErrorHandling({
|
||||
url,
|
||||
options,
|
||||
timeout,
|
||||
errorCodesToCatch,
|
||||
}: {
|
||||
url: string
|
||||
options?: RequestInit
|
||||
timeout?: number
|
||||
errorCodesToCatch?: number[]
|
||||
}) {
|
||||
let result
|
||||
try {
|
||||
if (timeout) {
|
||||
result = Promise.race([
|
||||
await handleFetch(url, options),
|
||||
new Promise<Response>((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(TIMEOUT_ERROR)
|
||||
}, timeout),
|
||||
),
|
||||
])
|
||||
} else {
|
||||
result = await handleFetch(url, options)
|
||||
}
|
||||
} catch (e) {
|
||||
logOrRethrowError(e, errorCodesToCatch)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch that fails after timeout.
|
||||
*
|
||||
* @param url - Url to fetch.
|
||||
* @param options - Options to send with the request.
|
||||
* @param timeout - Timeout to fail request.
|
||||
* @returns Promise resolving the request.
|
||||
*/
|
||||
export async function timeoutFetch(url: string, options?: RequestInit, timeout = 500): Promise<Response> {
|
||||
return Promise.race([
|
||||
successfulFetch(url, options),
|
||||
new Promise<Response>((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(TIMEOUT_ERROR)
|
||||
}, timeout),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to log if error is a common fetch error and otherwise rethrow it.
|
||||
*
|
||||
* @param error - Caught error that we should either rethrow or log to console
|
||||
* @param codesToCatch - array of error codes for errors we want to catch and log in a particular context
|
||||
*/
|
||||
function logOrRethrowError(error: any, codesToCatch: number[] = []) {
|
||||
if (!error) {
|
||||
return
|
||||
}
|
||||
|
||||
const includesErrorCodeToCatch = codesToCatch.some(code =>
|
||||
error.message.includes(`Fetch failed with status '${code}'`),
|
||||
)
|
||||
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(includesErrorCodeToCatch || error.message.includes('Failed to fetch') || error === TIMEOUT_ERROR)
|
||||
) {
|
||||
console.error(error)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function generateHeader() {
|
||||
let random = function (start, end) {
|
||||
return (Math.random() * (end - start) + start) | 0
|
||||
}
|
||||
let getIp = function () {
|
||||
return `${random(1, 254)}.${random(1, 254)}.${random(1, 254)}.${random(1, 254)}`
|
||||
}
|
||||
let time = Date.now()
|
||||
let useragent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${
|
||||
(70 + Math.random() * 10) | 0
|
||||
}.0.4324.${(Math.random() * 100) | 0} Safari/537.36`
|
||||
const ip = getIp()
|
||||
return {
|
||||
'Refresh-Token': (time -= 5000),
|
||||
'Cache-Control': 'no-cache',
|
||||
'User-Agent': useragent,
|
||||
'X-Forwarded-For': ip,
|
||||
'X-Real-IP': ip,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
216
src/utils/number.util.ts
Normal file
216
src/utils/number.util.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import Web3 from 'web3';
|
||||
import { BN } from 'ethereumjs-util';
|
||||
|
||||
/**
|
||||
* Converts some token minimal unit to render format string, showing 5 decimals
|
||||
*
|
||||
* @param {Number|String|BN} tokenValue - Token value to convert
|
||||
* @param {Number} decimals - Token decimals to convert
|
||||
* @param {Number} decimalsToShow - Decimals to 5
|
||||
* @returns {String} - Number of token minimal unit, in render format
|
||||
* If value is less than 5 precision decimals will show '< 0.00001'
|
||||
*/
|
||||
export function renderFromTokenMinimalUnit(
|
||||
tokenValue,
|
||||
decimals,
|
||||
decimalsToShow = 5
|
||||
) {
|
||||
const minimalUnit = fromTokenMinimalUnit(tokenValue || 0, decimals);
|
||||
const minimalUnitNumber = parseFloat(minimalUnit);
|
||||
let renderMinimalUnit;
|
||||
if (minimalUnitNumber < 0.00001 && minimalUnitNumber > 0) {
|
||||
renderMinimalUnit = "< 0.00001";
|
||||
} else {
|
||||
const base = Math.pow(10, decimalsToShow);
|
||||
renderMinimalUnit = (
|
||||
Math.round(minimalUnitNumber * base) / base
|
||||
).toString();
|
||||
}
|
||||
return renderMinimalUnit;
|
||||
}
|
||||
/**
|
||||
* Converts token minimal unit to readable string value
|
||||
*
|
||||
* @param {number|string|Object} minimalInput - Token minimal unit to convert
|
||||
* @param {string} decimals - Token decimals to convert
|
||||
* @returns {string} - String containing the new number
|
||||
*/
|
||||
export function fromTokenMinimalUnit(minimalInput, decimals) {
|
||||
minimalInput = addHexPrefix(Number(minimalInput).toString(16));
|
||||
let minimal = safeNumberToBN(minimalInput);
|
||||
const negative = minimal.lt(new BN(0));
|
||||
const base = Web3.utils.toBN(Math.pow(10, decimals).toString());
|
||||
|
||||
if (negative) {
|
||||
minimal = minimal.mul(new BN(-1));
|
||||
}
|
||||
let fraction = minimal.mod(base).toString(10);
|
||||
while (fraction.length < decimals) {
|
||||
fraction = "0" + fraction;
|
||||
}
|
||||
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
|
||||
const whole = minimal.div(base).toString(10);
|
||||
let value = "" + whole + (fraction === "0" ? "" : "." + fraction);
|
||||
if (negative) {
|
||||
value = "-" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts wei to render format string, showing 5 decimals
|
||||
*
|
||||
* @param {Number|String|BN} value - Wei to convert
|
||||
* @param {Number} decimalsToShow - Decimals to 5
|
||||
* @returns {String} - Number of token minimal unit, in render format
|
||||
* If value is less than 5 precision decimals will show '< 0.00001'
|
||||
*/
|
||||
export function renderFromWei(value, decimalsToShow = 5) {
|
||||
let renderWei = '0';
|
||||
// avoid undefined
|
||||
if (value) {
|
||||
const wei = Web3.utils.fromWei(value);
|
||||
const weiNumber = parseFloat(wei);
|
||||
if (weiNumber < 0.00001 && weiNumber > 0) {
|
||||
renderWei = '< 0.00001';
|
||||
} else {
|
||||
const base = Math.pow(10, decimalsToShow);
|
||||
renderWei = (Math.round(weiNumber * base) / base).toString();
|
||||
}
|
||||
}
|
||||
return renderWei;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts token BN value to hex string number to be sent
|
||||
*
|
||||
* @param {Object} value - BN instance to convert
|
||||
* @param {number} decimals - Decimals to be considered on the conversion
|
||||
* @returns {string} - String of the hex token value
|
||||
*/
|
||||
export function calcTokenValueToSend(value, decimals) {
|
||||
return value ? (value * Math.pow(10, decimals)).toString(16) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a string is a valid decimal
|
||||
*
|
||||
* @param {string} value - String to check
|
||||
* @returns {boolean} - True if the string is a valid decimal
|
||||
*/
|
||||
export function isDecimal(value) {
|
||||
return (
|
||||
Number.isFinite(parseFloat(value)) &&
|
||||
!Number.isNaN(parseFloat(value)) &&
|
||||
!isNaN(+value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BN object from a string
|
||||
*
|
||||
* @param {string} value - Some numeric value represented as a string
|
||||
* @returns {Object} - BN instance
|
||||
*/
|
||||
export function toBN(value) {
|
||||
return Web3.utils.toBN(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes a hex string with '0x' or '-0x' and returns it. Idempotent.
|
||||
*
|
||||
* @param {string} str - The string to prefix.
|
||||
* @returns {string} The prefixed string.
|
||||
*/
|
||||
export const addHexPrefix = (str: string) => {
|
||||
if (typeof str !== "string" || str.match(/^-?0x/u)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
if (str.match(/^-?0X/u)) {
|
||||
return str.replace("0X", "0x");
|
||||
}
|
||||
|
||||
if (str.startsWith("-")) {
|
||||
return str.replace("-", "-0x");
|
||||
}
|
||||
|
||||
return `0x${str}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps 'numberToBN' method to avoid potential undefined and decimal values
|
||||
*
|
||||
* @param {number|string} value - number
|
||||
* @returns {Object} - The converted value as BN instance
|
||||
*/
|
||||
export function safeNumberToBN(value: number | string) {
|
||||
const safeValue = fastSplit(value.toString()) || "0";
|
||||
return numberToBN(safeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a fast string split and returns the first item of the string based on the divider provided
|
||||
*
|
||||
* @param {number|string} value - number/string to be splitted
|
||||
* @param {string} divider - string value to use to split the string (default '.')
|
||||
* @returns {string} - the selected splitted element
|
||||
*/
|
||||
|
||||
export function fastSplit(value, divider = ".") {
|
||||
value += "";
|
||||
const [from, to] = [value.indexOf(divider), 0];
|
||||
return value.substring(from, to) || value;
|
||||
}
|
||||
|
||||
export function stripHexPrefix(str: string) {
|
||||
if (typeof str !== "string") {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.slice(0, 2) === "0x" ? str.slice(2) : str;
|
||||
}
|
||||
|
||||
export function numberToBN(arg) {
|
||||
if (typeof arg === "string" || typeof arg === "number") {
|
||||
var multiplier = Web3.utils.toBN(1); // eslint-disable-line
|
||||
var formattedString = String(arg).toLowerCase().trim();
|
||||
var isHexPrefixed =
|
||||
formattedString.substr(0, 2) === "0x" ||
|
||||
formattedString.substr(0, 3) === "-0x";
|
||||
var stringArg = stripHexPrefix(formattedString); // eslint-disable-line
|
||||
if (stringArg.substr(0, 1) === "-") {
|
||||
stringArg = stripHexPrefix(stringArg.slice(1));
|
||||
multiplier = Web3.utils.toBN(-1);
|
||||
}
|
||||
stringArg = stringArg === "" ? "0" : stringArg;
|
||||
|
||||
if (
|
||||
(!stringArg.match(/^-?[0-9]+$/) && stringArg.match(/^[0-9A-Fa-f]+$/)) ||
|
||||
stringArg.match(/^[a-fA-F]+$/) ||
|
||||
(isHexPrefixed === true && stringArg.match(/^[0-9A-Fa-f]+$/))
|
||||
) {
|
||||
return Web3.utils.toBN(stringArg).mul(multiplier);
|
||||
}
|
||||
|
||||
if (
|
||||
(stringArg.match(/^-?[0-9]+$/) || stringArg === "") &&
|
||||
isHexPrefixed === false
|
||||
) {
|
||||
return Web3.utils.toBN(stringArg).mul(multiplier);
|
||||
}
|
||||
} else if (typeof arg === "object" && arg.toString && !arg.pop && !arg.push) {
|
||||
if (
|
||||
arg.toString(10).match(/^-?[0-9]+$/) &&
|
||||
(arg.mul || arg.dividedToIntegerBy)
|
||||
) {
|
||||
return Web3.utils.toBN(arg.toString(10));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"[number-to-bn] while converting number " +
|
||||
JSON.stringify(arg) +
|
||||
" to BN.js instance, error: invalid number value. Value must be an integer, hex string, BN or BigNumber instance. Note, decimals are not supported."
|
||||
);
|
||||
}
|
138
src/utils/promise.util.ts
Normal file
138
src/utils/promise.util.ts
Normal file
@ -0,0 +1,138 @@
|
||||
type RetryOptions = {
|
||||
maxRetries: number
|
||||
whitelistErrors: Error[]
|
||||
}
|
||||
/**
|
||||
* 使用:
|
||||
* retry(() => fetch("https://example.com"), { maxRetries: 3, whitelistErrors: [] })
|
||||
* .then((response) => console.log(response))
|
||||
* .catch((error) => console.error(error));
|
||||
* @param promiseFn
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function retry<T>(promiseFn: () => Promise<T>, options: RetryOptions): Promise<T> {
|
||||
let retries = 0
|
||||
let defaultOptions = {
|
||||
maxRetries: 3,
|
||||
whitelistErrors: [],
|
||||
}
|
||||
Object.assign(defaultOptions, options)
|
||||
const { maxRetries, whitelistErrors } = options
|
||||
|
||||
const retryPromise = async (): Promise<T> => {
|
||||
try {
|
||||
return await promiseFn()
|
||||
} catch (err) {
|
||||
if (
|
||||
retries < maxRetries &&
|
||||
whitelistErrors.some(whitelistedError => err instanceof whitelistedError.constructor)
|
||||
) {
|
||||
retries++
|
||||
return retryPromise()
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return retryPromise()
|
||||
}
|
||||
/**
|
||||
* 构建一个promise, 在
|
||||
* usage:
|
||||
* function delay(ms: number): Promise<void> {
|
||||
const deferred = new Deferred<void>();
|
||||
|
||||
setTimeout(() => {
|
||||
deferred.resolve();
|
||||
}, ms);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
console.log("start");
|
||||
|
||||
delay(1000).then(() => {
|
||||
console.log("after 1 second");
|
||||
});
|
||||
|
||||
console.log("end");
|
||||
*/
|
||||
export class Deferred<T = any> {
|
||||
private _resolve!: (value: T | PromiseLike<T>) => void
|
||||
private _reject!: (reason?: any) => void
|
||||
|
||||
public readonly promise: Promise<T>
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this._resolve = resolve
|
||||
this._reject = reject
|
||||
})
|
||||
}
|
||||
|
||||
public resolve(value: T | PromiseLike<T>): void {
|
||||
this._resolve(value)
|
||||
}
|
||||
|
||||
public reject(reason?: any): void {
|
||||
this._reject(reason)
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onfulfilled, onrejected)
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined,
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onrejected)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单限流的 Promise 队列
|
||||
* usage:
|
||||
const q = new PromiseQueue();
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].forEach((v) => {
|
||||
q.add(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log(v);
|
||||
resolve();
|
||||
}, 1000);
|
||||
})
|
||||
);
|
||||
});
|
||||
*/
|
||||
export class PromiseQueue {
|
||||
private readonly concurrency: number
|
||||
private _current: number = 0
|
||||
private _list: (() => Promise<any>)[] = []
|
||||
|
||||
constructor({ concurrency = 2 }: { concurrency: number }) {
|
||||
this.concurrency = concurrency
|
||||
}
|
||||
|
||||
add(promiseFn: () => Promise<any>) {
|
||||
this._list.push(promiseFn)
|
||||
this.loadNext()
|
||||
}
|
||||
|
||||
loadNext() {
|
||||
if (this._list.length === 0 || this.concurrency === this._current) return
|
||||
this._current++
|
||||
const fn = this._list.shift()!
|
||||
const promise = fn.call(this)
|
||||
promise.then(this.onLoaded.bind(this)).catch(this.onLoaded.bind(this))
|
||||
}
|
||||
|
||||
onLoaded() {
|
||||
this._current--
|
||||
this.loadNext()
|
||||
}
|
||||
}
|
74
src/utils/security.util.ts
Normal file
74
src/utils/security.util.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function hmac(input, key, out) {
|
||||
return out
|
||||
? crypto.createHmac('sha1', key).update(input).digest(out)
|
||||
: crypto.createHmac('sha1', key).update(input).digest('hex')
|
||||
}
|
||||
|
||||
export function genRandomString(length) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length)
|
||||
}
|
||||
|
||||
export function sha512(password, salt) {
|
||||
let hash = crypto.createHmac('sha512', salt)
|
||||
hash.update(password)
|
||||
let value = hash.digest('hex')
|
||||
return {
|
||||
salt: salt,
|
||||
passwordHash: value,
|
||||
}
|
||||
}
|
||||
|
||||
export function sha1(str) {
|
||||
const md5sum = crypto.createHash('sha1')
|
||||
md5sum.update(str)
|
||||
str = md5sum.digest('hex')
|
||||
return str
|
||||
}
|
||||
|
||||
export function hmacSha256(text: string, secret: string) {
|
||||
const mac = crypto.createHmac('sha256', secret)
|
||||
const data = mac.update(text).digest('hex').toLowerCase()
|
||||
console.log(`HmacSHA256 rawContent is [${text}], key is [${secret}], hash result is [${data}]`)
|
||||
return data
|
||||
}
|
||||
|
||||
export function md5(str) {
|
||||
const md5sum = crypto.createHash('md5')
|
||||
md5sum.update(str)
|
||||
str = md5sum.digest('hex')
|
||||
return str
|
||||
}
|
||||
|
||||
export function createSign(secretKey, paramStr, timestamp) {
|
||||
paramStr = `${paramStr}:${timestamp}:${secretKey}`
|
||||
return sha1(paramStr)
|
||||
}
|
||||
|
||||
export function checkSign({
|
||||
secretKey,
|
||||
data,
|
||||
sign,
|
||||
signKeys,
|
||||
}: {
|
||||
secretKey: string
|
||||
data: {}
|
||||
sign: string
|
||||
signKeys: string[]
|
||||
}) {
|
||||
signKeys.sort()
|
||||
let signStr = ''
|
||||
for (let key of signKeys) {
|
||||
if (signStr.length > 0) {
|
||||
signStr += '&'
|
||||
}
|
||||
signStr += `${key}=${data[key]}`
|
||||
}
|
||||
console.log(signStr)
|
||||
let sign1 = hmacSha256(signStr, secretKey)
|
||||
return sign1 === sign
|
||||
}
|
106
src/utils/string.util.ts
Normal file
106
src/utils/string.util.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 根据key升序生成 key1=val1&key2=val2的字符串
|
||||
* @param {object} data 需要处理的对象
|
||||
* @param {boolean} ignoreNull 是否过滤空值(空格或者null值不参与拼接)
|
||||
* @param splitChar 连接的字符, 默认是&
|
||||
* @param equalChar =
|
||||
*/
|
||||
export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string = '&', equalChar = '=') {
|
||||
const keys = Object.keys(data)
|
||||
keys.sort()
|
||||
let result = ''
|
||||
let i = 0
|
||||
for (let key of keys) {
|
||||
if (ignoreNull && !data[key]) {
|
||||
return
|
||||
}
|
||||
if (i++ > 0) result += splitChar
|
||||
result += `${key}${equalChar}${data[key]}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 将key1=val&key2=val的字符串组装成对象
|
||||
* @param str key1=val&key2=val的字符串
|
||||
* @param splitChar 连接的字符, 默认是&
|
||||
* @param equalChar =
|
||||
*/
|
||||
export function keyValToObject(str: string, splitChar: string = '&', equalChar = '='): {} {
|
||||
let result = {}
|
||||
if (!str) {
|
||||
return result
|
||||
}
|
||||
let arrs = str.split(splitChar)
|
||||
for (let sub of arrs) {
|
||||
let subArr = sub.split(equalChar)
|
||||
result[subArr[0]] = subArr[1]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断传入的值是否为true
|
||||
* @param {Object} obj 传入值为'true','TRUE',1,'1','on','ON','YES','yes'时,返回true,其他值均返回false
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isTrue(obj) {
|
||||
return (
|
||||
obj === 'true' ||
|
||||
obj === 'TRUE' ||
|
||||
obj === 'True' ||
|
||||
obj === 'on' ||
|
||||
obj === 'ON' ||
|
||||
obj === true ||
|
||||
obj === 1 ||
|
||||
obj === '1' ||
|
||||
obj === 'YES' ||
|
||||
obj === 'yes'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证ObjectId格式是否正确
|
||||
* @param {string} id
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isObjectId(id: string): boolean {
|
||||
//mongoose.Types.ObjectId.isValid(id)
|
||||
return /^[a-fA-F0-9]{24}$/.test(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 10进制 -> 62进制
|
||||
* @param {string | number} number
|
||||
* @return {string}
|
||||
*/
|
||||
export function string10to62(number: string | number) {
|
||||
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('')
|
||||
const radix = chars.length
|
||||
let qutient = +number
|
||||
const arr = []
|
||||
do {
|
||||
const mod = qutient % radix
|
||||
qutient = (qutient - mod) / radix
|
||||
arr.unshift(chars[mod])
|
||||
} while (qutient)
|
||||
return arr.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 62进制 -> 10 进制
|
||||
* @param {string} numberCode
|
||||
* @return {number}
|
||||
*/
|
||||
export function string62to10(numberCode: string) {
|
||||
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
|
||||
const radix = chars.length
|
||||
numberCode = numberCode + ''
|
||||
const len = numberCode.length
|
||||
let i = 0
|
||||
let originNumber = 0
|
||||
while (i < len) {
|
||||
originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0)
|
||||
}
|
||||
return originNumber
|
||||
}
|
125
src/utils/wallet.util.ts
Normal file
125
src/utils/wallet.util.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { renderFromTokenMinimalUnit } from './number.util'
|
||||
import { asciiToHex } from 'web3-utils'
|
||||
|
||||
/**
|
||||
* Removes IPFS protocol prefix from input string.
|
||||
*
|
||||
* @param ipfsUrl - An IPFS url (e.g. ipfs://{content id})
|
||||
* @returns IPFS content identifier and (possibly) path in a string
|
||||
* @throws Will throw if the url passed is not IPFS.
|
||||
*/
|
||||
export function removeIpfsProtocolPrefix(ipfsUrl: string) {
|
||||
if (ipfsUrl.startsWith('ipfs://ipfs/')) {
|
||||
return ipfsUrl.replace('ipfs://ipfs/', '')
|
||||
} else if (ipfsUrl.startsWith('ipfs://')) {
|
||||
return ipfsUrl.replace('ipfs://', '')
|
||||
}
|
||||
// this method should not be used with non-ipfs urls (i.e. startsWith('ipfs://') === true)
|
||||
throw new Error('this method should not be used with non ipfs urls')
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts content identifier and path from an input string.
|
||||
*
|
||||
* @param ipfsUrl - An IPFS URL minus the IPFS protocol prefix
|
||||
* @returns IFPS content identifier (cid) and sub path as string.
|
||||
* @throws Will throw if the url passed is not ipfs.
|
||||
*/
|
||||
export function getIpfsCIDv1AndPath(ipfsUrl: string): {
|
||||
cid: string
|
||||
path?: string
|
||||
} {
|
||||
const url = removeIpfsProtocolPrefix(ipfsUrl)
|
||||
|
||||
// check if there is a path
|
||||
// (CID is everything preceding first forward slash, path is everything after)
|
||||
const index = url.indexOf('/')
|
||||
const cid = index !== -1 ? url.substring(0, index) : url
|
||||
const path = index !== -1 ? url.substring(index) : undefined
|
||||
//TODO:
|
||||
// We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats)
|
||||
// because most cid v0s appear to be incompatible with IPFS subdomains
|
||||
// return {
|
||||
// cid: CID.parse(cid).toV1().toString(),
|
||||
// path,
|
||||
// };
|
||||
return {
|
||||
cid,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds URL protocol prefix to input URL string if missing.
|
||||
*
|
||||
* @param urlString - An IPFS URL.
|
||||
* @returns A URL with a https:// prepended.
|
||||
*/
|
||||
export function addUrlProtocolPrefix(urlString: string): string {
|
||||
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) {
|
||||
return `https://${urlString}`
|
||||
}
|
||||
return urlString
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats URL correctly for use retrieving assets hosted on IPFS.
|
||||
*
|
||||
* @param ipfsGateway - The users preferred IPFS gateway (full URL or just host).
|
||||
* @param ipfsUrl - The IFPS URL pointed at the asset.
|
||||
* @param subdomainSupported - Boolean indicating whether the URL should be formatted with subdomains or not.
|
||||
* @returns A formatted URL, with the user's preferred IPFS gateway and format (subdomain or not), pointing to an asset hosted on IPFS.
|
||||
*/
|
||||
export function getFormattedIpfsUrl(ipfsGateway: string, ipfsUrl: string, subdomainSupported: boolean): string {
|
||||
const { host, protocol, origin } = new URL(addUrlProtocolPrefix(ipfsGateway))
|
||||
if (subdomainSupported) {
|
||||
const { cid, path } = getIpfsCIDv1AndPath(ipfsUrl)
|
||||
return `${protocol}//${cid}.ipfs.${host}${path || ''}`
|
||||
}
|
||||
const cidAndPath = removeIpfsProtocolPrefix(ipfsUrl)
|
||||
return `${origin}/ipfs/${cidAndPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given code corresponds to a smart contract.
|
||||
*
|
||||
* @param code - The potential smart contract code.
|
||||
* @returns Whether the code was smart contract code or not.
|
||||
*/
|
||||
export function isSmartContractCode(code: string) {
|
||||
/* istanbul ignore if */
|
||||
if (!code) {
|
||||
return false
|
||||
}
|
||||
// Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
|
||||
const smartContractCode = code !== '0x' && code !== '0x0'
|
||||
return smartContractCode
|
||||
}
|
||||
|
||||
export function formatAddress(address: string) {
|
||||
if (address.length >= 10) {
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4)
|
||||
} else if (address.length > 0 && address.length < 10) {
|
||||
return address
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMoney(balance: number | string, symbol: string) {
|
||||
if (balance === '-') {
|
||||
return `- ${symbol}`
|
||||
}
|
||||
let money = renderFromTokenMinimalUnit(balance, 18, 4)
|
||||
return `${money} ${symbol}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机的bytes32的字符串
|
||||
* @returns
|
||||
*/
|
||||
export function generateRandomBytes32() {
|
||||
const v1 = (Math.random() * 9000000 + 1000000) | 0
|
||||
const v2 = (Math.random() * 900000 + 100000) | 0
|
||||
return asciiToHex(v1 + '' + v2)
|
||||
}
|
22
start.json
Normal file
22
start.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "chain-client",
|
||||
"script": "npm",
|
||||
"args": "run prod:api",
|
||||
"cwd": "/data/apps/web_chain_client",
|
||||
"max_memory_restart": "1024M",
|
||||
"log_date_format": "YYYY-MM-DD HH:mm Z",
|
||||
"watch": false,
|
||||
"ignore_watch": ["node_modules", "logs", "fixtures", "tasks"],
|
||||
"instances": 1,
|
||||
"exec_mode": "fork",
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"env_production": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"ts-node": {
|
||||
"files": true,
|
||||
"require": ["tsconfig-paths/register"]
|
||||
},
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2019",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./src",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"lib": ["es2019"],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"typings/extend.d.ts"
|
||||
]
|
||||
}
|
11
tslint.json
Normal file
11
tslint.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"no-console": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
11
typings/extend.d.ts
vendored
Normal file
11
typings/extend.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import fastify from 'fastify'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
roles?: string[]
|
||||
user?: any
|
||||
token?: string
|
||||
permissions?: string[][]
|
||||
depts?: string[]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user