增加用户claim usdt的接口
This commit is contained in:
parent
e8e40d0d23
commit
a0975a852d
49
docs/api.md
49
docs/api.md
@ -489,3 +489,52 @@ body:
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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": "签名"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
20
src/configs/boost.ts
Normal file
20
src/configs/boost.ts
Normal file
@ -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
|
||||||
|
},
|
||||||
|
]
|
41
src/controllers/chain.controller.ts
Normal file
41
src/controllers/chain.controller.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { FUSION_CFG } from "configs/fusion";
|
|||||||
import { ALL_ITEMS } from "configs/items";
|
import { ALL_ITEMS } from "configs/items";
|
||||||
import { LOTTERY_CFG } from "configs/lottery";
|
import { LOTTERY_CFG } from "configs/lottery";
|
||||||
import { router } from "decorators/router";
|
import { router } from "decorators/router";
|
||||||
|
import { FastifyRequest } from "fastify";
|
||||||
import { ActivityItem } from "models/ActivityItem";
|
import { ActivityItem } from "models/ActivityItem";
|
||||||
import { LotteryRecord } from "models/LotteryRecord";
|
import { LotteryRecord } from "models/LotteryRecord";
|
||||||
import { updateRankScore } from "services/rank.svr";
|
import { updateRankScore } from "services/rank.svr";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import BaseController, {ROLE_ANON} from 'common/base.controller'
|
import BaseController, {ROLE_ANON} from 'common/base.controller'
|
||||||
import { SyncLocker } from 'common/SyncLocker'
|
import { SyncLocker } from 'common/SyncLocker'
|
||||||
import {ZError} from 'common/ZError'
|
import {ZError} from 'common/ZError'
|
||||||
|
import { BOOST_CFG } from 'configs/boost'
|
||||||
import { role, router } from 'decorators/router'
|
import { role, router } from 'decorators/router'
|
||||||
import logger from 'logger/logger'
|
import logger from 'logger/logger'
|
||||||
import { ActivityUser } from 'models/ActivityUser'
|
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 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 {
|
class SignController extends BaseController {
|
||||||
@role(ROLE_ANON)
|
@role(ROLE_ANON)
|
||||||
@router('get /api/wallet/nonce')
|
@router('get /api/wallet/nonce')
|
||||||
@ -126,7 +141,7 @@ class SignController extends BaseController {
|
|||||||
if (user.boost > 1 && user.boostExpire && user.boostExpire > Date.now()) {
|
if (user.boost > 1 && user.boostExpire && user.boostExpire > Date.now()) {
|
||||||
throw new ZError(11, 'already boosted')
|
throw new ZError(11, 'already boosted')
|
||||||
}
|
}
|
||||||
user.boost = 2;
|
user.boost = generateBoost(BOOST_CFG)
|
||||||
user.boostExpire = nextday();
|
user.boostExpire = nextday();
|
||||||
await user.save();
|
await user.save();
|
||||||
return { boost: user.boost, boostExpire: user.boostExpire };
|
return { boost: user.boost, boostExpire: user.boostExpire };
|
||||||
|
43
src/models/TokenClaimHistory.ts
Normal file
43
src/models/TokenClaimHistory.ts
Normal file
@ -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<TokenClaimHistoryClass>) {
|
||||||
|
const record = new TokenClaimHistory(params)
|
||||||
|
await record.save()
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TokenClaimHistory = getModelForClass(TokenClaimHistoryClass, { existingConnection: TokenClaimHistoryClass['db'] })
|
||||||
|
|
53
src/models/chain/NftStake.ts
Normal file
53
src/models/chain/NftStake.ts
Normal file
@ -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'],
|
||||||
|
})
|
@ -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'],
|
|
||||||
})
|
|
@ -1,4 +1,5 @@
|
|||||||
import { NftHolder } from "models/chain/NftHolder"
|
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) => {
|
export const queryCheckInList = async (address: string, days: string | number | string[], limit: number = 0) => {
|
||||||
const url = process.env.CHAIN_SVR + '/task/check_in'
|
const url = process.env.CHAIN_SVR + '/task/check_in'
|
||||||
@ -43,3 +44,10 @@ export const checkHadGacha = async (user: string) => {
|
|||||||
const record = await NftHolder.findOne({user, chain, address})
|
const record = await NftHolder.findOne({user, chain, address})
|
||||||
return !!record
|
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
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util'
|
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) {
|
export function recoverTypedSignatureV4(signObj: any, signature: string) {
|
||||||
return recoverTypedSignature({
|
return recoverTypedSignature({
|
||||||
@ -43,3 +45,23 @@ export function buildLoginSignMsg(nonce: string, tips: string) {
|
|||||||
}
|
}
|
||||||
return signObj
|
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}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user