Skip to content
This repository has been archived by the owner on Jul 1, 2024. It is now read-only.

Feat/142 collection stats #170

Merged
merged 11 commits into from
Apr 24, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@

.DS_Store
dump_*.sql

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

async up(db) {
await db.query(`ALTER TABLE "collection_entity" ADD "distribution" integer NOT NULL DEFAULT '0'`)
await db.query(`ALTER TABLE "collection_entity" ADD "floor" numeric NOT NULL DEFAULT '0'`)
await db.query(`ALTER TABLE "collection_entity" ADD "highest_sale" numeric NOT NULL DEFAULT '0'`)
await db.query(`ALTER TABLE "collection_entity" ADD "owner_count" integer NOT NULL DEFAULT '0'`)
await db.query(`ALTER TABLE "collection_entity" ADD "volume" numeric NOT NULL DEFAULT '0'`)
}

async down(db) {
await db.query(`ALTER TABLE "collection_entity" DROP COLUMN "distribution"`)
await db.query(`ALTER TABLE "collection_entity" DROP COLUMN "floor"`)
await db.query(`ALTER TABLE "collection_entity" DROP COLUMN "highest_sale"`)
await db.query(`ALTER TABLE "collection_entity" DROP COLUMN "owner_count"`)
await db.query(`ALTER TABLE "collection_entity" DROP COLUMN "volume"`)
}
}
7 changes: 6 additions & 1 deletion schema.graphql
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
type CollectionEntity @entity {
id: ID!
blockNumber: BigInt
burned: Boolean!
createdAt: DateTime!
currentOwner: String!
burned: Boolean!
distribution: Int!
events: [CollectionEvent!] @derivedFrom(field: "collection")
floor: BigInt!
highestSale: BigInt!
image: String
issuer: String!
meta: MetadataEntity
metadata: String
media: String
name: String @index
nfts: [NFTEntity!] @derivedFrom(field: "collection")
ownerCount: Int!
updatedAt: DateTime!
nftCount: Int!
supply: Int!
type: CollectionType!
volume: BigInt!
}

