project init

This commit is contained in:
CounterFire2023 2023-12-14 17:19:44 +08:00
commit ad094f7a47
47 changed files with 8694 additions and 0 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
dist/*.js

14
.eslintrc.js Normal file
View 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
View File

@ -0,0 +1,7 @@
.idea
node_modules
build
dist
.DS_Store
.env
.env.development

13
.prettierrc.js Normal file
View 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,
};

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "task-svr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev:api": "ts-node -r tsconfig-paths/register src/api.ts",
"prod:api": "node dist/api.js",
"lint": "eslint --ext .ts src/**",
"format": "eslint --ext .ts src/** --fix"
},
"author": "zhl",
"license": "ISC",
"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",
"fastify": "^4.15.0",
"fastify-file-upload": "^3.0.0",
"fastify-plugin": "^3.0.0",
"fastify-xml-body-parser": "^2.2.0",
"mongoose": "5.10.3",
"mongoose-findorcreate": "^3.0.0",
"node-schedule": "^2.0.0",
"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/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",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^3.9.0",
"tslint": "^6.1.1",
"typescript": "^4.1.3"
}
}

1
pm2_dev.sh Executable file
View File

@ -0,0 +1 @@
pm2 start npm --name "web-task-svr" --log-date-format "YYYY-MM-DD HH:mm:ss" -- run "dev:api"

168
src/api.server.ts Normal file
View File

@ -0,0 +1,168 @@
import fastify, { FastifyError, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import helmet from '@fastify/helmet'
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 path from 'path'
import { RedisClient } from 'redis/RedisClient'
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,
})
// if (process.env.NODE_ENV !== 'production') {
// 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,
)
}
}
}
/**
* 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() {}
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()
this.server.listen({ port: parseInt(process.env.API_PORT) }, (err: any, address: any) => {
if (err) {
logger.log(err)
process.exit(0)
}
resolve && resolve(address)
})
})
}
}

12
src/api.ts Normal file
View 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}`)
})

107
src/common/AsyncQueue.ts Normal file
View 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
View 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
View 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)

985
src/common/Extend.ts Normal file
View File

@ -0,0 +1,985 @@
/**
* 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`minmax
* [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>
/**
*
* @param chunkSize
*/
chunkArray?<T>(chunkSize: number): 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,
},
chunkArray: {
value: function <T>(this: T[], chunkSize: number): T[][] {
const chunks: T[][] = []
for (let i = 0; i < this.length; i += chunkSize) {
const chunk = this.slice(i, i + chunkSize)
chunks.push(chunk)
}
return chunks
},
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
View 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
}
}

View File

@ -0,0 +1,7 @@
import fastify = require('fastify')
export const ROLE_ANON = 'anon'
class BaseController {
aotoRoute(req: fastify.FastifyRequest, res) {}
}
export default BaseController

View File

@ -0,0 +1,20 @@
import { ZError } from "common/ZError";
import BaseController, { ROLE_ANON } from "common/base.controller";
import { role, router } from "decorators/router";
import { ActivityUser } from "models/ActivityUser";
export default class TasksController extends BaseController {
@role(ROLE_ANON)
@router('get /api/tasks/newuser')
async claimResult(req) {
let { address } = req.params
if (!address) {
throw new ZError(10, 'address not found')
}
address = address.toLowerCase()
let record = await ActivityUser.insertOrUpdate({ address, activity: '001' }, {})
await record.save()
return record.toJson()
}
}

19
src/decorators/dbconn.ts Normal file
View 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
View 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
View 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)
}
}
}

View 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]
},
})

9
src/logger/logger.ts Normal file
View File

@ -0,0 +1,9 @@
import { LoggerQueue } from 'queue/logger.queue'
const level = process.env.NODE_ENV === 'production' ? 'info' : 'log'
const logger = require('tracer').colorConsole({ dateformat: 'yyyy-mm-dd HH:MM:ss.L', level })
logger.db = function (name: string, req: any, logObj?: any) {
logObj = logObj || {}
new LoggerQueue().addLog(name, req, logObj)
}
export default logger

View File

@ -0,0 +1,26 @@
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
// @ts-ignore
import findOrCreate from 'mongoose-findorcreate'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
interface ActivityInfoClass extends Base, TimeStamps {}
@dbconn()
@modelOptions({ schemaOptions: { collection: 'activity_info', timestamps: true } })
class ActivityInfoClass extends BaseModule {
@prop({ required: true })
public name: string
@prop()
public startTime: number
@prop()
public endTime: number
@prop()
public comment?: string
}
export const ActivityUser = getModelForClass(ActivityInfoClass, { existingConnection: ActivityInfoClass.db })

