diff --git a/db/migrations/1689623692588-Data.js b/db/migrations/1689623692588-Data.js index 6f8d7e4..efe94d3 100644 --- a/db/migrations/1689623692588-Data.js +++ b/db/migrations/1689623692588-Data.js @@ -3,6 +3,7 @@ module.exports = class Data1689623692588 { async up(db) { await db.query(`CREATE TABLE "token_entity" ("id" character varying NOT NULL, "block_number" numeric, "hash" text NOT NULL, "image" text, "media" text, "name" text, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "count" integer NOT NULL, "collection_id" character varying, CONSTRAINT "PK_687443f2a51af49b5472e2c5ddc" PRIMARY KEY ("id"))`) + await db.query(`ALTER TABLE "token_entity" ADD "total_count" integer NOT NULL`) await db.query(`CREATE INDEX "IDX_0eb2ed7929c3e81941fa1b51b3" ON "token_entity" ("collection_id") `) await db.query(`CREATE INDEX "IDX_40d6049fd30532dada71922792" ON "token_entity" ("hash") `) await db.query(`CREATE INDEX "IDX_47b385945a425667b9e690bc02" ON "token_entity" ("name") `) diff --git a/schema.graphql b/schema.graphql index ae2b15a..54b4f7e 100644 --- a/schema.graphql +++ b/schema.graphql @@ -38,6 +38,7 @@ type TokenEntity @entity { updatedAt: DateTime! createdAt: DateTime! count: Int! + totalCount: Int! } type NFTEntity @entity { diff --git a/src/mappings/nfts/burn.ts b/src/mappings/nfts/burn.ts index 5fb2387..e11c842 100644 --- a/src/mappings/nfts/burn.ts +++ b/src/mappings/nfts/burn.ts @@ -1,10 +1,11 @@ -import { getWith } from '@kodadot1/metasquid/entity' +import { getWith } from '@kodadot1/metasquid/entity' import { NFTEntity as NE } from '../../model' import { unwrap } from '../utils/extract' import { debug, pending, success } from '../utils/logger' import { Action, Context, createTokenId } from '../utils/types' import { createEvent } from '../shared/event' import { calculateCollectionOwnerCountAndDistribution } from '../utils/helper' +import { eventHandlers } from '../shared/handleTokenEntity' import { getBurnTokenEvent } from './getters' const OPERATION = Action.BURN @@ -31,6 +32,8 @@ export async function handleTokenBurn(context: Context): Promise { entity.collection.ownerCount = ownerCount entity.collection.distribution = distribution + await eventHandlers.burnHandler(context, entity) + success(OPERATION, `${id} by ${event.caller}}`) await context.store.save(entity) const meta = entity.metadata ?? '' diff --git a/src/mappings/nfts/mint.ts b/src/mappings/nfts/mint.ts index 7c63eea..07d239c 100644 --- a/src/mappings/nfts/mint.ts +++ b/src/mappings/nfts/mint.ts @@ -8,6 +8,7 @@ import { unwrap } from '../utils/extract' import { debug, pending, success } from '../utils/logger' import { Action, Context, createTokenId } from '../utils/types' import { calculateCollectionOwnerCountAndDistribution, versionOf } from '../utils/helper' +import { eventHandlers } from '../shared/handleTokenEntity' import { getCreateTokenEvent } from './getters' const OPERATION = Action.MINT @@ -60,6 +61,8 @@ export async function handleTokenCreate(context: Context): Promise { final.name = metadata?.name final.image = metadata?.image final.media = metadata?.animationUrl + + await eventHandlers.mintHandler(context, collection, final) } success(OPERATION, `${final.id}`) diff --git a/src/mappings/nfts/setMetadata.ts b/src/mappings/nfts/setMetadata.ts index f914625..9ea3cf7 100644 --- a/src/mappings/nfts/setMetadata.ts +++ b/src/mappings/nfts/setMetadata.ts @@ -1,4 +1,4 @@ -import { get, getOptional } from '@kodadot1/metasquid/entity' +import { get, getOptional, getWith } from '@kodadot1/metasquid/entity' import { isFetchable } from '@kodadot1/minipfs' import { unwrap } from '../utils/extract' import { Context, isNFT } from '../utils/types' @@ -6,7 +6,7 @@ import { CollectionEntity, NFTEntity } from '../../model' import { handleMetadata } from '../shared/metadata' import { debug, warn } from '../utils/logger' import { updateItemMetadataByCollection } from '../utils/cache' -import { handleTokenEntity } from '../shared/handleTokenEntity' +import { eventHandlers } from '../shared/handleTokenEntity' import { tokenIdOf } from './types' import { getMetadataEvent } from './getters' @@ -51,17 +51,15 @@ export async function handleMetadataSet(context: Context): Promise { warn(OPERATION, `collection ${event.collectionId} not found`) return } - const nft = final as NFTEntity - const token = await handleTokenEntity(context, collection, nft) - if (token) { - nft.token = token + if (final instanceof NFTEntity) { + await eventHandlers.setMetadataHandler(context, collection, final) } } - } - await context.store.save(final) + await context.store.save(final) - if (!event.sn && final.metadata) { - await updateItemMetadataByCollection(context.store, event.collectionId) + if (!event.sn && final.metadata) { + await updateItemMetadataByCollection(context.store, event.collectionId) + } } } diff --git a/src/mappings/shared/handleTokenEntity.ts b/src/mappings/shared/handleTokenEntity.ts index c911b71..4eeb293 100644 --- a/src/mappings/shared/handleTokenEntity.ts +++ b/src/mappings/shared/handleTokenEntity.ts @@ -1,44 +1,134 @@ -import { create, getOptional } from '@kodadot1/metasquid/entity' +import { create, getOptional, getWith } from '@kodadot1/metasquid/entity' import md5 from 'md5' import { CollectionEntity as CE, NFTEntity as NE, TokenEntity as TE } from '../../model' -import { warn } from '../utils/logger' +import { debug, warn } from '../utils/logger' import { Context } from '../utils/types' const OPERATION = 'TokenEntity' as any -export async function handleTokenEntity(context: Context, collection: CE, nft: NE): Promise { - const nftMedia = nft.image || nft.media +function generateTokenId(collectionId: string, nftMedia: string): string { + return `${collectionId}-${md5(nftMedia)}` +} + +async function createToken(context: Context, collection: CE, nft: NE): Promise { + const nftMedia = nft.image ?? nft.media if (!nftMedia || nftMedia === '') { warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) return } + const tokenId = generateTokenId(collection.id, nftMedia) + debug(OPERATION, { createToken: `Create TOKEN ${tokenId} for NFT ${nft.id}` }) + const tokenName = typeof nft.name === 'string' ? nft.name?.replace(/([#_]\d+$)/g, '').trim() : '' - const tokenId = `${collection.id}-${md5(nftMedia)}` - let token = await getOptional(context.store, TE, tokenId) + const token = create(TE, tokenId, { + createdAt: nft.createdAt, + collection, + name: tokenName, + count: 1, + totalCount: 1, + hash: md5(tokenId), + image: nft.image, + media: nft.media, + blockNumber: nft.blockNumber, + updatedAt: nft.updatedAt, + id: tokenId, + }) - if (!token) { - const tokenName = (typeof nft.name === 'string' ? nft.name?.replace(/([#_]\d+$)/g, '').trim() : '') - - token = create(TE, tokenId, { - createdAt: nft.createdAt, - collection, - name: tokenName, - count: 1, - hash: md5(tokenId), - image: nft.image, - media: nft.media, - blockNumber: nft.blockNumber, - updatedAt: nft.updatedAt, - id: tokenId, - }) - } else { - token.count += 1 - } + nft.token = token + await context.store.save(token) + await context.store.save(nft) - token.updatedAt = nft.updatedAt - token.blockNumber = nft.blockNumber + return token +} +async function addNftToToken(context: Context, nft: NE, token: TE): Promise { + debug(OPERATION, { updateToken: `Add NFT ${nft.id} to TOKEN ${token.id} for ` }) + token.count += 1 + token.totalCount += 1 + token.updatedAt = nft.updatedAt + nft.token = token await context.store.save(token) + await context.store.save(nft) return token } + +async function removeNftFromToken(context: Context, nft: NE, token: TE): Promise { + if (!token) { + return + } + debug(OPERATION, { removeNftFromToken: `Unlink NFT ${nft.id} from TOKEN ${token.id}` }) + + await context.store.update(NE, nft.id, { token: null }) + const updatedTotalCount = token.totalCount - 1 + await context.store.update(TE, token.id, { count: token.count - 1, totalCount: updatedTotalCount , updatedAt: nft.updatedAt }) + + if (updatedTotalCount === 0) { + debug(OPERATION, { deleteEmptyToken: `delete empty token ${token.id}` }) + await context.store.delete(TE, token.id) + } +} + +async function mintHandler(context: Context, collection: CE, nft: NE): Promise { + const nftMedia = nft.image ?? nft.media + debug(OPERATION, { mintHandler: `Handle mint for NFT ${nft.id}` }) + + if (!nftMedia || nftMedia === '') { + warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) + return + } + + const existingToken = await getOptional(context.store, TE, generateTokenId(collection.id, nftMedia)) + return await (existingToken ? addNftToToken(context, nft, existingToken) : createToken(context, collection, nft)) +} + +async function handleMetadataSet(context: Context, collection: CE, nft: NE): Promise { + debug(OPERATION, { handleMetadataSet: `Handle set metadata for NFT ${nft.id}` }) + const nftMedia = nft.image ?? nft.media + + if (!nftMedia || nftMedia === '') { + warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) + return + } + + let nftWithToken, existingToken + try { + ;[nftWithToken, existingToken] = await Promise.all([ + getWith(context.store, NE, nft.id, { token: true }), + getOptional(context.store, TE, generateTokenId(collection.id, nftMedia)), + ]) + } catch (error) { + warn(OPERATION, `ERROR ${error}`) + return + } + if (nftWithToken.token) { + await removeNftFromToken(context, nft, nftWithToken.token) + } + return await (existingToken ? addNftToToken(context, nft, existingToken) : createToken(context, collection, nft)) +} + +async function handleBurn(context: Context, nft: NE): Promise { + debug(OPERATION, { handleBurn: `Handle Burn for NFT ${nft.id}` }) + const nftMedia = nft.image ?? nft.media + + if (!nftMedia || nftMedia === '') { + warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) + return + } + + const token = await getOptional(context.store, TE, generateTokenId(nft.collection.id, nftMedia)) + + if (!token) { + return + } + + debug(OPERATION, { BURN: `decrement Token's ${token.id} count` }) + + await context.store.update(TE, token.id, { count: token.count - 1, updatedAt: nft.updatedAt }) +} + +export const eventHandlers = { + setMetadataHandler: handleMetadataSet, + mintHandler, + burnHandler: handleBurn, +} diff --git a/src/mappings/uniques/burn.ts b/src/mappings/uniques/burn.ts index 5fb2387..00fa4f3 100644 --- a/src/mappings/uniques/burn.ts +++ b/src/mappings/uniques/burn.ts @@ -5,6 +5,7 @@ import { debug, pending, success } from '../utils/logger' import { Action, Context, createTokenId } from '../utils/types' import { createEvent } from '../shared/event' import { calculateCollectionOwnerCountAndDistribution } from '../utils/helper' +import { eventHandlers } from '../shared/handleTokenEntity' import { getBurnTokenEvent } from './getters' const OPERATION = Action.BURN @@ -31,6 +32,8 @@ export async function handleTokenBurn(context: Context): Promise { entity.collection.ownerCount = ownerCount entity.collection.distribution = distribution + await eventHandlers.burnHandler(context, entity) + success(OPERATION, `${id} by ${event.caller}}`) await context.store.save(entity) const meta = entity.metadata ?? '' diff --git a/src/mappings/uniques/mint.ts b/src/mappings/uniques/mint.ts index ee3d7a6..a704e8d 100644 --- a/src/mappings/uniques/mint.ts +++ b/src/mappings/uniques/mint.ts @@ -8,7 +8,7 @@ import { unwrap } from '../utils/extract' import { debug, pending, success } from '../utils/logger' import { Action, Context, createTokenId } from '../utils/types' import { versionOf , calculateCollectionOwnerCountAndDistribution } from '../utils/helper' -import { handleTokenEntity } from '../shared/handleTokenEntity' +import { eventHandlers } from '../shared/handleTokenEntity' import { getCreateTokenEvent } from './getters' const OPERATION = Action.MINT @@ -63,10 +63,8 @@ export async function handleTokenCreate(context: Context): Promise { final.media = metadata?.animationUrl } - const token = await handleTokenEntity(context, collection, final) - if (token) { - final.token = token - } + await eventHandlers.mintHandler(context, collection, final) + success(OPERATION, `${final.id}`) await context.store.save(final) diff --git a/src/mappings/uniques/setMetadata.ts b/src/mappings/uniques/setMetadata.ts index a140faa..26edcaf 100644 --- a/src/mappings/uniques/setMetadata.ts +++ b/src/mappings/uniques/setMetadata.ts @@ -1,4 +1,4 @@ -import { get, getOptional } from '@kodadot1/metasquid/entity' +import { get, getOptional, getWith } from '@kodadot1/metasquid/entity' import { isFetchable } from '@kodadot1/minipfs' import { unwrap } from '../utils/extract' import { Context, isNFT } from '../utils/types' @@ -6,7 +6,7 @@ import { CollectionEntity, NFTEntity } from '../../model' import { handleMetadata } from '../shared/metadata' import { debug, warn } from '../utils/logger' import { updateItemMetadataByCollection } from '../utils/cache' -import { handleTokenEntity } from '../shared/handleTokenEntity' +import { eventHandlers } from '../shared/handleTokenEntity' import { tokenIdOf } from './types' import { getMetadataEvent } from './getters' @@ -52,11 +52,8 @@ export async function handleMetadataSet(context: Context): Promise { warn(OPERATION, `collection ${event.collectionId} not found`) return } - const nft = final as NFTEntity - const token = await handleTokenEntity(context, collection, nft) - if (token) { - nft.token = token - } + + await eventHandlers.setMetadataHandler(context, collection, final as NFTEntity) } } diff --git a/src/model/generated/tokenEntity.model.ts b/src/model/generated/tokenEntity.model.ts index 3c46348..62c51a7 100644 --- a/src/model/generated/tokenEntity.model.ts +++ b/src/model/generated/tokenEntity.model.ts @@ -44,4 +44,7 @@ export class TokenEntity { @Column_("int4", {nullable: false}) count!: number + + @Column_("int4", {nullable: false}) + totalCount!: number }