From 8b1cb5c12d50b8960582384f7c9e1260414666f4 Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Fri, 27 Sep 2024 01:03:33 +0530 Subject: [PATCH 1/7] Accounts Data indexing and top accounts query --- packages/graphql/database/2.sql | 14 ++ .../src/application/uc/NewAddBlockRange.ts | 142 +++++++++++++++++- .../src/domain/Account/AccountEntity.ts | 64 ++++++++ .../src/domain/Account/vo/AccountBalance.ts | 27 ++++ .../src/domain/Account/vo/AccountModelID.ts | 20 +++ .../src/domain/Account/vo/AccountRef.ts | 18 +++ .../provider/paginatedAccounts.graphql | 16 ++ .../queries/sdk/paginatedAccounts.graphql | 16 ++ .../src/graphql/resolvers/AccountResolver.ts | 35 +++++ .../src/graphql/schemas/fuelcore.graphql | 13 ++ packages/graphql/src/infra/dao/AccountDAO.ts | 131 ++++++++++++++++ packages/graphql/src/schemas/fuelcore.graphql | 13 ++ 12 files changed, 502 insertions(+), 7 deletions(-) create mode 100644 packages/graphql/database/2.sql create mode 100644 packages/graphql/src/domain/Account/AccountEntity.ts create mode 100644 packages/graphql/src/domain/Account/vo/AccountBalance.ts create mode 100644 packages/graphql/src/domain/Account/vo/AccountModelID.ts create mode 100644 packages/graphql/src/domain/Account/vo/AccountRef.ts create mode 100644 packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql create mode 100644 packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql create mode 100644 packages/graphql/src/graphql/resolvers/AccountResolver.ts create mode 100644 packages/graphql/src/infra/dao/AccountDAO.ts diff --git a/packages/graphql/database/2.sql b/packages/graphql/database/2.sql new file mode 100644 index 000000000..98027c07c --- /dev/null +++ b/packages/graphql/database/2.sql @@ -0,0 +1,14 @@ +DROP TABLE indexer.accounts cascade; +CREATE TABLE indexer.accounts ( + _id SERIAL PRIMARY KEY, + account_id character varying(66) NOT NULL UNIQUE, + balance BIGINT NOT NULL DEFAULT 0, + transaction_count INTEGER NOT NULL DEFAULT 0, + first_transaction_timestamp timestamp without time zone NOT NULL, + recent_transaction_timestamp timestamp without time zone NOT NULL +); +CREATE UNIQUE INDEX ON indexer.accounts(_id); +CREATE UNIQUE INDEX ON indexer.accounts(account_id); +CREATE INDEX ON indexer.accounts(transaction_count); +CREATE INDEX ON indexer.accounts(recent_transaction_timestamp); +CREATE INDEX ON indexer.accounts(first_transaction_timestamp); \ No newline at end of file diff --git a/packages/graphql/src/application/uc/NewAddBlockRange.ts b/packages/graphql/src/application/uc/NewAddBlockRange.ts index 9a24dcfef..168d08899 100644 --- a/packages/graphql/src/application/uc/NewAddBlockRange.ts +++ b/packages/graphql/src/application/uc/NewAddBlockRange.ts @@ -12,26 +12,36 @@ import { import Block from '~/infra/dao/Block'; import Transaction from '~/infra/dao/Transaction'; import { DatabaseConnection } from '~/infra/database/DatabaseConnection'; +import { AccountEntity } from '../../domain/Account/AccountEntity'; +import AccountDAO from '../../infra/dao/AccountDAO'; import IndexAsset from './IndexAsset'; export default class NewAddBlockRange { + private accountDAO = new AccountDAO(); + async execute(input: Input) { const indexAsset = new IndexAsset(); + const uniqueAccountOwners = new Set(); const { from, to } = input; logger.info(`🔗 Syncing blocks: #${from} - #${to}`); + const blocksData = await this.getBlocks(from, to); if (blocksData.length === 0) { logger.info(`🔗 No blocks to sync: #${from} - #${to}`); return; } + const start = performance.now(); const connection = DatabaseConnection.getInstance(); + for (const blockData of blocksData) { const queries: { statement: string; params: any }[] = []; const block = new Block({ data: blockData }); + + // Add block data to queries queries.push({ statement: - 'insert into indexer.blocks (_id, id, timestamp, data, gas_used, producer) values ($1, $2, $3, $4, $5, $6) on conflict do nothing', + 'INSERT INTO indexer.blocks (_id, id, timestamp, data, gas_used, producer) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING', params: [ block.id, block.blockHash, @@ -41,11 +51,14 @@ export default class NewAddBlockRange { block.producer, ], }); + + // Process each transaction within the block for (const [index, transactionData] of blockData.transactions.entries()) { const transaction = new Transaction(transactionData, index, block.id); + queries.push({ statement: - 'insert into indexer.transactions (_id, tx_hash, timestamp, data, block_id) values ($1, $2, $3, $4, $5) on conflict do nothing', + 'INSERT INTO indexer.transactions (_id, tx_hash, timestamp, data, block_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING', params: [ transaction.id, transaction.transactionHash, @@ -54,11 +67,13 @@ export default class NewAddBlockRange { transaction.blockId, ], }); + + // Process transaction accounts const accounts = this.getAccounts(transactionData); for (const accountHash of accounts) { queries.push({ statement: - 'insert into indexer.transactions_accounts (_id, block_id, tx_hash, account_hash) values ($1, $2, $3, $4) on conflict do nothing', + 'INSERT INTO indexer.transactions_accounts (_id, block_id, tx_hash, account_hash) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING', params: [ transaction.id, transaction.blockId, @@ -66,7 +81,10 @@ export default class NewAddBlockRange { accountHash, ], }); + uniqueAccountOwners.add(accountHash); } + + // Handle assets in transaction receipts if (transaction.data?.status?.receipts) { try { await indexAsset.execute(transaction.data); @@ -74,31 +92,37 @@ export default class NewAddBlockRange { logger.error('Error fetching assets', e); } } + + // Insert inputs and predicates if (transactionData.inputs) { for (const inputData of transactionData.inputs) { queries.push({ statement: - 'insert into indexer.inputs (transaction_id, data) values ($1, $2) on conflict do nothing', + 'INSERT INTO indexer.inputs (transaction_id, data) VALUES ($1, $2) ON CONFLICT DO NOTHING', params: [transaction.id, inputData], }); + const predicate = this.getPredicate(inputData); if (predicate) { queries.push({ statement: - 'insert into indexer.predicates (address, bytecode) values ($1, $2) on conflict do nothing', + 'INSERT INTO indexer.predicates (address, bytecode) VALUES ($1, $2) ON CONFLICT DO NOTHING', params: [predicate.address, predicate.bytecode], }); } } } + + // Insert outputs and contracts if (transactionData.outputs) { for (const outputData of transactionData.outputs) { queries.push({ statement: - 'insert into indexer.outputs (transaction_id, data) values ($1, $2) on conflict do nothing', + 'INSERT INTO indexer.outputs (transaction_id, data) VALUES ($1, $2) ON CONFLICT DO NOTHING', params: [transaction.id, outputData], }); } + const contractIds = this.getContractIds(transactionData.outputs); for (const contractId of contractIds) { const contract = (await client.sdk.contract({ id: contractId })) @@ -106,15 +130,83 @@ export default class NewAddBlockRange { if (contract) { queries.push({ statement: - 'insert into indexer.contracts (contract_hash, data) values ($1, $2) on conflict do nothing', + 'INSERT INTO indexer.contracts (contract_hash, data) VALUES ($1, $2) ON CONFLICT DO NOTHING', params: [contract.id, contract], }); } } } } + + // Process unique account owners for each block + for (const owner of uniqueAccountOwners) { + try { + const existingAccount = await this.accountDAO.getAccountById(owner); + const transactionCountIncrement = blockData.transactions.filter( + (tx) => + tx.inputs?.some( + (input) => + input.__typename === 'InputCoin' && input.owner === owner, + ), + ).length; + + let newBalance: bigint; + + if (existingAccount) { + // Update existing account + queries.push({ + statement: ` + UPDATE indexer.accounts + SET transaction_count = transaction_count + $1, recent_transaction_timestamp = $2 + WHERE account_id = $3 + `, + params: [transactionCountIncrement, block.timestamp, owner], + }); + + newBalance = await this.fetchBalance(owner); + + queries.push({ + statement: ` + UPDATE indexer.accounts + SET balance = $1 + WHERE account_id = $2 + `, + params: [newBalance, owner], + }); + } else { + // Create a new account entry + newBalance = await this.fetchBalance(owner); + + const newAccount = AccountEntity.create({ + account_id: owner, + balance: newBalance, + transactionCount: transactionCountIncrement, + first_transaction_timestamp: block.timestamp, + }); + + queries.push({ + statement: ` + INSERT INTO indexer.accounts (account_id, balance, transaction_count, first_transaction_timestamp, recent_transaction_timestamp) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT DO NOTHING + `, + params: [ + newAccount.account_id, + newAccount.balance, + newAccount.transactionCount, + block.timestamp, + block.timestamp, + ], + }); + } + } catch (err) { + console.error(`Error processing owner ${owner}:`, err); + } + } + await connection.executeTransaction(queries); } + const end = performance.now(); const secs = Number.parseInt(`${(end - start) / 1000}`); logger.info(`✅ Synced blocks: #${from} - #${to} (${secs}s)`); @@ -187,6 +279,42 @@ export default class NewAddBlockRange { } return accounts; } + + async fetchBalance(owner: string): Promise { + const response = await client.sdk.balance({ + owner, + assetId: + '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07', + }); + return BigInt(response.data.balance.amount); + } + + async fetchAccountDataFromGraphQL(owner: string): Promise { + const allBalances: any[] = []; + let hasNextPage = true; + let after: string | null = null; + + while (hasNextPage) { + const response = await client.sdk.balances({ + filter: { owner }, + first: 1000, // Fetch 1000 records at a time + after, // Use the 'after' cursor for pagination + }); + + if (response.data?.balances?.nodes) { + const nodes = response.data.balances.nodes.map((node: any) => ({ + amount: BigInt(node.amount), + assetId: node.assetId, + })); + allBalances.push(...nodes); + } + + hasNextPage = response.data?.balances?.pageInfo?.hasNextPage || false; + after = response.data?.balances?.pageInfo?.endCursor || null; + } + + return allBalances; + } } type Input = { diff --git a/packages/graphql/src/domain/Account/AccountEntity.ts b/packages/graphql/src/domain/Account/AccountEntity.ts new file mode 100644 index 000000000..cb408eabd --- /dev/null +++ b/packages/graphql/src/domain/Account/AccountEntity.ts @@ -0,0 +1,64 @@ +import { Hash256 } from '../../application/vo/Hash256'; +import { Entity } from '../../core/Entity'; +import { AccountBalance } from './vo/AccountBalance'; +import { AccountModelID } from './vo/AccountModelID'; + +type AccountInputProps = { + account_id: Hash256; + balance: AccountBalance; + transactionCount: number; +}; + +export class AccountEntity extends Entity { + // Adjust the constructor to not require an ID initially + static create(account: any) { + const account_id = Hash256.create(account.account_id); + const balance = AccountBalance.create(account.balance); + const transactionCount = account.transactionCount || 0; + + const props: AccountInputProps = { + account_id, + balance, + transactionCount, + }; + + // If _id is not provided, set it as undefined + const id = account._id ? AccountModelID.create(account._id) : undefined; + + return new AccountEntity(props, id); + } + + static toDBItem(account: AccountEntity): any { + return { + account_id: account.props.account_id.value(), + balance: account.props.balance.value().toString(), + transaction_count: account.props.transactionCount, + }; + } + + static serializeData(data: any): string { + return JSON.stringify(data, (_, value) => + typeof value === 'bigint' ? value.toString() : value, + ); + } + + get cursor() { + return this.id ? this.id.value() : null; + } + + get id() { + return this._id; + } + + get account_id() { + return this.props.account_id.value(); + } + + get balance() { + return this.props.balance.value(); + } + + get transactionCount() { + return this.props.transactionCount; + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountBalance.ts b/packages/graphql/src/domain/Account/vo/AccountBalance.ts new file mode 100644 index 000000000..be538e838 --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountBalance.ts @@ -0,0 +1,27 @@ +import { bigint as DrizzleBigint } from 'drizzle-orm/pg-core'; +import { ValueObject } from '../../../core/ValueObject'; +interface Props { + value: bigint; +} + +export class AccountBalance extends ValueObject { + private constructor(props: Props) { + super(props); + } + + static type() { + return DrizzleBigint('balance', { mode: 'bigint' }).notNull(); + } + + static create(value: bigint) { + return new AccountBalance({ value }); + } + + value() { + return this.props.value; + } + + add(amount: bigint): AccountBalance { + return new AccountBalance({ value: this.value() + amount }); + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountModelID.ts b/packages/graphql/src/domain/Account/vo/AccountModelID.ts new file mode 100644 index 000000000..e5d6513f4 --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountModelID.ts @@ -0,0 +1,20 @@ +import { integer } from 'drizzle-orm/pg-core'; +import { Identifier } from '../../../core/Identifier'; + +export class AccountModelID extends Identifier { + private constructor(id: number) { + super(id); + } + + static type() { + return integer('_id').primaryKey(); + } + + static create(id: number): AccountModelID { + if (typeof id !== 'number' || Number.isNaN(id)) { + throw new Error('Invalid ID: ID must be a valid number.'); + } + + return new AccountModelID(id); + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountRef.ts b/packages/graphql/src/domain/Account/vo/AccountRef.ts new file mode 100644 index 000000000..ca4f4474e --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountRef.ts @@ -0,0 +1,18 @@ +import { ValueObject } from '../../../core/ValueObject'; +interface Props { + value: number; +} + +export class AccountRef extends ValueObject { + private constructor(props: Props) { + super(props); + } + + static create(id: number) { + return new AccountRef({ value: id }); + } + + value() { + return this.props.value; + } +} diff --git a/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql b/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql new file mode 100644 index 000000000..6d87787e9 --- /dev/null +++ b/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql @@ -0,0 +1,16 @@ +query paginatedAccounts($cursor: String, $direction: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts(cursor: $cursor, direction: $direction, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { + nodes { + id + account_id + balance + transaction_count + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql b/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql new file mode 100644 index 000000000..6d87787e9 --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql @@ -0,0 +1,16 @@ +query paginatedAccounts($cursor: String, $direction: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts(cursor: $cursor, direction: $direction, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { + nodes { + id + account_id + balance + transaction_count + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/resolvers/AccountResolver.ts b/packages/graphql/src/graphql/resolvers/AccountResolver.ts new file mode 100644 index 000000000..58d198610 --- /dev/null +++ b/packages/graphql/src/graphql/resolvers/AccountResolver.ts @@ -0,0 +1,35 @@ +import type { GQLQueryPaginatedAccountsArgs } from '../../graphql/generated/sdk-provider'; +import AccountDAO from '../../infra/dao/AccountDAO'; + +export class AccountResolver { + static create() { + const resolvers = new AccountResolver(); + return { + Query: { + paginatedAccounts: resolvers.paginatedAccounts, + }, + }; + } + + async paginatedAccounts(_: any, params: GQLQueryPaginatedAccountsArgs) { + const accountDAO = new AccountDAO(); + + // Set the default sorting by transaction_count, change to balance if needed + const sortBy = + params.sortBy === 'balance' ? 'balance' : 'transaction_count'; + const sortOrder = (params.sortOrder || 'desc') as 'desc' | 'asc'; + const first = params.first; + const cursor = params.cursor || undefined; + + console.log(sortBy, sortOrder, first, cursor); + + const accounts = await accountDAO.getPaginatedAccounts( + sortBy, + sortOrder, + first, + cursor, + ); + + return accounts; + } +} diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql index 5e880dd0d..6c0cbf157 100644 --- a/packages/graphql/src/graphql/schemas/fuelcore.graphql +++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql @@ -783,6 +783,7 @@ type Query { transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection! tps: TPSConnection! getBlocksDashboard : BlocksDashboardConnection! + paginatedAccounts(cursor: String, direction: String, first: Int, sortBy: String, sortOrder: String): PaginatedAccountConnection! } type Receipt { @@ -1045,4 +1046,16 @@ type BlocksDashboard { type BlocksDashboardConnection { nodes: [BlocksDashboard!]! +} + +type PaginatedAccountConnection { + nodes: [AccountNode!]! + pageInfo: PageInfo! +} + +type AccountNode { + id: ID! + account_id: String! + balance: String! + transaction_count: Int! } \ No newline at end of file diff --git a/packages/graphql/src/infra/dao/AccountDAO.ts b/packages/graphql/src/infra/dao/AccountDAO.ts new file mode 100644 index 000000000..d78ec7946 --- /dev/null +++ b/packages/graphql/src/infra/dao/AccountDAO.ts @@ -0,0 +1,131 @@ +import { AccountEntity } from '../../domain/Account/AccountEntity'; +import { DatabaseConnection } from '../database/DatabaseConnection'; + +export default class AccountDAO { + private databaseConnection: DatabaseConnection; + + constructor() { + this.databaseConnection = DatabaseConnection.getInstance(); + } + + // Custom function to stringify BigInt values + private stringifyBigInt(data: any): string { + return JSON.stringify(data, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ); + } + + // Keep this method as it is still being used + async getAccountById(id: string): Promise { + const result = await this.databaseConnection.query( + ` + SELECT * FROM indexer.accounts WHERE account_id = $1 + `, + [id], + ); + + return result.length ? AccountEntity.create(result[0]) : null; + } + + // Optionally keep this method if you need it elsewhere + async getAccountDataContent(account_id: string): Promise { + const result = await this.databaseConnection.query( + ` + SELECT data FROM indexer.accounts WHERE account_id = $1 + `, + [account_id], + ); + + return result.length ? result[0].data : null; + } + + async getPaginatedAccounts( + sortBy = 'transaction_count', + sortOrder: 'asc' | 'desc' = 'desc', + first?: number | null, + cursor?: string, + ) { + let cursorCondition = ''; + const queryParams: (number | string)[] = []; + + if (cursor !== undefined && cursor !== null) { + cursorCondition = `AND row_num ${sortOrder === 'asc' ? '<' : '>'} $${ + queryParams.length + 1 + }`; + queryParams.push(cursor); + } + + console.log(first); + const limitClause = + first !== undefined && first !== null + ? `LIMIT $${queryParams.length + 1}` + : ''; + + console.log(limitClause); + + if (first !== undefined && first !== null) { + queryParams.push(first); + } + + console.log(queryParams); + + const accountsData = await this.databaseConnection.query( + ` + WITH ranked_accounts AS ( + SELECT + ROW_NUMBER() OVER (ORDER BY ${sortBy} ${sortOrder}) AS row_num, + _id as id, + account_id, + balance, + transaction_count, + first_transaction_timestamp, + recent_transaction_timestamp + FROM + indexer.accounts + ) + SELECT * + FROM ranked_accounts + WHERE TRUE ${cursorCondition} + ORDER BY ${sortBy} ${sortOrder} + ${limitClause} + `, + queryParams, + ); + + console.log(accountsData); + + // Handle case where no data is returned + if (!accountsData.length) { + return { + nodes: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + endCursor: null, + startCursor: null, + }, + }; + } + + const startCursor = accountsData[0]?.row_num; + const endCursor = accountsData[accountsData.length - 1]?.row_num; + + const hasPreviousPage = startCursor > 1; + + const hasNextPage = + first !== undefined && first !== null && accountsData.length === first; + + return { + nodes: accountsData.map((account) => ({ + ...account, + id: account.id || null, // Ensure 'id' is either present or set to null + })), + pageInfo: { + hasNextPage, + hasPreviousPage, + endCursor, + startCursor, + }, + }; + } +} diff --git a/packages/graphql/src/schemas/fuelcore.graphql b/packages/graphql/src/schemas/fuelcore.graphql index f265af58a..90331296a 100644 --- a/packages/graphql/src/schemas/fuelcore.graphql +++ b/packages/graphql/src/schemas/fuelcore.graphql @@ -782,6 +782,7 @@ type Query { ): Transaction transactions(after: String, before: String, first: Int, last: Int): TransactionConnection! transactionsByOwner(after: String, before: String, first: Int, last: Int, owner: Address!): TransactionConnection! + paginatedAccounts(cursor: String, direction: String, first: Int, sortBy: String, sortOrder: String): PaginatedAccountConnection! } type Receipt { @@ -1022,4 +1023,16 @@ type VariableOutput { amount: U64! assetId: AssetId! to: Address! +} + +type PaginatedAccountConnection { + nodes: [AccountNode!]! + pageInfo: PageInfo! +} + +type AccountNode { + id: ID! + account_id: String! + balance: String! + transaction_count: Int! } \ No newline at end of file From d26998d2eeaf0ad5b806308f1d50a85396032f2d Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Fri, 27 Sep 2024 01:55:27 +0530 Subject: [PATCH 2/7] Fixes --- packages/graphql/src/application/uc/NewAddBlockRange.ts | 4 ++-- .../src/graphql/queries/provider/paginatedAccounts.graphql | 4 ++-- .../src/graphql/queries/sdk/paginatedAccounts.graphql | 4 ++-- packages/graphql/src/graphql/resolvers/AccountResolver.ts | 3 ++- packages/graphql/src/graphql/schemas/fuelcore.graphql | 2 +- packages/graphql/src/infra/dao/AccountDAO.ts | 5 ++--- packages/graphql/src/schemas/fuelcore.graphql | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/graphql/src/application/uc/NewAddBlockRange.ts b/packages/graphql/src/application/uc/NewAddBlockRange.ts index 168d08899..b873a1c9b 100644 --- a/packages/graphql/src/application/uc/NewAddBlockRange.ts +++ b/packages/graphql/src/application/uc/NewAddBlockRange.ts @@ -297,8 +297,8 @@ export default class NewAddBlockRange { while (hasNextPage) { const response = await client.sdk.balances({ filter: { owner }, - first: 1000, // Fetch 1000 records at a time - after, // Use the 'after' cursor for pagination + first: 1000, + after, }); if (response.data?.balances?.nodes) { diff --git a/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql b/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql index 6d87787e9..f5677d5c8 100644 --- a/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql +++ b/packages/graphql/src/graphql/queries/provider/paginatedAccounts.graphql @@ -1,5 +1,5 @@ -query paginatedAccounts($cursor: String, $direction: String, $sortBy: String, $sortOrder: String, $first: Int) { - paginatedAccounts(cursor: $cursor, direction: $direction, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { +query paginatedAccounts($cursor: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts(cursor: $cursor, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { nodes { id account_id diff --git a/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql b/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql index 6d87787e9..f5677d5c8 100644 --- a/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql +++ b/packages/graphql/src/graphql/queries/sdk/paginatedAccounts.graphql @@ -1,5 +1,5 @@ -query paginatedAccounts($cursor: String, $direction: String, $sortBy: String, $sortOrder: String, $first: Int) { - paginatedAccounts(cursor: $cursor, direction: $direction, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { +query paginatedAccounts($cursor: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts(cursor: $cursor, sortBy: $sortBy, sortOrder: $sortOrder, first: $first) { nodes { id account_id diff --git a/packages/graphql/src/graphql/resolvers/AccountResolver.ts b/packages/graphql/src/graphql/resolvers/AccountResolver.ts index 58d198610..663f852f2 100644 --- a/packages/graphql/src/graphql/resolvers/AccountResolver.ts +++ b/packages/graphql/src/graphql/resolvers/AccountResolver.ts @@ -1,3 +1,4 @@ +import { logger } from '~/core/Logger'; import type { GQLQueryPaginatedAccountsArgs } from '../../graphql/generated/sdk-provider'; import AccountDAO from '../../infra/dao/AccountDAO'; @@ -21,7 +22,7 @@ export class AccountResolver { const first = params.first; const cursor = params.cursor || undefined; - console.log(sortBy, sortOrder, first, cursor); + logger.info(`Logs: #${first}, #${cursor}, ${sortOrder}, ${sortBy}`); const accounts = await accountDAO.getPaginatedAccounts( sortBy, diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql index 6c0cbf157..01ff187a2 100644 --- a/packages/graphql/src/graphql/schemas/fuelcore.graphql +++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql @@ -783,7 +783,7 @@ type Query { transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection! tps: TPSConnection! getBlocksDashboard : BlocksDashboardConnection! - paginatedAccounts(cursor: String, direction: String, first: Int, sortBy: String, sortOrder: String): PaginatedAccountConnection! + paginatedAccounts(cursor: String, sortBy: String, sortOrder: String, first: Int): PaginatedAccountConnection! } type Receipt { diff --git a/packages/graphql/src/infra/dao/AccountDAO.ts b/packages/graphql/src/infra/dao/AccountDAO.ts index d78ec7946..fa37fa227 100644 --- a/packages/graphql/src/infra/dao/AccountDAO.ts +++ b/packages/graphql/src/infra/dao/AccountDAO.ts @@ -1,3 +1,4 @@ +import { logger } from '~/core/Logger'; import { AccountEntity } from '../../domain/Account/AccountEntity'; import { DatabaseConnection } from '../database/DatabaseConnection'; @@ -61,13 +62,11 @@ export default class AccountDAO { ? `LIMIT $${queryParams.length + 1}` : ''; - console.log(limitClause); - if (first !== undefined && first !== null) { queryParams.push(first); } - console.log(queryParams); + logger.info(`Logs: #${queryParams}`); const accountsData = await this.databaseConnection.query( ` diff --git a/packages/graphql/src/schemas/fuelcore.graphql b/packages/graphql/src/schemas/fuelcore.graphql index 90331296a..87dbb1b56 100644 --- a/packages/graphql/src/schemas/fuelcore.graphql +++ b/packages/graphql/src/schemas/fuelcore.graphql @@ -782,7 +782,7 @@ type Query { ): Transaction transactions(after: String, before: String, first: Int, last: Int): TransactionConnection! transactionsByOwner(after: String, before: String, first: Int, last: Int, owner: Address!): TransactionConnection! - paginatedAccounts(cursor: String, direction: String, first: Int, sortBy: String, sortOrder: String): PaginatedAccountConnection! + paginatedAccounts(cursor: String, sortBy: String, sortOrder: String, first: Int): PaginatedAccountConnection! } type Receipt { From 4524287f790faf23f0935c68fecb8b34a47ad876 Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Fri, 27 Sep 2024 02:22:36 +0530 Subject: [PATCH 3/7] Added in index.ts --- packages/graphql/src/graphql/resolvers/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/graphql/src/graphql/resolvers/index.ts b/packages/graphql/src/graphql/resolvers/index.ts index 3823cb6f5..8d7d7e668 100644 --- a/packages/graphql/src/graphql/resolvers/index.ts +++ b/packages/graphql/src/graphql/resolvers/index.ts @@ -1,4 +1,5 @@ import GraphQLAuth from '~/infra/auth/GraphQLAuth'; +import { AccountResolver } from './AccountResolver'; import { BalanceResolver } from './BalanceResolver'; import { BlockResolver } from './BlockResolver'; import { ChainResolver } from './ChainResolver'; @@ -17,6 +18,7 @@ const nodeResolver = NodeResolver.create(); const predicateResolver = PredicateResolver.create(); const searchResolver = SearchResolver.create(); const transactionResolver = TransactionResolver.create(); +const accountResolver = AccountResolver.create(); const publicResolver = PublicResolver.create(); @@ -31,6 +33,7 @@ export const resolvers = { ...predicateResolver.Query, ...searchResolver.Query, ...transactionResolver.Query, + ...accountResolver.Query, }), ...publicResolver.Query, }, From a1816f6ed8ff60e4871390bf9ec5648534d95333 Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Fri, 27 Sep 2024 02:27:44 +0530 Subject: [PATCH 4/7] Removed logging --- packages/graphql/src/graphql/resolvers/AccountResolver.ts | 3 --- packages/graphql/src/infra/dao/AccountDAO.ts | 6 ------ 2 files changed, 9 deletions(-) diff --git a/packages/graphql/src/graphql/resolvers/AccountResolver.ts b/packages/graphql/src/graphql/resolvers/AccountResolver.ts index 663f852f2..e111ea7b4 100644 --- a/packages/graphql/src/graphql/resolvers/AccountResolver.ts +++ b/packages/graphql/src/graphql/resolvers/AccountResolver.ts @@ -1,4 +1,3 @@ -import { logger } from '~/core/Logger'; import type { GQLQueryPaginatedAccountsArgs } from '../../graphql/generated/sdk-provider'; import AccountDAO from '../../infra/dao/AccountDAO'; @@ -22,8 +21,6 @@ export class AccountResolver { const first = params.first; const cursor = params.cursor || undefined; - logger.info(`Logs: #${first}, #${cursor}, ${sortOrder}, ${sortBy}`); - const accounts = await accountDAO.getPaginatedAccounts( sortBy, sortOrder, diff --git a/packages/graphql/src/infra/dao/AccountDAO.ts b/packages/graphql/src/infra/dao/AccountDAO.ts index fa37fa227..271a40a40 100644 --- a/packages/graphql/src/infra/dao/AccountDAO.ts +++ b/packages/graphql/src/infra/dao/AccountDAO.ts @@ -1,4 +1,3 @@ -import { logger } from '~/core/Logger'; import { AccountEntity } from '../../domain/Account/AccountEntity'; import { DatabaseConnection } from '../database/DatabaseConnection'; @@ -56,7 +55,6 @@ export default class AccountDAO { queryParams.push(cursor); } - console.log(first); const limitClause = first !== undefined && first !== null ? `LIMIT $${queryParams.length + 1}` @@ -66,8 +64,6 @@ export default class AccountDAO { queryParams.push(first); } - logger.info(`Logs: #${queryParams}`); - const accountsData = await this.databaseConnection.query( ` WITH ranked_accounts AS ( @@ -91,8 +87,6 @@ export default class AccountDAO { queryParams, ); - console.log(accountsData); - // Handle case where no data is returned if (!accountsData.length) { return { From b574805d31791714c80cfe69f7011ef56e74067a Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Fri, 27 Sep 2024 03:18:10 +0530 Subject: [PATCH 5/7] getTopAccounts.ts --- .../systems/Account/actions/getTopAccounts.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/app-explorer/src/systems/Account/actions/getTopAccounts.ts diff --git a/packages/app-explorer/src/systems/Account/actions/getTopAccounts.ts b/packages/app-explorer/src/systems/Account/actions/getTopAccounts.ts new file mode 100644 index 000000000..5e4f30a9d --- /dev/null +++ b/packages/app-explorer/src/systems/Account/actions/getTopAccounts.ts @@ -0,0 +1,72 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '~/systems/Core/utils/act-server'; +import { sdk } from '~/systems/Core/utils/sdk'; + +// Schema to validate inputs +const schema = z.object({ + sortBy: z.string().optional(), // Sorting criteria (balance, transaction_count, etc.) + sortOrder: z.string().optional(), // asc or desc + first: z.number().optional().nullable(), // Number of accounts to fetch, can be null + cursor: z.string().optional().nullable(), // Pagination cursor +}); + +// Common function to fetch top accounts +async function fetchTopAccounts( + cursor?: string | null, + sortBy = 'transaction_count', // Default to transaction_count + sortOrder: 'asc' | 'desc' = 'desc', // Default to descending order + first: number | null = null, // Allow null to fetch all records if no limit is provided +) { + const queryParams: Record = { + cursor, + first, + sortBy, + sortOrder, + }; + + console.log('Params Here', queryParams); + + const data = await sdk.paginatedAccounts(queryParams); + + if (!data.data.paginatedAccounts.nodes.length) { + return { + accounts: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }; + } + + const { nodes, pageInfo } = data.data.paginatedAccounts; + + const accounts = nodes.map((account: any) => ({ + id: account.id, + account_id: account.account_id, + balance: account.balance, + transaction_count: account.transaction_count, + })); + + return { + accounts, + pageInfo: { + hasNextPage: pageInfo.hasNextPage, + hasPreviousPage: pageInfo.hasPreviousPage, + startCursor: pageInfo.startCursor, + endCursor: pageInfo.endCursor, + }, + }; +} + +export const getTopAccounts = act(schema, async (params) => { + const sortBy = params.sortBy || 'transaction_count'; + const sortOrder = (params.sortOrder || 'desc') as 'asc' | 'desc'; + const first = params.first === null ? null : params.first; + const cursor = params.cursor || null; + + return fetchTopAccounts(cursor, sortBy, sortOrder, first); +}); From 29d1879b1e11d8a8fd0f94136b19742417400f9d Mon Sep 17 00:00:00 2001 From: raghukapur9 Date: Fri, 27 Sep 2024 13:27:33 -0400 Subject: [PATCH 6/7] added generated account files --- .../graphql/src/graphql/generated/mocks.ts | 21 ++++++- .../src/graphql/generated/sdk-provider.ts | 60 +++++++++++++++++++ packages/graphql/src/graphql/generated/sdk.ts | 60 +++++++++++++++++++ .../src/graphql/schemas/explorer.graphql | 13 ++++ .../src/graphql/schemas/fuelcore.graphql | 13 ---- 5 files changed, 153 insertions(+), 14 deletions(-) diff --git a/packages/graphql/src/graphql/generated/mocks.ts b/packages/graphql/src/graphql/generated/mocks.ts index a2c6148db..d2e026179 100644 --- a/packages/graphql/src/graphql/generated/mocks.ts +++ b/packages/graphql/src/graphql/generated/mocks.ts @@ -1,4 +1,14 @@ -import type { GQLAsset, GQLAssetNetworkEthereum, GQLAssetNetworkFuel, GQLBalance, GQLBalanceConnection, GQLBalanceEdge, GQLBalanceFilterInput, GQLBlock, GQLBlockConnection, GQLBlockEdge, GQLBlocksDashboard, GQLBlocksDashboardConnection, GQLBreakpoint, GQLChainInfo, GQLChangeOutput, GQLCoin, GQLCoinConnection, GQLCoinEdge, GQLCoinFilterInput, GQLCoinOutput, GQLConsensusParameters, GQLConsensusParametersPurpose, GQLContract, GQLContractBalance, GQLContractBalanceConnection, GQLContractBalanceEdge, GQLContractBalanceFilterInput, GQLContractConnection, GQLContractCreated, GQLContractOutput, GQLContractParameters, GQLDryRunFailureStatus, GQLDryRunSuccessStatus, GQLDryRunTransactionExecutionStatus, GQLEstimateGasPrice, GQLExcludeInput, GQLFailureStatus, GQLFeeParameters, GQLGasCosts, GQLGenesis, GQLGroupedInputCoin, GQLGroupedInputContract, GQLGroupedInputMessage, GQLGroupedOutputChanged, GQLGroupedOutputCoin, GQLGroupedOutputContractCreated, GQLHeader, GQLHeavyOperation, GQLInputCoin, GQLInputContract, GQLInputMessage, GQLLatestGasPrice, GQLLightOperation, GQLMerkleProof, GQLMessage, GQLMessageCoin, GQLMessageConnection, GQLMessageEdge, GQLMessageProof, GQLMessageStatus, GQLMutation, GQLNodeInfo, GQLOperation, GQLOperationReceipt, GQLOperationsFilterInput, GQLOutputBreakpoint, GQLPageInfo, GQLParsedTime, GQLPeerInfo, GQLPoAConsensus, GQLPolicies, GQLPredicateItem, GQLPredicateParameters, GQLProgramState, GQLQuery, GQLReceipt, GQLRelayedTransactionFailed, GQLRunResult, GQLScriptParameters, GQLSearchAccount, GQLSearchBlock, GQLSearchContract, GQLSearchResult, GQLSearchTransaction, GQLSpendQueryElementInput, GQLSqueezedOutStatus, GQLStateTransitionPurpose, GQLSubmittedStatus, GQLSubscription, GQLSuccessStatus, GQLTps, GQLTpsConnection, GQLTransaction, GQLTransactionConnection, GQLTransactionEdge, GQLTransactionGasCosts, GQLTxParameters, GQLUtxoItem, GQLVariableOutput, GQLBlockVersion, GQLConsensusParametersVersion, GQLContractParametersVersion, GQLFeeParametersVersion, GQLGasCostsVersion, GQLGroupedInputType, GQLGroupedOutputType, GQLHeaderVersion, GQLMessageState, GQLOperationType, GQLPredicateParametersVersion, GQLReceiptType, GQLReturnType, GQLRunState, GQLScriptParametersVersion, GQLTxParametersVersion } from './sdk'; +import type { GQLAccountNode, GQLAsset, GQLAssetNetworkEthereum, GQLAssetNetworkFuel, GQLBalance, GQLBalanceConnection, GQLBalanceEdge, GQLBalanceFilterInput, GQLBlock, GQLBlockConnection, GQLBlockEdge, GQLBlocksDashboard, GQLBlocksDashboardConnection, GQLBreakpoint, GQLChainInfo, GQLChangeOutput, GQLCoin, GQLCoinConnection, GQLCoinEdge, GQLCoinFilterInput, GQLCoinOutput, GQLConsensusParameters, GQLConsensusParametersPurpose, GQLContract, GQLContractBalance, GQLContractBalanceConnection, GQLContractBalanceEdge, GQLContractBalanceFilterInput, GQLContractConnection, GQLContractCreated, GQLContractOutput, GQLContractParameters, GQLDryRunFailureStatus, GQLDryRunSuccessStatus, GQLDryRunTransactionExecutionStatus, GQLEstimateGasPrice, GQLExcludeInput, GQLFailureStatus, GQLFeeParameters, GQLGasCosts, GQLGenesis, GQLGroupedInputCoin, GQLGroupedInputContract, GQLGroupedInputMessage, GQLGroupedOutputChanged, GQLGroupedOutputCoin, GQLGroupedOutputContractCreated, GQLHeader, GQLHeavyOperation, GQLInputCoin, GQLInputContract, GQLInputMessage, GQLLatestGasPrice, GQLLightOperation, GQLMerkleProof, GQLMessage, GQLMessageCoin, GQLMessageConnection, GQLMessageEdge, GQLMessageProof, GQLMessageStatus, GQLMutation, GQLNodeInfo, GQLOperation, GQLOperationReceipt, GQLOperationsFilterInput, GQLOutputBreakpoint, GQLPageInfo, GQLPaginatedAccountConnection, GQLParsedTime, GQLPeerInfo, GQLPoAConsensus, GQLPolicies, GQLPredicateItem, GQLPredicateParameters, GQLProgramState, GQLQuery, GQLReceipt, GQLRelayedTransactionFailed, GQLRunResult, GQLScriptParameters, GQLSearchAccount, GQLSearchBlock, GQLSearchContract, GQLSearchResult, GQLSearchTransaction, GQLSpendQueryElementInput, GQLSqueezedOutStatus, GQLStateTransitionPurpose, GQLSubmittedStatus, GQLSubscription, GQLSuccessStatus, GQLTps, GQLTpsConnection, GQLTransaction, GQLTransactionConnection, GQLTransactionEdge, GQLTransactionGasCosts, GQLTxParameters, GQLUtxoItem, GQLVariableOutput, GQLBlockVersion, GQLConsensusParametersVersion, GQLContractParametersVersion, GQLFeeParametersVersion, GQLGasCostsVersion, GQLGroupedInputType, GQLGroupedOutputType, GQLHeaderVersion, GQLMessageState, GQLOperationType, GQLPredicateParametersVersion, GQLReceiptType, GQLReturnType, GQLRunState, GQLScriptParametersVersion, GQLTxParametersVersion } from './sdk'; + +export const anAccountNode = (overrides?: Partial): { __typename: 'AccountNode' } & GQLAccountNode => { + return { + __typename: 'AccountNode', + account_id: overrides && overrides.hasOwnProperty('account_id') ? overrides.account_id! : 'illo', + balance: overrides && overrides.hasOwnProperty('balance') ? overrides.balance! : 'dolore', + id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : '303700b4-6ccd-4b9c-bbfb-b798703382be', + transaction_count: overrides && overrides.hasOwnProperty('transaction_count') ? overrides.transaction_count! : 8132, + }; +}; export const anAsset = (overrides?: Partial): { __typename: 'Asset' } & GQLAsset => { return { @@ -775,6 +785,14 @@ export const aPageInfo = (overrides?: Partial): { __typename: 'Page }; }; +export const aPaginatedAccountConnection = (overrides?: Partial): { __typename: 'PaginatedAccountConnection' } & GQLPaginatedAccountConnection => { + return { + __typename: 'PaginatedAccountConnection', + nodes: overrides && overrides.hasOwnProperty('nodes') ? overrides.nodes! : [anAccountNode()], + pageInfo: overrides && overrides.hasOwnProperty('pageInfo') ? overrides.pageInfo! : aPageInfo(), + }; +}; + export const aParsedTime = (overrides?: Partial): { __typename: 'ParsedTime' } & GQLParsedTime => { return { __typename: 'ParsedTime', @@ -868,6 +886,7 @@ export const aQuery = (overrides?: Partial): { __typename: 'Query' } & messageStatus: overrides && overrides.hasOwnProperty('messageStatus') ? overrides.messageStatus! : aMessageStatus(), messages: overrides && overrides.hasOwnProperty('messages') ? overrides.messages! : aMessageConnection(), nodeInfo: overrides && overrides.hasOwnProperty('nodeInfo') ? overrides.nodeInfo! : aNodeInfo(), + paginatedAccounts: overrides && overrides.hasOwnProperty('paginatedAccounts') ? overrides.paginatedAccounts! : aPaginatedAccountConnection(), predicate: overrides && overrides.hasOwnProperty('predicate') ? overrides.predicate! : aPredicateItem(), register: overrides && overrides.hasOwnProperty('register') ? overrides.register! : '0x3', relayedTransactionStatus: overrides && overrides.hasOwnProperty('relayedTransactionStatus') ? overrides.relayedTransactionStatus! : aRelayedTransactionFailed(), diff --git a/packages/graphql/src/graphql/generated/sdk-provider.ts b/packages/graphql/src/graphql/generated/sdk-provider.ts index 1e1659d6c..d7cc8cb38 100644 --- a/packages/graphql/src/graphql/generated/sdk-provider.ts +++ b/packages/graphql/src/graphql/generated/sdk-provider.ts @@ -35,6 +35,14 @@ export type Scalars = { UtxoId: { input: string; output: string; } }; +export type GQLAccountNode = { + __typename: 'AccountNode'; + account_id: Scalars['String']['output']; + balance: Scalars['String']['output']; + id: Scalars['ID']['output']; + transaction_count: Scalars['Int']['output']; +}; + export type GQLAsset = { __typename: 'Asset'; assetId?: Maybe; @@ -902,6 +910,12 @@ export type GQLPageInfo = { startCursor?: Maybe; }; +export type GQLPaginatedAccountConnection = { + __typename: 'PaginatedAccountConnection'; + nodes: Array; + pageInfo: GQLPageInfo; +}; + export type GQLParsedTime = { __typename: 'ParsedTime'; fromNow?: Maybe; @@ -1008,6 +1022,7 @@ export type GQLQuery = { messageStatus: GQLMessageStatus; messages: GQLMessageConnection; nodeInfo: GQLNodeInfo; + paginatedAccounts: GQLPaginatedAccountConnection; predicate?: Maybe; /** Read register value by index. */ register: Scalars['U64']['output']; @@ -1148,6 +1163,14 @@ export type GQLQueryMessagesArgs = { }; +export type GQLQueryPaginatedAccountsArgs = { + cursor?: InputMaybe; + first?: InputMaybe; + sortBy?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type GQLQueryPredicateArgs = { address: Scalars['String']['input']; }; @@ -1588,6 +1611,16 @@ export type GQLNodeInfoQueryVariables = Exact<{ [key: string]: never; }>; export type GQLNodeInfoQuery = { __typename: 'Query', nodeInfo: { __typename: 'NodeInfo', maxDepth: string, maxTx: string, nodeVersion: string, utxoValidation: boolean, vmBacktrace: boolean, peers: Array<{ __typename: 'PeerInfo', addresses: Array, appScore: number, blockHeight?: string | null, clientVersion?: string | null, id: string, lastHeartbeatMs: string }> } }; +export type GQLPaginatedAccountsQueryVariables = Exact<{ + cursor?: InputMaybe; + sortBy?: InputMaybe; + sortOrder?: InputMaybe; + first?: InputMaybe; +}>; + + +export type GQLPaginatedAccountsQuery = { __typename: 'Query', paginatedAccounts: { __typename: 'PaginatedAccountConnection', nodes: Array<{ __typename: 'AccountNode', id: string, account_id: string, balance: string, transaction_count: number }>, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } }; + export type GQLTpsQueryVariables = Exact<{ [key: string]: never; }>; @@ -3321,6 +3354,29 @@ export const NodeInfoDocument = gql` } } `; +export const PaginatedAccountsDocument = gql` + query paginatedAccounts($cursor: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts( + cursor: $cursor + sortBy: $sortBy + sortOrder: $sortOrder + first: $first + ) { + nodes { + id + account_id + balance + transaction_count + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} + `; export const TpsDocument = gql` query tps { tps { @@ -3347,6 +3403,7 @@ const ContractDocumentString = print(ContractDocument); const ContractBalanceDocumentString = print(ContractBalanceDocument); const ContractBalancesDocumentString = print(ContractBalancesDocument); const NodeInfoDocumentString = print(NodeInfoDocument); +const PaginatedAccountsDocumentString = print(PaginatedAccountsDocument); const TpsDocumentString = print(TpsDocument); export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) { return { @@ -3377,6 +3434,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = nodeInfo(variables?: GQLNodeInfoQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLNodeInfoQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { return withWrapper((wrappedRequestHeaders) => client.rawRequest(NodeInfoDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'nodeInfo', 'query', variables); }, + paginatedAccounts(variables?: GQLPaginatedAccountsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLPaginatedAccountsQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { + return withWrapper((wrappedRequestHeaders) => client.rawRequest(PaginatedAccountsDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'paginatedAccounts', 'query', variables); + }, tps(variables?: GQLTpsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLTpsQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { return withWrapper((wrappedRequestHeaders) => client.rawRequest(TpsDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'tps', 'query', variables); } diff --git a/packages/graphql/src/graphql/generated/sdk.ts b/packages/graphql/src/graphql/generated/sdk.ts index 4048cc2cd..0e99d2b83 100644 --- a/packages/graphql/src/graphql/generated/sdk.ts +++ b/packages/graphql/src/graphql/generated/sdk.ts @@ -35,6 +35,14 @@ export type Scalars = { UtxoId: { input: string; output: string; } }; +export type GQLAccountNode = { + __typename: 'AccountNode'; + account_id: Scalars['String']['output']; + balance: Scalars['String']['output']; + id: Scalars['ID']['output']; + transaction_count: Scalars['Int']['output']; +}; + export type GQLAsset = { __typename: 'Asset'; assetId?: Maybe; @@ -902,6 +910,12 @@ export type GQLPageInfo = { startCursor?: Maybe; }; +export type GQLPaginatedAccountConnection = { + __typename: 'PaginatedAccountConnection'; + nodes: Array; + pageInfo: GQLPageInfo; +}; + export type GQLParsedTime = { __typename: 'ParsedTime'; fromNow?: Maybe; @@ -1008,6 +1022,7 @@ export type GQLQuery = { messageStatus: GQLMessageStatus; messages: GQLMessageConnection; nodeInfo: GQLNodeInfo; + paginatedAccounts: GQLPaginatedAccountConnection; predicate?: Maybe; /** Read register value by index. */ register: Scalars['U64']['output']; @@ -1148,6 +1163,14 @@ export type GQLQueryMessagesArgs = { }; +export type GQLQueryPaginatedAccountsArgs = { + cursor?: InputMaybe; + first?: InputMaybe; + sortBy?: InputMaybe; + sortOrder?: InputMaybe; +}; + + export type GQLQueryPredicateArgs = { address: Scalars['String']['input']; }; @@ -1589,6 +1612,16 @@ export type GQLGetBlocksDashboardQueryVariables = Exact<{ [key: string]: never; export type GQLGetBlocksDashboardQuery = { __typename: 'Query', getBlocksDashboard: { __typename: 'BlocksDashboardConnection', nodes: Array<{ __typename: 'BlocksDashboard', timestamp: string, gasUsed: string, blockNo: string, producer?: string | null }> } }; +export type GQLPaginatedAccountsQueryVariables = Exact<{ + cursor?: InputMaybe; + sortBy?: InputMaybe; + sortOrder?: InputMaybe; + first?: InputMaybe; +}>; + + +export type GQLPaginatedAccountsQuery = { __typename: 'Query', paginatedAccounts: { __typename: 'PaginatedAccountConnection', nodes: Array<{ __typename: 'AccountNode', id: string, account_id: string, balance: string, transaction_count: number }>, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } }; + export type GQLPredicateQueryVariables = Exact<{ address: Scalars['String']['input']; }>; @@ -3492,6 +3525,29 @@ export const GetBlocksDashboardDocument = gql` } } `; +export const PaginatedAccountsDocument = gql` + query paginatedAccounts($cursor: String, $sortBy: String, $sortOrder: String, $first: Int) { + paginatedAccounts( + cursor: $cursor + sortBy: $sortBy + sortOrder: $sortOrder + first: $first + ) { + nodes { + id + account_id + balance + transaction_count + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} + `; export const PredicateDocument = gql` query predicate($address: String!) { predicate(address: $address) { @@ -3612,6 +3668,7 @@ const CoinsDocumentString = print(CoinsDocument); const ContractDocumentString = print(ContractDocument); const ContractBalancesDocumentString = print(ContractBalancesDocument); const GetBlocksDashboardDocumentString = print(GetBlocksDashboardDocument); +const PaginatedAccountsDocumentString = print(PaginatedAccountsDocument); const PredicateDocumentString = print(PredicateDocument); const RecentTransactionsDocumentString = print(RecentTransactionsDocument); const SearchDocumentString = print(SearchDocument); @@ -3648,6 +3705,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getBlocksDashboard(variables?: GQLGetBlocksDashboardQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLGetBlocksDashboardQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { return withWrapper((wrappedRequestHeaders) => client.rawRequest(GetBlocksDashboardDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getBlocksDashboard', 'query', variables); }, + paginatedAccounts(variables?: GQLPaginatedAccountsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLPaginatedAccountsQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { + return withWrapper((wrappedRequestHeaders) => client.rawRequest(PaginatedAccountsDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'paginatedAccounts', 'query', variables); + }, predicate(variables: GQLPredicateQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<{ data: GQLPredicateQuery; errors?: GraphQLError[]; extensions?: any; headers: Headers; status: number; }> { return withWrapper((wrappedRequestHeaders) => client.rawRequest(PredicateDocumentString, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'predicate', 'query', variables); }, diff --git a/packages/graphql/src/graphql/schemas/explorer.graphql b/packages/graphql/src/graphql/schemas/explorer.graphql index d80fe8b1f..749fdbed6 100644 --- a/packages/graphql/src/graphql/schemas/explorer.graphql +++ b/packages/graphql/src/graphql/schemas/explorer.graphql @@ -226,6 +226,7 @@ type Query { tps: TPSConnection! getBlocksDashboard: BlocksDashboardConnection! asset (assetId: String!): Asset + paginatedAccounts(cursor: String, sortBy: String, sortOrder: String, first: Int): PaginatedAccountConnection! } type TPS { @@ -279,3 +280,15 @@ type AssetNetworkEthereum { decimals: U64 address: String } + +type PaginatedAccountConnection { + nodes: [AccountNode!]! + pageInfo: PageInfo! +} + +type AccountNode { + id: ID! + account_id: String! + balance: String! + transaction_count: Int! +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql index 01ff187a2..5e880dd0d 100644 --- a/packages/graphql/src/graphql/schemas/fuelcore.graphql +++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql @@ -783,7 +783,6 @@ type Query { transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection! tps: TPSConnection! getBlocksDashboard : BlocksDashboardConnection! - paginatedAccounts(cursor: String, sortBy: String, sortOrder: String, first: Int): PaginatedAccountConnection! } type Receipt { @@ -1046,16 +1045,4 @@ type BlocksDashboard { type BlocksDashboardConnection { nodes: [BlocksDashboard!]! -} - -type PaginatedAccountConnection { - nodes: [AccountNode!]! - pageInfo: PageInfo! -} - -type AccountNode { - id: ID! - account_id: String! - balance: String! - transaction_count: Int! } \ No newline at end of file From ec037d3ea6484eee1106a0ececfdbf7e97d563c8 Mon Sep 17 00:00:00 2001 From: shivam-25 Date: Mon, 30 Sep 2024 01:10:31 +0530 Subject: [PATCH 7/7] Account Statistics optimized --- .../src/systems/Statistics/accounts.ts | 39 ++++ packages/graphql/database/4.sql | 9 + packages/graphql/src/core/Date.ts | 12 + .../cumulativeAccountStatistics.graphql | 8 + .../provider/dailyActiveAccounts.graphql | 8 + .../provider/newAccountStatistics.graphql | 8 + .../sdk/cumulativeAccountStatistics.graphql | 8 + .../queries/sdk/dailyActiveAccounts.graphql | 8 + .../queries/sdk/newAccountStatistics.graphql | 8 + .../src/graphql/resolvers/AccountResolver.ts | 87 ++++++- .../src/graphql/schemas/explorer.graphql | 30 +++ .../src/graphql/schemas/fuelcore.graphql | 30 +++ packages/graphql/src/infra/dao/AccountDAO.ts | 213 +++++++++++++++++- packages/graphql/src/infra/dao/utils.ts | 30 +++ .../infra/statJobs/accountStatisticsJob.ts | 49 ++++ 15 files changed, 539 insertions(+), 8 deletions(-) create mode 100644 packages/app-explorer/src/systems/Statistics/accounts.ts create mode 100644 packages/graphql/database/4.sql create mode 100644 packages/graphql/src/graphql/queries/provider/cumulativeAccountStatistics.graphql create mode 100644 packages/graphql/src/graphql/queries/provider/dailyActiveAccounts.graphql create mode 100644 packages/graphql/src/graphql/queries/provider/newAccountStatistics.graphql create mode 100644 packages/graphql/src/graphql/queries/sdk/cumulativeAccountStatistics.graphql create mode 100644 packages/graphql/src/graphql/queries/sdk/dailyActiveAccounts.graphql create mode 100644 packages/graphql/src/graphql/queries/sdk/newAccountStatistics.graphql create mode 100644 packages/graphql/src/infra/statJobs/accountStatisticsJob.ts diff --git a/packages/app-explorer/src/systems/Statistics/accounts.ts b/packages/app-explorer/src/systems/Statistics/accounts.ts new file mode 100644 index 000000000..6d8076f43 --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/accounts.ts @@ -0,0 +1,39 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; + +// Schema for timeFilter validation +const schema = z.object({ + timeFilter: z.string().optional().nullable(), +}); + +// Utility function to provide a default value for timeFilter +const ensureTimeFilter = (timeFilter?: string | null): string => { + return timeFilter ?? 'default'; // 'default' can be replaced with an appropriate default value +}; + +// Function to get New Account Statistics +export const getNewAccountStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: ensureTimeFilter(timeFilter) }; + const data = await sdk.newAccountStatistics(params); + return data; +}); + +// Function to get Daily Active Account Statistics +export const getDailyActiveAccountStats = act( + schema, + async ({ timeFilter }) => { + const params = { timeFilter: ensureTimeFilter(timeFilter) }; + const data = await sdk.dailyActiveAccountStatistics(params); + return data; + }, +); + +// Function to get Cumulative Account Statistics +export const getCumulativeAccountStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: ensureTimeFilter(timeFilter) }; + const data = await sdk.cumulativeAccountStatistics(params); + return data; +}); diff --git a/packages/graphql/database/4.sql b/packages/graphql/database/4.sql new file mode 100644 index 000000000..1722c04e7 --- /dev/null +++ b/packages/graphql/database/4.sql @@ -0,0 +1,9 @@ +CREATE TABLE indexer.account_statistics ( + id SERIAL PRIMARY KEY, + timestamp TIMESTAMP NOT NULL, + new_accounts INTEGER NOT NULL, + active_accounts INTEGER NOT NULL, + cumulative_accounts INTEGER NOT NULL +); +CREATE UNIQUE INDEX ON indexer.account_statistics(id); +CREATE INDEX ON indexer.account_statistics(timestamp); \ No newline at end of file diff --git a/packages/graphql/src/core/Date.ts b/packages/graphql/src/core/Date.ts index 9995818df..47ed78b79 100644 --- a/packages/graphql/src/core/Date.ts +++ b/packages/graphql/src/core/Date.ts @@ -13,4 +13,16 @@ export class DateHelper { static dateToTai64(date: Date) { return TAI64.fromUnix(Math.floor(date.getTime() / 1000)).toString(10); } + + static addHours(date: string | Date, hours: number): string { + const dateObj = typeof date === 'string' ? new Date(date) : date; + const newDate = new Date(dateObj.getTime() + hours * 60 * 60 * 1000); + return newDate.toISOString(); + } + + static floorToHour(date: string | Date): string { + const flooredDate = typeof date === 'string' ? new Date(date) : date; + flooredDate.setMinutes(0, 0, 0); // Set minutes, seconds, and milliseconds to 0 + return flooredDate.toISOString(); + } } diff --git a/packages/graphql/src/graphql/queries/provider/cumulativeAccountStatistics.graphql b/packages/graphql/src/graphql/queries/provider/cumulativeAccountStatistics.graphql new file mode 100644 index 000000000..80bbdfb87 --- /dev/null +++ b/packages/graphql/src/graphql/queries/provider/cumulativeAccountStatistics.graphql @@ -0,0 +1,8 @@ +query cumulativeAccountStatistics($timeFilter: String!) { + cumulativeAccountStatistics(timeFilter: $timeFilter) { + nodes { + cumulativeAccounts + timestamp + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/queries/provider/dailyActiveAccounts.graphql b/packages/graphql/src/graphql/queries/provider/dailyActiveAccounts.graphql new file mode 100644 index 000000000..076519192 --- /dev/null +++ b/packages/graphql/src/graphql/queries/provider/dailyActiveAccounts.graphql @@ -0,0 +1,8 @@ +query dailyActiveAccountStatistics($timeFilter: String!) { + dailyActiveAccountStatistics(timeFilter: $timeFilter) { + nodes { + activeAccounts + timestamp + } + } +} diff --git a/packages/graphql/src/graphql/queries/provider/newAccountStatistics.graphql b/packages/graphql/src/graphql/queries/provider/newAccountStatistics.graphql new file mode 100644 index 000000000..0440e6725 --- /dev/null +++ b/packages/graphql/src/graphql/queries/provider/newAccountStatistics.graphql @@ -0,0 +1,8 @@ +query newAccountStatistics($timeFilter: String!) { + newAccountStatistics(timeFilter: $timeFilter) { + nodes { + newAccounts + timestamp + } + } +} diff --git a/packages/graphql/src/graphql/queries/sdk/cumulativeAccountStatistics.graphql b/packages/graphql/src/graphql/queries/sdk/cumulativeAccountStatistics.graphql new file mode 100644 index 000000000..89391d09e --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/cumulativeAccountStatistics.graphql @@ -0,0 +1,8 @@ +query cumulativeAccountStatistics($timeFilter: String!) { + cumulativeAccountStatistics(timeFilter: $timeFilter) { + nodes { + cumulativeAccounts + timestamp + } + } +} diff --git a/packages/graphql/src/graphql/queries/sdk/dailyActiveAccounts.graphql b/packages/graphql/src/graphql/queries/sdk/dailyActiveAccounts.graphql new file mode 100644 index 000000000..076519192 --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/dailyActiveAccounts.graphql @@ -0,0 +1,8 @@ +query dailyActiveAccountStatistics($timeFilter: String!) { + dailyActiveAccountStatistics(timeFilter: $timeFilter) { + nodes { + activeAccounts + timestamp + } + } +} diff --git a/packages/graphql/src/graphql/queries/sdk/newAccountStatistics.graphql b/packages/graphql/src/graphql/queries/sdk/newAccountStatistics.graphql new file mode 100644 index 000000000..0440e6725 --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/newAccountStatistics.graphql @@ -0,0 +1,8 @@ +query newAccountStatistics($timeFilter: String!) { + newAccountStatistics(timeFilter: $timeFilter) { + nodes { + newAccounts + timestamp + } + } +} diff --git a/packages/graphql/src/graphql/resolvers/AccountResolver.ts b/packages/graphql/src/graphql/resolvers/AccountResolver.ts index e111ea7b4..ebbb5ddb4 100644 --- a/packages/graphql/src/graphql/resolvers/AccountResolver.ts +++ b/packages/graphql/src/graphql/resolvers/AccountResolver.ts @@ -1,12 +1,36 @@ -import type { GQLQueryPaginatedAccountsArgs } from '../../graphql/generated/sdk-provider'; +import type { + GQLQueryCumulativeAccountStatisticsArgs, + GQLQueryDailyActiveAccountStatisticsArgs, + GQLQueryNewAccountStatisticsArgs, + GQLQueryPaginatedAccountsArgs, +} from '../../graphql/generated/sdk-provider'; import AccountDAO from '../../infra/dao/AccountDAO'; +// Define types for statistics +type NewAccountStat = { + time: string; + new_accounts: number; +}; + +type DailyActiveAccountStat = { + time: string; + active_accounts: number; +}; + +type CumulativeAccountStat = { + time: string; + cumulative_accounts: number; +}; + export class AccountResolver { static create() { const resolvers = new AccountResolver(); return { Query: { paginatedAccounts: resolvers.paginatedAccounts, + newAccountStatistics: resolvers.newAccountStatistics, + dailyActiveAccountStatistics: resolvers.dailyActiveAccountStatistics, + cumulativeAccountStatistics: resolvers.cumulativeAccountStatistics, }, }; } @@ -30,4 +54,65 @@ export class AccountResolver { return accounts; } + + async newAccountStatistics(_: any, params: GQLQueryNewAccountStatisticsArgs) { + const accountDAO = new AccountDAO(); + + // Use the time filter parameter to fetch new account statistics + const timeFilter = params.timeFilter; + const newAccountStatsResponse = + await accountDAO.getNewAccountStatistics(timeFilter); + + // Extract nodes from the response and map them + const nodes = newAccountStatsResponse.nodes.map((stat: NewAccountStat) => ({ + timestamp: stat.time, + newAccounts: stat.new_accounts, + })); + + return { nodes }; + } + + async dailyActiveAccountStatistics( + _: any, + params: GQLQueryDailyActiveAccountStatisticsArgs, + ) { + const accountDAO = new AccountDAO(); + + // Use the time filter parameter to fetch daily active account statistics + const timeFilter = params.timeFilter; + const activeAccountStatsResponse = + await accountDAO.getDailyActiveAccountStatistics(timeFilter); + + // Extract nodes from the response and map them + const nodes = activeAccountStatsResponse.nodes.map( + (stat: DailyActiveAccountStat) => ({ + timestamp: stat.time, + activeAccounts: stat.active_accounts, + }), + ); + + return { nodes }; + } + + async cumulativeAccountStatistics( + _: any, + params: GQLQueryCumulativeAccountStatisticsArgs, + ) { + const accountDAO = new AccountDAO(); + + // Use the time filter parameter to fetch cumulative account statistics + const timeFilter = params.timeFilter; + const cumulativeStatisticsResponse = + await accountDAO.getCumulativeAccountStatistics(timeFilter); + + // Extract nodes from the response and map them + const nodes = cumulativeStatisticsResponse.nodes.map( + (stat: CumulativeAccountStat) => ({ + timestamp: stat.time, + cumulativeAccounts: stat.cumulative_accounts, + }), + ); + + return { nodes }; + } } diff --git a/packages/graphql/src/graphql/schemas/explorer.graphql b/packages/graphql/src/graphql/schemas/explorer.graphql index 749fdbed6..2672a76f0 100644 --- a/packages/graphql/src/graphql/schemas/explorer.graphql +++ b/packages/graphql/src/graphql/schemas/explorer.graphql @@ -227,6 +227,9 @@ type Query { getBlocksDashboard: BlocksDashboardConnection! asset (assetId: String!): Asset paginatedAccounts(cursor: String, sortBy: String, sortOrder: String, first: Int): PaginatedAccountConnection! + newAccountStatistics(timeFilter: String!): NewAccountStatisticsResponse! + dailyActiveAccountStatistics(timeFilter: String!): DailyActiveAccountStatisticsResponse! + cumulativeAccountStatistics(timeFilter: String!): CumulativeAccountStatisticsResponse! } type TPS { @@ -291,4 +294,31 @@ type AccountNode { account_id: String! balance: String! transaction_count: Int! +} + +type NewAccountStatistics { + timestamp: String! + newAccounts: Int! +} + +type DailyActiveAccountStatistics { + timestamp: String! + activeAccounts: Int! +} + +type CumulativeAccountStatistics { + timestamp: String! + cumulativeAccounts: Int! +} + +type NewAccountStatisticsResponse { + nodes: [NewAccountStatistics!]! +} + +type DailyActiveAccountStatisticsResponse { + nodes: [DailyActiveAccountStatistics!]! +} + +type CumulativeAccountStatisticsResponse { + nodes: [CumulativeAccountStatistics!]! } \ No newline at end of file diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql index 5e880dd0d..7afe54890 100644 --- a/packages/graphql/src/graphql/schemas/fuelcore.graphql +++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql @@ -783,6 +783,9 @@ type Query { transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection! tps: TPSConnection! getBlocksDashboard : BlocksDashboardConnection! + newAccountStatistics(timeFilter: String!): NewAccountStatisticsResponse! + dailyActiveAccountStatistics(timeFilter: String!): DailyActiveAccountStatisticsResponse! + cumulativeAccountStatistics(timeFilter: String!): CumulativeAccountStatisticsResponse! } type Receipt { @@ -1045,4 +1048,31 @@ type BlocksDashboard { type BlocksDashboardConnection { nodes: [BlocksDashboard!]! +} + +type NewAccountStatistics { + timestamp: String! + newAccounts: Int! +} + +type DailyActiveAccountStatistics { + timestamp: String! + activeAccounts: Int! +} + +type CumulativeAccountStatistics { + timestamp: String! + cumulativeAccounts: Int! +} + +type NewAccountStatisticsResponse { + nodes: [NewAccountStatistics!]! +} + +type DailyActiveAccountStatisticsResponse { + nodes: [DailyActiveAccountStatistics!]! +} + +type CumulativeAccountStatisticsResponse { + nodes: [CumulativeAccountStatistics!]! } \ No newline at end of file diff --git a/packages/graphql/src/infra/dao/AccountDAO.ts b/packages/graphql/src/infra/dao/AccountDAO.ts index 271a40a40..aa543ab7e 100644 --- a/packages/graphql/src/infra/dao/AccountDAO.ts +++ b/packages/graphql/src/infra/dao/AccountDAO.ts @@ -1,5 +1,6 @@ import { AccountEntity } from '../../domain/Account/AccountEntity'; import { DatabaseConnection } from '../database/DatabaseConnection'; +import { getTimeInterval } from './utils'; export default class AccountDAO { private databaseConnection: DatabaseConnection; @@ -8,13 +9,6 @@ export default class AccountDAO { this.databaseConnection = DatabaseConnection.getInstance(); } - // Custom function to stringify BigInt values - private stringifyBigInt(data: any): string { - return JSON.stringify(data, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ); - } - // Keep this method as it is still being used async getAccountById(id: string): Promise { const result = await this.databaseConnection.query( @@ -121,4 +115,209 @@ export default class AccountDAO { }, }; } + + // Retrieve the latest account statistics (for cumulative calculations) + async findLatestStatistics() { + const [result] = await this.databaseConnection.query( + ` + SELECT * FROM indexer.account_statistics + ORDER BY timestamp DESC + LIMIT 1 + `, + [], + ); + return result; + } + + // Retrieve the earliest account timestamp + async getEarliestAccountTimestamp(): Promise { + const [result] = await this.databaseConnection.query( + ` + SELECT timestamp + FROM indexer.accounts + ORDER BY timestamp ASC + LIMIT 1 + `, + [], + ); + return result ? result.timestamp : null; + } + + // Insert new statistics for accounts + async insertAccountStatistics( + timestamp: string, + new_accounts: number, + active_accounts: number, + cumulative_accounts: number, + ) { + await this.databaseConnection.query( + ` + INSERT INTO indexer.account_statistics (timestamp, new_accounts, active_accounts, cumulative_accounts) + VALUES ($1, $2, $3, $4) + `, + [timestamp, new_accounts, active_accounts, cumulative_accounts], + ); + } + + // Get account statistics within a specified time range + async getAccountsInRange(startTimestamp: string, endTimestamp: string) { + const accountsData = await this.databaseConnection.query( + ` + SELECT * + FROM indexer.accounts + WHERE timestamp >= $1 AND timestamp < $2 + ORDER BY timestamp ASC + `, + [startTimestamp, endTimestamp], + ); + return accountsData; + } + + // Get new account statistics over a specified time filter + async getNewAccountStatistics(timeFilter: string) { + const _hours = getTimeInterval(timeFilter); + let query; + if (timeFilter === '1day') { + query = ` + SELECT + to_char(t.timestamp, 'dd/mm/yyyy HH') as time, + sum(t.new_accounts::numeric) as new_accounts + FROM + indexer.account_statistics t + `; + } else { + query = ` + SELECT + to_char(t.timestamp, 'dd/mm/yyyy') as time, + sum(t.new_accounts::numeric) as new_accounts + FROM + indexer.account_statistics t + `; + } + + if (_hours !== null) { + query += ` + WHERE t.timestamp > (now() - interval '${_hours} hours') + `; + } + if (timeFilter === '1day') { + query += ` + GROUP BY to_char(t.timestamp, 'dd/mm/yyyy HH'); + `; + } else { + query += ` + GROUP BY to_char(t.timestamp, 'dd/mm/yyyy'); + `; + } + const result = await this.databaseConnection.query(query, []); + return { + nodes: result, + }; + } + + // Get daily active account statistics over a specified time filter + async getDailyActiveAccountStatistics(timeFilter: string) { + const _hours = getTimeInterval(timeFilter); + let query; + if (timeFilter === '1day') { + query = ` + SELECT + to_char(t.timestamp, 'dd/mm/yyyy HH') as time, + sum(t.active_accounts::numeric) as active_accounts + FROM + indexer.account_statistics t + `; + } else { + query = ` + SELECT + to_char(t.timestamp, 'dd/mm/yyyy') as time, + sum(t.active_accounts::numeric) as active_accounts + FROM + indexer.account_statistics t + `; + } + + if (_hours !== null) { + query += ` + WHERE t.timestamp > (now() - interval '${_hours} hours') + `; + } + if (timeFilter === '1day') { + query += ` + GROUP BY to_char(t.timestamp, 'dd/mm/yyyy HH'); + `; + } else { + query += ` + GROUP BY to_char(t.timestamp, 'dd/mm/yyyy'); + `; + } + const result = await this.databaseConnection.query(query, []); + return { + nodes: result, + }; + } + + // Get cumulative account statistics over a specified time filter + async getCumulativeAccountStatistics(timeFilter: string) { + const _hours = getTimeInterval(timeFilter); + let query; + + if (timeFilter === '1day') { + query = ` + WITH ranked_accounts AS ( + SELECT + to_char(t.timestamp, 'dd/mm/yyyy HH') as time, + t.cumulative_accounts, + ROW_NUMBER() OVER (PARTITION BY to_char(t.timestamp, 'dd/mm/yyyy HH') ORDER BY t.timestamp DESC) as row_num + FROM + indexer.account_statistics t + `; + } else { + query = ` + WITH ranked_accounts AS ( + SELECT + to_char(t.timestamp, 'dd/mm/yyyy') as time, + t.cumulative_accounts, + ROW_NUMBER() OVER (PARTITION BY to_char(t.timestamp, 'dd/mm/yyyy') ORDER BY t.timestamp DESC) as row_num + FROM + indexer.account_statistics t + `; + } + + if (_hours !== null) { + query += ` + WHERE t.timestamp > (now() - interval '${_hours} hours') + `; + } + + query += ` + ) + SELECT + time, + cumulative_accounts + FROM ranked_accounts + WHERE row_num = 1 + `; + + const result = await this.databaseConnection.query(query, []); + return { + nodes: result, + }; + } + + async getDailyActiveAccountsInRange( + startTimestamp: string, + endTimestamp: string, + ) { + const activeAccountsData = await this.databaseConnection.query( + ` + SELECT DISTINCT account_id + FROM indexer.transactions + WHERE timestamp >= $1 AND timestamp < $2 + `, + [startTimestamp, endTimestamp], + ); + + return activeAccountsData.length; // Number of unique active accounts in the range + } } diff --git a/packages/graphql/src/infra/dao/utils.ts b/packages/graphql/src/infra/dao/utils.ts index fef2cb26a..f80b5b851 100644 --- a/packages/graphql/src/infra/dao/utils.ts +++ b/packages/graphql/src/infra/dao/utils.ts @@ -47,3 +47,33 @@ export function createIntervals( return intervals; } + +export function getTimeInterval(timeFilter: string): number | null { + let _interval: number | null = null; + switch (timeFilter) { + case '1hr': + _interval = 1; + break; + case '12hr': + _interval = 12; + break; + case '1day': + _interval = 24; + break; + case '7days': + _interval = 24 * 7; + break; + case '14days': + _interval = 24 * 14; + break; + case '30days': + _interval = 24 * 30; + break; + case '90days': + _interval = 24 * 90; + break; + default: + _interval = null; + } + return _interval; +} diff --git a/packages/graphql/src/infra/statJobs/accountStatisticsJob.ts b/packages/graphql/src/infra/statJobs/accountStatisticsJob.ts new file mode 100644 index 000000000..b6e22fc59 --- /dev/null +++ b/packages/graphql/src/infra/statJobs/accountStatisticsJob.ts @@ -0,0 +1,49 @@ +import { DateHelper } from '../../core/Date'; +import AccountDAO from '../dao/AccountDAO'; + +export async function generateHourlyAccountStatistics() { + const accountDAO = new AccountDAO(); + + // Get the last processed timestamp from the statistics table + const latestStatistics = await accountDAO.findLatestStatistics(); + + // If no statistics exist, start from the earliest account timestamp + const initialStartTimestamp = latestStatistics?.timestamp + ? DateHelper.addHours(latestStatistics.timestamp, 1) // Next hour window + : await accountDAO.getEarliestAccountTimestamp(); // Start from the earliest account + + // Adjust the startTimestamp to the beginning of the hour + const startTimestamp = DateHelper.floorToHour(initialStartTimestamp); + // Calculate the end timestamp as one hour after the start timestamp + const endTimestamp = DateHelper.addHours(startTimestamp, 1); + + // Fetch accounts created in the next 1-hour window + const newAccountsInRange = await accountDAO.getAccountsInRange( + startTimestamp, + endTimestamp, + ); + + if (newAccountsInRange.length > 0) { + // New accounts created in the window + const new_accounts = newAccountsInRange.length; + + // Calculate daily active accounts in the current hour window + const active_accounts = await accountDAO.getDailyActiveAccountsInRange( + startTimestamp, + endTimestamp, + ); + + // Calculate cumulative accounts by adding to the previous cumulative count + const cumulative_accounts = latestStatistics?.cumulative_accounts + ? latestStatistics.cumulative_accounts + new_accounts + : new_accounts; + + // Insert statistics into the account_statistics table + await accountDAO.insertAccountStatistics( + startTimestamp, + new_accounts, + active_accounts, + cumulative_accounts, + ); + } +}