Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

W3F M2: Atomic Swaps #294

Merged
merged 59 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
264d8d3
:zap: getters
vikiival Jun 27, 2024
d3ffff7
:zap: add swaps to processor
vikiival Jun 27, 2024
3028b31
chore: Add handleCreateSwap function to createSwap.ts file
vikiival Jul 1, 2024
6556b1f
feat: Add handleClaimSwap function to claimSwap.ts file
vikiival Jul 1, 2024
4a9f5e3
feat: Add cancelSwap.ts file for handling swap cancellation
vikiival Jul 1, 2024
2324357
:alien: Swap
vikiival Jul 1, 2024
f0eca6c
:squid: swap
vikiival Jul 1, 2024
5c21147
feat: Add Surcharge to SwapData in types.ts
vikiival Jul 1, 2024
391d9a6
refactor: Update Swap model associations and nullable fields
vikiival Jul 1, 2024
3ea459a
refactor: Update Swap model associations and nullable fields
vikiival Jul 1, 2024
228dbf3
:ambulance: magic code
vikiival Jul 1, 2024
595fa85
feat: Update createSwap.ts to handle swap creation and saving
vikiival Jul 1, 2024
9a199c8
:alien: schenma for swap ://
vikiival Jul 1, 2024
1995fcb
feat: Update getSwapCancelledEvent to include surcharge in CreateSwap…
vikiival Jul 1, 2024
cc4b25c
feat: Update cancelSwap.ts to handle swap cancellation and update status
vikiival Jul 1, 2024
f38bb97
refactor: Update logger.ts to include OfferStatus in Action type
vikiival Jul 1, 2024
6a21f83
feat: Include ClaimSwapEvent in getSwapClaimedEvent
vikiival Jul 1, 2024
76f6c84
feat: Update claimSwap.ts to handle swap claim and update status
vikiival Jul 1, 2024
9c92c59
refactor: Update cancelSwap.ts to use OfferStatus.WITHDRAWN for swap …
vikiival Jul 1, 2024
2aa6dcc
feat: Add swap cancellation and claim functionality
vikiival Jul 1, 2024
37c4470
:zap: works at this point
vikiival Jul 1, 2024
28512f4
Merge remote-tracking branch 'origin/main' into feat/swap-it-up
vikiival Jul 1, 2024
3314e03
feat: Add name field to BaseCall in extract.ts
vikiival Jul 2, 2024
f16543a
refactor: Add skip function to logger.ts
vikiival Jul 2, 2024
0b3b979
feat: Add transfer and buyItem options to NonFungibleCall enum
vikiival Jul 2, 2024
ba7eadd
refactor: Skip NonFungibleCall.buyItem event in handleTokenTransfer
vikiival Jul 2, 2024
012c57b
feat: Add name field to BaseCall in types.ts
vikiival Jul 2, 2024
152b701
feat: Add Nfts.claim_swap option to NonFungibleCall enum
vikiival Jul 2, 2024
0dd36a0
refactor: Update handleTokenTransfer to handle NonFungibleCall.claimS…
vikiival Jul 2, 2024
b5a2aae
feat: Add unique constraint to nft field in Swap entity
vikiival Jul 15, 2024
1bd348d
:bug: forgot GW
vikiival Jul 15, 2024
1d4a3cb
:bug: incorrect value for desired nft
vikiival Jul 21, 2024
d00d535
:zap: Remove swap if it exists in transfer
vikiival Jul 22, 2024
6ef50fb
:zap: withdraw swap if it exists in burn
vikiival Jul 22, 2024
daef929
:bug: incorrectly think something is a swap
vikiival Jul 22, 2024
8e77756
Offer Collections
vikiival Jul 22, 2024
74deb39
:zap: isOffer
vikiival Jul 22, 2024
630315f
:test_tube: misc test for offer
vikiival Jul 22, 2024
067f76d
:squid: magick generation
vikiival Jul 22, 2024
f2825ef
:truck: Offerstatus -> TradeStatus
vikiival Jul 22, 2024
0645667
:alien: Offers
vikiival Jul 22, 2024
b0cddf4
refactor: Update createSwap.ts to handle offers and trade status
vikiival Jul 22, 2024
0a4c292
:loud_sound: debug log
vikiival Jul 22, 2024
678d30c
:bug: mark swap as cancelled when removed by chain
vikiival Jul 22, 2024
2314760
:memo: docs for ops
vikiival Jul 22, 2024
017cbb7
:bug: not proper offer handling
vikiival Jul 22, 2024
26858e4
:bug: does not alter name
vikiival Jul 22, 2024
50544d9
:card_file_box: offers and swaps
vikiival Jul 22, 2024
4bb4f3c
:memo: updated memo
vikiival Jul 22, 2024
8d0ef06
:memo: correct link to image
vikiival Jul 22, 2024
18c9155
:squid: tokenEntity
vikiival Jul 22, 2024
136327f
:memo: offer
vikiival Jul 22, 2024
41e86d7
Refactor isNFT and isOffer functions for better readability and maint…
vikiival Jul 24, 2024
4793853
Update README.md to enable different chain support
vikiival Jul 24, 2024
e8f46f4
:broom: format
vikiival Jul 24, 2024
2dbb3c5
:note: createSwap.ts
vikiival Jul 31, 2024
8b73f7a
:memo: :bug: typo in word case
vikiival Aug 2, 2024
6fefe71
:memo: notes on Swaps
vikiival Aug 2, 2024
f1bbe87
:zap: save swap in nft if it is enabled by schema
vikiival Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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