From 3cc1908d4646c213208c07a8963fcc6d381fc0eb Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:42:03 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E9=A2=86=E5=8F=96stake=20reward=E7=9A=84?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 13 ++++ package.json | 1 + src/controllers/simplestake.controller.ts | 75 +++++++++++++++++++-- src/models/CECRecordTotal.ts | 1 + src/models/stake/CecStakeReward.ts | 58 +++++++++++++++++ src/schedule/stake.schedule.ts | 4 ++ src/scripts/stakeReward.ts | 79 +++++++++++++++++++++++ src/services/chain.svr.ts | 37 +++++++++++ 8 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/models/stake/CecStakeReward.ts create mode 100644 src/scripts/stakeReward.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index cbae541..92ebc22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,19 @@ ], "type": "node" }, + { + "name": "Debug Script", + "request": "launch", + "runtimeArgs": [ + "run-script", + "cecstake" + ], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node" + }, { "name": "Debug Admin", "request": "launch", diff --git a/package.json b/package.json index 4fcab0f..16583ed 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "testdraw": "ts-node -r tsconfig-paths/register src/scripts/testdraw.ts", "importcec": "ts-node -r tsconfig-paths/register src/scripts/importCEC.ts", "parsecec": "ts-node -r tsconfig-paths/register src/scripts/cecStatic.ts", + "cecstake": "ts-node -r tsconfig-paths/register src/scripts/stakeReward.ts", "test:watch": "jest --watch", "test": "jest" }, diff --git a/src/controllers/simplestake.controller.ts b/src/controllers/simplestake.controller.ts index 2e6b9f7..ae41d32 100644 --- a/src/controllers/simplestake.controller.ts +++ b/src/controllers/simplestake.controller.ts @@ -1,18 +1,31 @@ -import { BaseController, ROLE_ANON, role, router } from 'zutils' +import { BaseController, ROLE_ANON, ZError, role, router } from 'zutils' import { CecTvl } from 'models/stake/CecTvl' -import { retry } from 'zutils/utils/promise.util' -import { queryCecTvl } from 'services/chain.svr' +import { ethers } from 'ethers' import { fromWei } from 'zutils/utils/bn.util' +import { buildTokenClaimData, updateStakeRewardStatus } from 'services/chain.svr' +import { CecStakeReward, ClaimStatusEnum } from 'models/stake/CecStakeReward' const CHAIN = process.env.CLAIM_CHAIN +const CEC_ADDRESS = process.env.CEC_CONTRACT +const CEC_CLAIM_CONTRACT = process.env.CLAIM_CONTRACT const awardList = [ { amount: 5000000, reward: 0}, + // { amount: 5000000, reward: 10000}, { amount: 10000000, reward: 400000}, { amount: 18000000, reward: 900000}, { amount: 20000000, reward: 2000000}, ] +const checkAddress = (address: string) => { + if (!address) { + throw new ZError(11, 'address is required') + } + if (!ethers.utils.isAddress(address)) { + throw new ZError(12, 'address is invalid') + } + return ethers.utils.getAddress(address).toLowerCase() +} export default class SimpleStakeController extends BaseController { @role(ROLE_ANON) @@ -37,8 +50,62 @@ export default class SimpleStakeController extends BaseController { } let reward = award.reward let result = (reward / totalNum)/30*360 - result = result || 0 return result } + @role(ROLE_ANON) + @router('get /api/simple_stake/reward_list/:address') + async rewardList(req) { + let { address } = req.params + const now = Date.now() / 1000 + const stakeEndTime = parseInt(process.env.STAKE_END_TIME) + if (now < stakeEndTime) { + throw new ZError(13, 'stake not end') + } + address = checkAddress(address) + const records = await CecStakeReward.find({ address }).sort({ stakeTime: 1 }) + await updateStakeRewardStatus({ address: CEC_CLAIM_CONTRACT, account: address, token: CEC_ADDRESS, records }) + let results = [] + for (const record of records) { + results.push(record.toJson()) + } + return results + } + + @role(ROLE_ANON) + @router('post /api/simple_stake/claim') + async claim(req) { + let { address } = req.params + address = checkAddress(address) + const now = Date.now() + const stakeEndTime = parseInt(process.env.STAKE_END_TIME) * 1000 + if (now < stakeEndTime) { + throw new ZError(13, 'stake not end') + } + const records = await CecStakeReward.find({ address }).sort({ stakeTime: 1 }) + await updateStakeRewardStatus({ address: CEC_CLAIM_CONTRACT, account: address, token: CEC_ADDRESS, records }) + let available = BigInt(0) + let claimed = false + for (const record of records) { + if (record.status === ClaimStatusEnum.CLAIMED) { + claimed = true + break + } + available = BigInt(record.amount) + } + if (claimed) { + throw new ZError(14, 'already claimed') + } + const nonce = now + '' + ((Math.random() * 1000) | 0) + const bit = 0n | (1n << BigInt(27n)) + let data = await buildTokenClaimData({ + address, + account: address, + token: CEC_ADDRESS, + amount: available.toString(), + bit: bit.toString(), + nonce, + }) + return { calls: [{ trans_req: data, trans_id: '' }], direct: true } + } } diff --git a/src/models/CECRecordTotal.ts b/src/models/CECRecordTotal.ts index 06bfa14..2853f09 100644 --- a/src/models/CECRecordTotal.ts +++ b/src/models/CECRecordTotal.ts @@ -64,6 +64,7 @@ export class CECRecordTotalClass extends BaseModule { * discord ticket: 26, 7, 9, 11, 13, 15, 17, 20, 21, 22, 23, 24, 25 * p2e season 2(Contribution Clash): 18 * founder's tag holder: 19 + * CEC stake reward: 27 // 2025年1月添加 * 机动: 100, 101, 102, 103, 104, 105, 106, 107, 108, 109 * 机动位的用法: 比如现在是第三期, 要给某用户补发badge的奖励, 但他已经领了1,2期的奖励, 那就把他1,2期rate设为0, * 把第3期的rate改为16, bit位改为未使用的机动位 diff --git a/src/models/stake/CecStakeReward.ts b/src/models/stake/CecStakeReward.ts new file mode 100644 index 0000000..d08b9c2 --- /dev/null +++ b/src/models/stake/CecStakeReward.ts @@ -0,0 +1,58 @@ +import { dbconn } from 'decorators/dbconn' +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { BaseModule } from '../Base' + +export enum ClaimStatusEnum { + NORMAL = 1, + CLAIMED = 2, +} + +/** + * 2025/01/25 CEC质押活动 + * bit: 27 + */ +@dbconn() +@index({ address: 1, stakeTime: 1 }, { unique: false }) +@modelOptions({ + schemaOptions: { collection: 'cec_stake_reward', timestamps: true }, +}) +export class CecStakeRewardClass extends BaseModule { + @prop() + public address: string + + @prop() + public stakeAmount: string + @prop() + public stakeNum: number + @prop() + public stakeTime: number + + @prop() + public amount: string + // 用于显示 + @prop() + public num: number + + @prop() + public apy: number + + @prop({ enum: ClaimStatusEnum, default: ClaimStatusEnum.NORMAL }) + public status: ClaimStatusEnum + + @prop() + public desc: string + + public toJson() { + return { + address: this.address, + amount: this.amount.toString(), + num: this.num, + desc: this.desc, + apy: this.apy, + status: this.status, + stakeAmount: this.stakeAmount, + stakeTime: this.stakeTime, + } + } +} +export const CecStakeReward = getModelForClass(CecStakeRewardClass, { existingConnection: CecStakeRewardClass['db'] }) diff --git a/src/schedule/stake.schedule.ts b/src/schedule/stake.schedule.ts index ca8a6c9..b2e5b16 100644 --- a/src/schedule/stake.schedule.ts +++ b/src/schedule/stake.schedule.ts @@ -11,12 +11,16 @@ import { singleton } from 'zutils' import { CecTvl } from 'models/stake/CecTvl' const CHAIN = process.env.CLAIM_CHAIN +const startTime = Number(process.env.STAKE_START_TIME) /** * 每日定时更新TVL缓存 */ @singleton export default class StakeSchedule { async updateCache() { + if (Date.now() / 1000 < startTime) { + return + } try { let preday = getDayBegin(yesterday(new Date())) diff --git a/src/scripts/stakeReward.ts b/src/scripts/stakeReward.ts new file mode 100644 index 0000000..2a4eca7 --- /dev/null +++ b/src/scripts/stakeReward.ts @@ -0,0 +1,79 @@ +import * as dotenv from 'dotenv' +const envFile = process.env.NODE_ENV && process.env.NODE_ENV === 'production' ? `.env.production` : '.env.development' +dotenv.config({ path: envFile }) +console.log(process.env.DB_MAIN) + +import { fromWei } from 'zutils/utils/bn.util' + +import { GeneralEvent } from '../models/chain/GeneralEvent' +import { CecStakeReward } from '../models/stake/CecStakeReward' + +const awardList = [ + // { amount: 5000000, reward: 10000}, // for test + { amount: 5000000, reward: 0}, + { amount: 10000000, reward: 400000}, + { amount: 18000000, reward: 900000}, + { amount: 20000000, reward: 2000000}, +] + +const main = async () => { + const startTime = Number(process.env.STAKE_START_TIME) + const endTime = Number(process.env.STAKE_END_TIME) + const events = await GeneralEvent.find({ + chain: process.env.CLAIM_CHAIN, + address: process.env.CEC_CONTRACT.toLowerCase(), + event: 'Transfer', + 'decodedData.to': process.env.STAKE_CONTRACT.toLowerCase(), + $and: [{blockTime: { $gte: startTime }}, {blockTime: { $lte: endTime }}] + }) + let total = 0n; + let timeAndAmount = 0n + for (let event of events) { + total += BigInt(event.decodedData.value) + timeAndAmount += BigInt(event.decodedData.value) * BigInt(endTime - event.blockTime) + } + let reward = 0n + let max = 20000000n * 10n**18n + let totalOrMax = total < max ? total : max + for (let item of awardList) { + if (totalOrMax < BigInt(item.amount * 10**18)) { + reward = BigInt(item.reward) + break + } + } + if (reward === 0n) { + console.log('total:', total.toString()) + return + } + + // const rewardNum = parseFloat(reward.toString()) + // const totalNum = parseFloat(fromWei(total, 'ether')) + // const apy = rewardNum/totalNum / 30 * 360 + reward = reward * 10n**18n + for (let event of events) { + let amount = BigInt(endTime - event.blockTime) * BigInt(event.decodedData.value) * BigInt(reward) / timeAndAmount + let amountNm = parseFloat(fromWei(amount, 'ether')) + let stakeAmount = parseFloat(fromWei(event.decodedData.value, 'ether')) + let apy = amountNm/stakeAmount / 30 * 360 + let record = new CecStakeReward() + record.address = event.decodedData.from + record.stakeAmount = event.decodedData.value + record.stakeNum = stakeAmount + record.stakeTime = event.blockTime + record.amount = amount.toString() + record.num = amountNm + record.apy = apy + record.status = 1 + record.desc = '' + await record.save() + } +} + +;(async () => { + try { + await main(); + } catch (e) { + console.log(e) + } + process.exit(0) +})() \ No newline at end of file diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index dbf6fe4..1742500 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -4,6 +4,7 @@ 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 { CecStakeRewardClass } from 'models/stake/CecStakeReward' import { sign } from 'utils/sign.utils' import { getMonthBegin, getNDayAgo } from 'utils/utcdate.util' import { timeoutFetch } from 'zutils/utils/net.util' @@ -208,6 +209,42 @@ export const updateClaimStatus = async ({ } } + +export const updateStakeRewardStatus = async ({ + address, + account, + token, + records, +}: { + address: string + account: string + token: string + records: Partial[] +}) => { + const chain = process.env.CLAIM_CHAIN + '' + const record = await TokenClaimRecord.findOne({ + chain, + address: address.toLowerCase(), + token: token.toLowerCase(), + account, + }) + if (!record) { + return + } + const bitTotal = BigInt(record.bit) + for (let record of records) { + let changed = false + let bit = BigInt(27) + if (record.status == ClaimStatusEnum.NORMAL && (bitTotal & (1n << bit)) > 0n) { + record.status = ClaimStatusEnum.CLAIMED + changed = true + //@ts-ignore + await record.save() + } + } +} + + const claimTokenAbi = ['function claim(address,address,uint256[4],bytes)'] const claimKeyArr = ['address', 'address', 'address', 'address', 'uint256', 'uint256', 'uint256', 'uint256', 'uint256']