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

Feat/Flippers-for-collection-activity #35

Closed
27 changes: 27 additions & 0 deletions db/migrations/1694688464308-Data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = class Data1694688464308 {
name = 'Data1694688464308'

async up(db) {
await db.query(`CREATE TABLE "flip_event" ("id" character varying NOT NULL, "sold_price" numeric, "sold_to" text, "sell_timestamp" TIMESTAMP WITH TIME ZONE, "bought_price" numeric NOT NULL, "profit" numeric, "flipper_id" character varying, "nft_id" character varying, CONSTRAINT "PK_fa045c959eb49fd330b7130cabb" PRIMARY KEY ("id"))`)
await db.query(`CREATE INDEX "IDX_b440835d296fabbade4a08f56d" ON "flip_event" ("flipper_id") `)
await db.query(`CREATE INDEX "IDX_0896c2680b0a6fbff1e68d3ada" ON "flip_event" ("nft_id") `)
await db.query(`CREATE TABLE "flipper_entity" ("id" character varying NOT NULL, "address" text NOT NULL, "owned" integer NOT NULL, "total_bought" numeric NOT NULL, "total_sold" numeric NOT NULL, "best_flip" numeric NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "collection_id" character varying, CONSTRAINT "PK_1715ba6adaecb7e138d4716e73e" PRIMARY KEY ("id"))`)
await db.query(`CREATE INDEX "IDX_caf50ab81db0d2aa16265da559" ON "flipper_entity" ("address") `)
await db.query(`CREATE INDEX "IDX_020450854a403a64ea22a1487f" ON "flipper_entity" ("collection_id") `)
await db.query(`ALTER TABLE "flip_event" ADD CONSTRAINT "FK_b440835d296fabbade4a08f56db" FOREIGN KEY ("flipper_id") REFERENCES "flipper_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
await db.query(`ALTER TABLE "flip_event" ADD CONSTRAINT "FK_0896c2680b0a6fbff1e68d3adaa" FOREIGN KEY ("nft_id") REFERENCES "nft_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
await db.query(`ALTER TABLE "flipper_entity" ADD CONSTRAINT "FK_020450854a403a64ea22a1487f2" FOREIGN KEY ("collection_id") REFERENCES "collection_entity"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
}

async down(db) {
await db.query(`DROP TABLE "flip_event"`)
await db.query(`DROP INDEX "public"."IDX_b440835d296fabbade4a08f56d"`)
await db.query(`DROP INDEX "public"."IDX_0896c2680b0a6fbff1e68d3ada"`)
await db.query(`DROP TABLE "flipper_entity"`)
await db.query(`DROP INDEX "public"."IDX_caf50ab81db0d2aa16265da559"`)
await db.query(`DROP INDEX "public"."IDX_020450854a403a64ea22a1487f"`)
await db.query(`ALTER TABLE "flip_event" DROP CONSTRAINT "FK_b440835d296fabbade4a08f56db"`)
await db.query(`ALTER TABLE "flip_event" DROP CONSTRAINT "FK_0896c2680b0a6fbff1e68d3adaa"`)
await db.query(`ALTER TABLE "flipper_entity" DROP CONSTRAINT "FK_020450854a403a64ea22a1487f2"`)
}
}
25 changes: 25 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ type CollectionEntity @entity {
updatedAt: DateTime! @index
version: Int!
volume: BigInt! @index
flippers: [FlipperEntity!] @derivedFrom(field: "collection")
}

type FlipEvent @entity {
id: ID!
flipper: FlipperEntity!
nft: NFTEntity!
soldPrice: BigInt
soldTo: String
sellTimestamp: DateTime
boughtPrice: BigInt!
profit: BigInt
}
Comment on lines +29 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. afaik we don't have initial and closing prices. that will fit more into a trading system with candles
  2. anyway, this is not the info I'm looking to capture, I am looking to capture flip events for collection activity



type FlipperEntity @entity {
id: ID!
address: String! @index
collection: CollectionEntity!
flips: [FlipEvent!] @derivedFrom(field: "flipper")
owned: Int!
totalBought: BigInt!
totalSold: BigInt!
bestFlip: BigInt!
timestamp: DateTime!
}

type TokenEntity @entity {
Expand Down
2 changes: 2 additions & 0 deletions src/mappings/nfts/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { handleBuy } from '../shared/handleFlips'
import { getBuyTokenEvent } from './getters'

const OPERATION = Action.BUY
Expand Down Expand Up @@ -37,6 +38,7 @@ export async function handleTokenBuy(context: Context): Promise<void> {
)
entity.collection.ownerCount = ownerCount
entity.collection.distribution = distribution
await handleBuy(context.store, { event, nft: entity })

success(OPERATION, `${id} by ${event.caller} for ${String(event.price)}`)
await context.store.save(entity)
Expand Down
140 changes: 140 additions & 0 deletions src/mappings/shared/handleFlips.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { EntityManager } from 'typeorm'
import { create, findByRawQuery, findWhere, getOptional } from '@kodadot1/metasquid/entity'
import { FlipperEntity, FlipEvent, CollectionEntity, NFTEntity } from '../../model'
import { CallWith } from '../utils/types'
import { BuyTokenEvent } from '../nfts/types'
import { pending, warn } from '../utils/logger'

const OPERATION = 'BUY => FlipperEventHandler' as any

type HandleFlipParams = {
event: CallWith<BuyTokenEvent>
nft: NFTEntity
}

function getFlipperId(address: string, collectionId: string): string {
return `${address}-${collectionId}`
}

function getFlipEventId(address: string, collectionId: string, blockNumber: string): string {
return `${blockNumber}-${address}-${collectionId}`
}

async function getOrCreateFlipper(
store: EntityManager,
address: string,
collection: CollectionEntity,
timestamp: Date
): Promise<FlipperEntity> {
const id = getFlipperId(address, collection.id)
let flipper = await getOptional<FlipperEntity>(store, FlipperEntity, id)

if (!flipper) {
flipper = create(FlipperEntity, id, {
address,
collection,
owned: 0,
totalBought: BigInt(0),
totalSold: BigInt(0),
bestFlip: BigInt(0),
timestamp,
})
await store.save(flipper)
}

return flipper
}

async function handleNewOwner(
store: EntityManager,
event: CallWith<BuyTokenEvent>,
nft: NFTEntity
): Promise<FlipperEntity> {
const { caller: newOwner, collectionId, timestamp, price, blockNumber } = event
const flipper = await getOrCreateFlipper(store, newOwner, nft.collection, timestamp)

const flipEventId = getFlipEventId(newOwner, collectionId, blockNumber)
const flipEvent = create(FlipEvent, flipEventId, {
flipper,
nft,
boughtPrice: price,
})

await store.save(flipEvent)
await store.update(
FlipperEntity,
{ id: flipper.id },
{
owned: flipper.owned + 1,
totalBought: BigInt(flipper.totalBought) + BigInt(price || 0),
timestamp,
}
)

return flipper
}

async function handlePreviousOwner(
store: EntityManager,
event: CallWith<BuyTokenEvent>,
nft: NFTEntity
): Promise<void> {
const { caller: newOwner, currentOwner: previousOwner, timestamp, price } = event

const previousFlipperId = getFlipperId(previousOwner, nft.collection.id)
let previousFlipper = await getOptional<FlipperEntity>(store, FlipperEntity, previousFlipperId)

if (!previousFlipper) {
pending(
OPERATION,
`No previous flipper found for the previous owner (${previousOwner}). No previous flip to handle.`
)
return
}

const previousBuyEvents = await findWhere<FlipEvent>(store, FlipEvent, { nft: { id: nft.id }, flipper: { id: previousFlipper.id } })


if (!previousBuyEvents || previousBuyEvents.length === 0) {
warn(OPERATION, `No previous flips found for the previous owner (${previousOwner}). No previous flip to handle.`)
return
}


const previousBuyEvent = previousBuyEvents[0] // there should be only 1
pending(OPERATION, `Previous flip event: ${previousBuyEvent}`)



const soldPrice = BigInt(price || 0)
const profit = soldPrice - previousBuyEvent.boughtPrice
const profitPercentage =
previousBuyEvent.boughtPrice !== BigInt(0) ? (Number(profit) / Number(previousBuyEvent.boughtPrice)) * 100 : 0

await store.update(
FlipEvent,
{ id: previousBuyEvent.id },
{
soldPrice,
soldTo: newOwner,
sellTimestamp: timestamp,
profit,
}
)
await store.update(
FlipperEntity,
{ id: previousFlipper.id },
{
owned: previousFlipper.owned - 1,
totalSold: BigInt(previousFlipper.totalSold) + soldPrice,
timestamp,
bestFlip:
previousFlipper.bestFlip > BigInt(profitPercentage) ? previousFlipper.bestFlip : BigInt(profitPercentage),
}
)
}

export async function handleBuy(store: EntityManager, params: HandleFlipParams): Promise<void> {
await handleNewOwner(store, params.event, params.nft)
await handlePreviousOwner(store, params.event, params.nft)
}
2 changes: 2 additions & 0 deletions src/mappings/uniques/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 { handleBuy } from '../shared/handleFlips'
import { getBuyTokenEvent } from './getters'

const OPERATION = Action.BUY
Expand Down Expand Up @@ -37,6 +38,7 @@ export async function handleTokenBuy(context: Context): Promise<void> {
)
entity.collection.ownerCount = ownerCount
entity.collection.distribution = distribution
await handleBuy(context.store, { event, nft: entity })

success(OPERATION, `${id} by ${event.caller} for ${String(event.price)}`)
await context.store.save(entity)
Expand Down
4 changes: 4 additions & 0 deletions src/model/generated/collectionEntity.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Attribute} from "./_attribute"
import {CollectionEvent} from "./collectionEvent.model"
import {MetadataEntity} from "./metadataEntity.model"
import {NFTEntity} from "./nftEntity.model"
import {FlipperEntity} from "./flipperEntity.model"

@Entity_()
export class CollectionEntity {
Expand Down Expand Up @@ -96,4 +97,7 @@ export class CollectionEntity {
@Index_()
@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
volume!: bigint

@OneToMany_(() => FlipperEntity, e => e.collection)
flippers!: FlipperEntity[]
}
37 changes: 37 additions & 0 deletions src/model/generated/flipEvent.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, ManyToOne as ManyToOne_, Index as Index_} from "typeorm"
import * as marshal from "./marshal"
import {FlipperEntity} from "./flipperEntity.model"
import {NFTEntity} from "./nftEntity.model"

@Entity_()
export class FlipEvent {
constructor(props?: Partial<FlipEvent>) {
Object.assign(this, props)
}

@PrimaryColumn_()
id!: string

@Index_()
@ManyToOne_(() => FlipperEntity, {nullable: true})
flipper!: FlipperEntity

@Index_()
@ManyToOne_(() => NFTEntity, {nullable: true})
nft!: NFTEntity

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: true})
soldPrice!: bigint | undefined | null

@Column_("text", {nullable: true})
soldTo!: string | undefined | null

@Column_("timestamp with time zone", {nullable: true})
sellTimestamp!: Date | undefined | null

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
boughtPrice!: bigint

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: true})
profit!: bigint | undefined | null
}
40 changes: 40 additions & 0 deletions src/model/generated/flipperEntity.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, Index as Index_, ManyToOne as ManyToOne_, OneToMany as OneToMany_} from "typeorm"
import * as marshal from "./marshal"
import {CollectionEntity} from "./collectionEntity.model"
import {FlipEvent} from "./flipEvent.model"

@Entity_()
export class FlipperEntity {
constructor(props?: Partial<FlipperEntity>) {
Object.assign(this, props)
}

@PrimaryColumn_()
id!: string

@Index_()
@Column_("text", {nullable: false})
address!: string

@Index_()
@ManyToOne_(() => CollectionEntity, {nullable: true})
collection!: CollectionEntity

@OneToMany_(() => FlipEvent, e => e.flipper)
flips!: FlipEvent[]

@Column_("int4", {nullable: false})
owned!: number

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
totalBought!: bigint

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
totalSold!: bigint

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
bestFlip!: bigint

@Column_("timestamp with time zone", {nullable: false})
timestamp!: Date
}
2 changes: 2 additions & 0 deletions src/model/generated/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from "./collectionEntity.model"
export * from "./_attribute"
export * from "./flipEvent.model"
export * from "./flipperEntity.model"
export * from "./tokenEntity.model"
export * from "./nftEntity.model"
export * from "./metadataEntity.model"
Expand Down
Empty file modified src/model/generated/marshal.ts
100644 → 100755
Empty file.