From 49fa01ce7e2bde7006a3a1b00d4209bdf5a9cf59 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:02:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=9C=80=E6=96=B0=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E4=BF=AE=E6=94=B9cec=20claim=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cec_claim.md | 26 ++--- src/common/Utils.ts | 9 ++ src/controllers/cec.controller.ts | 155 ++++++++++++--------------- src/models/BitgitBindInfo.ts | 21 +++- src/models/CECRecordTotal.ts | 51 ++++++--- src/models/chain/TokenClaimRecord.ts | 66 ++++++++++++ src/services/chain.svr.ts | 30 ++++++ 7 files changed, 234 insertions(+), 124 deletions(-) create mode 100644 src/models/chain/TokenClaimRecord.ts diff --git a/docs/cec_claim.md b/docs/cec_claim.md index 884a590..9e9cf23 100644 --- a/docs/cec_claim.md +++ b/docs/cec_claim.md @@ -11,27 +11,14 @@ ```js { + "stage": 0, // 当前所处阶段, 0: 预充值阶段, 1, 2: 对应的阶段 "total": "200000000000000000000", // 总量 "available": "100000000000000000000", // 当前可获取的数量 "claimed": "0", // 已领取的数量 - "outerAccount": 0, // 是否已绑定交易所账号 - "stages": [ // 阶段信息 - { - "stage": 1, - "amount": "100000000000000000000", // 当前阶段可获取的数量 - "status": 0, // 领取状态, 0: 未领取, 1: 领取中, 9: 已领取 - "unlocked": true, // 是否已解锁 - "claimTime": 1720685893000, // 领取时间 - "unlockTime": 1720685893000 // 解锁时间 - }, - { - "stage": 2, - "amount": "100000000000000000000", - "status": 0, - "unlocked": false, - "unlockTime": 1720772293000 - } - ], + "unavailable": "100000000000000000000", // 不可领取的数量 + "bindUid": "1231****3333", // 绑定了的交易所账号 + "bindAddress": "0x44****3333", // 绑定了的交易所钱包地址 + "records": [ { "address": "0x50a8e60041a206acaa5f844a1104896224be6f39", @@ -103,7 +90,8 @@ body: ```js { - "accid": "bitget交易所账号id" + "accid": "bitget交易所账号id", + "address": "bitget交易所账号绑定的钱包地址" } diff --git a/src/common/Utils.ts b/src/common/Utils.ts index aef84b6..f5e61cc 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -75,3 +75,12 @@ export const randomSign = () => { hex += Math.random() > 0.5 ? '1b' : '1c' return hex } + +export const hidePartString = (str: string, showNum: number = 8) => { + if (str.length <= showNum) { + return str + } + let len = str.length + let half = Math.floor(showNum / 2) + return str.slice(0, half) + '...' + str.slice(len - half) +} \ No newline at end of file diff --git a/src/controllers/cec.controller.ts b/src/controllers/cec.controller.ts index dcae9ee..f3472c3 100644 --- a/src/controllers/cec.controller.ts +++ b/src/controllers/cec.controller.ts @@ -5,14 +5,16 @@ import { CECRecord } from 'models/CECRecord' import { ethers } from 'ethers' import { CECClaimHistory, CECClaimStatus } from 'models/CECClaimHistory' import { SyncLocker } from 'common/SyncLocker' -import { CECRecordTotal, CECStatusEnum } from 'models/CECRecordTotal' -import { buildTokenClaimData } from 'services/chain.svr' +import { CECRecordTotal, CECStatusEnum, ClaimStatus, ClaimStatusEnum } from 'models/CECRecordTotal' +import { buildTokenClaimData, updateClaimStatus } from 'services/chain.svr' import { Wallet } from 'models/Wallet' import { PlatEnum } from 'enums/PlatEnum' import { BitgetBindInfo } from 'models/BitgitBindInfo' +import { hidePartString } from 'common/Utils' const STAGE1_UNLOCK_TIME = Number(process.env.CEC_CLAIM_STAGE1) const STAGE2_UNLOCK_TIME = Number(process.env.CEC_CLAIM_STAGE2) +const CEC_CLAIM_STAGE = Number(process.env.CEC_CLAIM_STAGE) const CEC_ADDRESS = process.env.CEC_CONTRACT const checkAddress = (address: string) => { @@ -24,6 +26,52 @@ const checkAddress = (address: string) => { } return ethers.utils.getAddress(address).toLowerCase() } + +/** + * 查询cec claim状态 + * 第一期是比较特殊的记录, 在预售阶段, 需要展示第一期可领取的数量, 所以要特殊处理 + * @param address 有资格的钱包地址 + * @returns + */ +const queryCECClaimStatus = async (address: string) => { + let total = 0n + let available = 0n + let claimed = 0n + let unavailable = 0n + const records = await CECRecordTotal.find({ address }) + const bindRecord = await BitgetBindInfo.findOne({ wallet: address }) + let bit = 0n + for (const record of records) { + let currentTotal = BigInt(record.amount) + total += currentTotal + // fetch status from claimStatus + for (const [key, val] of record.claimStatus.entries()) { + const stage = parseInt(key) + // 预充值阶段, 需要展示第一期可领取的数量 + if (stage === 1) { + if (CEC_CLAIM_STAGE === 0) { + available += (currentTotal * BigInt(val.rate)) / 100n + } else { + if (bindRecord && !bindRecord.invalid) { + claimed += (currentTotal * BigInt(val.rate)) / 100n + } else if (val.status === ClaimStatusEnum.NORMAL){ + available += (currentTotal * BigInt(val.rate)) / 100n + bit = bit | 1n << BigInt(val.bit) + } else if ( val.status === ClaimStatusEnum.CLAIMED) { + claimed += (currentTotal * BigInt(val.rate)) / 100n + } + } + } else if (stage <= CEC_CLAIM_STAGE && val.status === ClaimStatusEnum.NORMAL) { + available += (currentTotal * BigInt(val.rate)) / 100n + bit = bit | 1n << BigInt(val.bit) + } else if ( val.status === ClaimStatusEnum.CLAIMED) { + claimed += (currentTotal * BigInt(val.rate)) / 100n + } + } + } + unavailable = total - available - claimed + return { total, available, claimed, unavailable, bit, records, bindRecord } +} /** * CEC领取相关接口 */ @@ -48,6 +96,8 @@ class CECController extends BaseController { let available = 0n let claimed = 0n const records = await CECRecord.find({ address }) + //@ts-ignore + await updateClaimStatus({ address, token: CEC_ADDRESS, records}) const now = Date.now() const stages = [] const lists = [] @@ -106,59 +156,16 @@ class CECController extends BaseController { async scoreInfoTotal(req: any) { let { address } = req.params address = checkAddress(address) - let total = 0n - let available = 0n - let claimed = 0n - const records = await CECRecordTotal.find({ address }) - const now = Date.now() - const stages = [] - const lists = [] - let firstTotal = 0n - let firstAvailable = 0n - let secondAvailable = 0n - for (const record of records) { - let currentTotal = BigInt(record.amount) - total += currentTotal - let firstRate = record.firstRate || 50 - let firstAmount = currentTotal * BigInt(firstRate) / 100n - firstTotal += firstAmount - if (STAGE1_UNLOCK_TIME < now && record.status == CECStatusEnum.NORMAL) { - available += firstAmount - firstAvailable += firstAmount - } - if (STAGE2_UNLOCK_TIME < now - && record.status != CECStatusEnum.STAGE2_CLAIMED - && record.firstRate < 100) { - available += (currentTotal - firstAmount) - secondAvailable += (currentTotal - firstAmount) - } - } - - stages.push({ - stage: 1, - amount: firstTotal.toString(), - available: firstAvailable.toString(), - status: 0, - unlocked: STAGE1_UNLOCK_TIME < now, - unlockTime: STAGE1_UNLOCK_TIME - }) - stages.push({ - stage: 2, - amount: (total - firstTotal).toString(), - available: secondAvailable.toString(), - status: 0, - unlocked: STAGE2_UNLOCK_TIME < now, - unlockTime: STAGE2_UNLOCK_TIME - }) - const bindRecord = await BitgetBindInfo.findOne({ address: address }) - + const { total, available, claimed, unavailable, records, bindRecord } = await queryCECClaimStatus(address) return { + stage: CEC_CLAIM_STAGE, total: total.toString(), available: available.toString(), claimed: claimed.toString(), - stages, - records: lists, - outerAccount: bindRecord? 1: 0 + unavailable: unavailable.toString(), + records: records.map(record => record.toJson()), + bindUid: bindRecord ? hidePartString(bindRecord.biggetAcc) : '', + bindAddress: bindRecord ? hidePartString(bindRecord.address) : '' } } @@ -168,7 +175,7 @@ class CECController extends BaseController { logger.db('claim_cec', req) const user = req.user const now = Date.now() - if (STAGE1_UNLOCK_TIME > now) { + if (CEC_CLAIM_STAGE === 0) { throw new ZError(14, 'not begin') } let wallet: string; // 通过该地址查询可以claim的cec数量 @@ -187,33 +194,7 @@ class CECController extends BaseController { wallet = wallet.toLowerCase() let { address } = req.params // 这个地址用于执行claim的动作 address = checkAddress(address) - const bindRecord = await BitgetBindInfo.findOne({ address: wallet }) - if (bindRecord) { - throw new ZError(18, 'already bind exchange account') - } - const records = await CECRecordTotal.find({ address: wallet }) - if (records.length === 0) { - throw new ZError(15, 'record not found') - } - let total = 0n - let available = 0n - let bit = 0 - for (const record of records) { - let currentTotal = BigInt(record.amount) - total += currentTotal - let firstRate = record.firstRate || 50 - let firstAmount = currentTotal * BigInt(firstRate) / 100n - if (STAGE1_UNLOCK_TIME < now && record.status == CECStatusEnum.NORMAL) { - available += firstAmount - bit = bit | 1 << (record.bit * 2) - } - if (STAGE2_UNLOCK_TIME < now - && record.status != CECStatusEnum.STAGE2_CLAIMED - && record.firstRate < 100) { - available += (currentTotal - firstAmount) - bit = bit | 1 << (record.bit * 2 + 1) - } - } + const { available, bit } = await queryCECClaimStatus(wallet) if (available === 0n) { throw new ZError(16, 'no cec to claim') } @@ -223,7 +204,7 @@ class CECController extends BaseController { account: wallet, token: CEC_ADDRESS, amount: available.toString(), - bit, + bit: bit.toString(), nonce }) return { calls: [{trans_req: data, trans_id: ''}], direct: true } @@ -232,14 +213,14 @@ class CECController extends BaseController { @router('post /api/cec/bind_account') async bindAccount(req: any) { const user = req.user - const { accid } = req.body - if (!accid) { + const { accid, address } = req.body + if (!accid || !address) { throw new ZError(11, 'accid is required') } - const now = Date.now() - if (STAGE1_UNLOCK_TIME > now) { - throw new ZError(14, 'not begin') + if (!ethers.utils.isAddress(address)) { + throw new ZError(12, 'address is invalid') } + let wallet: string; // 通过该地址查询可以claim的cec数量 if (user.plat === PlatEnum.EXTERNAL_WALLET) { wallet = user.openId || user.openid @@ -260,11 +241,11 @@ class CECController extends BaseController { throw new ZError(15, 'record not found') } - let record = await BitgetBindInfo.findOne({ address: wallet }) + let record = await BitgetBindInfo.findOne({ wallet }) if (record) { throw new ZError(17, 'already bind') } - record = new BitgetBindInfo({ address: wallet, biggetAcc: accid }) + record = new BitgetBindInfo({ address, wallet, biggetAcc: accid }) await record.save() return {} } diff --git a/src/models/BitgitBindInfo.ts b/src/models/BitgitBindInfo.ts index 9990ded..f5a1dda 100644 --- a/src/models/BitgitBindInfo.ts +++ b/src/models/BitgitBindInfo.ts @@ -16,18 +16,33 @@ import { BaseModule } from './Base' export interface BitgetUserClass extends Base, TimeStamps {} @dbconn() -@index({ address: 1 }, { unique: true }) +@index({ wallet: 1 }, { unique: true }) @index({ biggetAcc: 1 }, { unique: false}) @modelOptions({ schemaOptions: { collection: 'bitget_bing_info', timestamps: true }, - options: { allowMixed: Severity.ALLOW }, }) export class BitgetBindInfoClass extends BaseModule { + /** + * 用户钱包地址, 内置钱包, metamask等 + */ + @prop({ required: true }) + public wallet: string + + /** + * 交易所账号对应的钱包地址, 需用户提交 + */ @prop({ required: true }) public address: string - + /** + * 交易所账号uid + */ @prop() public biggetAcc: string + /** + * 由交易所反馈, 绑定信息无效 + */ + @prop({ default: false }) + public invalid: boolean } diff --git a/src/models/CECRecordTotal.ts b/src/models/CECRecordTotal.ts index 227835c..9a4aa48 100644 --- a/src/models/CECRecordTotal.ts +++ b/src/models/CECRecordTotal.ts @@ -13,6 +13,23 @@ export enum CECStatusEnum { STAGE1_CLAIMED = 2, STAGE2_CLAIMED = 3 } + +export enum ClaimStatusEnum { + NORMAL = 1, + CLAIMED = 2 +} + +@modelOptions({ schemaOptions: { _id: false } }) +export class ClaimStatus { + @prop() + public bit: number + @prop() + public rate: number + @prop({ enum: ClaimStatusEnum, default: ClaimStatusEnum.NORMAL }) + public status: ClaimStatusEnum + @prop() + public time: number +} /** * CEC赚取记录 */ @@ -21,7 +38,7 @@ export enum CECStatusEnum { @modelOptions({ schemaOptions: { collection: 'cec_record_total', timestamps: true }, }) -class CECRecordTotalClass extends BaseModule { +export class CECRecordTotalClass extends BaseModule { @prop() public address: string @@ -31,23 +48,27 @@ class CECRecordTotalClass extends BaseModule { @prop() public num: number - /** - * claim时作为标记位 - * - 0: uaw - 1: p2a - 2: game test parse 1 - 3: Loyalty Points Rewards - 4: Badge staking rewards - 5: Gacha Journey - 6: Rase of Gacha - 7: game season rank - 8: hash rate rewards - 9: old game event - **/ @prop() public bit: number + /** + * 各claim阶段代表的bit位 + * uaw: 0, 1 + * p2a: 2, 3 + * game test parse 1: 4 + * Loyalty Points Rewards: 5 + * Badge staking rewards: 6, 7 + * Gacha Journey: 8, 9 + * Rase of Gacha: 10, 11 + * game season rank: 12, 13 + * hash rate rewards: 14, 15 + * old game event: 16, 17 + * p2e season 2: 18 + * founder's tag holder: 19 + */ + @prop({ type: () => ClaimStatus, _id: false }) + public claimStatus?: Map; + @prop() public earnTime: string diff --git a/src/models/chain/TokenClaimRecord.ts b/src/models/chain/TokenClaimRecord.ts new file mode 100644 index 0000000..68da644 --- /dev/null +++ b/src/models/chain/TokenClaimRecord.ts @@ -0,0 +1,66 @@ +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' +import { GeneralEventClass } from './GeneralEvent' + +@dbconn() +@index({ chain: 1, address: 1, token: 1, account: 1 }, { unique: true }) +@index({ chain: 1, address: 1, token: 1, user: 1 }, { unique: true }) +@modelOptions({ + schemaOptions: { collection: 'token_claim_record', timestamps: true }, +}) +export class TokenClaimRecordClass extends BaseModule { + @prop({ required: true }) + public address!: string + @prop({ required: true }) + public chain: string + + @prop({ type: () => [Number], default: [] }) + public blockNumbers: number[] + + @prop({ type: () => [String], default: [] }) + public users: string[] + + @prop() + public account: string + @prop() + public token: string + @prop() + public amount: string + @prop() + public bit: string + + + public static async parseEvent(event: Partial) { + const address = event.address + const chain = event.chain + const user = event.decodedData?.user + const account = event.decodedData?.account + const token = event.decodedData?.token + const amount = event.decodedData?.amount + const bit = event.decodedData?.bit + const blockNumber = event.blockNumber + let bitBn = BigInt(bit) + let record = await TokenClaimRecord.findOne({ chain, address, account, token }) + if (!record) { + record = new TokenClaimRecord({ chain, address, account, token, amount, bit, blockNumbers: [blockNumber], users: [user] }) + await record.save() + } else { + if ((BigInt(record.bit) & bitBn) == 0n) { + record.amount = (BigInt(record.amount) + BigInt(amount)).toString() + record.bit = (BigInt(record.bit) | bitBn).toString() + if (!record.blockNumbers.includes(blockNumber)) { + record.blockNumbers.push(blockNumber) + } + if (!record.users.includes(user)) { + record.users.push(user) + } + await record.save() + } + } + } +} + +export const TokenClaimRecord = getModelForClass(TokenClaimRecordClass, { + existingConnection: TokenClaimRecordClass['db'], +}) diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index 62a161b..f5167cc 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -1,7 +1,9 @@ import { Contract } from 'ethers' +import { CECRecordTotalClass, CECStatusEnum } from 'models/CECRecordTotal' import { CheckIn } from 'models/chain/CheckIn' import { NftHolder } from 'models/chain/NftHolder' import { NftStake } from 'models/chain/NftStake' +import { TokenClaimRecord } from 'models/chain/TokenClaimRecord' import { sign } from 'utils/sign.utils' import { getMonthBegin, getNDayAgo } from 'utils/utcdate.util' import { timeoutFetch } from 'zutils/utils/net.util' @@ -167,6 +169,34 @@ export const fetchClaimStatus = async (address: string, taskId: string) => { return fetchChainStatus(address, `0x${method}${addressStr}${valStr}`) } +export const updateClaimStatus = async ( + {address, account, token, records}: + {address: string, account: string, token: string, records: Partial[]}) => { + const chain = process.env.CHAIN + '' + const record = await TokenClaimRecord.findOne({ chain, address, account, token }) + if (!record) { + return + } + for (let item of records) { + let bit = BigInt(item.bit * 2) + let bit1 = bit + 1n + let changed = false + if (item.status == CECStatusEnum.NORMAL && (BigInt(record.bit) & 1n << bit) > 0n ) { + item.status = CECStatusEnum.STAGE1_CLAIMED + changed = true + } + if (item.firstRate < 100 && item.status != CECStatusEnum.STAGE2_CLAIMED && (BigInt(record.bit) & 1n << bit1) > 0n) { + item.status = CECStatusEnum.STAGE2_CLAIMED + changed = true + } + if (changed) { + //@ts-ignore + await item.save() + } + + } +} + const claimTokenAbi = [ 'function claim(address,address,uint256[4],bytes)', ]