完善分发流程

This commit is contained in:
zhl 2023-04-21 10:13:50 +08:00
parent 9cb37291fc
commit f1e8d058c2
11 changed files with 448 additions and 34 deletions

21
.vscode/launch.json vendored Normal file
View 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"
},
]
}

View File

@ -34,16 +34,6 @@ export class ApiServer {
this.server.register(zReqParserPlugin)
this.server.register(helmet, { hidePoweredBy: false })
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, {
secret: process.env.API_TOKEN_SECRET,

View File

@ -652,6 +652,12 @@ interface Array<T> {
* @return {Map<any, T>}
*/
toMap?<T>(key: string): Map<any, T>
/**
*
* @param chunkSize
*/
chunkArray?<T>(chunkSize: number): T[][]
}
Object.defineProperties(Array.prototype, {
@ -940,6 +946,19 @@ Object.defineProperties(Array.prototype, {
},
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> {

View File

@ -49,33 +49,10 @@ export default class AccountController extends BaseController {
accountData.lastLogin = Date.now()
await accountData.save()
const token = await res.jwtSign({ id: accountData.id })
const token = await res.jwtSign({ id: accountData.id, address: accountData.address })
setImmediate(() => {
NonceRecord.removeExpired()
})
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 }
}
}

View 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 }
}
}

View 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
View 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
View 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
View 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
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 nfttype + val
}

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
}