完善分发流程
This commit is contained in:
parent
9cb37291fc
commit
f1e8d058c2
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Api",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run-script",
|
||||||
|
"dev:api"
|
||||||
|
],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"type": "pwa-node"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
@ -34,16 +34,6 @@ export class ApiServer {
|
|||||||
this.server.register(zReqParserPlugin)
|
this.server.register(zReqParserPlugin)
|
||||||
this.server.register(helmet, { hidePoweredBy: false })
|
this.server.register(helmet, { hidePoweredBy: false })
|
||||||
this.server.register(zTokenParserPlugin)
|
this.server.register(zTokenParserPlugin)
|
||||||
this.server.register(require('@fastify/view'), {
|
|
||||||
engine: {
|
|
||||||
ejs: require('ejs'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.server.register(require('@fastify/static'), {
|
|
||||||
root: path.join(__dirname, '../public'),
|
|
||||||
prefix: '/public/', // optional: default '/'
|
|
||||||
constraints: {}, // optional: default {}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.server.register(apiAuthPlugin, {
|
this.server.register(apiAuthPlugin, {
|
||||||
secret: process.env.API_TOKEN_SECRET,
|
secret: process.env.API_TOKEN_SECRET,
|
||||||
|
@ -652,6 +652,12 @@ interface Array<T> {
|
|||||||
* @return {Map<any, T>}
|
* @return {Map<any, T>}
|
||||||
*/
|
*/
|
||||||
toMap?<T>(key: string): Map<any, T>
|
toMap?<T>(key: string): Map<any, T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数组分块
|
||||||
|
* @param chunkSize
|
||||||
|
*/
|
||||||
|
chunkArray?<T>(chunkSize: number): T[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperties(Array.prototype, {
|
Object.defineProperties(Array.prototype, {
|
||||||
@ -940,6 +946,19 @@ Object.defineProperties(Array.prototype, {
|
|||||||
},
|
},
|
||||||
writable: true,
|
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> {
|
interface Map<K, V> {
|
||||||
|
@ -49,33 +49,10 @@ export default class AccountController extends BaseController {
|
|||||||
accountData.lastLogin = Date.now()
|
accountData.lastLogin = Date.now()
|
||||||
await accountData.save()
|
await accountData.save()
|
||||||
|
|
||||||
const token = await res.jwtSign({ id: accountData.id })
|
const token = await res.jwtSign({ id: accountData.id, address: accountData.address })
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
NonceRecord.removeExpired()
|
NonceRecord.removeExpired()
|
||||||
})
|
})
|
||||||
return { token }
|
return { token }
|
||||||
}
|
}
|
||||||
|
|
||||||
@router('get /api/user/status')
|
|
||||||
async info(req) {
|
|
||||||
let user = req.user
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@router('post /api/user/claim')
|
|
||||||
async claim(req, res) {
|
|
||||||
let user = req.user
|
|
||||||
logger.db('claim', req)
|
|
||||||
const {} = req.params
|
|
||||||
const taskId = ''
|
|
||||||
return { taskId }
|
|
||||||
}
|
|
||||||
|
|
||||||
@router('get /api/user/claim/:taskId')
|
|
||||||
async claimStatus(req, res) {
|
|
||||||
const { taskId } = req.params
|
|
||||||
const txHashList = []
|
|
||||||
let status = 0
|
|
||||||
return { status, txHashList }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
78
src/controllers/chain.controller.ts
Normal file
78
src/controllers/chain.controller.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { router } from 'decorators/router'
|
||||||
|
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
import { getMintableCount } from 'service/ChainSvr'
|
||||||
|
import { ClaimTask } from 'models/ClaimTask'
|
||||||
|
import { ChainQueue } from 'queue/chain.queue'
|
||||||
|
import { MINT_CHANNEL, NFT_TYPE } from 'utils/nft.util'
|
||||||
|
|
||||||
|
const NFT_TYPES = {
|
||||||
|
'1': NFT_TYPE.badge1,
|
||||||
|
'2': NFT_TYPE.badge2,
|
||||||
|
'3': NFT_TYPE.badge3,
|
||||||
|
'4': NFT_TYPE.badge4,
|
||||||
|
}
|
||||||
|
|
||||||
|
const D_ADDRESS = {
|
||||||
|
'1': process.env.CHAIN_DISTRIBUTOR_ADDRESS,
|
||||||
|
'2': process.env.CHAIN_DISTRIBUTOR_ADDRESS,
|
||||||
|
'3': process.env.CHAIN_DISTRIBUTOR_ADDRESS,
|
||||||
|
'4': process.env.CHAIN_DISTRIBUTOR_ADDRESS,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChainController extends BaseController {
|
||||||
|
@router('get /api/user/status')
|
||||||
|
async info(req) {
|
||||||
|
let { address } = req.user
|
||||||
|
if (!address) {
|
||||||
|
throw new ZError(10, 'user address not found')
|
||||||
|
}
|
||||||
|
let count = await getMintableCount(address)
|
||||||
|
let result = {
|
||||||
|
'1': !!count ? 1 : 0,
|
||||||
|
'2': 0,
|
||||||
|
'3': 0,
|
||||||
|
'4': 0,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@router('post /api/user/claim')
|
||||||
|
async claim(req, res) {
|
||||||
|
let user = req.user
|
||||||
|
logger.db('claim', req)
|
||||||
|
let { id } = req.params
|
||||||
|
id = id || '1'
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new ZError(10, 'id must be number')
|
||||||
|
}
|
||||||
|
let idNum = parseInt(id)
|
||||||
|
if (idNum > 4 || idNum < 1) {
|
||||||
|
throw new ZError(11, 'id can not > 4 and < 1')
|
||||||
|
}
|
||||||
|
let record = await ClaimTask.insertOrUpdate(
|
||||||
|
{ uid: user.id, type: id },
|
||||||
|
{
|
||||||
|
address: user.address,
|
||||||
|
$inc: { version: 1 },
|
||||||
|
nftType: NFT_TYPES[id],
|
||||||
|
channel: MINT_CHANNEL.claim,
|
||||||
|
dAddress: D_ADDRESS[id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const taskId = record.id
|
||||||
|
if (record.version === 1) {
|
||||||
|
new ChainQueue().addTask(record)
|
||||||
|
}
|
||||||
|
return { taskId }
|
||||||
|
}
|
||||||
|
|
||||||
|
@router('get /api/user/claim/:taskId')
|
||||||
|
async claimStatus(req, res) {
|
||||||
|
const { taskId } = req.params
|
||||||
|
const txHashList = []
|
||||||
|
let status = 0
|
||||||
|
return { status, txHashList }
|
||||||
|
}
|
||||||
|
}
|
49
src/controllers/internal.controller.ts
Normal file
49
src/controllers/internal.controller.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { role, router } from 'decorators/router'
|
||||||
|
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
import { ClaimTask } from 'models/ClaimTask'
|
||||||
|
import { hmacSha256 } from 'utils/security.util'
|
||||||
|
|
||||||
|
const calcHash = function (data: any) {
|
||||||
|
return hmacSha256(JSON.stringify(data), process.env.HASH_SALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InternalController extends BaseController {
|
||||||
|
@role(ROLE_ANON)
|
||||||
|
@router('post /api/internal/update_task')
|
||||||
|
async updateTaskInfo(req) {
|
||||||
|
let { sign, taskId, result, successCount, errorCount, hashList } = req.params
|
||||||
|
if (!sign) {
|
||||||
|
throw new ZError(10, 'sign not found')
|
||||||
|
}
|
||||||
|
let hash = calcHash({ taskId, result, successCount, errorCount, hashList })
|
||||||
|
if (sign !== hash) {
|
||||||
|
throw new ZError(11, 'sign not match')
|
||||||
|
}
|
||||||
|
logger.info(`task report:: ${taskId}|${result}|${successCount}|${errorCount}|${JSON.stringify(hashList)}}`)
|
||||||
|
if (!taskId) {
|
||||||
|
throw new ZError(11, 'taskId not found')
|
||||||
|
}
|
||||||
|
let record = await ClaimTask.findById(taskId)
|
||||||
|
if (!record) {
|
||||||
|
throw new ZError(12, 'task not found')
|
||||||
|
}
|
||||||
|
switch (result) {
|
||||||
|
case 2:
|
||||||
|
record.status = 9
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
record.status = 11
|
||||||
|
break
|
||||||
|
case 9:
|
||||||
|
record.status = 10
|
||||||
|
break
|
||||||
|
}
|
||||||
|
record.successCount = successCount
|
||||||
|
record.errorCount = errorCount
|
||||||
|
record.hashList = hashList
|
||||||
|
await record.save()
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
58
src/models/ClaimTask.ts
Normal file
58
src/models/ClaimTask.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||||
|
import { dbconn } from 'decorators/dbconn'
|
||||||
|
import { BaseModule } from './Base'
|
||||||
|
|
||||||
|
@dbconn()
|
||||||
|
@index({ uid: 1, type: 1 }, { unique: true })
|
||||||
|
@modelOptions({ schemaOptions: { collection: 'claim_task', timestamps: true } })
|
||||||
|
export class ClaimTaskClass extends BaseModule {
|
||||||
|
@prop({ required: true })
|
||||||
|
public uid: string
|
||||||
|
|
||||||
|
@prop({ required: true })
|
||||||
|
public type: string
|
||||||
|
// nft类型编码
|
||||||
|
@prop()
|
||||||
|
public nftType: string
|
||||||
|
// 渠道编号
|
||||||
|
@prop()
|
||||||
|
public channel: string
|
||||||
|
|
||||||
|
// 用户的钱包地址
|
||||||
|
@prop()
|
||||||
|
public address: string
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public dAddress: string
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public status: number
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public totalCount: number
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public successCount: number
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public errorCount: number
|
||||||
|
|
||||||
|
@prop({ type: () => [String] })
|
||||||
|
public nftList: string[]
|
||||||
|
|
||||||
|
@prop({ type: () => [String] })
|
||||||
|
public hashList: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0: 队列中, 等待上链
|
||||||
|
* 1: 已请求上链, 等待结果
|
||||||
|
* 2: 成功上链, 等待确认
|
||||||
|
* 9: 已确认成功(成功的最终状态)
|
||||||
|
* 10: 失败
|
||||||
|
* 11: 部分失败
|
||||||
|
*/
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public version: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClaimTask = getModelForClass(ClaimTaskClass, { existingConnection: ClaimTaskClass.db })
|
79
src/queue/chain.queue.ts
Normal file
79
src/queue/chain.queue.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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'
|
||||||
|
import { generateNftID } from 'utils/nft.util'
|
||||||
|
|
||||||
|
const MAX_BATCH_COUNT = 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* let data = {
|
||||||
|
taskId: '1',
|
||||||
|
type: 2,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
address: '0xd45A464a2412A2f83498d13635698a041b9dBe9b',
|
||||||
|
to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
|
tokenIds: ['3'],
|
||||||
|
configIds: ['1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
export class ChainQueue {
|
||||||
|
private queue: AsyncQueue
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.queue = createAsyncQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTask(task: DocumentType<ClaimTaskClass>) {
|
||||||
|
this.queue.push(async () => {
|
||||||
|
try {
|
||||||
|
let numRes = await getMintableCount(task.address)
|
||||||
|
task.countTotal = numRes.data
|
||||||
|
if (task.countTotal === 0) {
|
||||||
|
task.status = 10
|
||||||
|
await task.save()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// datas: [{address: distributor's address, to: userAddress, nftList: []}]
|
||||||
|
const address = task.dAddress
|
||||||
|
const to = task.address
|
||||||
|
let nftList: string[] = []
|
||||||
|
for (let i = 0; i < task.countTotal; i++) {
|
||||||
|
let tokenId = await generateNftID(task.nftType, task.channel)
|
||||||
|
nftList.push(tokenId)
|
||||||
|
}
|
||||||
|
let all = nftList.chunkArray(MAX_BATCH_COUNT)
|
||||||
|
let datas: any = []
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
datas.push({
|
||||||
|
address,
|
||||||
|
to,
|
||||||
|
nftList: all[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
task.nftList = nftList
|
||||||
|
let reqData = {
|
||||||
|
taskId: task.id,
|
||||||
|
type: 5,
|
||||||
|
data: datas,
|
||||||
|
}
|
||||||
|
|
||||||
|
await addTask(reqData)
|
||||||
|
task.status = 1
|
||||||
|
await task.save()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('error add chain task: ')
|
||||||
|
logger.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
30
src/service/ChainSvr.ts
Normal file
30
src/service/ChainSvr.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const ADD_TASK_URI = '/chain/req'
|
||||||
|
const USER_INFO_URI = '/chain/query_info'
|
||||||
|
|
||||||
|
export async function addTask(data) {
|
||||||
|
let url = `${process.env.CHAIN_CLIENT_URL}${ADD_TASK_URI}`
|
||||||
|
let reqConfig: any = {
|
||||||
|
method: 'post',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
return axios(reqConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMintableCount(address: string) {
|
||||||
|
let url = `${process.env.CHAIN_CLIENT_URL}${USER_INFO_URI}`
|
||||||
|
let reqConfig: any = {
|
||||||
|
method: 'post',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify({ address }),
|
||||||
|
}
|
||||||
|
return axios(reqConfig).then(response => response.data)
|
||||||
|
}
|
39
src/utils/nft.util.ts
Normal file
39
src/utils/nft.util.ts
Normal 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 nfttype + val
|
||||||
|
}
|
74
src/utils/security.util.ts
Normal file
74
src/utils/security.util.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
export function hmac(input, key, out) {
|
||||||
|
return out
|
||||||
|
? crypto.createHmac('sha1', key).update(input).digest(out)
|
||||||
|
: crypto.createHmac('sha1', key).update(input).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genRandomString(length) {
|
||||||
|
return crypto
|
||||||
|
.randomBytes(Math.ceil(length / 2))
|
||||||
|
.toString('hex')
|
||||||
|
.slice(0, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha512(password, salt) {
|
||||||
|
let hash = crypto.createHmac('sha512', salt)
|
||||||
|
hash.update(password)
|
||||||
|
let value = hash.digest('hex')
|
||||||
|
return {
|
||||||
|
salt: salt,
|
||||||
|
passwordHash: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha1(str) {
|
||||||
|
const md5sum = crypto.createHash('sha1')
|
||||||
|
md5sum.update(str)
|
||||||
|
str = md5sum.digest('hex')
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hmacSha256(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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user