增加stake相关事件, 修改nft交易事件处理逻辑

This commit is contained in:
CounterFire2023 2024-01-12 11:36:57 +08:00
parent 4b246daf0d
commit 58aa777f33
12 changed files with 414 additions and 59 deletions

View File

@ -48,4 +48,98 @@ export const NFT_TRANSFER_EVENT_ABI = {
],
"name": "Transfer",
"type": "event"
}
export const NFT_STAKE_EVENT_ABI = {
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "user",
"type": "address"
},
{
"internalType": "address",
"name": "nft",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "start",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "stakeTime",
"type": "uint64"
}
],
"indexed": false,
"internalType": "struct ERC721Staking.Staker[]",
"name": "infos",
"type": "tuple[]"
}
],
"name": "Staked",
"type": "event"
}
export const NFT_REDEEM_EVENT_ABI = {
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "user",
"type": "address"
},
{
"internalType": "address",
"name": "nft",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "start",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "stakeTime",
"type": "uint64"
}
],
"indexed": false,
"internalType": "struct ERC721Staking.Staker[]",
"name": "infos",
"type": "tuple[]"
}
],
"name": "Redeem",
"type": "event"
}

View File

@ -1,12 +1,13 @@
import { NFT_TRANSFER_EVENT_ABI } from 'abis/Transfer'
import { NftTransferEvent } from 'models/NftTransferEvent'
import { NFT_REDEEM_EVENT_ABI, NFT_STAKE_EVENT_ABI, NFT_TRANSFER_EVENT_ABI } from 'abis/Transfer'
import { NftStake } from 'models/NftStake'
import { NftHolder } from 'models/NftHolder'
export interface IEventCfg {
address: string,
event: string,
abi: any,
fromBlock: number,
eventProcesser: string,
eventProcesser?: string,
chain: number,
topic?: string,
}
@ -18,7 +19,7 @@ export const EVENTS_CFG: IEventCfg[] = [
event: "Transfer",
abi: NFT_TRANSFER_EVENT_ABI,
fromBlock: 3549077,
eventProcesser: NftTransferEvent,
eventProcesser: NftHolder,
},
{ // candy
chain: 421614,
@ -26,7 +27,7 @@ export const EVENTS_CFG: IEventCfg[] = [
event: "Transfer",
abi: NFT_TRANSFER_EVENT_ABI,
fromBlock: 5814045,
eventProcesser: NftTransferEvent,
eventProcesser: NftHolder,
},
{ // Explorer
chain: 421614,
@ -34,7 +35,7 @@ export const EVENTS_CFG: IEventCfg[] = [
event: "Transfer",
abi: NFT_TRANSFER_EVENT_ABI,
fromBlock: 5814491,
eventProcesser: NftTransferEvent,
eventProcesser: NftHolder,
},
{ // Gacha
chain: 421614,
@ -42,6 +43,22 @@ export const EVENTS_CFG: IEventCfg[] = [
event: "Transfer",
abi: NFT_TRANSFER_EVENT_ABI,
fromBlock: 3549613,
eventProcesser: NftTransferEvent,
eventProcesser: NftHolder,
},
{ // Stake
chain: 421614,
address: "0xd46fA2E72BA0F54092D0eF6a6e0D1d5660259C7a",
event: "Staked",
abi: NFT_STAKE_EVENT_ABI,
fromBlock: 3549891,
eventProcesser: NftStake,
},
{ // Stake
chain: 421614,
address: "0xd46fA2E72BA0F54092D0eF6a6e0D1d5660259C7a",
event: "Staked",
abi: NFT_REDEEM_EVENT_ABI,
fromBlock: 3549891,
eventProcesser: NftStake,
},
]

View File

@ -1,9 +1,8 @@
import { ZERO_ADDRESS } from 'common/Constants'
import { ZError } from 'common/ZError'
import BaseController from 'common/base.controller'
import { role, router } from 'decorators/router'
import { CheckIn } from 'models/CheckIn'
import { NftTransferEvent } from 'models/NftTransferEvent'
import { NftHolder } from 'models/NftHolder'
import { getMonthBegin, getNDayAgo } from 'utils/date.util'
@ -65,13 +64,13 @@ class TaskController extends BaseController {
address = address.toLowerCase()
user = user.toLowerCase()
let records = await NftTransferEvent.find({address, chain, from: user, to: ZERO_ADDRESS}).sort({_id: -1})
let records = await NftHolder.find({address, chain, user, burn: true }).sort({blockNumber: -1})
let result = []
for (let record of records) {
result.push({
address: record.address,
chain: record.chain,
user: record.from,
user: record.user,
tokenId: record.tokenId,
})
}

39
src/models/FtHolder.ts Normal file
View File

@ -0,0 +1,39 @@
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
import { GeneralEvent } from './GeneralEvent'
@dbconn()
@index({ chain: 1, address: 1, user: 1 }, { unique: true })
@modelOptions({
schemaOptions: { collection: 'ft_holder', timestamps: true },
})
export class FtHolderClass extends BaseModule {
@prop({ required: true })
public address!: string
@prop({ required: true })
public chain: string
@prop({ required: true })
public amount: string
@prop()
public blockNumber: number
@prop()
public user: string
//TODO::
public static async parseEvent(event: typeof GeneralEvent) {
const address = event.address;
const chain = event.chain;
const amount = event.amount;
const blockNumer = event.blockNumber;
}
}
export const FtHolder = getModelForClass(FtHolderClass, {
existingConnection: FtHolderClass['db'],
})

View File

@ -1,56 +1,68 @@
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
import { FtHolder } from './FtHolder'
@dbconn()
@index({ address: 1 }, { unique: false })
@index({ transactionHash: 1, from: 1, to: 1 }, { unique: true })
@index({ chain: 1, hash: 1, logIndex: 1}, { unique: true })
@modelOptions({
schemaOptions: { collection: 'ft_transfer_event', timestamps: true },
})
export class FtTransferEventClass extends BaseModule {
@prop({ required: true })
public address!: string
@prop({ required: true })
public chain: string
@prop({ required: true })
public logIndex: number
@prop()
public event: string
// event hash
@prop({ required: true })
public transactionHash: string
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 amount: string
@prop()
public blockTime: number
@prop({ default: 0 })
public version: number
public static async saveEvent(event: any) {
const amount = event.returnValues?.value
if (amount == undefined) {
return
}
const from = event.returnValues?.from
const to = event.returnValues?.to
const logIndex = parseInt(event.logIndex || '0')
const from = event.decodedData.from.toLowerCase()
const to = event.decodedData.to.toLowerCase()
const hash = event.hash || event.transactionHash
const data = {
address: event.address,
blockNumber: event.blockNumber,
blockHash: event.blockHash,
address: event.address.toLowerCase(),
blockNumber: parseInt(event.blockNumber),
removed: event.removed,
event: event.event,
from,
to,
transactionHash: event.transactionHash,
amount,
blockTime: event.timestamp * 1000
$inc: { version: 1 },
}
return FtTransferEvent.insertOrUpdate({ transactionHash: event.transactionHash, amount, from, to }, data)
}
let record = await FtTransferEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data)
await FtHolder.saveData(record)
}
}
export const FtTransferEvent = getModelForClass(FtTransferEventClass, {

View File

@ -0,0 +1,60 @@
import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
import { NftHolder } from './NftHolder'
@dbconn()
@index({ chain: 1, hash: 1, logIndex: 1}, { unique: true })
@modelOptions({
schemaOptions: { collection: 'general_event', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class GeneralEventClass extends BaseModule {
@prop({ required: true })
public address!: string
@prop({ required: true })
public chain: string
@prop({ required: true })
public logIndex: number
@prop()
public event: string
// event hash
@prop({ required: true })
public hash: string
@prop()
public blockNumber: number
@prop()
public blockHash: string
@prop()
public removed: boolean
@prop({ type: mongoose.Schema.Types.Mixed})
public decodedData: any
@prop()
public blockTime: number
@prop({ default: 0 })
public version: number
public static async saveEvent(event: any) {
const logIndex = parseInt(event.logIndex || '0')
const hash = event.hash || event.transactionHash
const data = {
address: event.address.toLowerCase(),
blockNumber: parseInt(event.blockNumber),
removed: event.removed,
decodedData: event.decodedData,
blockHash: event.blockHash,
event: event.event,
$inc: { version: 1 },
}
let record = await GeneralEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data)
return record
}
}
export const GeneralEvent = getModelForClass(GeneralEventClass, {
existingConnection: GeneralEventClass['db'],
})

View File

@ -2,6 +2,7 @@ import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoos
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
import { ZERO_ADDRESS } from 'common/Constants'
import { GeneralEvent } from './GeneralEvent'
@dbconn()
@index({ chain: 1, address: 1, tokenId: 1 }, { unique: true })
@ -23,29 +24,24 @@ export class NftHolderClass extends BaseModule {
@prop({default: false})
public burn: boolean
public static async saveData(event: any) {
public static async parseEvent(event: typeof GeneralEvent) {
const address = event.address;
const chain = event.chain;
const tokenId = event.tokenId;
const tokenId = event.decodedData.tokenId;
const blockNumer = event.blockNumber;
const burn = event.to === ZERO_ADDRESS
const burn = event.decodedData.to === ZERO_ADDRESS
const user = burn ? event.decodedData.from : event.decodedData.to
let record = await NftHolder.findOne({ address, chain, tokenId })
if (!record) {
record = new NftHolder({ address, chain, tokenId, blockNumber: blockNumer, user: event.to, burn })
record = new NftHolder({ address, chain, tokenId, blockNumber: blockNumer, user, burn })
} else {
if (record.blockNumber < blockNumer) {
if (burn) {
record.burn = true
} else {
record.user = event.to
}
record.user = user
record.burn = burn
record.blockNumber = blockNumer
}
}
await record.save();
}
}

106
src/models/NftStake.ts Normal file
View File

@ -0,0 +1,106 @@
import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
import logger from 'logger/logger'
import { GeneralEvent } from './GeneralEvent'
const STAKE_EVENT = 'Staked'
const REDEEM_EVENT = 'Redeem'
@dbconn()
@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
/**
*
{
user: '0x50A8e60041A206AcaA5F844a1104896224be6F39',
infos: [
{
user: '0x50A8e60041A206AcaA5F844a1104896224be6F39',
nft: '0x09F0dFFA584B1277D7c4E44265a6b5D03303Fc99',
tokenId: '1',
start: '1704870143',
stakeTime: '2592000'
}
]
}
*/
public static async parseEvent(event: typeof GeneralEvent) {
const { user, infos } = event.decodedData
const isStake = event.event === STAKE_EVENT
const blockNumber = event.blockNumber
const chain = event.chain
if (infos.length === 0) {
return
}
for (let info of infos) {
let { nft, tokenId, start, stakeTime } = info
start = parseInt(start)
stakeTime = parseInt(stakeTime)
if (isStake) {
logger.info(`stake nft: ${nft}, tokenId: ${tokenId}, user: ${user}, blockNumber: ${blockNumber}`)
let record = new NftStake({
address: event.address,
chain,
nft,
tokenId,
user,
blockNumber,
status: 1,
start,
stakeTime
})
await record.save()
} else {
logger.info(`redeem nft: ${nft}, tokenId: ${tokenId}, user: ${user}, blockNumber: ${blockNumber}`)
await NftStake.insertOrUpdate(
{ chain, nft, tokenId, start},
{
status: 2,
address: event.address,
user,
redeemTime: Date.now() / 1000 | 0,
blockNumber,
$inc: { version: 1 },
})
}
}
}
}
export const NftStake = getModelForClass(NftStakeClass, {
existingConnection: NftStakeClass['db'],
})

View File

@ -39,13 +39,13 @@ export class NftTransferEventClass extends BaseModule {
public version: number
public static async saveEvent(event: any) {
const tokenId = event.tokenId || event.value
const tokenId = event.decodedData.tokenId || event.decodedData.value
if (!tokenId) {
return
}
const logIndex = parseInt(event.logIndex || '0')
const from = event.from.toLowerCase()
const to = event.to.toLowerCase()
const from = event.decodedData.from.toLowerCase()
const to = event.decodedData.to.toLowerCase()
const hash = event.hash || event.transactionHash
const data = {
address: event.address.toLowerCase(),
@ -58,8 +58,7 @@ export class NftTransferEventClass extends BaseModule {
$inc: { version: 1 },
}
let record = NftTransferEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data)
await NftHolder.saveData(record)
await NftTransferEvent.insertOrUpdate({ hash, logIndex, chain: event.chain }, data)
}
}

View File

@ -2,11 +2,10 @@ import { IChain } from "chain/allchain";
import { batchEthLogs, ethBlockNumber } from "chain/chain.api";
import { IEventCfg } from "config/events_cfg";
import logger from "logger/logger";
import { GeneralEvent } from "models/GeneralEvent";
import { RedisClient } from "redis/RedisClient";
import { getPastBlocksIter } from "utils/block.util";
import { getTopics } from "utils/event.util";
import web3abi from 'web3-eth-abi';
import { decodeEvent, getTopics } from "utils/event.util";
export class EventBatchSvr {
@ -88,14 +87,18 @@ export class EventBatchSvr {
const topic = events[0].topics[0]
const cfg = this.processer.get(address+topic)
logger.info(`process events: ${cfg.chain} | ${address} | ${cfg.event} | ${events.length}`)
const abiInputs = cfg.abi.inputs;
for (const event of events) {
let result = web3abi.decodeLog(abiInputs, event.data, event.topics.slice(1));
result.chain = this.chainCfg.id + ''
event.chain = this.chainCfg.id + ''
event.event = cfg.event
let result = decodeEvent(cfg, event);
cfg.fromBlock = Math.max (parseInt(event.blockNumber, 16) + 1, cfg.fromBlock)
Object.assign(result, event)
// @ts-ignore
await cfg.eventProcesser.saveEvent(result)
event.decodedData = result
const record = await GeneralEvent.saveEvent(event)
if (cfg.eventProcesser) {
// @ts-ignore
await cfg.eventProcesser.parseEvent(record)
}
}
const redisKey = this.buildRedisKey(cfg)
await new RedisClient().set(redisKey, cfg.fromBlock + '')

View File

@ -1,8 +1,6 @@
import { batchEthBlocks } from "chain/chain.api";
import logger from "logger/logger";
import { RedisClient } from "redis/RedisClient";
import { retry } from "./promise.util";
import { parse } from "path";
const MAX_BATCH_AMOUNT = 500
const REQUEST_INTERVAL = 0.5 * 1000

View File

@ -1,13 +1,45 @@
import { IEventCfg } from "config/events_cfg";
import { keccak256 } from "web3-utils";
//@ts-ignore
import { keccak256, _jsonInterfaceMethodToString, AbiInput } from "web3-utils";
import web3abi from 'web3-eth-abi';
export const getTopics = (cfg: IEventCfg) => {
let abi = cfg.abi
let topic = 'Transfer('
for (let item of abi.inputs) {
topic += item.type + ','
// let abi = cfg.abi
// let topic = `${abi.name}(`
// for (let item of abi.inputs) {
// topic += item.type + ','
// }
// topic = topic.slice(0, -1)
// topic += ')'
return keccak256(_jsonInterfaceMethodToString(cfg.abi))
}
export const decodeEvent = (cfg: IEventCfg, eventData: {data: string, topics: string[]}) => {
const abiInputs = cfg.abi.inputs;
let result = web3abi.decodeLog(abiInputs, eventData.data, eventData.topics.slice(1));
let decodedData: any = {}
for (let i = 0; i < abiInputs.length; i++) {
const input: AbiInput = abiInputs[i];
if (input.type === 'tuple[]') {
// @ts-ignore
decodedData[input.name] = result[i].map(item => {
let itemData = {}
for (let j = 0; j < input.components.length; j++) {
const component = input.components[j];
itemData[component.name] = item[j]
}
return itemData
})
} else if (input.type === 'tuple') {
let itemData = {}
for (let j = 0; j < input.components.length; j++) {
const component = input.components[j];
itemData[component.name] = result[i][j]
}
decodedData[input.name] = itemData
} else {
decodedData[input.name] = result[i]
}
}
topic = topic.slice(0, -1)
topic += ')'
return keccak256(topic)
return decodedData
}