diff --git a/docs/api.md b/docs/api.md index 646f676..c2041e9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -488,4 +488,53 @@ body: "amount": "数量" }] } -``` \ No newline at end of file +``` + +### 18.\* 用户已质押列表 + +#### Request + +- URL:`/api/stake/list` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json +[ + { + "nft": "nft地址", + "nftid": "1", // nftid + "start": 12312111, //质押开始时间 + "cd": 11111, // cd时间, 从开始质押开始计算cd, cd未到的话, 无法赎回 + "stakeTime": 11222, //质押时长 + } +] +``` + +### 19.\* Claim USDT + +#### Request + +- URL:`/api/lottery/claim_usdt` +- 方法:`GET` +- 头部: + - Authorization: Bearer JWT_token + + +#### Response + +```json + + { + "token": "0x1304E6AA241eE3C9ea44Db9e593e85Ae76eC41F1", + "amount": "20000000000000000", + "startTime": 1705041164, + "saltNonce": "111", + "signature": "签名" + } + + +``` diff --git a/src/configs/boost.ts b/src/configs/boost.ts new file mode 100644 index 0000000..4f8be37 --- /dev/null +++ b/src/configs/boost.ts @@ -0,0 +1,20 @@ +const ROUND = 1000000; + +export const BOOST_CFG = [ + { + value: 2, + probability: 500000 + }, + { + value: 3, + probability: 300000 + }, + { + value: 4, + probability: 150000 + }, + { + value: 5, + probability: 50000 + }, +] \ No newline at end of file diff --git a/src/controllers/chain.controller.ts b/src/controllers/chain.controller.ts new file mode 100644 index 0000000..db349dc --- /dev/null +++ b/src/controllers/chain.controller.ts @@ -0,0 +1,41 @@ + +import { SyncLocker } from "common/SyncLocker"; +import { ZError } from "common/ZError"; +import BaseController from "common/base.controller"; +import { router } from "decorators/router"; +import { FastifyRequest } from "fastify"; +import { ActivityItem } from "models/ActivityItem"; +import { TokenClaimHistory } from "models/TokenClaimHistory"; +import { queryStakeList } from "services/chain.svr"; +import { sign } from "utils/chain.util"; + + +const MAX_LIMIT = 50 +export default class ChainController extends BaseController { + + @router('get /api/stake/list') + async stakeList(req) { + const user = req.user; + const records = await queryStakeList(user.address) + const result = records.map((r) => r.toJson()) + return result + } + + @router('post /api/lottery/claim_usdt') + async preClaimUsdt(req: FastifyRequest) { + new SyncLocker().checkLock(req); + let user = req.user; + const minClaimNum = +process.env.MINI_CLAIM_USDT + const record = await ActivityItem.findOne({user: user.id, activity: user.activity, item: 'usdt'}) + if (!record || record.amount < minClaimNum) { + throw new ZError(10, 'no enough usdt') + } + const amount = record.amount + ''; + record.amount = 0; + await record.save() + const token = process.env.USDT_CONTRACT; + await TokenClaimHistory.addOne({user: user.id, activity: user.activity, token, amount}) + const res = await sign({user: user.address, token, amount}) + return res + } +} \ No newline at end of file diff --git a/src/controllers/lottery.controller.ts b/src/controllers/lottery.controller.ts index 1c87039..7ed3d3f 100644 --- a/src/controllers/lottery.controller.ts +++ b/src/controllers/lottery.controller.ts @@ -7,6 +7,7 @@ import { FUSION_CFG } from "configs/fusion"; import { ALL_ITEMS } from "configs/items"; import { LOTTERY_CFG } from "configs/lottery"; import { router } from "decorators/router"; +import { FastifyRequest } from "fastify"; import { ActivityItem } from "models/ActivityItem"; import { LotteryRecord } from "models/LotteryRecord"; import { updateRankScore } from "services/rank.svr"; diff --git a/src/controllers/sign.controller.ts b/src/controllers/sign.controller.ts index c9fffc8..fce8ace 100644 --- a/src/controllers/sign.controller.ts +++ b/src/controllers/sign.controller.ts @@ -1,6 +1,7 @@ import BaseController, {ROLE_ANON} from 'common/base.controller' import { SyncLocker } from 'common/SyncLocker' import {ZError} from 'common/ZError' +import { BOOST_CFG } from 'configs/boost' import { role, router } from 'decorators/router' import logger from 'logger/logger' import { ActivityUser } from 'models/ActivityUser' @@ -17,6 +18,20 @@ import { aesDecrypt, base58ToHex } from 'utils/security.util' const LOGIN_TIP = 'This signature is just to verify your identity' +const ROUND = 1000000; +const generateBoost = (rewards: {probability: number}[]) => { + let total = 0; + let random = Math.floor(Math.random() * ROUND); + let reward = null; + for (let r of rewards) { + total += r.probability; + if (random < total) { + reward = r; + break; + } + } + return reward.value +} class SignController extends BaseController { @role(ROLE_ANON) @router('get /api/wallet/nonce') @@ -126,7 +141,7 @@ class SignController extends BaseController { if (user.boost > 1 && user.boostExpire && user.boostExpire > Date.now()) { throw new ZError(11, 'already boosted') } - user.boost = 2; + user.boost = generateBoost(BOOST_CFG) user.boostExpire = nextday(); await user.save(); return { boost: user.boost, boostExpire: user.boostExpire }; diff --git a/src/models/TokenClaimHistory.ts b/src/models/TokenClaimHistory.ts new file mode 100644 index 0000000..17055e8 --- /dev/null +++ b/src/models/TokenClaimHistory.ts @@ -0,0 +1,43 @@ +import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + + +@dbconn() +@index({ user: 1 }, { unique: false }) +@index({ activity: 1 }, { unique: false }) +@index({user: 1, activity: 1, type: 1}, { unique: false }) +@modelOptions({ schemaOptions: { collection: 'token_claim_record', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class TokenClaimHistoryClass extends BaseModule { + @prop({ required: true}) + public user: string + + @prop({ required: true}) + public activity: string + + @prop() + public token: string + + @prop() + public amount: number + + /** + * 0: 待确认 + * 1: 已确认 + * -1: 已明确失败 + */ + @prop({default: 0}) + public status: number + // 转账交易hash + @prop() + public hash: string + + public static async addOne(params: Partial) { + const record = new TokenClaimHistory(params) + await record.save() + return record + } +} + +export const TokenClaimHistory = getModelForClass(TokenClaimHistoryClass, { existingConnection: TokenClaimHistoryClass['db'] }) + diff --git a/src/models/chain/NftStake.ts b/src/models/chain/NftStake.ts new file mode 100644 index 0000000..b918f99 --- /dev/null +++ b/src/models/chain/NftStake.ts @@ -0,0 +1,53 @@ +import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' + + +@dbconn('chain') +@index({ chain: 1, nft: 1, tokenId: 1, start: 1 }, { unique: true }) +@index({ chain:1, user: 1, nft: 1}, {unique: false}) +@modelOptions({ + schemaOptions: { collection: 'nft_stake_info', timestamps: true }, +}) +export class NftStakeClass extends BaseModule { + @prop({ required: true }) + public chain: string + @prop() + public blockNumber: number + // stake info + @prop() + public address: string + @prop() + public user: string + @prop() + public nft: string + @prop() + public tokenId: string + @prop() + public start: number + @prop() + public stakeTime: number + // 1: staked, 2: redeemed + @prop() + public status: number + + @prop() + public redeemTime: number + + @prop({ default: 0 }) + public version: number + + public toJson() { + return { + nft: this.nft, + nftid: this.tokenId, + start: this.start, + cd: +process.env.STAKE_CD, + stakeTime: this.stakeTime + } + } +} + +export const NftStake = getModelForClass(NftStakeClass, { + existingConnection: NftStakeClass['db'], +}) diff --git a/src/models/chain/NftTransferEvent.ts b/src/models/chain/NftTransferEvent.ts deleted file mode 100644 index 4f3fcbb..0000000 --- a/src/models/chain/NftTransferEvent.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' -import { dbconn } from 'decorators/dbconn' -import { BaseModule } from '../Base' - -@dbconn('chain') -@index({ chain: 1, address: 1, tokenId: 1 }, { unique: false }) -@index({ chain: 1, address: 1, from: 1, to: 1 }, { unique: false }) -@index({ chain: 1, hash: 1, logIndex: 1}, { unique: true }) -@modelOptions({ - schemaOptions: { collection: 'nft_transfer_event', timestamps: true }, -}) -export class NftTransferEventClass extends BaseModule { - @prop({ required: true }) - public address!: string - @prop({ required: true }) - public chain: string - @prop({ required: true }) - public logIndex: number - @prop() - public event: string - @prop({ required: true }) - public hash: string - @prop() - public blockNumber: number - @prop() - public blockHash: string - @prop() - public removed: boolean - @prop() - public from: string - @prop() - public to: string - @prop() - public tokenId: string - @prop() - public blockTime: number - @prop({ default: 0 }) - public version: number - - public static async saveEvent(event: any) { - const tokenId = event.tokenId || event.value - if (!tokenId) { - return - } - const logIndex = parseInt(event.logIndex || '0') - const from = event.from.toLowerCase() - const to = event.to.toLowerCase() - const hash = event.hash || event.transactionHash - const data = { - address: event.address.toLowerCase(), - blockNumber: parseInt(event.blockNumber), - removed: event.removed, - from, - to, - tokenId, - // blockTime: new Date(event.time).getTime(), - $inc: { version: 1 }, - } - - return NftTransferEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data) - } -} - -export const NftTransferEvent = getModelForClass(NftTransferEventClass, { - existingConnection: NftTransferEventClass['db'], -}) diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index 2a36103..752eae9 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -1,4 +1,5 @@ import { NftHolder } from "models/chain/NftHolder" +import { NftStake } from "models/chain/NftStake" export const queryCheckInList = async (address: string, days: string | number | string[], limit: number = 0) => { const url = process.env.CHAIN_SVR + '/task/check_in' @@ -42,4 +43,11 @@ export const checkHadGacha = async (user: string) => { const address = process.env.GACHA_CONTRACT const record = await NftHolder.findOne({user, chain, address}) return !!record +} + +export const queryStakeList = async (userAddress: string) => { + const chain = process.env.CHAIN+'' + const address = process.env.BADGE_CONTRACT + let records = await NftStake.find({chain, nft: address, user: userAddress.toLowerCase()}) + return records } \ No newline at end of file diff --git a/src/utils/chain.util.ts b/src/utils/chain.util.ts index dcaf813..d28631e 100644 --- a/src/utils/chain.util.ts +++ b/src/utils/chain.util.ts @@ -1,4 +1,6 @@ import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util' +import { soliditySha3, toWei } from 'web3-utils' +import Web3 from 'web3'; export function recoverTypedSignatureV4(signObj: any, signature: string) { return recoverTypedSignature({ @@ -43,3 +45,23 @@ export function buildLoginSignMsg(nonce: string, tips: string) { } return signObj } + + +export const sign = async ({ user, token, amount, saltNonce } + : {user: string, token: string, amount: number | string, saltNonce?: string}) => { + const web3 = new Web3(); + let privateKey = process.env.SIGN_PRIVATE_KEY; + const acc = web3.eth.accounts.privateKeyToAccount(privateKey); + const account = web3.eth.accounts.wallet.add(acc); + const executor = account.address + const amountBn = toWei(amount+''); + const chainId = process.env.CHAIN; + const claimContract = process.env.CLAIM_CONTRACT; + const startTime = Date.now() / 1000 | 0 + saltNonce = saltNonce || ((Math.random() * 1000) | 0) + ''; + let signStr = soliditySha3.apply(this, + [user, token, claimContract, chainId, amountBn, startTime, saltNonce]); + let signature = await web3.eth.sign(signStr, executor); + signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); + return {token, amount: amountBn, startTime, saltNonce, signature} +}