完善分发流程
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(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,
|
||||
|
@ -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> {
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
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