diff --git a/db/migrations/1695642839676-Data.js b/db/migrations/1695642839676-Data.js new file mode 100644 index 00000000..8a060815 --- /dev/null +++ b/db/migrations/1695642839676-Data.js @@ -0,0 +1,25 @@ +module.exports = class Data1695642839676 { + name = 'Data1695642839676' + + 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(`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") `) + await db.query(`ALTER TABLE "nft_entity" ADD "token_id" character varying`) + await db.query(`CREATE INDEX "IDX_060d0f515d293fac1d81ee61a7" ON "nft_entity" ("token_id") `) + await db.query(`ALTER TABLE "token_entity" ADD CONSTRAINT "FK_0eb2ed7929c3e81941fa1b51b35" FOREIGN KEY ("collection_id") REFERENCES "collection_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "nft_entity" ADD CONSTRAINT "FK_060d0f515d293fac1d81ee61a79" FOREIGN KEY ("token_id") REFERENCES "token_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + } + + async down(db) { + await db.query(`DROP TABLE "token_entity"`) + await db.query(`DROP INDEX "public"."IDX_0eb2ed7929c3e81941fa1b51b3"`) + await db.query(`DROP INDEX "public"."IDX_40d6049fd30532dada71922792"`) + await db.query(`DROP INDEX "public"."IDX_47b385945a425667b9e690bc02"`) + await db.query(`ALTER TABLE "nft_entity" DROP COLUMN "token_id"`) + await db.query(`DROP INDEX "public"."IDX_060d0f515d293fac1d81ee61a7"`) + await db.query(`ALTER TABLE "token_entity" DROP CONSTRAINT "FK_0eb2ed7929c3e81941fa1b51b35"`) + await db.query(`ALTER TABLE "nft_entity" DROP CONSTRAINT "FK_060d0f515d293fac1d81ee61a79"`) + } +} diff --git a/schema.graphql b/schema.graphql index e51392de..e24d3914 100644 --- a/schema.graphql +++ b/schema.graphql @@ -27,6 +27,20 @@ type CollectionEntity @entity { volume: BigInt! } +type TokenEntity @entity { + id: ID! + blockNumber: BigInt + collection: CollectionEntity + nfts: [NFTEntity!] @derivedFrom(field: "token") + hash: String! @index + image: String + media: String + name: String @index + updatedAt: DateTime! + createdAt: DateTime! + count: Int! +} + type NFTEntity @entity { blockNumber: BigInt burned: Boolean! @@ -58,6 +72,7 @@ type NFTEntity @entity { transferable: Int updatedAt: DateTime! version: String! + token: TokenEntity } type MetadataEntity @entity { diff --git a/src/mappings/shared/handleTokenEntity.ts b/src/mappings/shared/handleTokenEntity.ts new file mode 100644 index 00000000..d10bebc3 --- /dev/null +++ b/src/mappings/shared/handleTokenEntity.ts @@ -0,0 +1,50 @@ +import { create, getOptional } 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 { 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 ?? + nft.meta?.image ?? + nft.meta?.animationUrl ?? + nft.resources[0].src ?? + nft.resources[0].thumb + if (!nftMedia || nftMedia === '') { + warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) + return + } + + const tokenId = `${collection.id}-${md5(nftMedia)}` + let token = await getOptional(context.store, TE, 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 + } + + token.updatedAt = nft.updatedAt + token.blockNumber = nft.blockNumber + + await context.store.save(token) + + return token +} diff --git a/src/mappings/v1/mint.ts b/src/mappings/v1/mint.ts index 4b6dc895..406d21ad 100644 --- a/src/mappings/v1/mint.ts +++ b/src/mappings/v1/mint.ts @@ -11,6 +11,7 @@ import { Action, Context, getNftId, NFT, Optional } from '../utils/types' import { createEvent } from '../shared/event' import { handleMetadata, isLewd } from '../shared/metadata' import { calculateCollectionOwnerCountAndDistribution } from '../utils/helper' +import { handleTokenEntity } from '../shared/handleTokenEntity' const OPERATION = Action.MINT @@ -69,6 +70,9 @@ export async function mintItem(context: Context): Promise { } } + final.token = await handleTokenEntity(context, collection, final) + + await context.store.save(final) await context.store.save(collection) success(OPERATION, `${final.id} from ${caller}`) diff --git a/src/mappings/v2/mint.ts b/src/mappings/v2/mint.ts index ff9070c9..1cc44d0a 100644 --- a/src/mappings/v2/mint.ts +++ b/src/mappings/v2/mint.ts @@ -9,9 +9,10 @@ import { CollectionEntity, NFTEntity, Property } from '../../model/generated' import { createEvent } from '../shared/event' import { handleMetadata, isLewd } from '../shared/metadata' import { findRootItemById } from '../utils/entity' -import { calculateCollectionOwnerCountAndDistribution, isDummyAddress } from '../utils/helper' +import { isDummyAddress } from '../utils/helper' import logger, { error, success } from '../utils/logger' import { Action, Context, Optional, getNftId } from '../utils/types' +import { handleTokenEntity } from '../shared/handleTokenEntity' import { getCreateToken } from './getters' const OPERATION = Action.MINT @@ -88,6 +89,8 @@ export async function mintItem(context: Context): Promise { final.pending = false } + final.token = await handleTokenEntity(context, collection, final) + await context.store.save(final) await context.store.save(collection) success(OPERATION, `${final.id} from ${caller}`) diff --git a/src/model/generated/index.ts b/src/model/generated/index.ts index 2f94c279..1d6dc42d 100644 --- a/src/model/generated/index.ts +++ b/src/model/generated/index.ts @@ -1,5 +1,6 @@ export * from "./collectionEntity.model" export * from "./_collectionEvent" +export * from "./tokenEntity.model" export * from "./nftEntity.model" export * from "./metadataEntity.model" export * from "./_attribute" diff --git a/src/model/generated/nftEntity.model.ts b/src/model/generated/nftEntity.model.ts index afc0ce73..306c5803 100644 --- a/src/model/generated/nftEntity.model.ts +++ b/src/model/generated/nftEntity.model.ts @@ -7,6 +7,7 @@ import {Part} from "./part.model" import {MetadataEntity} from "./metadataEntity.model" import {Property} from "./property.model" import {Resource} from "./resource.model" +import {TokenEntity} from "./tokenEntity.model" @Entity_() export class NFTEntity { @@ -110,4 +111,8 @@ export class NFTEntity { @Column_("text", {nullable: false}) version!: string + + @Index_() + @ManyToOne_(() => TokenEntity, {nullable: true}) + token!: TokenEntity | undefined | null } diff --git a/src/model/generated/tokenEntity.model.ts b/src/model/generated/tokenEntity.model.ts new file mode 100644 index 00000000..3c463484 --- /dev/null +++ b/src/model/generated/tokenEntity.model.ts @@ -0,0 +1,47 @@ +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, ManyToOne as ManyToOne_, Index as Index_, OneToMany as OneToMany_} from "typeorm" +import * as marshal from "./marshal" +import {CollectionEntity} from "./collectionEntity.model" +import {NFTEntity} from "./nftEntity.model" + +@Entity_() +export class TokenEntity { + constructor(props?: Partial) { + Object.assign(this, props) + } + + @PrimaryColumn_() + id!: string + + @Column_("numeric", {transformer: marshal.bigintTransformer, nullable: true}) + blockNumber!: bigint | undefined | null + + @Index_() + @ManyToOne_(() => CollectionEntity, {nullable: true}) + collection!: CollectionEntity | undefined | null + + @OneToMany_(() => NFTEntity, e => e.token) + nfts!: NFTEntity[] + + @Index_() + @Column_("text", {nullable: false}) + hash!: string + + @Column_("text", {nullable: true}) + image!: string | undefined | null + + @Column_("text", {nullable: true}) + media!: string | undefined | null + + @Index_() + @Column_("text", {nullable: true}) + name!: string | undefined | null + + @Column_("timestamp with time zone", {nullable: false}) + updatedAt!: Date + + @Column_("timestamp with time zone", {nullable: false}) + createdAt!: Date + + @Column_("int4", {nullable: false}) + count!: number +}