Skip to content

Commit

Permalink
Merge pull request #294 from kodadot/feat/swap-it-up
Browse files Browse the repository at this point in the history
W3F M2: Atomic Swaps
  • Loading branch information
vikiival authored Aug 16, 2024
2 parents ca363e6 + f1bbe87 commit 8d444ee
Show file tree
Hide file tree
Showing 35 changed files with 985 additions and 115 deletions.
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# 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.

## Hosted Squids

* 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

Expand Down Expand Up @@ -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=<ID_OF_THE_COLLECTION>
```

### 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

Expand Down
37 changes: 37 additions & 0 deletions db/migrations/1721653971599-Data.js
Original file line number Diff line number Diff line change
@@ -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"`)
}
}
94 changes: 86 additions & 8 deletions schema.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Entity to represent a collection
# defined on chain as pub type Collection<T: Config<I>, I: 'static = ()>
# defined on chain as pub type Collection<T: Config<I>, I: 'static = ()>
# https://github.com/paritytech/polkadot-sdk/blob/b8ad0d1f565659f004165c5244acba78828d0bf7/substrate/frame/nfts/src/lib.rs#L217
type CollectionEntity @entity {
attributes: [Attribute!]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<T: Config<I>, 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<T: Config<I>, 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
Expand All @@ -168,6 +224,8 @@ enum Interaction {
LOCK
CHANGEISSUER
PAY_ROYALTY
OFFER
SWAP
# ROYALTY
}

Expand All @@ -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<T: Config<I>, I: 'static = ()>
# https://github.com/paritytech/polkadot-sdk/blob/99234440f0f8b24f7e4d1d3a0102a9b19a408dd3/substrate/frame/assets/src/lib.rs#L325
Expand All @@ -195,4 +273,4 @@ type AssetEntity @entity {
type CacheStatus @entity {
id: ID!
lastBlockTimestamp: DateTime!
}
}
1 change: 1 addition & 0 deletions speck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ deploy:
- lib/processor
env:
CHAIN: polkadot
OFFER: 174
api:
cmd:
- npx
Expand Down
1 change: 1 addition & 0 deletions squid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ deploy:
- lib/processor
env:
CHAIN: kusama
OFFER: 464
api:
cmd:
- npx
Expand Down
2 changes: 2 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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',
})
Expand Down
9 changes: 9 additions & 0 deletions src/mappings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ export async function nfts<T extends SelectedEvent>(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}`)
}
Expand Down
11 changes: 9 additions & 2 deletions src/mappings/nfts/burn.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -47,4 +47,11 @@ export async function handleTokenBurn(context: Context): Promise<void> {
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)
}
}
39 changes: 39 additions & 0 deletions src/mappings/nfts/cancelSwap.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<T::ItemId>,
// price: Option<PriceWithDirection<ItemPrice<T, I>>>,
// deadline: BlockNumberFor<T>,
// },
}
Loading

0 comments on commit 8d444ee

Please sign in to comment.