View File

@ -0,0 +1,57 @@
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
// @ts-ignore
import findOrCreate from 'mongoose-findorcreate'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
import { convert } from 'utils/number.util'
const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI'
interface ActivityUserClass extends Base, TimeStamps {}
@dbconn()
@index({ address: 1, activity: 1 }, { unique: true })
@index({ inviteCode: 1, activity: 1 }, { unique: true })
@index({ inviteUser: 1, activity: 1 }, { unique: false })
@modelOptions({ schemaOptions: { collection: 'activity_user', timestamps: true } })
@pre<ActivityUserClass>('save', async function () {
if (!this.inviteCode) {
// 取ObjectId的time和inc段,
// 将time段倒序(倒序后, 如果以0开始, 则移除0, 随机拼接一个hex字符), 然后拼接inc段, 再转换成52进制
let timeStr = this.id.slice(0, 8).split("").reverse().join("");
if (timeStr.indexOf('0') === 0) {
timeStr = (Math.random() * 15 | 0 + 1).toString(16) + timeStr.slice(1)
}
let shortId = timeStr + this.id.slice(-6)
this.inviteCode = convert({numStr: shortId, base: 16, to: 52, alphabet})
}
})
class ActivityUserClass extends BaseModule {
@prop({ required: true })
public address: string
@prop({ required: true })
public activity: string
// 用户的邀请码
@prop()
public inviteCode: string
// 用户的邀请人
@prop()
public inviteUser: string
@prop({ default: false })
public locked: boolean
@prop()
public lockedTime?: Date
@prop()
public comment?: string
@prop()
public lastLogin?: Date
public static async findByAddress(this: ReturnModelType<typeof ActivityUserClass>, address: string) {
return this.findOne({address, deleted: { $ne: true }}).exec()
}
}
export const ActivityUser = getModelForClass(ActivityUserClass, { existingConnection: ActivityUserClass.db })

206
src/models/Base.ts Normal file
View File

@ -0,0 +1,206 @@
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: { $ne: true } }
// @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 }
}
public getTimestampOfID() {
// Extract the timestamp from the ObjectId
// @ts-ignore
return this._id.getTimestamp();
}
}

31
src/models/UserLog.ts Normal file
View File

@ -0,0 +1,31 @@
import { dbconn } from 'decorators/dbconn'
import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
import { BaseModule } from './Base'
/**
*
*/
@dbconn()
@index({ user: 1 }, { unique: false })
@index({ name: 1 }, { unique: false })
@modelOptions({ schemaOptions: { collection: 'user_log', timestamps: true }, options: { allowMixed: Severity.ALLOW } })
class UserLogClass extends BaseModule {
@prop()
public user: string
@prop()
public name: string
@prop()
public method: string
@prop()
public path: string
@prop()
public referer: string
@prop()
public user_agent: string
@prop()
public ip: string
@prop({ type: mongoose.Schema.Types.Mixed })
public params: any
}
export const UserLog = getModelForClass(UserLogClass, { existingConnection: UserLogClass['db'] })

51
src/plugins/apiauth.ts Normal file
View File