type NFTEntity @entity {
Expand Down
58 changes: 54 additions & 4 deletions src/mappings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
import { CollectionType } from '../model/generated/_collectionType';
import { Extrinsic } from '../processable';
import { plsBe, real, remintable } from './utils/consolidator';
import { create, get, getOrCreate } from './utils/entity';
import {
calculateCollectionOwnerCountAndDistribution, create, get, getOrCreate,
} from './utils/entity';
import { unwrap } from './utils/extract';
import {
getAcceptOfferEvent,
Expand Down Expand Up @@ -85,15 +87,19 @@ export async function handleCollectionCreate(context: Context): Promise<void> {

final.id = event.id;
final.issuer = event.caller;
final.currentOwner = event.caller;
final.blockNumber = BigInt(event.blockNumber);
final.metadata = ensureMetadataUri(event.metadata, type);
final.burned = false;
final.currentOwner = event.caller;
final.distribution = 0;
final.floor = BigInt(0);
final.highestSale = BigInt(0);
final.metadata = ensureMetadataUri(event.metadata, type);
final.createdAt = event.timestamp;
final.updatedAt = event.timestamp;
final.nftCount = 0;
final.supply = 0;
final.type = type;
final.volume = BigInt(0);

logger.debug(`metadata: ${final.metadata}`);

Expand Down Expand Up @@ -153,7 +159,6 @@ export async function handleTokenCreate(context: Context): Promise<void> {
collection.updatedAt = event.timestamp;
collection.nftCount += 1;
collection.supply += 1;

logger.debug(`metadata: ${final.metadata}`);

if (final.metadata) {
Expand Down Expand Up @@ -187,10 +192,24 @@ export async function handleTokenTransfer(context: Context): Promise<void> {
entity.currentOwner = event.to;
entity.updatedAt = event.timestamp;

const collection = ensure<CE>(
await get<CE>(context.store, CE, event.collectionId),
);
plsBe(real, collection);
const { ownerCount, distribution } = await calculateCollectionOwnerCountAndDistribution(
context.store,
entity.id,
event.to,
currentOwner,
);
collection.ownerCount = ownerCount;
collection.distribution = distribution;

logger.success(
`[SEND] ${id} from ${event.caller} to ${event.to}`,
);
await context.store.save(entity);
await context.store.save(collection);
await createEvent(entity, Interaction.SEND, event, event.to || '', context.store, currentOwner);
await updateCache(event.timestamp, context.store);
}
Expand All @@ -212,6 +231,15 @@ export async function handleTokenBurn(context: Context): Promise<void> {
collection.updatedAt = event.timestamp;
collection.supply -= 1;

const { ownerCount, distribution } = await calculateCollectionOwnerCountAndDistribution(
context.store,
entity.id,
undefined,
entity.currentOwner,
);
collection.ownerCount = ownerCount;
collection.distribution = distribution;

await context.store.save(entity);
await context.store.save(collection);
const meta = entity.metadata ?? '';
Expand All @@ -228,8 +256,16 @@ export async function handleTokenList(context: Context): Promise<void> {
plsBe(real, entity);

entity.price = event.price;

const collection = ensure<CE>(await get<CE>(context.store, CE, event.collectionId));
plsBe(real, collection);
if (event.price && event.price < collection.floor) {
collection.floor = event.price;
}

logger.success(`[LIST] ${id} by ${event.caller}} for ${String(event.price)}`);
await context.store.save(entity);
await context.store.save(collection);
const meta = String(event.price || '');
const interaction = event.price ? Interaction.LIST : Interaction.UNLIST;
await createEvent(entity, interaction, event, meta, context.store);
Expand All @@ -242,14 +278,28 @@ export async function handleTokenBuy(context: Context): Promise<void> {
const id = createTokenId(event.collectionId, event.sn);
const entity = ensure<NE>(await get(context.store, NE, id));
plsBe(real, entity);
const { currentOwner } = entity;
entity.price = BigInt(0);
entity.currentOwner = event.caller;
entity.updatedAt = event.timestamp;

const collection = ensure<CE>(await get<CE>(context.store, CE, event.collectionId));
plsBe(real, collection);
collection.volume += event.price || BigInt(0);
if (event.price && collection.highestSale < event.price) {
collection.highestSale = event.price;
}
collection.updatedAt = event.timestamp;

const { ownerCount, distribution } = await calculateCollectionOwnerCountAndDistribution(
context.store,
entity.id,
event.caller,
currentOwner,
);
collection.ownerCount = ownerCount;
collection.distribution = distribution;

logger.success(`[BUY] ${id} by ${event.caller}`);
await context.store.save(entity);
await context.store.save(collection);
Expand Down
40 changes: 40 additions & 0 deletions src/mappings/utils/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,43 @@ export function create<T extends EntityWithId>(
Object.assign(entity, init);
return entity;
}

export async function calculateCollectionOwnerCountAndDistribution(
store: Store,
collectionId: string,
newOwner?: string,
originalOwner?: string,
): Promise<{ ownerCount: number; distribution: number }> {
let query;
if (newOwner && originalOwner) {
query = `SELECT
COUNT(DISTINCT current_owner) AS distribution,
COUNT(current_owner) AS owner_count,
(
SELECT max(
CASE
WHEN current_owner = '${newOwner}' THEN 0
ELSE 1
END
)
FROM nft_entity
) AS adjustment
FROM nft_entity
WHERE collection_id = '${collectionId}'
AND current_owner != '${originalOwner}'`;
}

query = `SELECT
COUNT(DISTINCT current_owner) AS distribution,
COUNT(current_owner) AS owner_count
FROM nft_entity
WHERE collection_id = '${collectionId}'`;
const [result]: { owner_count: number; distribution: number; adjustment?: number }[] = await store.query(query);

const adjustedResults = {
ownerCount: result.owner_count - (result.adjustment ?? 0),
distribution: result.distribution - (result.adjustment ?? 0),
};

return adjustedResults;
}
2 changes: 1 addition & 1 deletion src/mappings/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ss58 from '@subsquid/ss58';
import { decodeHex } from '@subsquid/substrate-processor';
import { Event } from '../../processable';
import { Context, SomethingWithOptionalMeta } from './types';
import { Context, SomethingWithOptionalMeta, Store } from './types';

export function isEmpty(obj: Record<string, unknown>): boolean {
// eslint-disable-next-line guard-for-in, @typescript-eslint/naming-convention
Expand Down
19 changes: 17 additions & 2 deletions src/model/generated/collectionEntity.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,27 @@ export class CollectionEntity {
@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: true})
blockNumber!: bigint | undefined | null

@Column_("bool", {nullable: false})
burned!: boolean

@Column_("timestamp with time zone", {nullable: false})
createdAt!: Date

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

@Column_("bool", {nullable: false})
burned!: boolean
@Column_("int4", {nullable: false})
distribution!: number

@OneToMany_(() => CollectionEvent, e => e.collection)
events!: CollectionEvent[]

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

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

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

Expand All @@ -52,6 +61,9 @@ export class CollectionEntity {
@OneToMany_(() => NFTEntity, e => e.collection)
nfts!: NFTEntity[]

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

@Column_("timestamp with time zone", {nullable: false})
updatedAt!: Date

Expand All @@ -63,4 +75,7 @@ export class CollectionEntity {

@Column_("varchar", {length: 15, nullable: false})
type!: CollectionType

@Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false})
volume!: bigint
}
3 changes: 3 additions & 0 deletions src/model/generated/nftEntity.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {MetadataEntity} from "./metadataEntity.model"

@Entity_()
export class NFTEntity {
static createQueryBuilder(arg0: string) {
throw new Error('Method not implemented.')
}
Comment on lines +10 to +12
Copy link
Member

Choose a reason for hiding this comment

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

was this generated by squid @alko89 ?

constructor(props?: Partial<NFTEntity>) {
Object.assign(this, props)
}
Expand Down