diff --git a/apps/nestjs-indexer/src/indexer/indexer.module.ts b/apps/nestjs-indexer/src/indexer/indexer.module.ts index 4ed6732f..10bc6fde 100644 --- a/apps/nestjs-indexer/src/indexer/indexer.module.ts +++ b/apps/nestjs-indexer/src/indexer/indexer.module.ts @@ -4,10 +4,12 @@ import { TokenLaunchIndexer } from './token-launch.indexer'; import { DeployTokenIndexer } from './deploy-token.indexer'; import { BuyTokenIndexer } from './buy-token.indexer'; import { SellTokenIndexer } from './sell-token.indexer'; +import { NameServiceIndexer } from './name-service.indexer'; import { TokenLaunchModule } from 'src/services/token-launch/token-launch.module'; import { DeployTokenModule } from 'src/services/deploy-token/deploy-token.module'; import { BuyTokenModule } from 'src/services/buy-token/buy-token.module'; import { SellTokenModule } from 'src/services/sell-token/sell-token.module'; +import { NameServiceModule } from 'src/services/name-service/name-service.module'; @Module({ imports: [ @@ -15,12 +17,14 @@ import { SellTokenModule } from 'src/services/sell-token/sell-token.module'; DeployTokenModule, BuyTokenModule, SellTokenModule, + NameServiceModule, ], providers: [ TokenLaunchIndexer, DeployTokenIndexer, BuyTokenIndexer, SellTokenIndexer, + NameServiceIndexer, IndexerService, ], exports: [ @@ -28,6 +32,7 @@ import { SellTokenModule } from 'src/services/sell-token/sell-token.module'; DeployTokenIndexer, BuyTokenIndexer, SellTokenIndexer, + NameServiceIndexer, ], }) export class IndexerModule {} diff --git a/apps/nestjs-indexer/src/indexer/name-service.indexer.ts b/apps/nestjs-indexer/src/indexer/name-service.indexer.ts new file mode 100644 index 00000000..a5bf1330 --- /dev/null +++ b/apps/nestjs-indexer/src/indexer/name-service.indexer.ts @@ -0,0 +1,116 @@ +import { FieldElement, v1alpha2 as starknet } from '@apibara/starknet'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { formatUnits } from 'viem'; +import constants from 'src/common/constants'; +import { uint256, validateAndParseAddress, hash, shortString } from 'starknet'; +import { NameServiceService } from 'src/services/name-service/name-service.service'; +import { IndexerService } from './indexer.service'; +import { ContractAddress } from 'src/common/types'; + +@Injectable() +export class NameServiceIndexer { + private readonly logger = new Logger(NameServiceIndexer.name); + private readonly eventKeys: string[]; + + constructor( + @Inject(NameServiceService) + private readonly nameServiceService: NameServiceService, + + @Inject(IndexerService) + private readonly indexerService: IndexerService, + ) { + this.eventKeys = [ + validateAndParseAddress(hash.getSelectorFromName('UsernameClaimed')), + ]; + } + + async onModuleInit() { + this.indexerService.registerIndexer( + this.eventKeys, + this.handleEvents.bind(this), + ); + } + + private async handleEvents( + header: starknet.IBlockHeader, + event: starknet.IEvent, + transaction: starknet.ITransaction, + ) { + this.logger.log('Received event'); + const eventKey = validateAndParseAddress(FieldElement.toHex(event.keys[0])); + + switch (eventKey) { + case validateAndParseAddress(hash.getSelectorFromName('UsernameClaimed')): + this.logger.log('Event name: CreateLaunch'); + this.handleUsernameClaimedEvent(header, event, transaction); + break; + default: + this.logger.warn(`Unknown event type: ${eventKey}`); + } + } + + private async handleUsernameClaimedEvent( + header: starknet.IBlockHeader, + event: starknet.IEvent, + transaction: starknet.ITransaction, + ) { + const { + blockNumber, + blockHash: blockHashFelt, + timestamp: blockTimestamp, + } = header; + + const blockHash = validateAndParseAddress( + `0x${FieldElement.toBigInt(blockHashFelt).toString(16)}`, + ) as ContractAddress; + + const transactionHashFelt = transaction.meta.hash; + const transactionHash = validateAndParseAddress( + `0x${FieldElement.toBigInt(transactionHashFelt).toString(16)}`, + ) as ContractAddress; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, addressFelt] = event.keys; + + const address = validateAndParseAddress( + `0x${FieldElement.toBigInt(addressFelt).toString(16)}`, + ) as ContractAddress; + + const [usernameFelt, expiryFelt, paidLow, paidHigh, quoteTokenFelt] = + event.data; + + const username = usernameFelt + ? shortString.decodeShortString( + FieldElement.toBigInt(usernameFelt).toString(), + ) + : ''; + + const expiry = new Date(Number(FieldElement.toBigInt(expiryFelt)) * 1000); + + const paidRaw = uint256.uint256ToBN({ + low: FieldElement.toBigInt(paidLow), + high: FieldElement.toBigInt(paidHigh), + }); + const paid = formatUnits(paidRaw, constants.DECIMALS).toString(); + + const quoteToken = validateAndParseAddress( + `0x${FieldElement.toBigInt(quoteTokenFelt).toString(16)}`, + ) as ContractAddress; + + const data = { + transactionHash, + network: 'starknet-sepolia', + blockNumber: Number(blockNumber), + blockHash, + blockTimestamp: new Date(Number(blockTimestamp.seconds) * 1000), + ownerAddress: address, + expiry, + name: username, + username, + paid, + quoteToken: quoteToken, + }; + + await this.nameServiceService.create(data); + } +} diff --git a/apps/nestjs-indexer/src/services/name-service/interfaces/index.ts b/apps/nestjs-indexer/src/services/name-service/interfaces/index.ts new file mode 100644 index 00000000..172d8e81 --- /dev/null +++ b/apps/nestjs-indexer/src/services/name-service/interfaces/index.ts @@ -0,0 +1 @@ +export * from './name-service.interface'; diff --git a/apps/nestjs-indexer/src/services/name-service/interfaces/name-service.interface.ts b/apps/nestjs-indexer/src/services/name-service/interfaces/name-service.interface.ts new file mode 100644 index 00000000..b764d6e4 --- /dev/null +++ b/apps/nestjs-indexer/src/services/name-service/interfaces/name-service.interface.ts @@ -0,0 +1,17 @@ +export interface NameService { + transactionHash: string; + network?: string; + blockHash?: string; + blockNumber?: number; + blockTimestamp?: Date; + ownerAddress?: string; + expiry?: Date; + username?: string; + name?: string; + symbol?: string; + paid?: string; + quoteAddress?: string; + cursor?: number; + timestamp?: Date; + createdAt?: Date; +} diff --git a/apps/nestjs-indexer/src/services/name-service/name-service.module.ts b/apps/nestjs-indexer/src/services/name-service/name-service.module.ts new file mode 100644 index 00000000..cb01a5bb --- /dev/null +++ b/apps/nestjs-indexer/src/services/name-service/name-service.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { NameServiceService } from './name-service.service'; + +@Module({ + imports: [], + providers: [NameServiceService], + exports: [NameServiceService], +}) +export class NameServiceModule {} diff --git a/apps/nestjs-indexer/src/services/name-service/name-service.service.ts b/apps/nestjs-indexer/src/services/name-service/name-service.service.ts new file mode 100644 index 00000000..75bbd0d3 --- /dev/null +++ b/apps/nestjs-indexer/src/services/name-service/name-service.service.ts @@ -0,0 +1,47 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { NameService } from './interfaces'; + +@Injectable() +export class NameServiceService { + private readonly logger = new Logger(NameServiceService.name); + constructor(private readonly prismaService: PrismaService) {} + + async create(data: NameService) { + try { + const nameServiceRecord = + await this.prismaService.username_claimed.findUnique({ + where: { transaction_hash: data.transactionHash }, + }); + + if (nameServiceRecord) { + this.logger.warn( + `Record with transaction hash ${data.transactionHash} already exists`, + ); + return; + } + + await this.prismaService.username_claimed.create({ + data: { + transaction_hash: data.transactionHash, + network: data.network, + block_hash: data.blockHash, + block_number: data.blockNumber, + block_timestamp: data.blockTimestamp, + owner_address: data.ownerAddress, + expiry: data.expiry, + username: data.username, + symbol: data.symbol, + paid: data.paid, + quote_address: data.quoteAddress, + time_stamp: data.timestamp, + }, + }); + } catch (error) { + this.logger.error( + `Error creating name service record: ${error.message}`, + error.stack, + ); + } + } +} diff --git a/packages/indexer-prisma/prisma/schema.prisma b/packages/indexer-prisma/prisma/schema.prisma index b28c597d..4389c747 100644 --- a/packages/indexer-prisma/prisma/schema.prisma +++ b/packages/indexer-prisma/prisma/schema.prisma @@ -151,13 +151,13 @@ model username_claimed { block_number BigInt? block_timestamp DateTime? @db.Timestamp(6) transaction_hash String @id - expiry String? + expiry DateTime? @db.Timestamp(6) username String? name String? symbol String? paid String? quote_address String? - created_at DateTime? @default(now()) @db.Timestamp(6) cursor BigInt? @map("_cursor") - time_stamp String? + time_stamp DateTime? @db.Timestamp(6) + created_at DateTime? @default(now()) @db.Timestamp(6) }