@ -0,0 +1,51 @@
import { ROLE_ANON } from 'common/base.controller'
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import { ActivityUser } from 'models/ActivityUser'
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(ROLE_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 ActivityUser.findById(data.id)
if (!account) {
return reply.send({ errcode: 10, errmsg: '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
View 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')

View 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
View 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',
})

70
src/queue/chain.queue.ts Normal file
View File

@ -0,0 +1,70 @@
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
import { singleton } from 'decorators/singleton'
import { DocumentType } from '@typegoose/typegoose'
import logger from 'logger/logger'
import { ClaimTaskClass } from 'models/ClaimTask'
import { addTask, getMintableCount } from 'service/ChainSvr'
const MAX_BATCH_COUNT = 20
/**
* let data = {
taskId: '1',
type: 2,
data: [
{
address: '0xd45A464a2412A2f83498d13635698a041b9dBe9b',
to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
count: 10
},
],
}
*/
@singleton
export class ChainQueue {
private queue: AsyncQueue
constructor() {
this.queue = createAsyncQueue()
}
public addTask(task: DocumentType<ClaimTaskClass>) {
this.queue.push(async () => {
try {
if (task.totalCount === 0) {
task.status = 10
await task.save()
return
}
// datas: [{address: distributor's address, to: userAddress, count: number}]
const address = task.dAddress
const to = task.address
const batchs = Math.ceil(task.totalCount / MAX_BATCH_COUNT)
let datas: any = []
for (let i = 0; i < batchs; i++) {
datas.push({
address,
to,
type: 5,
count: Math.min(MAX_BATCH_COUNT, task.totalCount - i * MAX_BATCH_COUNT),
})
}
let reqData = {
taskId: task.id,
source: 'claim',
data: datas,
}
await addTask(reqData)
task.status = 1
await task.save()
} catch (err) {
logger.error('error add chain task: ')
logger.error(err)
}
})
}
}

39
src/queue/logger.queue.ts Normal file
View File

@ -0,0 +1,39 @@
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
import { singleton } from 'decorators/singleton'
import logger from 'logger/logger'
import { UserLog } from 'models/UserLog'
@singleton
export class LoggerQueue {
private queue: AsyncQueue
constructor() {
this.queue = createAsyncQueue()
}
public addLog(name, req, logObj) {
this.queue.push(async () => {
const user = req.user
const ip = req.headers['x-forwarded-for'] || req.ip
const path = req.url
const params = req.method === 'GET' ? req.query : req.body
const dataObj = JSON.stringify(logObj) === '{}' ? params : logObj
try {
const history = new UserLog({
user: user ? user.id : '',
path: path,
method: req.method,
params: dataObj,
referer: req.headers['referer'],
user_agent: req.headers['user-agent'],
ip,
name,
})
await history.save()
} catch (err) {
logger.error('error add user log: ')
logger.error(err)
}
})
}
}

306
src/redis/RedisClient.ts Normal file
View 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))
}
}
}
}

218
src/utils/bn.util.ts Normal file
View File

