-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: NFTIO-2406 attribute rarity ranker (#1122)
* attribute scoring NFTIO-2406 * improve * add migration * add job to the end of traits job * wrap up * add relations * fixes * rarity
- Loading branch information
Showing
15 changed files
with
15,249 additions
and
669 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module.exports = class Data1717977154451 { | ||
name = 'Data1717977154451' | ||
|
||
async up(db) { | ||
await db.query(`CREATE TABLE "token_rarity" ("id" character varying NOT NULL, "score" numeric NOT NULL, "rank" integer NOT NULL, "collection_id" character varying, "token_id" character varying, CONSTRAINT "PK_aa34e9209b73d6ac33b006b2f6d" PRIMARY KEY ("id"))`) | ||
await db.query(`CREATE INDEX "IDX_2983370aa94af81928e41175e0" ON "token_rarity" ("collection_id") `) | ||
await db.query(`CREATE INDEX "IDX_a708c8b728c92f895762b5d027" ON "token_rarity" ("token_id") `) | ||
await db.query(`ALTER TABLE "token_rarity" ADD CONSTRAINT "FK_2983370aa94af81928e41175e0e" FOREIGN KEY ("collection_id") REFERENCES "collection"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) | ||
await db.query(`ALTER TABLE "token_rarity" ADD CONSTRAINT "FK_a708c8b728c92f895762b5d0278" FOREIGN KEY ("token_id") REFERENCES "token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) | ||
} | ||
|
||
async down(db) { | ||
await db.query(`DROP TABLE "token_rarity"`) | ||
await db.query(`DROP INDEX "public"."IDX_2983370aa94af81928e41175e0"`) | ||
await db.query(`DROP INDEX "public"."IDX_a708c8b728c92f895762b5d027"`) | ||
await db.query(`ALTER TABLE "token_rarity" DROP CONSTRAINT "FK_2983370aa94af81928e41175e0e"`) | ||
await db.query(`ALTER TABLE "token_rarity" DROP CONSTRAINT "FK_a708c8b728c92f895762b5d0278"`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module.exports = class Data1718013477503 { | ||
name = 'Data1718013477503' | ||
|
||
async up(db) { | ||
await db.query(`DROP INDEX "public"."IDX_a708c8b728c92f895762b5d027"`) | ||
await db.query(`ALTER TABLE "token_rarity" DROP CONSTRAINT "FK_a708c8b728c92f895762b5d0278"`) | ||
await db.query(`ALTER TABLE "token_rarity" ADD CONSTRAINT "UQ_a708c8b728c92f895762b5d0278" UNIQUE ("token_id")`) | ||
await db.query(`CREATE UNIQUE INDEX "IDX_a708c8b728c92f895762b5d027" ON "token_rarity" ("token_id") `) | ||
await db.query(`ALTER TABLE "token_rarity" ADD CONSTRAINT "FK_a708c8b728c92f895762b5d0278" FOREIGN KEY ("token_id") REFERENCES "token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) | ||
} | ||
|
||
async down(db) { | ||
await db.query(`CREATE INDEX "IDX_a708c8b728c92f895762b5d027" ON "token_rarity" ("token_id") `) | ||
await db.query(`ALTER TABLE "token_rarity" ADD CONSTRAINT "FK_a708c8b728c92f895762b5d0278" FOREIGN KEY ("token_id") REFERENCES "token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`) | ||
await db.query(`ALTER TABLE "token_rarity" DROP CONSTRAINT "UQ_a708c8b728c92f895762b5d0278"`) | ||
await db.query(`DROP INDEX "public"."IDX_a708c8b728c92f895762b5d027"`) | ||
await db.query(`ALTER TABLE "token_rarity" DROP CONSTRAINT "FK_a708c8b728c92f895762b5d0278"`) | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import Queue from 'bull' | ||
import * as mathjs from 'mathjs' | ||
import { informationContentScoring } from '../open-rarity/handlers/information-content-scoring' | ||
import connection from '../connection' | ||
import { Collection, Token, TokenRarity } from '../model' | ||
import { JobData } from '../jobs/rarity-ranker' | ||
|
||
export default async (job: Queue.Job<JobData>, done: Queue.DoneCallback) => { | ||
if (!job.data.collectionId) { | ||
throw new Error('Collection ID not provided.') | ||
} | ||
|
||
if (!connection.isInitialized) { | ||
await connection.initialize().catch(() => { | ||
done(new Error('Failed to initialize connection')) | ||
}) | ||
} | ||
|
||
const em = connection.manager | ||
|
||
console.time('rarity-ranker') | ||
|
||
const [collection, tokens] = await Promise.all([ | ||
em.findOneOrFail(Collection, { | ||
relations: { | ||
traits: true, | ||
}, | ||
where: { id: job.data.collectionId }, | ||
}), | ||
em.find(Token, { | ||
relations: { | ||
traits: { | ||
trait: true, | ||
}, | ||
}, | ||
where: { collection: { id: job.data.collectionId } }, | ||
}), | ||
]) | ||
|
||
const totalSupply = collection.stats.supply | ||
|
||
// check if total supply is greater than 0 | ||
if (!totalSupply || totalSupply <= 0) { | ||
return done(new Error('Total supply is 0')) | ||
} | ||
|
||
const entropy = informationContentScoring.collectionEntropy(totalSupply, collection.traits) | ||
|
||
if (!entropy) { | ||
return done(new Error('Collection entropy is 0')) | ||
} | ||
|
||
const tokenRarities = tokens.map((token) => { | ||
return { score: informationContentScoring.scoreToken(totalSupply, entropy, token), token } | ||
}) | ||
|
||
tokenRarities.sort((a, b) => Number(mathjs.compare(b.score, a.score))) | ||
|
||
// sort by token.id if two tokens have the same score | ||
tokenRarities.sort((a, b) => { | ||
if (Number(mathjs.compare(a.score, b.score)) === 0) { | ||
return Number(mathjs.compare(mathjs.bignumber(a.token.tokenId), mathjs.bignumber(b.token.tokenId))) | ||
} | ||
return 0 | ||
}) | ||
|
||
const tokenRanks = tokenRarities.map((tokenRarity, index) => { | ||
return new TokenRarity({ | ||
id: `${tokenRarity.token.id}`, | ||
collection, | ||
token: tokenRarity.token, | ||
score: tokenRarity.score.toNumber(), | ||
rank: index + 1, | ||
}) | ||
}) | ||
|
||
// delete existing token rarities | ||
await em.delete(TokenRarity, { collection: { id: job.data.collectionId } }) | ||
|
||
// save new token rarities | ||
await em.save(tokenRanks) | ||
|
||
console.timeEnd('rarity-ranker') | ||
|
||
return done() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import Queue from 'bull' | ||
import { redisConfig } from './common' | ||
|
||
export type JobData = { collectionId: string } | ||
|
||
export const rarityQueue = new Queue<JobData>('rarityQueue', { | ||
defaultJobOptions: { delay: 1000, attempts: 2, removeOnComplete: true }, | ||
redis: redisConfig, | ||
settings: { | ||
maxStalledCount: 3, | ||
}, | ||
}) | ||
|
||
export const computeRarityRank = async (collectionId: string) => { | ||
if (!collectionId) { | ||
throw new Error('Collection ID not provided.') | ||
} | ||
|
||
if (collectionId === '0') { | ||
return | ||
} | ||
|
||
rarityQueue.add({ collectionId }, { jobId: collectionId }).catch(() => { | ||
// eslint-disable-next-line no-console | ||
console.log('Closing connection as Redis is not available') | ||
rarityQueue.close(true) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, ManyToOne as ManyToOne_, Index as Index_, OneToOne as OneToOne_, JoinColumn as JoinColumn_, FloatColumn as FloatColumn_, IntColumn as IntColumn_} from "@subsquid/typeorm-store" | ||
import {Collection} from "./collection.model" | ||
import {Token} from "./token.model" | ||
|
||
@Entity_() | ||
export class TokenRarity { | ||
constructor(props?: Partial<TokenRarity>) { | ||
Object.assign(this, props) | ||
} | ||
|
||
@PrimaryColumn_() | ||
id!: string | ||
|
||
@Index_() | ||
@ManyToOne_(() => Collection, {nullable: true}) | ||
collection!: Collection | ||
|
||
@Index_({unique: true}) | ||
@OneToOne_(() => Token, {nullable: true}) | ||
@JoinColumn_() | ||
token!: Token | ||
|
||
@FloatColumn_({nullable: false}) | ||
score!: number | ||
|
||
@IntColumn_({nullable: false}) | ||
rank!: number | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as mathjs from 'mathjs' | ||
import { Trait, Token } from '../../model' | ||
|
||
export const informationContentScoring = { | ||
scoreToken(totalSupply: bigint, entropy: number, token: Token) { | ||
const traits = token.traits.map((trait) => trait.trait) | ||
|
||
if (traits.length === 0) { | ||
return mathjs.bignumber(0) | ||
} | ||
|
||
const tokenAttributesScore = this.getTokenAttributesScore(totalSupply, traits) | ||
|
||
return mathjs.bignumber(tokenAttributesScore).div(entropy) | ||
}, | ||
|
||
getTokenAttributesScore(totalSupply: bigint, tokenTraits: Trait[]) { | ||
const scores: mathjs.BigNumber[] = [] | ||
|
||
tokenTraits.forEach((trait) => { | ||
scores.push(mathjs.bignumber(trait.count).div(mathjs.bignumber(totalSupply))) | ||
}) | ||
|
||
return mathjs.sum(mathjs.log2(scores)) | ||
}, | ||
|
||
collectionEntropy(totalSupply: bigint, traits: Trait[]) { | ||
const collectionProbabilities: mathjs.BigNumber[] = [] | ||
|
||
traits.forEach((trait) => { | ||
collectionProbabilities.push(mathjs.bignumber(trait.count).div(mathjs.bignumber(totalSupply))) | ||
}) | ||
|
||
return mathjs.dot(collectionProbabilities, mathjs.log2(collectionProbabilities)) | ||
}, | ||
} |