diff --git a/src/abis/Transfer.ts b/src/abis/Transfer.ts index 0c35393..fe3c2f2 100644 --- a/src/abis/Transfer.ts +++ b/src/abis/Transfer.ts @@ -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" } \ No newline at end of file diff --git a/src/config/events_cfg.ts b/src/config/events_cfg.ts index bfacac2..6110f32 100644 --- a/src/config/events_cfg.ts +++ b/src/config/events_cfg.ts @@ -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, }, ] \ No newline at end of file diff --git a/src/controllers/task.controllers.ts b/src/controllers/task.controllers.ts index 2fd3379..ffbb333 100644 --- a/src/controllers/task.controllers.ts +++ b/src/controllers/task.controllers.ts @@ -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, }) } diff --git a/src/models/FtHolder.ts b/src/models/FtHolder.ts new file mode 100644 index 0000000..c8e9c8b --- /dev/null +++ b/src/models/FtHolder.ts @@ -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'], +}) diff --git a/src/models/FtTransferEvent.ts b/src/models/FtTransferEvent.ts index 1f47a7d..205edb3 100644 --- a/src/models/FtTransferEvent.ts +++ b/src/models/FtTransferEvent.ts @@ -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, { diff --git a/src/models/GeneralEvent.ts b/src/models/GeneralEvent.ts new file mode 100644 index 0000000..86bb7f7 --- /dev/null +++ b/src/models/GeneralEvent.ts @@ -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'], +}) diff --git a/src/models/NftHolder.ts b/src/models/NftHolder.ts index cc716d1..ab183f2 100644 --- a/src/models/NftHolder.ts +++ b/src/models/NftHolder.ts @@ -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(); - } } diff --git a/src/models/NftStake.ts b/src/models/NftStake.ts new file mode 100644 index 0000000..e479724 --- /dev/null +++ b/src/models/NftStake.ts @@ -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'], +}) diff --git a/src/models/NftTransferEvent.ts b/src/models/NftTransferEvent.ts index 511dbc4..315b1cb 100644 --- a/src/models/NftTransferEvent.ts +++ b/src/models/NftTransferEvent.ts @@ -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) } } diff --git a/src/service/event.batch.service.ts b/src/service/event.batch.service.ts index 955b523..11d1bd6 100644 --- a/src/service/event.batch.service.ts +++ b/src/service/event.batch.service.ts @@ -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 + '') diff --git a/src/utils/block.util.ts b/src/utils/block.util.ts index 9ced7e4..59c2f23 100644 --- a/src/utils/block.util.ts +++ b/src/utils/block.util.ts @@ -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 diff --git a/src/utils/event.util.ts b/src/utils/event.util.ts index 00ce6df..8173edc 100644 --- a/src/utils/event.util.ts +++ b/src/utils/event.util.ts @@ -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 } \ No newline at end of file