@ -0,0 +1,218 @@
export declare type HexString = string
export declare type Numbers = number | bigint | string | HexString
const isHexStrict = hex => typeof hex === 'string' && /^((-)?0x[0-9a-f]+|(0x))$/i.test(hex)
export declare type ValidInputTypes = Uint8Array | bigint | string | number | boolean
export const isHex = (hex: ValidInputTypes): boolean =>
typeof hex === 'number' ||
typeof hex === 'bigint' ||
(typeof hex === 'string' && /^((-0x|0x|-)?[0-9a-f]+|(0x))$/i.test(hex))
const base = BigInt(10)
const expo10 = (expo: number) => base ** BigInt(expo)
export const ethUnitMap = {
noether: BigInt('0'),
wei: BigInt(1),
kwei: expo10(3),
Kwei: expo10(3),
babbage: expo10(3),
femtoether: expo10(3),
mwei: expo10(6),
Mwei: expo10(6),
lovelace: expo10(6),
picoether: expo10(6),
gwei: expo10(9),
Gwei: expo10(9),
shannon: expo10(9),
nanoether: expo10(9),
nano: expo10(9),
szabo: expo10(12),
microether: expo10(12),
micro: expo10(12),
finney: expo10(15),
milliether: expo10(15),
milli: expo10(15),
ether: expo10(18),
kether: expo10(21),
grand: expo10(21),
mether: expo10(24),
gether: expo10(27),
tether: expo10(30),
}
export type EtherUnits = keyof typeof ethUnitMap
/**
* Converts value to it's number representation
*/
export const hexToNumber = (value: string): bigint | number => {
if (!isHexStrict(value)) {
throw new Error('Invalid hex string')
}
const [negative, hexValue] = value.startsWith('-') ? [true, value.slice(1)] : [false, value]
const num = BigInt(hexValue)
if (num > Number.MAX_SAFE_INTEGER) {
return negative ? -num : num
}
if (num < Number.MIN_SAFE_INTEGER) {
return num
}
return negative ? -1 * Number(num) : Number(num)
}
export const toNumber = (value: Numbers): number | bigint => {
if (typeof value === 'number') {
return value
}
if (typeof value === 'bigint') {
return value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER ? Number(value) : value
}
if (typeof value === 'string' && isHexStrict(value)) {
return hexToNumber(value)
}
try {
return toNumber(BigInt(value))
} catch {
throw new Error('ivalid number: ' + value)
}
}
/**
* Auto converts any given value into it's bigint representation
*
* @param value - The value to convert
* @returns - Returns the value in bigint representation
* @example
* ```ts
* console.log(web3.utils.toBigInt(1));
* > 1n
* ```
*/
export const toBigInt = (value: unknown): bigint => {
if (typeof value === 'number') {
return BigInt(value)
}
if (typeof value === 'bigint') {
return value
}
// isHex passes for dec, too
if (typeof value === 'string' && isHex(value)) {
return BigInt(value)
}
if (typeof value === 'string' && value.indexOf(',') >= 0) {
return BigInt(value.replace(/,/g, ''))
}
throw new Error('invalid number' + value)
}
export const toBigWei = (number: Numbers, unit: EtherUnits = 'ether'): bigint => {
return toBigInt(toWei(number, unit))
}
export const toWei = (number: Numbers, unit: EtherUnits = 'ether'): string => {
const denomination = ethUnitMap[unit]
if (!denomination) {
throw new Error('error unit: ' + unit)
}
// if value is decimal e.g. 24.56 extract `integer` and `fraction` part
// to avoid `fraction` to be null use `concat` with empty string
typeof number === 'string' && number.indexOf(',') >= 0 && (number = number.replace(/,/g, ''))
const [integer, fraction] = String(typeof number === 'string' && !isHexStrict(number) ? number : toNumber(number))
.split('.')
.concat('')
// join the value removing `.` from
// 24.56 -> 2456
const value = BigInt(`${integer}${fraction}`)
// multiply value with denomination
// 2456 * 1000000 -> 2456000000
const updatedValue = value * denomination
// count number of zeros in denomination
const numberOfZerosInDenomination = denomination.toString().length - 1
// check which either `fraction` or `denomination` have lower number of zeros
const decimals = Math.min(fraction.length, numberOfZerosInDenomination)
if (decimals === 0) {
return updatedValue.toString()
}
// Add zeros to make length equal to required decimal points
// If string is larger than decimal points required then remove last zeros
return updatedValue.toString().padStart(decimals, '0').slice(0, -decimals)
}
/**
* Takes a number of wei and converts it to any other ether unit.
* @param number - The value in wei
* @param unit - The unit to convert to
* @returns - Returns the converted value in the given unit
*
* @example
* ```ts
* console.log(web3.utils.fromWei("1", "ether"));
* > 0.000000000000000001
*
* console.log(web3.utils.fromWei("1", "shannon"));
* > 0.000000001
* ```
*/
export const fromWei = (number: Numbers, unit: EtherUnits = 'ether'): string => {
const denomination = ethUnitMap[unit]
if (!denomination) {
throw new Error('invalid unit: ' + unit)
}
// value in wei would always be integer
// 13456789, 1234
const value = String(toNumber(number))
// count number of zeros in denomination
// 1000000 -> 6
const numberOfZerosInDenomination = denomination.toString().length - 1
if (numberOfZerosInDenomination <= 0) {
return value.toString()
}
// pad the value with required zeros
// 13456789 -> 13456789, 1234 -> 001234
const zeroPaddedValue = value.padStart(numberOfZerosInDenomination, '0')
// get the integer part of value by counting number of zeros from start
// 13456789 -> '13'
// 001234 -> ''
const integer = zeroPaddedValue.slice(0, -numberOfZerosInDenomination)
// get the fraction part of value by counting number of zeros backward
// 13456789 -> '456789'
// 001234 -> '001234'
const fraction = zeroPaddedValue.slice(-numberOfZerosInDenomination).replace(/\.?0+$/, '')
if (integer === '') {
return `0.${fraction}`
}
if (fraction === '') {
return integer
}
return `${integer}.${fraction}`
}

45
src/utils/chain.util.ts Normal file
View File

@ -0,0 +1,45 @@
import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util'
export function recoverTypedSignatureV4(signObj: any, signature: string) {
return recoverTypedSignature({
data: signObj,
signature,
version: SignTypedDataVersion.V4,
})
}
export function formatAddress(address: string) {
if (address.length >= 10) {
return address.substring(0, 8) + '...' + address.substring(address.length - 8)
} else if (address.length > 0 && address.length < 10) {
return address
} else {
return ''
}
}
export function buildLoginSignMsg(nonce: string, tips: string) {
const signMsg = {
tips,
nonce,
}
const signObj = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
],
set: [
{ name: 'tips', type: 'string' },
{ name: 'nonce', type: 'string' },
],
},
primaryType: 'set',
domain: {
name: 'Auth',
version: '1',
},
message: signMsg,
}
return signObj
}

