diff --git a/README.md b/README.md index bfd9d1f3..cb4896eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # stick -![](https://media.tenor.com/eK1dyB3TOLsAAAAC/anime-stick.gif) +![](https://media.tenor.com/Eu0LNbU4hQMAAAAC/jeanne-darc-vanitas-no-carte.gif) [Squid](https://docs.subsquid.io) based data used to index, process, and query on top of AssetHub for [KodaDot](https://kodadot.xyz) NFT Marketplace. @@ -8,7 +8,7 @@ * Kusama AssetHub Processor (Statemine -> KSM): https://squid.subsquid.io/stick/graphql * Polkadot AssetHub Processor (Statemint -> DOT): https://squid.subsquid.io/speck/graphql -* Pasoe Testnet Processor: 🚧 Coming soon 🚧 +* Paseo Testnet Processor: 🚧 Coming soon 🚧 ## Project structure @@ -129,22 +129,51 @@ The architecture of this project is following: 1. fast generate event handlers -``` +```bash pbpaste | cut -d '=' -f 1 | tr -d ' ' | xargs -I_ echo "processor.addEventHandler(Event._, dummy);" ``` 2. enable debug logs (in .env) -``` +```bash SQD_DEBUG=squid:log ``` 3. generate metagetters from getters -``` +```bash pbpaste | grep 'export' | xargs -I_ echo "_ return proc. }" ``` +4. Enable different chain (currently only Kusama and Polkadot are supported) + +> [!NOTE] +> By default the chain is set to `kusama` + +```bash +CHAIN=polkadot # or kusama +``` + +5. enable offers + +`Offers` support is a hack on top of the `Atomic Swap` to enable `Offers` set in `.env` file + +```bash +OFFER= +``` + +### Note on Swaps + +1. Swaps can be overwritten at any time + +Therefore if you have a swap, and will create a new one, the old one will be overwritten. This is mentioned in `createSwap.ts` Line 31. + +2. Swaps are autocancelled by few conditions + +- if you `burn` the NFT +- if you `transfer` the NFT + +in any other condition the swap will have to be cancelled manually. ## Funding diff --git a/db/migrations/1721653971599-Data.js b/db/migrations/1721653971599-Data.js new file mode 100644 index 00000000..ed0c3561 --- /dev/null +++ b/db/migrations/1721653971599-Data.js @@ -0,0 +1,37 @@ +module.exports = class Data1721653971599 { + name = 'Data1721653971599' + + async up(db) { + await db.query(`CREATE TABLE "offer" ("id" character varying NOT NULL, "block_number" numeric NOT NULL, "caller" text NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiration" numeric NOT NULL, "price" numeric NOT NULL, "status" character varying(9) NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE, "considered_id" character varying, "desired_id" character varying, "nft_id" character varying, CONSTRAINT "REL_71609884f4478ed41be6672a66" UNIQUE ("nft_id"), CONSTRAINT "PK_57c6ae1abe49201919ef68de900" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_004a20a1eed4189bc23b13efa0" ON "offer" ("considered_id") `) + await db.query(`CREATE INDEX "IDX_f8c1e3faf9cdba27703e0ea2c5" ON "offer" ("desired_id") `) + await db.query(`CREATE UNIQUE INDEX "IDX_71609884f4478ed41be6672a66" ON "offer" ("nft_id") `) + await db.query(`CREATE TABLE "swap" ("id" character varying NOT NULL, "block_number" numeric NOT NULL, "caller" text NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiration" numeric NOT NULL, "price" numeric, "status" character varying(9) NOT NULL, "surcharge" character varying(7), "updated_at" TIMESTAMP WITH TIME ZONE, "considered_id" character varying, "desired_id" character varying, "nft_id" character varying, CONSTRAINT "REL_4a045cf15c5c5c44e6cf52e70c" UNIQUE ("nft_id"), CONSTRAINT "PK_4a10d0f359339acef77e7f986d9" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_ef7a3bc067c4f3dd314c90f79a" ON "swap" ("considered_id") `) + await db.query(`CREATE INDEX "IDX_ded173f5a5ff89483d9ffa4dce" ON "swap" ("desired_id") `) + await db.query(`CREATE UNIQUE INDEX "IDX_4a045cf15c5c5c44e6cf52e70c" ON "swap" ("nft_id") `) + await db.query(`ALTER TABLE "offer" ADD CONSTRAINT "FK_004a20a1eed4189bc23b13efa0d" FOREIGN KEY ("considered_id") REFERENCES "collection_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "offer" ADD CONSTRAINT "FK_f8c1e3faf9cdba27703e0ea2c54" FOREIGN KEY ("desired_id") REFERENCES "nft_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "offer" ADD CONSTRAINT "FK_71609884f4478ed41be6672a668" FOREIGN KEY ("nft_id") REFERENCES "nft_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_ef7a3bc067c4f3dd314c90f79a5" FOREIGN KEY ("considered_id") REFERENCES "collection_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_ded173f5a5ff89483d9ffa4dce6" FOREIGN KEY ("desired_id") REFERENCES "nft_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + await db.query(`ALTER TABLE "swap" ADD CONSTRAINT "FK_4a045cf15c5c5c44e6cf52e70c2" FOREIGN KEY ("nft_id") REFERENCES "nft_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) + } + + async down(db) { + await db.query(`DROP TABLE "offer"`) + await db.query(`DROP INDEX "public"."IDX_004a20a1eed4189bc23b13efa0"`) + await db.query(`DROP INDEX "public"."IDX_f8c1e3faf9cdba27703e0ea2c5"`) + await db.query(`DROP INDEX "public"."IDX_71609884f4478ed41be6672a66"`) + await db.query(`DROP TABLE "swap"`) + await db.query(`DROP INDEX "public"."IDX_ef7a3bc067c4f3dd314c90f79a"`) + await db.query(`DROP INDEX "public"."IDX_ded173f5a5ff89483d9ffa4dce"`) + await db.query(`DROP INDEX "public"."IDX_4a045cf15c5c5c44e6cf52e70c"`) + await db.query(`ALTER TABLE "offer" DROP CONSTRAINT "FK_004a20a1eed4189bc23b13efa0d"`) + await db.query(`ALTER TABLE "offer" DROP CONSTRAINT "FK_f8c1e3faf9cdba27703e0ea2c54"`) + await db.query(`ALTER TABLE "offer" DROP CONSTRAINT "FK_71609884f4478ed41be6672a668"`) + await db.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_ef7a3bc067c4f3dd314c90f79a5"`) + await db.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_ded173f5a5ff89483d9ffa4dce6"`) + await db.query(`ALTER TABLE "swap" DROP CONSTRAINT "FK_4a045cf15c5c5c44e6cf52e70c2"`) + } +} diff --git a/schema.graphql b/schema.graphql index fc17caca..55304d99 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,5 @@ # Entity to represent a collection -# defined on chain as pub type Collection, I: 'static = ()> +# defined on chain as pub type Collection, I: 'static = ()> # https://github.com/paritytech/polkadot-sdk/blob/b8ad0d1f565659f004165c5244acba78828d0bf7/substrate/frame/nfts/src/lib.rs#L217 type CollectionEntity @entity { attributes: [Attribute!] @@ -35,24 +35,24 @@ type CollectionEntity @entity { } # Entity to group NFTEntity by common metadata -# grouping is done either by NFTEntity.image or NFTEntity.media +# grouping is done either by NFTEntity.image or NFTEntity.media # https://github.com/paritytech/polkadot-sdk/blob/b8ad0d1f565659f004165c5244acba78828d0bf7/substrate/frame/nfts/src/lib.rs#L293 type TokenEntity @entity { id: ID! blockNumber: BigInt collection: CollectionEntity - nfts: [NFTEntity!] @derivedFrom(field: "token") + count: Int! + createdAt: DateTime! + deleted: Boolean! hash: String! @index image: String media: String meta: MetadataEntity metadata: String name: String @index - updatedAt: DateTime! - createdAt: DateTime! + nfts: [NFTEntity!] @derivedFrom(field: "token") supply: Int! - count: Int! - deleted: Boolean! + updatedAt: DateTime! } # Entity to represent a collection @@ -79,6 +79,7 @@ type NFTEntity @entity { recipient: String royalty: Float sn: BigInt! @index + # swap: Swap @derivedFrom(field: "nft") updatedAt: DateTime! @index version: Int! token: TokenEntity @@ -155,6 +156,61 @@ type CollectionEvent implements EventType @entity { # version: Int! } +# type TradeEvent implements EventType @entity { +# id: ID! +# blockNumber: BigInt +# caller: String! +# currentOwner: String # currentOwner +# interaction: OfferInteraction! +# meta: String! +# trade: Swap! +# timestamp: DateTime! +# } + +# Entity to represent a Offer +# defined on chain as pub type PendingSwapOf, I: 'static = ()> +# https://github.com/paritytech/polkadot-sdk/blob/d0d8e29197a783f3ea300569afc50244a280cafa/substrate/frame/nfts/src/types.rs#L207 +type Offer @entity { + id: ID! # collection-id // same as NFTEntity.id + # events: [TradeEvent!] @derivedFrom(field: "offer") + blockNumber: BigInt! + caller: String! + considered: CollectionEntity! + createdAt: DateTime! + desired: NFTEntity + expiration: BigInt! + nft: NFTEntity! @unique + price: BigInt! + status: TradeStatus! + updatedAt: DateTime +} + +# DEV: Consideration is not used +# type Consideration @entity { +# id: ID! +# collection: CollectionEntity! +# nft: NFTEntity +# } + +# Entity to represent a Swap +# defined on chain as pub type PendingSwapOf, I: 'static = ()> +# https://github.com/paritytech/polkadot-sdk/blob/d0d8e29197a783f3ea300569afc50244a280cafa/substrate/frame/nfts/src/types.rs#L207 +type Swap @entity { + id: ID! # collection-id // same as NFTEntity.id + # events: [TradeEvent!] @derivedFrom(field: "offer") + blockNumber: BigInt! + caller: String! + considered: CollectionEntity! + createdAt: DateTime! + desired: NFTEntity + expiration: BigInt! + nft: NFTEntity! @unique + price: BigInt + status: TradeStatus! + surcharge: Surcharge + updatedAt: DateTime +} + # Possible on-chain interactions that we listen for enum Interaction { BURN @@ -168,6 +224,8 @@ enum Interaction { LOCK CHANGEISSUER PAY_ROYALTY + OFFER + SWAP # ROYALTY } @@ -181,6 +239,26 @@ enum CollectionType { Public } +enum Surcharge { + Receive + Send +} + +enum TradeInteraction { + CREATE + ACCEPT + CANCEL +} + +enum TradeStatus { + ACCEPTED + ACTIVE + CANCELLED + EXPIRED + INVALID + WITHDRAWN +} + # Entity to represent a Fungible Asset # defined on chain as pub type Asset, I: 'static = ()> # https://github.com/paritytech/polkadot-sdk/blob/99234440f0f8b24f7e4d1d3a0102a9b19a408dd3/substrate/frame/assets/src/lib.rs#L325 @@ -195,4 +273,4 @@ type AssetEntity @entity { type CacheStatus @entity { id: ID! lastBlockTimestamp: DateTime! -} \ No newline at end of file +} diff --git a/speck.yaml b/speck.yaml index 5b419957..218e3d79 100644 --- a/speck.yaml +++ b/speck.yaml @@ -15,6 +15,7 @@ deploy: - lib/processor env: CHAIN: polkadot + OFFER: 174 api: cmd: - npx diff --git a/squid.yaml b/squid.yaml index f0fe1265..57d66cec 100644 --- a/squid.yaml +++ b/squid.yaml @@ -1,6 +1,6 @@ manifestVersion: subsquid.io/v0.1 name: stick -version: 13 +version: 12 description: 'SubSquid indexer for Uniques and Assets on Statemine' build: deploy: @@ -15,6 +15,7 @@ deploy: - lib/processor env: CHAIN: kusama + OFFER: 464 api: cmd: - npx diff --git a/src/environment.ts b/src/environment.ts index df912be8..bb710b99 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -1,6 +1,7 @@ export type Chain = 'kusama' | 'rococo' | 'polkadot' export const CHAIN: Chain = process.env.CHAIN as Chain || 'kusama' +export const COLLECTION_OFFER: string = process.env.OFFER || '' const UNIQUE_STARTING_BLOCK = 323_750 // 618838; // const _NFT_STARTING_BLOCK = 4_556_552 @@ -14,6 +15,7 @@ export const isProd = CHAIN !== 'rococo' console.table({ CHAIN, ARCHIVE_URL, NODE_URL, STARTING_BLOCK, + COLLECTION_OFFER, disabledRPC: false, environment: isProd ? 'production' : 'development', }) diff --git a/src/mappings/index.ts b/src/mappings/index.ts index 9d402d11..c724418b 100644 --- a/src/mappings/index.ts +++ b/src/mappings/index.ts @@ -151,6 +151,15 @@ export async function nfts(item: T, ctx: Context): Prom case NewNonFungible.sendTip: await n.handleTipSend(ctx) break + case NewNonFungible.createSwap: + await n.handleCreateSwap(ctx) + break + case NewNonFungible.claimSwap: + await n.handleClaimSwap(ctx) + break + case NewNonFungible.cancelSwap: + await n.handleCancelSwap(ctx) + break default: throw new Error(`Unknown event ${item.name}`) } diff --git a/src/mappings/nfts/burn.ts b/src/mappings/nfts/burn.ts index 4bf93dcf..559763ad 100644 --- a/src/mappings/nfts/burn.ts +++ b/src/mappings/nfts/burn.ts @@ -1,5 +1,5 @@ -import { getWith } from '@kodadot1/metasquid/entity' -import { NFTEntity as NE } from '../../model' +import { getOptional, getWith } from '@kodadot1/metasquid/entity' +import { NFTEntity as NE, TradeStatus, Swap } from '../../model' import { unwrap } from '../utils/extract' import { debug, pending, success } from '../utils/logger' import { Action, Context, createTokenId } from '../utils/types' @@ -47,4 +47,11 @@ export async function handleTokenBurn(context: Context): Promise { await context.store.save(entity.collection) const meta = entity.metadata ?? '' await createEvent(entity, OPERATION, event, meta, context.store) + + const swap = await getOptional(context.store, Swap, id) + if (swap && swap.status === TradeStatus.ACTIVE) { + swap.status = TradeStatus.CANCELLED + swap.updatedAt = event.timestamp + await context.store.save(swap) + } } diff --git a/src/mappings/nfts/cancelSwap.ts b/src/mappings/nfts/cancelSwap.ts new file mode 100644 index 00000000..fd126e23 --- /dev/null +++ b/src/mappings/nfts/cancelSwap.ts @@ -0,0 +1,39 @@ +import { getOrFail as get } from '@kodadot1/metasquid/entity' +import { Offer, Swap, TradeStatus } from '../../model' +import { unwrap } from '../utils/extract' +import { debug, pending, success } from '../utils/logger' +import { Context, createTokenId, isOffer } from '../utils/types' +import { getSwapCancelledEvent } from './getters' + +const OPERATION = TradeStatus.WITHDRAWN + +/** + * Handle the atomic swap cancel event (Nfts.SwapCancelled) + * Marks the swap as withdrawn + * Logs Nothing + * @param context - the context for the event + **/ +export async function handleCancelSwap(context: Context): Promise { + pending(OPERATION, `${context.block.height}`) + const event = unwrap(context, getSwapCancelledEvent) + debug(OPERATION, event, true) + + const id = createTokenId(event.collectionId, event.sn) + const offer = isOffer(event) + const entity = offer ? await get(context.store, Offer, id) : await get(context.store, Swap, id) + + entity.status = TradeStatus.WITHDRAWN + entity.updatedAt = event.timestamp + + success(OPERATION, `${id} by ${event.caller}`) + + await context.store.save(entity) + // SwapCancelled { + // offered_collection: T::CollectionId, + // offered_item: T::ItemId, + // desired_collection: T::CollectionId, + // desired_item: Option, + // price: Option>>, + // deadline: BlockNumberFor, + // }, +} diff --git a/src/mappings/nfts/claimSwap.ts b/src/mappings/nfts/claimSwap.ts new file mode 100644 index 00000000..42b6bb02 --- /dev/null +++ b/src/mappings/nfts/claimSwap.ts @@ -0,0 +1,42 @@ +import { getOrFail as get } from '@kodadot1/metasquid/entity' +import { Offer, Swap, TradeStatus } from '../../model' +import { unwrap } from '../utils/extract' +import { debug, pending, success } from '../utils/logger' +import { Context, createTokenId, isOffer } from '../utils/types' +import { getSwapClaimedEvent } from './getters' + +const OPERATION = TradeStatus.ACCEPTED + +/** + * Handle the atomic swap claim event (Nfts.SwapClaimed) + * Marks the swap as accepted + * Logs Nothing + * @param context - the context for the event +**/ +export async function handleClaimSwap(context: Context): Promise { + pending(OPERATION, `${context.block.height}`) + const event = unwrap(context, getSwapClaimedEvent) + debug(OPERATION, event, true) + + const id = createTokenId(event.collectionId, event.sn) + const offer = isOffer(event) + const entity = offer ? await get(context.store, Offer, id) : await get(context.store, Swap, id) + + entity.status = TradeStatus.ACCEPTED + entity.updatedAt = event.timestamp + + success(OPERATION, `${id} by ${event.caller}`) + + await context.store.save(entity) + + // SwapClaimed { + // sent_collection: T::CollectionId, + // sent_item: T::ItemId, + // sent_item_owner: T::AccountId, + // received_collection: T::CollectionId, + // received_item: T::ItemId, + // received_item_owner: T::AccountId, + // price: Option>>, + // deadline: BlockNumberFor, + // }, +} diff --git a/src/mappings/nfts/createSwap.ts b/src/mappings/nfts/createSwap.ts new file mode 100644 index 00000000..991bbda1 --- /dev/null +++ b/src/mappings/nfts/createSwap.ts @@ -0,0 +1,72 @@ +import { getOrFail as get, getOrCreate } from '@kodadot1/metasquid/entity' +import { CollectionEntity as CE, NFTEntity as NE, Offer, Swap, TradeStatus } from '../../model' +import { unwrap } from '../utils/extract' +import { debug, pending, success, warn } from '../utils/logger' +import { Action, Context, createTokenId, isNFT, isOffer } from '../utils/types' +import { getSwapCreatedEvent } from './getters' +import { tokenIdOf } from './types' + +const OPERATION = Action.SWAP + +/** + * Handle the atomic swap create event (Nfts.SwapCreated) + * Marks the swap as active + * Logs Action.SWAP event + * @param context - the context for the event + **/ +export async function handleCreateSwap(context: Context): Promise { + // let TRUE_OPERATION = OPERATION; + + pending(OPERATION, `${context.block.height}`) + const event = unwrap(context, getSwapCreatedEvent) + debug(OPERATION, event, true) + let offer = false + + if (isOffer(event)) { + // Validate offer + offer = Boolean(event.price && event.price > 0n && event.surcharge === 'Send') + warn(OPERATION, `Will be treated as **${Action.OFFER}**`) + // TRUE_OPERATION = Action.OFFER + } + // DEV_NOT: SWAP CAN BE OVERWRITTEN! + const id = createTokenId(event.collectionId, event.sn) + const final = offer ? await getOrCreate(context.store, Offer, id, {}) : await getOrCreate(context.store, Swap, id, {}) + const deadline = BigInt(event.deadline) + // the nft that is being swapped + const nft = await get(context.store, NE, id) + const considered = await get(context.store, CE, event.consideration.collectionId) + const desired = isNFT(event.consideration) ? await get(context.store, NE, tokenIdOf(event.consideration as any)) : undefined + + final.blockNumber = BigInt(event.blockNumber) + final.createdAt = event.timestamp + final.caller = event.caller + final.nft = nft + final.considered = considered + final.desired = desired + final.expiration = deadline + final.price = event.price + if ('surcharge' in final) { + final.surcharge = event.surcharge + } + final.status = final.blockNumber >= deadline ? TradeStatus.EXPIRED : TradeStatus.ACTIVE + final.updatedAt = event.timestamp + + await context.store.save(final) + + // DEV_NOTE: need to be first enabled by schema (line 82) + // if ('swap' in nft) { + // nft.swap = final + // await context.store.save(nft) + // } + + success(OPERATION, `${id} by ${event.caller} by ${event.caller}`) + + // SwapCreated { + // offered_collection: T::CollectionId, + // offered_item: T::ItemId, + // desired_collection: T::CollectionId, + // desired_item: Option, + // price: Option>>, + // deadline: BlockNumberFor, + // }, +} diff --git a/src/mappings/nfts/getters/index.ts b/src/mappings/nfts/getters/index.ts index 463d7872..8afbbf8f 100644 --- a/src/mappings/nfts/getters/index.ts +++ b/src/mappings/nfts/getters/index.ts @@ -5,7 +5,9 @@ import { BuyTokenEvent, ChangeCollectionOwnerEvent, ChangeCollectionTeam, + ClaimSwapEvent, CreateCollectionEvent, + CreateSwapEvent, CreateTokenEvent, DestroyCollectionEvent, ForceCreateCollectionEvent, @@ -21,126 +23,141 @@ import { const proc = require(`./${CHAIN}`) export function getCreateCollectionEvent(_ctx: Context): CreateCollectionEvent { - const ctx = _ctx.event - return proc.getCreateCollectionEvent(ctx) + const ctx = _ctx.event + return proc.getCreateCollectionEvent(ctx) } export function getForceCreateCollectionEvent(_ctx: Context): ForceCreateCollectionEvent { - const ctx = _ctx.event - return proc.getForceCreateCollectionEvent(ctx) + const ctx = _ctx.event + return proc.getForceCreateCollectionEvent(ctx) } export function getCreateTokenEvent(_ctx: Context): CreateTokenEvent { - const ctx = _ctx.event - return proc.getCreateTokenEvent(ctx) + const ctx = _ctx.event + return proc.getCreateTokenEvent(ctx) } export function getTransferTokenEvent(_ctx: Context): TransferTokenEvent { - const ctx = _ctx.event - return proc.getTransferTokenEvent(ctx) + const ctx = _ctx.event + return proc.getTransferTokenEvent(ctx) } export function getTipSentEvent(_ctx: Context) { - const ctx = _ctx.event - return proc.getTipSentEvent(ctx) + const ctx = _ctx.event + return proc.getTipSentEvent(ctx) } export function getBurnTokenEvent(_ctx: Context): BurnTokenEvent { - const ctx = _ctx.event - return proc.getBurnTokenEvent(ctx) + const ctx = _ctx.event + return proc.getBurnTokenEvent(ctx) } export function getDestroyCollectionEvent(_ctx: Context): DestroyCollectionEvent { - const ctx = _ctx.event - return proc.getDestroyCollectionEvent(ctx) + const ctx = _ctx.event + return proc.getDestroyCollectionEvent(ctx) } export function getListTokenEvent(_ctx: Context): ListTokenEvent { - const ctx = _ctx.event - return proc.getListTokenEvent(ctx) + const ctx = _ctx.event + return proc.getListTokenEvent(ctx) } export function getUnListTokenEvent(_ctx: Context): ListTokenEvent { - const ctx = _ctx.event - return proc.getUnListTokenEvent(ctx) + const ctx = _ctx.event + return proc.getUnListTokenEvent(ctx) } export function getPriceTokenEvent(_ctx: Context): ListTokenEvent { - const ctx = _ctx.event - return proc.getPriceTokenEvent(ctx) + const ctx = _ctx.event + return proc.getPriceTokenEvent(ctx) } export function getBuyTokenEvent(_ctx: Context): BuyTokenEvent { - const ctx = _ctx.event - return proc.getBuyTokenEvent(ctx) + const ctx = _ctx.event + return proc.getBuyTokenEvent(ctx) } export function getLockCollectionEvent(_ctx: Context): LockCollectionEvent { - const ctx = _ctx.event - return proc.getLockCollectionEvent(ctx) + const ctx = _ctx.event + return proc.getLockCollectionEvent(ctx) } export function getChangeCollectionOwnerEvent(_ctx: Context): ChangeCollectionOwnerEvent { - const ctx = _ctx.event - return proc.getChangeCollectionOwnerEvent(ctx) + const ctx = _ctx.event + return proc.getChangeCollectionOwnerEvent(ctx) } export function getClearCollectionMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getClearCollectionMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getClearCollectionMetadataEvent(ctx) } export function getCreateCollectionMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getCreateCollectionMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getCreateCollectionMetadataEvent(ctx) } export function getClearClassMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getClearClassMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getClearClassMetadataEvent(ctx) } export function getCreateClassMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getCreateClassMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getCreateClassMetadataEvent(ctx) } export function getCreateMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getCreateMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getCreateMetadataEvent(ctx) } export function getClearMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getClearMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getClearMetadataEvent(ctx) } export function getMetadataEvent(_ctx: Context): SetMetadata { - const ctx = _ctx.event - return proc.getMetadataEvent(ctx) + const ctx = _ctx.event + return proc.getMetadataEvent(ctx) } export function getSetAttributeEvent(_ctx: Context): SetAttribute { - const ctx = _ctx.event - return proc.getSetAttributeEvent(ctx) + const ctx = _ctx.event + return proc.getSetAttributeEvent(ctx) } export function getClearAttributeEvent(_ctx: Context): SetAttribute { - const ctx = _ctx.event - return proc.getClearAttributeEvent(ctx) + const ctx = _ctx.event + return proc.getClearAttributeEvent(ctx) } export function getAttributeEvent(_ctx: Context): SetAttribute { - const ctx = _ctx.event - return proc.getAttributeEvent(ctx) + const ctx = _ctx.event + return proc.getAttributeEvent(ctx) } export function getChangeTeamEvent(_ctx: Context): ChangeCollectionTeam { - const ctx = _ctx.event - return proc.getChangeTeamEvent(ctx) + const ctx = _ctx.event + return proc.getChangeTeamEvent(ctx) } export function getUpdateMintCall(_ctx: Context): UpdateMintSettings { - const ctx = _ctx.call - return proc.getUpdateMintCall(ctx) + const ctx = _ctx.call + return proc.getUpdateMintCall(ctx) } + +export function getSwapCreatedEvent(_ctx: Context): CreateSwapEvent { + const ctx = _ctx.event + return proc.getSwapCreatedEvent(ctx) +} + +export function getSwapCancelledEvent(_ctx: Context): CreateSwapEvent { + const ctx = _ctx.event + return proc.getSwapCancelledEvent(ctx) +} + +export function getSwapClaimedEvent(_ctx: Context): ClaimSwapEvent { + const ctx = _ctx.event + return proc.getSwapClaimedEvent(ctx) +} \ No newline at end of file diff --git a/src/mappings/nfts/getters/kusama.ts b/src/mappings/nfts/getters/kusama.ts index 1e95d253..b73cb69f 100644 --- a/src/mappings/nfts/getters/kusama.ts +++ b/src/mappings/nfts/getters/kusama.ts @@ -1,14 +1,16 @@ import { NonFungible } from '../../../processable' import { nfts as events } from '../../../types/kusama/events' import { nfts as calls } from '../../../types/kusama/calls' -import { addressOf, onlyValue, unHex } from '../../utils/helper' -import { Event, Call } from '../../utils/types' +import { addressOf, unHex } from '../../utils/helper' +import { Event, Call, Optional } from '../../utils/types' import { BurnTokenEvent, BuyTokenEvent, ChangeCollectionOwnerEvent, ChangeCollectionTeam, + ClaimSwapEvent, CreateCollectionEvent, + CreateSwapEvent, CreateTokenEvent, DestroyCollectionEvent, ForceCreateCollectionEvent, @@ -19,6 +21,7 @@ import { TransferTokenEvent, UpdateMintSettings, } from '../types' +import { Surcharge } from '../../../model' export function getCreateCollectionEvent(ctx: Event): CreateCollectionEvent { const event = events.created @@ -370,4 +373,119 @@ export function getUpdateMintCall(ctx: Call): UpdateMintSettings { const { collection: classId, mintSettings: { mintType, startBlock, endBlock, price } } = call.v9420.decode(ctx) return { id: classId.toString(), type: mintType, startBlock, endBlock, price } +} + +export function getSwapCreatedEvent(ctx: Event): CreateSwapEvent { + const event = events.swapCreated + + if (event.v9420.is(ctx)) { + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9420.decode(ctx) + + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline + } + } + + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9420.decode(ctx) + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } +} + +export function getSwapCancelledEvent(ctx: Event): CreateSwapEvent { + const event = events.swapCancelled + + if (event.v9420.is(ctx)) { + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9420.decode(ctx) + + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } + } + + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9420.decode( + ctx + ) + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } +} + +export function getSwapClaimedEvent(ctx: Event): ClaimSwapEvent { + const event = events.swapClaimed + + if (event.v9420.is(ctx)) { + const { sentCollection, sentItem, sentItemOwner, receivedCollection, receivedItem, receivedItemOwner, price, deadline } = event.v9420.decode(ctx) + + return { + collectionId: receivedCollection.toString(), + sn: receivedItem.toString(), + currentOwner: receivedItemOwner ? addressOf(receivedItemOwner) : '', + sent: { + collectionId: sentCollection.toString(), + sn: sentItem.toString(), + owner: sentItemOwner ? addressOf(sentItemOwner) : '', + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } + } + + const { + sentCollection, + sentItem, + sentItemOwner, + receivedCollection, + receivedItem, + receivedItemOwner, + price, + deadline, + } = event.v9420.decode(ctx) + + return { + collectionId: receivedCollection.toString(), + sn: receivedItem.toString(), + currentOwner: receivedItemOwner ? addressOf(receivedItemOwner) : '', + sent: { + collectionId: sentCollection.toString(), + sn: sentItem.toString(), + owner: sentItemOwner ? addressOf(sentItemOwner) : '', + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } } \ No newline at end of file diff --git a/src/mappings/nfts/getters/polkadot.ts b/src/mappings/nfts/getters/polkadot.ts index 9592ab38..daf0390d 100644 --- a/src/mappings/nfts/getters/polkadot.ts +++ b/src/mappings/nfts/getters/polkadot.ts @@ -9,7 +9,9 @@ import { BuyTokenEvent, ChangeCollectionOwnerEvent, ChangeCollectionTeam, + ClaimSwapEvent, CreateCollectionEvent, + CreateSwapEvent, CreateTokenEvent, DestroyCollectionEvent, ForceCreateCollectionEvent, @@ -21,6 +23,8 @@ import { UpdateMintSettings, } from '../types' import { debug } from '../../utils/logger' +import { Surcharge } from '../../../model' +import { Optional } from '@kodadot1/metasquid/types' export function getCreateCollectionEvent(ctx: Event): CreateCollectionEvent { const event = events.created @@ -374,3 +378,118 @@ export function getUpdateMintCall(ctx: Call): UpdateMintSettings { const { collection: classId, mintSettings: { mintType, startBlock, endBlock, price } } = call.v9430.decode(ctx) return { id: classId.toString(), type: mintType, startBlock, endBlock, price } } + +export function getSwapCreatedEvent(ctx: Event): CreateSwapEvent { + const event = events.swapCreated + + if (event.v9430.is(ctx)) { + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9430.decode(ctx) + + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline + } + } + + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9430.decode(ctx) + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } +} + +export function getSwapCancelledEvent(ctx: Event): CreateSwapEvent { + const event = events.swapCancelled + + if (event.v9430.is(ctx)) { + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9430.decode(ctx) + + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } + } + + const { offeredCollection, offeredItem, desiredCollection, desiredItem, price, deadline } = event.v9430.decode( + ctx + ) + return { + collectionId: offeredCollection.toString(), + sn: offeredItem.toString(), + consideration: { + collectionId: desiredCollection.toString(), + sn: desiredItem?.toString(), + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } +} + +export function getSwapClaimedEvent(ctx: Event): ClaimSwapEvent { + const event = events.swapClaimed + + if (event.v9430.is(ctx)) { + const { sentCollection, sentItem, sentItemOwner, receivedCollection, receivedItem, receivedItemOwner, price, deadline } = event.v9430.decode(ctx) + + return { + collectionId: receivedCollection.toString(), + sn: receivedItem.toString(), + currentOwner: receivedItemOwner ? addressOf(receivedItemOwner) : '', + sent: { + collectionId: sentCollection.toString(), + sn: sentItem.toString(), + owner: sentItemOwner ? addressOf(sentItemOwner) : '', + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } + } + + const { + sentCollection, + sentItem, + sentItemOwner, + receivedCollection, + receivedItem, + receivedItemOwner, + price, + deadline, + } = event.v9430.decode(ctx) + + return { + collectionId: receivedCollection.toString(), + sn: receivedItem.toString(), + currentOwner: receivedItemOwner ? addressOf(receivedItemOwner) : '', + sent: { + collectionId: sentCollection.toString(), + sn: sentItem.toString(), + owner: sentItemOwner ? addressOf(sentItemOwner) : '', + }, + price: price?.amount, + surcharge: price?.direction.__kind as Optional, + deadline, + } +} diff --git a/src/mappings/nfts/index.ts b/src/mappings/nfts/index.ts index 958b9628..a349194f 100644 --- a/src/mappings/nfts/index.ts +++ b/src/mappings/nfts/index.ts @@ -1,8 +1,11 @@ export * from './burn' export * from './buy' +export * from './cancelSwap' export * from './change' export * from './changeTeam' +export * from './claimSwap' export * from './create' +export * from './createSwap' export * from './destroy' export * from './forceCreate' export * from './list' diff --git a/src/mappings/nfts/transfer.ts b/src/mappings/nfts/transfer.ts index 2590ee31..6c3cbedf 100644 --- a/src/mappings/nfts/transfer.ts +++ b/src/mappings/nfts/transfer.ts @@ -1,10 +1,11 @@ -import { getWith } from '@kodadot1/metasquid/entity' -import { NFTEntity as NE } from '../../model' +import { getOptional, getWith } from '@kodadot1/metasquid/entity' +import { NFTEntity as NE, TradeStatus, Swap } from '../../model' +import { NonFungibleCall } from '../../processable' import { createEvent } from '../shared/event' import { unwrap } from '../utils/extract' -import { debug, pending, success } from '../utils/logger' -import { Action, Context, createTokenId } from '../utils/types' import { calculateCollectionOwnerCountAndDistribution } from '../utils/helper' +import { debug, pending, skip, success, warn } from '../utils/logger' +import { Action, Context, createTokenId } from '../utils/types' import { getTransferTokenEvent } from './getters' const OPERATION = Action.SEND @@ -16,10 +17,26 @@ const OPERATION = Action.SEND * @param context - the context for the event **/ export async function handleTokenTransfer(context: Context): Promise { + // Handling swaps and other operations + let TRUE_OPERATION = OPERATION; + pending(OPERATION, `${context.block.height}`) const event = unwrap(context, getTransferTokenEvent) debug(OPERATION, event) + // Check if event has a name and can be skipped in some cases + switch (event.name) { + case NonFungibleCall.buyItem: + skip(OPERATION, `because it is **${event.name}**`) + return + case NonFungibleCall.claimSwap: + warn(OPERATION, `Will be treated as **${Action.SWAP}**`) + TRUE_OPERATION = Action.SWAP + break + default: + break + } + const id = createTokenId(event.collectionId, event.sn) const entity = await getWith(context.store, NE, id, { collection: true }) @@ -37,8 +54,17 @@ export async function handleTokenTransfer(context: Context): Promise { entity.collection.ownerCount = ownerCount entity.collection.distribution = distribution - success(OPERATION, `${id} from ${event.caller} to ${event.to}`) + success(TRUE_OPERATION, `${id} from ${event.caller} to ${event.to}`) await context.store.save(entity) await context.store.save(entity.collection) - await createEvent(entity, OPERATION, event, event.to, context.store, oldOwner) + await createEvent(entity, TRUE_OPERATION, event, event.to, context.store, oldOwner) + + // remove swap if exists + // PendingSwapOf::::remove(&collection, &item); + const swap = await getOptional(context.store, Swap, id) + if (swap && swap.status === TradeStatus.ACTIVE) { + swap.status = TradeStatus.CANCELLED + swap.updatedAt = event.timestamp + await context.store.save(swap) + } } diff --git a/src/mappings/nfts/types.ts b/src/mappings/nfts/types.ts index a862f267..814320df 100644 --- a/src/mappings/nfts/types.ts +++ b/src/mappings/nfts/types.ts @@ -1,5 +1,5 @@ import { ArchiveCall, ArchiveCallWithOptionalValue, MetadataAttribute, Optional } from '@kodadot1/metasquid/types' -import { Attribute, CollectionSettings } from '../../model' +import { Attribute, CollectionSettings, Surcharge } from '../../model' import { createTokenId } from '../utils/types' export type WithId = { @@ -86,6 +86,30 @@ export type UpdateMintSettings = WithId & { price: Optional, } +type SwapData = { + price: Optional, + surcharge: Optional, + deadline: number, +} + +type BaseSwapEvent = BaseTokenEvent & SwapData + +export type CreateSwapEvent = BaseSwapEvent & { + consideration: { + collectionId: string + sn?: string + } +} + +export type ClaimSwapEvent = BaseSwapEvent & { + sent: { + collectionId: string + sn: string + owner: string + } + currentOwner: string +} + export const tokenIdOf = (base: BaseTokenEvent): string => createTokenId(base.collectionId, base.sn) export function attributeFrom(attribute: MetadataAttribute): Attribute { diff --git a/src/mappings/shared/token/burn.ts b/src/mappings/shared/token/burn.ts index 1c9eb3e1..513d7e0d 100644 --- a/src/mappings/shared/token/burn.ts +++ b/src/mappings/shared/token/burn.ts @@ -2,7 +2,7 @@ import { getWith } from '@kodadot1/metasquid/entity' import { Context } from '../../utils/types' import { NFTEntity as NE } from '../../../model' import { debug, warn } from '../../utils/logger' -import { OPERATION, generateTokenId } from './utils' +import { generateTokenId, OPERATION } from './utils' import { TokenAPI } from './tokenAPI' export async function burnHandler(context: Context, nft: NE): Promise { @@ -15,7 +15,9 @@ export async function burnHandler(context: Context, nft: NE): Promise { const tokenAPI = new TokenAPI(context.store) try { - const nftWithToken = await getWith(context.store, NE, nft.id, { token: true }) + const nftWithToken = await getWith(context.store, NE, nft.id, { + token: true, + }) if (nftWithToken?.token) { await tokenAPI.removeNftFromToken(nft, nftWithToken.token) } diff --git a/src/mappings/shared/token/mint.ts b/src/mappings/shared/token/mint.ts index 5c9144c2..0bd6d7bb 100644 --- a/src/mappings/shared/token/mint.ts +++ b/src/mappings/shared/token/mint.ts @@ -1,11 +1,19 @@ import { getOptional } from '@kodadot1/metasquid/entity' import { Context } from '../../utils/types' -import { CollectionEntity as CE, NFTEntity as NE, TokenEntity as TE } from '../../../model' +import { + CollectionEntity as CE, + NFTEntity as NE, + TokenEntity as TE, +} from '../../../model' import { debug } from '../../utils/logger' -import { OPERATION, generateTokenId } from './utils' +import { generateTokenId, OPERATION } from './utils' import { TokenAPI } from './tokenAPI' -export async function mintHandler(context: Context, collection: CE, nft: NE): Promise { +export async function mintHandler( + context: Context, + collection: CE, + nft: NE, +): Promise { debug(OPERATION, { mintHandler: `Handle mint for NFT ${nft.id}` }) const tokenId = generateTokenId(collection.id, nft) @@ -16,5 +24,7 @@ export async function mintHandler(context: Context, collection: CE, nft: NE): Pr const tokenApi = new TokenAPI(context.store) const existingToken = await getOptional(context.store, TE, tokenId) - return await (existingToken ? tokenApi.addNftToToken(nft, existingToken) : tokenApi.create(collection, nft)) + return await (existingToken + ? tokenApi.addNftToToken(nft, existingToken) + : tokenApi.create(collection, nft)) } diff --git a/src/mappings/shared/token/setMetadata.ts b/src/mappings/shared/token/setMetadata.ts index 44c52ed8..3670a58a 100644 --- a/src/mappings/shared/token/setMetadata.ts +++ b/src/mappings/shared/token/setMetadata.ts @@ -1,12 +1,22 @@ import { getOptional, getWith } from '@kodadot1/metasquid/entity' import { Context } from '../../utils/types' -import { CollectionEntity as CE, NFTEntity as NE, TokenEntity as TE } from '../../../model' +import { + CollectionEntity as CE, + NFTEntity as NE, + TokenEntity as TE, +} from '../../../model' import { debug, warn } from '../../utils/logger' -import { OPERATION, generateTokenId } from './utils' +import { generateTokenId, OPERATION } from './utils' import { TokenAPI } from './tokenAPI' -export async function setMetadataHandler(context: Context, collection: CE, nft: NE): Promise { - debug(OPERATION, { handleMetadataSet: `Handle set metadata for NFT ${nft.id}` }) +export async function setMetadataHandler( + context: Context, + collection: CE, + nft: NE, +): Promise { + debug(OPERATION, { + handleMetadataSet: `Handle set metadata for NFT ${nft.id}`, + }) const tokenId = generateTokenId(collection.id, nft) if (!tokenId) { @@ -16,7 +26,9 @@ export async function setMetadataHandler(context: Context, collection: CE, nft: const tokenAPI = new TokenAPI(context.store) try { - const nftWithToken = await getWith(context.store, NE, nft.id, { token: true }) + const nftWithToken = await getWith(context.store, NE, nft.id, { + token: true, + }) if (nftWithToken?.token) { await tokenAPI.removeNftFromToken(nft, nftWithToken.token) } @@ -26,5 +38,7 @@ export async function setMetadataHandler(context: Context, collection: CE, nft: } const existingToken = await getOptional(context.store, TE, tokenId) - return await (existingToken ? tokenAPI.addNftToToken(nft, existingToken) : tokenAPI.create(collection, nft)) + return await (existingToken + ? tokenAPI.addNftToToken(nft, existingToken) + : tokenAPI.create(collection, nft)) } diff --git a/src/mappings/shared/token/tokenAPI.ts b/src/mappings/shared/token/tokenAPI.ts index 8895a0b8..41646be6 100644 --- a/src/mappings/shared/token/tokenAPI.ts +++ b/src/mappings/shared/token/tokenAPI.ts @@ -1,9 +1,13 @@ import { create as createEntity, emOf } from '@kodadot1/metasquid/entity' import md5 from 'md5' import { Store } from '../../utils/types' -import { CollectionEntity as CE, NFTEntity as NE, TokenEntity as TE } from '../../../model' +import { + CollectionEntity as CE, + NFTEntity as NE, + TokenEntity as TE, +} from '../../../model' import { debug } from '../../utils/logger' -import { OPERATION, generateTokenId, tokenName } from './utils' +import { generateTokenId, OPERATION, tokenName } from './utils' export class TokenAPI { constructor(private store: Store) {} @@ -13,7 +17,9 @@ export class TokenAPI { if (!tokenId) { return } - debug(OPERATION, { createToken: `Create TOKEN ${tokenId} for NFT ${nft.id}` }) + debug(OPERATION, { + createToken: `Create TOKEN ${tokenId} for NFT ${nft.id}`, + }) const token = createEntity(TE, tokenId, { createdAt: nft.createdAt, @@ -42,7 +48,9 @@ export class TokenAPI { if (!token) { return } - debug(OPERATION, { removeNftFromToken: `Unlink NFT ${nft.id} from TOKEN ${token.id}` }) + debug(OPERATION, { + removeNftFromToken: `Unlink NFT ${nft.id} from TOKEN ${token.id}`, + }) await emOf(this.store).update(NE, nft.id, { token: null }) const updatedCount = await emOf(this.store).countBy(NE, { @@ -80,7 +88,9 @@ export class TokenAPI { if (nft.token?.id === token.id) { return token } - debug(OPERATION, { updateToken: `Add NFT ${nft.id} to TOKEN ${token.id} for ` }) + debug(OPERATION, { + updateToken: `Add NFT ${nft.id} to TOKEN ${token.id} for `, + }) token.count += 1 token.supply += nft.burned ? 0 : 1 token.updatedAt = nft.updatedAt diff --git a/src/mappings/shared/token/utils.ts b/src/mappings/shared/token/utils.ts index 615c2ac1..aeaf2b57 100644 --- a/src/mappings/shared/token/utils.ts +++ b/src/mappings/shared/token/utils.ts @@ -5,7 +5,10 @@ import { CHAIN } from '../../../environment' export const OPERATION = 'TokenEntity' as any -export function generateTokenId(collectionId: string, nft: NE): string | undefined { +export function generateTokenId( + collectionId: string, + nft: NE, +): string | undefined { if (!nft.image && !nft.media) { warn(OPERATION, `MISSING NFT MEDIA ${nft.id}`) return undefined @@ -16,12 +19,15 @@ export function generateTokenId(collectionId: string, nft: NE): string | undefin } export const collectionsToKeepNameAsIs: Record = { - statemine: [ + kusama: [ '176', // chained - generative art ], } -export const tokenName = (nftName: string | undefined | null, collectionId: string): string => { +export const tokenName = ( + nftName: string | undefined | null, + collectionId: string, +): string => { if (typeof nftName !== 'string') { return '' } @@ -30,4 +36,3 @@ export const tokenName = (nftName: string | undefined | null, collectionId: stri return doNotAlter ? nftName : nftName.replace(/([#_]\d+$)/g, '') } - diff --git a/src/mappings/utils/extract.ts b/src/mappings/utils/extract.ts index c11eab24..5dd8f2a1 100644 --- a/src/mappings/utils/extract.ts +++ b/src/mappings/utils/extract.ts @@ -19,8 +19,9 @@ function toBaseEvent(ctx: Context): BaseCall { const caller = addressOf(address) const blockNumber = ctx.block.height.toString() const timestamp = ctx.block.timestamp ? new Date(ctx.block.timestamp) : new Date() + const name = ctx.call?.name - return { caller, blockNumber, timestamp } + return { caller, blockNumber, timestamp, name } } /** diff --git a/src/mappings/utils/logger.ts b/src/mappings/utils/logger.ts index 82802db9..9b84b2c8 100644 --- a/src/mappings/utils/logger.ts +++ b/src/mappings/utils/logger.ts @@ -1,8 +1,8 @@ import { serializer } from '@kodadot1/metasquid' import { logger } from '@kodadot1/metasquid/logger' -import { Interaction } from '../../model' +import { Interaction, TradeStatus } from '../../model' -type Action = Interaction +type Action = Interaction | TradeStatus type ErrorCallback = (error: Error) => void @@ -34,6 +34,15 @@ export const pending = (action: Action, message: string) => { logger.info(`⏳ [${action}] ${message}`) } +/** + * Log a started action + * @param action - the action being performed + * @param message - the message to log +**/ +export const skip = (action: Action, message: string) => { + logger.info(`⏩ [${action}] ${message}`) +} + /** * Log a debug message * @param action - the action being performed diff --git a/src/mappings/utils/types.ts b/src/mappings/utils/types.ts index 71b8386b..de3a6e79 100644 --- a/src/mappings/utils/types.ts +++ b/src/mappings/utils/types.ts @@ -17,11 +17,13 @@ import { Attribute } from '../../model/generated/_attribute' import { Interaction } from '../../model' import { SetMetadata } from '../nfts/types' +import { COLLECTION_OFFER } from '../../environment' export type BaseCall = { caller: string blockNumber: string timestamp: Date + name?: string } // In case of fire consult this repo: // https://github.com/subsquid-labs/squid-substrate-template/tree/main @@ -65,10 +67,22 @@ export function collectionEventFrom( return eventFrom(interaction, basecall, meta) } +/** + * Check is current entity is NFT + * @param event - event should satisfy { collectionId: string, sn: string } interface +**/ export function isNFT(event: T) { return event.sn !== undefined } +/** + * Check is current entity is Offer + * @param event - event should satisfy { collectionId: string, sn: string } interface +**/ +export function isOffer(event: T): boolean { + return event.collectionId === COLLECTION_OFFER +} + export function eventFrom( interaction: T, { blockNumber, caller, timestamp }: BaseCall, diff --git a/src/model/generated/_interaction.ts b/src/model/generated/_interaction.ts index 5f1bba48..44debadb 100644 --- a/src/model/generated/_interaction.ts +++ b/src/model/generated/_interaction.ts @@ -10,4 +10,6 @@ export enum Interaction { LOCK = "LOCK", CHANGEISSUER = "CHANGEISSUER", PAY_ROYALTY = "PAY_ROYALTY", + OFFER = "OFFER", + SWAP = "SWAP", } diff --git a/src/model/generated/_surcharge.ts b/src/model/generated/_surcharge.ts new file mode 100644 index 00000000..39988f5b --- /dev/null +++ b/src/model/generated/_surcharge.ts @@ -0,0 +1,4 @@ +export enum Surcharge { + Receive = "Receive", + Send = "Send", +} diff --git a/src/model/generated/_tradeStatus.ts b/src/model/generated/_tradeStatus.ts new file mode 100644 index 00000000..27c13b7a --- /dev/null +++ b/src/model/generated/_tradeStatus.ts @@ -0,0 +1,8 @@ +export enum TradeStatus { + ACCEPTED = "ACCEPTED", + ACTIVE = "ACTIVE", + CANCELLED = "CANCELLED", + EXPIRED = "EXPIRED", + INVALID = "INVALID", + WITHDRAWN = "WITHDRAWN", +} diff --git a/src/model/generated/index.ts b/src/model/generated/index.ts index fc5e7f8a..2d4d4954 100644 --- a/src/model/generated/index.ts +++ b/src/model/generated/index.ts @@ -8,5 +8,9 @@ export * from "./metadataEntity.model" export * from "./event.model" export * from "./_interaction" export * from "./collectionEvent.model" +export * from "./offer.model" +export * from "./_tradeStatus" +export * from "./swap.model" +export * from "./_surcharge" export * from "./assetEntity.model" export * from "./cacheStatus.model" diff --git a/src/model/generated/offer.model.ts b/src/model/generated/offer.model.ts new file mode 100644 index 00000000..cf2921c9 --- /dev/null +++ b/src/model/generated/offer.model.ts @@ -0,0 +1,48 @@ +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, BigIntColumn as BigIntColumn_, StringColumn as StringColumn_, ManyToOne as ManyToOne_, Index as Index_, DateTimeColumn as DateTimeColumn_, OneToOne as OneToOne_, JoinColumn as JoinColumn_} from "@subsquid/typeorm-store" +import {CollectionEntity} from "./collectionEntity.model" +import {NFTEntity} from "./nftEntity.model" +import {TradeStatus} from "./_tradeStatus" + +@Entity_() +export class Offer { + constructor(props?: Partial) { + Object.assign(this, props) + } + + @PrimaryColumn_() + id!: string + + @BigIntColumn_({nullable: false}) + blockNumber!: bigint + + @StringColumn_({nullable: false}) + caller!: string + + @Index_() + @ManyToOne_(() => CollectionEntity, {nullable: true}) + considered!: CollectionEntity + + @DateTimeColumn_({nullable: false}) + createdAt!: Date + + @Index_() + @ManyToOne_(() => NFTEntity, {nullable: true}) + desired!: NFTEntity | undefined | null + + @BigIntColumn_({nullable: false}) + expiration!: bigint + + @Index_({unique: true}) + @OneToOne_(() => NFTEntity, {nullable: true}) + @JoinColumn_() + nft!: NFTEntity + + @BigIntColumn_({nullable: false}) + price!: bigint + + @Column_("varchar", {length: 9, nullable: false}) + status!: TradeStatus + + @DateTimeColumn_({nullable: true}) + updatedAt!: Date | undefined | null +} diff --git a/src/model/generated/swap.model.ts b/src/model/generated/swap.model.ts new file mode 100644 index 00000000..94af8ff4 --- /dev/null +++ b/src/model/generated/swap.model.ts @@ -0,0 +1,52 @@ +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, BigIntColumn as BigIntColumn_, StringColumn as StringColumn_, ManyToOne as ManyToOne_, Index as Index_, DateTimeColumn as DateTimeColumn_, OneToOne as OneToOne_, JoinColumn as JoinColumn_} from "@subsquid/typeorm-store" +import {CollectionEntity} from "./collectionEntity.model" +import {NFTEntity} from "./nftEntity.model" +import {TradeStatus} from "./_tradeStatus" +import {Surcharge} from "./_surcharge" + +@Entity_() +export class Swap { + constructor(props?: Partial) { + Object.assign(this, props) + } + + @PrimaryColumn_() + id!: string + + @BigIntColumn_({nullable: false}) + blockNumber!: bigint + + @StringColumn_({nullable: false}) + caller!: string + + @Index_() + @ManyToOne_(() => CollectionEntity, {nullable: true}) + considered!: CollectionEntity + + @DateTimeColumn_({nullable: false}) + createdAt!: Date + + @Index_() + @ManyToOne_(() => NFTEntity, {nullable: true}) + desired!: NFTEntity | undefined | null + + @BigIntColumn_({nullable: false}) + expiration!: bigint + + @Index_({unique: true}) + @OneToOne_(() => NFTEntity, {nullable: true}) + @JoinColumn_() + nft!: NFTEntity + + @BigIntColumn_({nullable: true}) + price!: bigint | undefined | null + + @Column_("varchar", {length: 9, nullable: false}) + status!: TradeStatus + + @Column_("varchar", {length: 7, nullable: true}) + surcharge!: Surcharge | undefined | null + + @DateTimeColumn_({nullable: true}) + updatedAt!: Date | undefined | null +} diff --git a/src/model/generated/tokenEntity.model.ts b/src/model/generated/tokenEntity.model.ts index 6ffd7341..5f0522ae 100644 --- a/src/model/generated/tokenEntity.model.ts +++ b/src/model/generated/tokenEntity.model.ts @@ -1,7 +1,7 @@ -import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, BigIntColumn as BigIntColumn_, ManyToOne as ManyToOne_, Index as Index_, OneToMany as OneToMany_, StringColumn as StringColumn_, DateTimeColumn as DateTimeColumn_, IntColumn as IntColumn_, BooleanColumn as BooleanColumn_} from "@subsquid/typeorm-store" +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, BigIntColumn as BigIntColumn_, ManyToOne as ManyToOne_, Index as Index_, IntColumn as IntColumn_, DateTimeColumn as DateTimeColumn_, BooleanColumn as BooleanColumn_, StringColumn as StringColumn_, OneToMany as OneToMany_} from "@subsquid/typeorm-store" import {CollectionEntity} from "./collectionEntity.model" -import {NFTEntity} from "./nftEntity.model" import {MetadataEntity} from "./metadataEntity.model" +import {NFTEntity} from "./nftEntity.model" @Entity_() export class TokenEntity { @@ -19,8 +19,14 @@ export class TokenEntity { @ManyToOne_(() => CollectionEntity, {nullable: true}) collection!: CollectionEntity | undefined | null - @OneToMany_(() => NFTEntity, e => e.token) - nfts!: NFTEntity[] + @IntColumn_({nullable: false}) + count!: number + + @DateTimeColumn_({nullable: false}) + createdAt!: Date + + @BooleanColumn_({nullable: false}) + deleted!: boolean @Index_() @StringColumn_({nullable: false}) @@ -43,18 +49,12 @@ export class TokenEntity { @StringColumn_({nullable: true}) name!: string | undefined | null - @DateTimeColumn_({nullable: false}) - updatedAt!: Date - - @DateTimeColumn_({nullable: false}) - createdAt!: Date + @OneToMany_(() => NFTEntity, e => e.token) + nfts!: NFTEntity[] @IntColumn_({nullable: false}) supply!: number - @IntColumn_({nullable: false}) - count!: number - - @BooleanColumn_({nullable: false}) - deleted!: boolean + @DateTimeColumn_({nullable: false}) + updatedAt!: Date } diff --git a/src/processable.ts b/src/processable.ts index 603aca00..dc610922 100644 --- a/src/processable.ts +++ b/src/processable.ts @@ -104,6 +104,9 @@ export enum NewNonFungible { */ export enum NonFungibleCall { updateMintSettings = 'Nfts.update_mint_settings', + transfer = 'Nfts.transfer', + buyItem = 'Nfts.buy_item', + claimSwap = 'Nfts.claim_swap', } /** diff --git a/src/processor.ts b/src/processor.ts index b5413e65..7e3d3e4b 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -3,7 +3,7 @@ import { } from '@subsquid/substrate-processor' import { TypeormDatabase as Database } from '@subsquid/typeorm-store' import logger from './mappings/utils/logger' -import { Asset, NonFungible, NonFungibleCall, Unique } from './processable' +import { Asset, NewNonFungible, NonFungible, NonFungibleCall, Unique } from './processable' import { CHAIN, getArchiveUrl, getNodeUrl } from './environment' import { mainFrame } from './mappings' @@ -13,7 +13,7 @@ const database = new Database({ supportHotBlocks: false }) const processor = new SubstrateProcessor() const UNIQUE_STARTING_BLOCK = 323_750 // 618838; -// const _NFT_STARTING_BLOCK = 4_556_552 +const _NFT_STARTING_BLOCK = 4_556_552 const STARTING_BLOCK = UNIQUE_STARTING_BLOCK const ONLY_ARCHIVE = false @@ -27,12 +27,14 @@ processor.setBlockRange({ from: STARTING_BLOCK }) const archive = getArchiveUrl() const chain = getNodeUrl() -processor.setGateway(archive) + processor.setRpcEndpoint({ url: chain, rateLimit: 10 }) +processor.setGateway(archive); + // disables RPC ingestion and drastically reduce no of RPC calls processor.setRpcDataIngestionSettings({ disabled: ONLY_ARCHIVE }) @@ -101,6 +103,11 @@ processor.addEvent({ name: [NonFungible.changeTeam], call: true, extrinsic: true // processor.addEvent({ name: [NonFungible.thaw, dummy); processor.addEvent({ name: [NonFungible.transfer], call: true, extrinsic: true }) // n.handleTokenTransfer) + +processor.addEvent({ name: [NewNonFungible.createSwap], call: true, extrinsic: true }) +processor.addEvent({ name: [NewNonFungible.cancelSwap], call: true, extrinsic: true }) +processor.addEvent({ name: [NewNonFungible.claimSwap], call: true, extrinsic: true }) + processor.addCall({ name: [NonFungibleCall.updateMintSettings], extrinsic: true }) processor.setFields(fieldSelection) diff --git a/tests/misc.test.ts b/tests/misc.test.ts new file mode 100644 index 00000000..f1f18e0f --- /dev/null +++ b/tests/misc.test.ts @@ -0,0 +1,53 @@ +import { Optional } from "@kodadot1/metasquid/types" +import { describe, expect, it } from 'vitest' + +export type SwapData = { + price: Optional, + surcharge: Optional, + deadline: number, +} + +export enum Surcharge { + Receive = "Receive", + Send = "Send", +} + +describe('Misc', () => { + function isOffer(event: SwapData): boolean { + return Boolean(event.price && event.price > 0n && event.surcharge === 'Send') + } + // let store: SquidStore; + + + describe('isOffer', () => { + it('should be valid offer', () => { + const swapdata: SwapData = { + price: BigInt(1e10), + surcharge: Surcharge.Send, + deadline: 0 + } + const value = isOffer(swapdata) + expect(value).toBe(true) + }) + + it('should be invalid offer (bad surchage)', () => { + const swapdata: SwapData = { + price: BigInt(1e10), + surcharge: Surcharge.Receive, + deadline: 0 + } + const value = isOffer(swapdata) + expect(value).toBe(false) + }) + + it('should be invalid offer (bad surchage)', () => { + const swapdata: SwapData = { + price: undefined, + surcharge: Surcharge.Send, + deadline: 0 + } + const value = isOffer(swapdata) + expect(value).toBe(false) + }) + }) +})