221
src/utils/contract.util.ts Normal file
View 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, 5154
* 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
View 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',
}
}

39
src/utils/nft.util.ts Normal file
View File

@ -0,0 +1,39 @@
import { IDCounter } from 'models/IDCounter'
export const ONE_DAY = 24 * 60 * 60 * 1000
export const NFT_BEGIN_DAY = new Date(2023, 4, 8)
export const NFT_TYPE = {
badge1: 100, //2022NFT购买用户奖励徽章
badge2: 101,
badge3: 102,
badge4: 103,
}
export const MINT_CHANNEL = {
claim: '01', // 2022购买用户claim
}
// calc days between two Date
export function daysBetween(date1: Date, date2: Date) {
// hours*minutes*seconds*milliseconds
const diffInMs = Math.abs(date1.getTime() - date2.getTime())
const diffInDays = Math.round(diffInMs / ONE_DAY)
return diffInDays
}
/**
* nft的tokenid
* :
* 100 9999 00 0000001
* NFT类型
*/
export async function generateNftID(nfttype: number, channel: number) {
const days = daysBetween(new Date(), NFT_BEGIN_DAY)
const dayKey = (days + '').padStart(4, '0')
const channelKey = (channel + '').padStart(2, '0')
const idkey = nfttype + dayKey + channelKey
const idobj = await IDCounter.nextID(idkey)
const val = (idobj.seq + '').padStart(7, '0')
return idkey + val
}

287
src/utils/number.util.ts Normal file
View File

@ -0,0 +1,287 @@
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."
);
}
function checkRadixLegal(radix) {
return radix >= 2 && radix <= 62;
}
/**
* letter转为纯数字
* @param {string} letter
* @returns {number}
*/
function transformCharToNum(letter, base) {
if (base <= 36) {
letter = letter.toLowerCase();
}
if (letter >= '0' && letter <= '9') {
return parseInt(letter);
}
if (letter >= 'a' && letter <= 'z') {
return letter.charCodeAt(0) - 'a'.charCodeAt(0) + 10;
}
if (letter >= 'A' && letter <= 'Z') {
return letter.charCodeAt(0) - 'A'.charCodeAt(0) + 36;
}
return 0;
}
/**
*
* @param {number} num
* @return {string}
*/
function transformNumToChar(num, alphabet) {
alphabet = alphabet || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return alphabet.charAt(num);
}
/**
* num从base进制转为to指定的进制
* @param {string} numStr
* @param {number} base num的进制
* @param {number} to
* @return {string}
*/
export function convert({numStr, base, to, alphabet}) {
// 当base和to相等 或 base和to超出转换范围则原样返回
if (base === to || !checkRadixLegal(base) || !checkRadixLegal(to)) {
return numStr;
}
// 先转成10进制
let p = 0;
let number10 = 0;
while (p < numStr.length) {
number10 *= base;
number10 += transformCharToNum(numStr.charAt(p), base);
p++;
}
// 若要转换的正好是进制,则直接返回
if (to === 10) {
return number10.toString();
}
let result = '';
let cur;
while (number10) {
cur = number10 % to;
result = transformNumToChar(cur, alphabet) + result;
number10 = Math.floor(number10 / to);
}
return result;
}

138
src/utils/promise.util.ts Normal file
View 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()
}
}

View 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(str: string, key: any) {
const md5sum = crypto.createHmac('sha256', key)
md5sum.update(str)
str = md5sum.digest('hex')
return str
}
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
View 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
View 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_release.json Normal file
View File

@ -0,0 +1,22 @@
{
"apps": [
{
"name": "web-task-svr",
"script": "npm",
"args": "run prod:api",
"cwd": "/home/kingsome/code/web_task_svr",
"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"
}
}
]
}

22
start_test.json Normal file
View File

@ -0,0 +1,22 @@
{
"apps": [
{
"name": "web-task-svr",
"script": "npm",
"args": "run dev:api",
"cwd": "/home/kingsome/code/web_task_svr",
"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": "development"
},
"env_production": {
"NODE_ENV": "development"
}
}
]
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"ts-node": {
"files": true
},
"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
View File

@ -0,0 +1,11 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"no-console": false
},
"rulesDirectory": []
}

11
typings/extend.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import fastify from 'fastify'
declare module 'fastify' {
interface FastifyRequest {
roles?: string[]
user?: any
token?: string
permissions?: string[][]
depts?: string[]
}
}

4662
yarn.lock Normal file

File diff suppressed because it is too large Load Diff