UNPKG

@prepaid-gas/data

Version:

Data layer for Prepaid Gas with fluent query builders

1 lines 102 kB
{"version":3,"sources":["../src/client/subgraph-client.ts","../src/query/builders/base-query-builder.ts","../src/transformers/index.ts","../src/query/builders/paymaster-query-builder.ts","../src/query/builders/activity-query-builder.ts","../src/query/builders/user-operation-query-builder.ts","../src/query/query-builder.ts","../src/index.ts"],"sourcesContent":["import { GraphQLClient } from 'graphql-request';\nimport { getNetworkPreset, type ChainId } from '@prepaid-gas/constants';\nimport { QueryBuilder } from '../query/query-builder.js';\n\n/**\n * Configuration for the subgraph client\n * Updated to support multiple paymaster contracts\n */\nexport type SubClientOptions = {\n subgraphUrl?: string;\n timeout?: number;\n};\n\n/**\n * Pagination options for queries\n */\nexport interface PaginationOptions {\n /** Number of items to fetch (default: 100) */\n first?: number;\n /** Number of items to skip (default: 0) */\n skip?: number;\n}\n\n/**\n * Options for pool queries with field selection\n */\nexport interface PoolQueryOptions extends PaginationOptions {\n /** Specific fields to fetch */\n fields?: string[];\n}\n\n/**\n * Options for paymaster queries with field selection\n */\nexport interface PaymasterQueryOptions extends PaginationOptions {\n /** Specific fields to fetch */\n fields?: string[];\n /** Filter by contract type */\n contractType?: 'GasLimited' | 'OneTimeUse';\n}\n\n/**\n * Options for transaction queries with field selection\n */\nexport interface TransactionQueryOptions extends PaginationOptions {\n /** Specific fields to fetch */\n fields?: string[];\n /** Filter by paymaster address */\n paymasterAddress?: string;\n /** Filter by pool ID */\n poolId?: string;\n /** Filter by sender address */\n senderAddress?: string;\n}\n\n/**\n * Clean, focused subgraph client for data access\n * Handles GraphQL communication and basic data transformation\n * Updated for new PaymasterContract-based subgraph structure\n */\nexport class SubgraphClient {\n private client: GraphQLClient;\n private chainId: ChainId;\n private options: SubClientOptions;\n private requestMap: Map<string, Promise<any>> = new Map();\n private readonly maxPendingRequests = 100;\n\n constructor(chainId: ChainId, options: SubClientOptions = {}) {\n const preset = getNetworkPreset(chainId);\n this.chainId = chainId;\n this.options = options;\n // Use provided subgraph URL or fall back to preset default\n const finalSubgraphUrl = options.subgraphUrl || preset?.defaultSubgraphUrl;\n\n if (!finalSubgraphUrl) {\n throw new Error(\n `No subgraph URL available for network(chainId: ${chainId}). Please provide one in options.subgraphUrl`\n );\n }\n\n this.client = new GraphQLClient(finalSubgraphUrl);\n }\n\n /**\n * Generate a unique key for a query/variables combination\n *\n * @param query - GraphQL query string\n * @param variables - Query variables\n * @returns Unique key for the request\n */\n private generateRequestKey(query: string, variables: Record<string, any>): string {\n // Create a consistent key from query and variables\n const normalizedQuery = query.replace(/\\s+/g, ' ').trim();\n const sortedVariables = Object.keys(variables)\n .sort()\n .reduce(\n (sorted, key) => {\n sorted[key] = variables[key];\n return sorted;\n },\n {} as Record<string, any>\n );\n\n return `${normalizedQuery}|${JSON.stringify(sortedVariables)}`;\n }\n\n /**\n * Clean up completed request from the map\n *\n * @param requestKey - Key of the request to clean up\n */\n private cleanupRequest(requestKey: string): void {\n this.requestMap.delete(requestKey);\n }\n\n /**\n * Clean up old requests if map gets too large\n */\n private cleanupOldRequests(): void {\n if (this.requestMap.size > this.maxPendingRequests) {\n // Clear half of the oldest requests\n const keysToDelete = Array.from(this.requestMap.keys()).slice(0, this.maxPendingRequests / 2);\n keysToDelete.forEach((key) => this.requestMap.delete(key));\n }\n }\n\n /**\n * Create SubgraphClient for a specific network using presets\n *\n * @param chainId - The chain ID to create client for\n * @param options - Optional configuration overrides\n * @returns Configured SubgraphClient instance\n *\n * @example\n * ```typescript\n * // Create for Base Sepolia using preset\n * const client = SubgraphClient.createForNetwork(84532);\n *\n * // Create with custom subgraph URL override\n * const client = SubgraphClient.createForNetwork(84532, {\n * subgraphUrl: \"https://custom-subgraph.com\",\n * timeout: 60000\n * });\n * ```\n */\n static createForNetwork(\n chainId: ChainId,\n options: {\n /** Custom subgraph URL (optional, uses preset default if not provided) */\n subgraphUrl?: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n } = {}\n ): SubgraphClient {\n return new SubgraphClient(chainId, options);\n }\n\n /**\n * ✨ Create a fluent query builder instance\n *\n * This is the main entry point for the new query builder API.\n * Provides a fluent interface for building complex queries with type safety.\n *\n * @returns QueryBuilder for fluent query building\n *\n * @example\n * ```typescript\n * // Simple paymaster query\n * const paymasters = await client\n * .query()\n * .paymasters()\n * .byType(\"GasLimited\")\n * .withMinRevenue(\"1000000000000000000\")\n * .orderByRevenue()\n * .limit(10)\n * .execute();\n *\n * // Complex pool query with members\n * const pools = await client\n * .query()\n * .pools()\n * .byPaymaster(\"0x456...\")\n * .withMinMembers(10)\n * .withMembers(50)\n * .orderByPopularity()\n * .limit(20)\n * .execute();\n *\n * // Analytics query\n * const analytics = await client\n * .query()\n * .dailyGlobalStats()\n * .forDateRange(\"2024-01-01\", \"2024-01-31\")\n * .withMinNewPools(2)\n * .orderByNewest()\n * .execute();\n * ```\n */\n query(): QueryBuilder {\n return new QueryBuilder(this);\n }\n\n /**\n * Execute a raw GraphQL query with request deduplication\n *\n * This method provides a generic interface for executing any GraphQL query,\n * with built-in deduplication to prevent duplicate requests for identical queries.\n *\n * @template T - The expected response type\n * @param query - GraphQL query string\n * @param variables - Query variables object\n * @returns Promise resolving to the query response data\n *\n * @example\n * ```typescript\n * const response = await client.executeQuery<{ pools: Pool[] }>(\n * 'query GetPools($first: Int!) { pools(first: $first) { id poolId } }',\n * { first: 10 }\n * );\n * console.log(response.pools);\n * ```\n */\n async executeQuery<T = any>(query: string, variables: Record<string, any> = {}): Promise<T> {\n // Generate unique key for this request\n const requestKey = this.generateRequestKey(query, variables);\n\n // Check if identical request is already in progress\n const existingRequest = this.requestMap.get(requestKey);\n if (existingRequest) {\n return existingRequest;\n }\n\n // Clean up old requests if needed\n this.cleanupOldRequests();\n\n // Create new request promise\n const requestPromise = this.client\n .request<T>(query, variables)\n .then((response) => {\n // Clean up this request from the map\n this.cleanupRequest(requestKey);\n return response;\n })\n .catch((error) => {\n // Clean up this request from the map even on error\n this.cleanupRequest(requestKey);\n\n throw error;\n });\n\n // Store the promise in the map\n this.requestMap.set(requestKey, requestPromise);\n\n return requestPromise;\n }\n\n /**\n * Execute multiple queries in parallel with deduplication\n *\n * @param queries - Array of query objects with query string and variables\n * @returns Promise resolving to array of query responses\n *\n * @example\n * ```typescript\n * const results = await client.executeQueries([\n * { query: 'query GetPools { pools { id } }', variables: {} },\n * { query: 'query GetPaymasters { paymasterContracts { id } }', variables: {} }\n * ]);\n * ```\n */\n async executeQueries<T = any>(queries: Array<{ query: string; variables?: Record<string, any> }>): Promise<T[]> {\n const promises = queries.map(({ query, variables = {} }) => this.executeQuery<T>(query, variables));\n return Promise.all(promises);\n }\n}\n","import { SubgraphClient } from '../../client/subgraph-client.js';\nimport { QueryConfig } from '../types.js';\n\n/**\n * Base query builder with common functionality\n *\n * This abstract class provides the foundation for all entity-specific query builders.\n * Each concrete builder must implement the three core methods with proper typing.\n *\n * @template TEntity - The raw entity type from GraphQL (Pool, PaymasterContract, etc.)\n * @template TSerializedEntity - The serialized entity type (SerializedPool, etc.)\n * @template TFields - Union type of available fields for the entity\n * @template TWhereInput - Type for where condition input for the entity\n * @template TOrderBy - Union type of valid orderBy fields\n */\nexport abstract class BaseQueryBuilder<\n TEntity,\n TSerializedEntity,\n TFields extends string,\n TWhereInput,\n TOrderBy extends string,\n> {\n protected config: QueryConfig<TWhereInput, TOrderBy> = {};\n\n // Safety limits\n private static readonly MAX_SAFE_LIMIT = 1000;\n private static readonly DEFAULT_LIMIT = 100;\n\n constructor(\n protected client: SubgraphClient,\n protected entityName: string,\n defaultOrderBy: TOrderBy,\n defaultOrderDirection: 'asc' | 'desc' = 'desc'\n ) {\n // Basic validation\n if (!entityName || typeof entityName !== 'string') {\n throw new Error('Entity name must be a non-empty string');\n }\n\n this.entityName = entityName;\n this.config.orderBy = defaultOrderBy;\n this.config.orderDirection = defaultOrderDirection;\n }\n\n // ========================================\n // ABSTRACT METHODS - Each builder implements these\n // ========================================\n\n /**\n * Build the GraphQL query string with proper types and validation\n * Each builder knows its own schema and field types\n */\n protected abstract buildDynamicQuery(): string;\n\n /**\n * Build variables object with proper type conversion\n * Each builder handles its own BigInt/string conversions\n */\n protected abstract buildVariables(): Record<string, any>;\n\n /**\n * Build where clause string with typed fields and validation\n * Each builder validates its own where conditions\n */\n protected abstract buildWhereClauseString(): string;\n\n /**\n * Get the serializer function for this entity type\n * Each builder provides its own serializer from transformers\n */\n protected abstract getSerializer(): (entity: TEntity) => TSerializedEntity;\n\n // ========================================\n // COMMON FLUENT API METHODS\n // ========================================\n\n /**\n * Select specific fields to be returned\n * @param fields - Array of field names to select\n * @returns The query builder instance for chaining\n */\n select(fields: TFields[]): this {\n if (!Array.isArray(fields)) {\n throw new Error('Fields must be an array');\n }\n\n if (fields.length === 0) {\n throw new Error('Fields array cannot be empty');\n }\n\n this.config.selectedFields = fields;\n return this;\n }\n\n /**\n * Add where conditions to the query\n * @param where - Object representing where conditions\n * @returns The query builder instance for chaining\n */\n where(where: Partial<TWhereInput>): this {\n if (typeof where !== 'object' || where === null) {\n throw new Error('Where conditions must be an object');\n }\n\n this.config.where = this.deepMergeWhereConditions(this.config.where || {}, where);\n return this;\n }\n\n private deepMergeWhereConditions(existing: any, newWhere: any): any {\n const result = { ...existing };\n\n for (const [key, value] of Object.entries(newWhere)) {\n if (key.endsWith('_') && typeof value === 'object' && value !== null) {\n // Deep merge for GraphQL relationship fields (pool_, paymaster_, etc.)\n if (typeof result[key] === 'object' && result[key] !== null) {\n result[key] = { ...result[key], ...value };\n } else {\n result[key] = value;\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n }\n\n /**\n * Limit the number of results\n * @param limit - Maximum number of results (capped at 1000 for safety)\n * @returns The query builder instance for chaining\n */\n limit(limit: number): this {\n if (!Number.isInteger(limit) || limit <= 0) {\n throw new Error('Limit must be a positive integer');\n }\n\n const safeLimit = Math.min(limit, BaseQueryBuilder.MAX_SAFE_LIMIT);\n\n if (safeLimit < limit) {\n console.warn(`Limit ${limit} exceeds maximum ${BaseQueryBuilder.MAX_SAFE_LIMIT}, using ${safeLimit}`);\n }\n\n this.config.first = safeLimit;\n return this;\n }\n\n /**\n * Skip a number of results for pagination\n * @param skip - Number of results to skip\n * @returns The query builder instance for chaining\n */\n skip(skip: number): this {\n if (!Number.isInteger(skip) || skip < 0) {\n throw new Error('Skip must be a non-negative integer');\n }\n\n this.config.skip = skip;\n return this;\n }\n\n /**\n * Set the ordering for the results\n * @param orderBy - Field to order by\n * @param direction - Order direction (\"asc\" or \"desc\")\n * @returns The query builder instance for chaining\n */\n orderBy(orderBy: TOrderBy, direction: 'asc' | 'desc' = 'desc'): this {\n if (!orderBy || typeof orderBy !== 'string') {\n throw new Error('OrderBy field must be a non-empty string');\n }\n\n if (direction !== 'asc' && direction !== 'desc') {\n throw new Error('Order direction must be \"asc\" or \"desc\"');\n }\n\n this.config.orderBy = orderBy;\n this.config.orderDirection = direction;\n return this;\n }\n\n // ========================================\n // EXECUTION METHODS\n // ========================================\n\n /**\n * Execute the query and return raw entities\n * @returns Promise resolving to an array of TEntity\n */\n async execute(): Promise<TEntity[]> {\n // Apply default limit if none specified\n if (!this.config.first) {\n this.config.first = BaseQueryBuilder.DEFAULT_LIMIT;\n }\n\n const query = this.buildDynamicQuery();\n const variables = this.buildVariables();\n const result = await this.client.executeQuery<{ [key: string]: TEntity[] }>(query, variables);\n\n const data = result[this.entityName] || [];\n\n // Warn if hitting limits\n if (data.length === this.config.first) {\n console.warn(`Query returned maximum results (${this.config.first}). Consider using pagination.`);\n }\n\n return data;\n }\n\n /**\n * Execute the query and return properly typed serialized entities\n * @returns Promise resolving to an array of TSerializedEntity (not any!)\n */\n async executeAndSerialize(): Promise<TSerializedEntity[]> {\n const rawResults = await this.execute();\n const serializer = this.getSerializer();\n return rawResults.map((entity) => serializer(entity));\n }\n\n /**\n * Execute the query and return the first result\n * @returns Promise resolving to the first TEntity or null\n */\n async first(): Promise<TEntity | null> {\n const originalFirst = this.config.first;\n this.config.first = 1;\n\n try {\n const results = await this.execute();\n return results.length > 0 ? (results[0] ?? null) : null;\n } finally {\n // Always restore original limit\n this.config.first = originalFirst;\n }\n }\n\n /**\n * Execute the query and return the first result as serialized entity\n * @returns Promise resolving to the first TSerializedEntity or null\n */\n async firstSerialized(): Promise<TSerializedEntity | null> {\n const entity = await this.first();\n if (!entity) return null;\n\n const serializer = this.getSerializer();\n return serializer(entity);\n }\n\n /**\n * Execute the query and check if any results exist\n * @returns Promise resolving to true if any results exist\n */\n async exists(): Promise<boolean> {\n const result = await this.first();\n return result !== null;\n }\n\n /**\n * Execute the query and return the count of results\n * Note: This fetches all results and counts them\n * @returns Promise resolving to the count of matching entities\n */\n async count(): Promise<number> {\n const results = await this.execute();\n return results.length;\n }\n\n /**\n * Clone the current query builder instance\n * @returns A new instance of the query builder with the same configuration\n */\n clone(): this {\n // Create new instance using the same constructor\n const cloned = new (this.constructor as any)(\n this.client,\n this.entityName,\n this.config.orderBy,\n this.config.orderDirection\n );\n\n // Deep clone the configuration\n cloned.config = {\n ...this.config,\n where: this.config.where ? { ...this.config.where } : undefined,\n selectedFields: this.config.selectedFields ? [...this.config.selectedFields] : undefined,\n };\n\n return cloned;\n }\n\n // ========================================\n // UTILITY METHODS\n // ========================================\n\n /**\n * Get current query configuration (for debugging)\n * @returns Current configuration object\n */\n getConfig(): QueryConfig<TWhereInput, TOrderBy> {\n return { ...this.config };\n }\n\n /**\n * Reset the query builder to default state\n * @returns The query builder instance for chaining\n */\n reset(): this {\n this.config = {\n orderBy: this.config.orderBy, // Keep default orderBy\n orderDirection: this.config.orderDirection, // Keep default direction\n };\n return this;\n }\n}\n","/**\n * Data transformers for the paymaster system (Updated)\n * For the new single-pool-per-contract architecture\n *\n * Handles BigInt serialization/deserialization for:\n * - PaymasterContract (each contract IS a pool)\n * - Activity (unified timeline)\n * - UserOperation (detailed tracking)\n */\n\nimport type {\n PaymasterContract,\n SerializedPaymasterContract,\n Activity,\n SerializedActivity,\n UserOperation,\n SerializedUserOperation,\n} from '../types/subgraph.js';\n\n/**\n * ========================================\n * UTILITY FUNCTIONS\n * ========================================\n */\n\n/**\n * Safely parse a BigInt from a string\n *\n * @param value - String value to parse\n * @returns BigInt value or 0n if invalid\n */\nexport function safeBigIntParse(value: string | number | bigint): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n\n if (typeof value === 'number') {\n return BigInt(Math.floor(value));\n }\n\n try {\n return BigInt(value);\n } catch (error) {\n console.warn(`Failed to parse BigInt from value: ${value}`, error);\n return 0n;\n }\n}\n\n/**\n * ========================================\n * GENERIC CONVERSION FUNCTIONS\n * ========================================\n */\n\n/**\n * Convert BigInt values to strings recursively\n *\n * @param obj - Entity object to convert\n * @returns Serialized entity with BigInt values as strings\n */\nexport function convertBigIntsToStrings<T>(obj: T): any {\n if (typeof obj === 'bigint') {\n return obj.toString();\n }\n\n if (obj === null || obj === undefined) {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => convertBigIntsToStrings(item));\n }\n\n if (typeof obj === 'object') {\n const result: any = {};\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = convertBigIntsToStrings(obj[key]);\n }\n }\n return result;\n }\n\n return obj;\n}\n\n/**\n * Convert string values back to BigInt for specified fields\n *\n * @param obj - Serialized entity object\n * @param bigIntFields - Array of field names that should be converted to BigInt\n * @returns Entity with BigInt values restored\n */\nexport function convertStringsToBigInts<T>(obj: T, bigIntFields: string[]): any {\n if (obj === null || obj === undefined) {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => convertStringsToBigInts(item, bigIntFields));\n }\n\n if (typeof obj === 'object') {\n const result: any = {};\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n if (bigIntFields.includes(key) && typeof obj[key] === 'string') {\n result[key] = safeBigIntParse(obj[key]);\n } else if (typeof obj[key] === 'object') {\n result[key] = convertStringsToBigInts(obj[key], bigIntFields);\n } else {\n result[key] = obj[key];\n }\n }\n }\n return result;\n }\n\n return obj;\n}\n\n/**\n * ========================================\n * BIGINT FIELD DEFINITIONS (Updated for new schema)\n * ========================================\n */\n\n// Define which fields are BigInt for each entity type\nconst BIGINT_FIELDS = {\n paymasterContract: [\n 'chainId',\n 'joiningAmount',\n 'scope',\n 'totalDeposit',\n 'currentDeposit',\n 'revenue',\n 'root',\n 'rootIndex',\n 'treeDepth',\n 'treeSize',\n 'deployedBlock',\n 'deployedTimestamp',\n 'lastBlock',\n 'lastTimestamp',\n ],\n activity: [\n 'chainId',\n 'block',\n 'timestamp',\n // Optional BigInt fields (based on activity type)\n 'commitment',\n 'memberIndex',\n 'newRoot',\n 'actualGasCost',\n 'amount',\n ],\n userOperation: ['chainId', 'actualGasCost', 'nullifier', 'block', 'timestamp'],\n};\n\n/**\n * ========================================\n * SERIALIZATION FUNCTIONS (Updated)\n * ========================================\n */\n\nexport function serializePaymasterContract(entity: PaymasterContract): SerializedPaymasterContract {\n return convertBigIntsToStrings(entity);\n}\n\nexport function serializeActivity(entity: Activity): SerializedActivity {\n return convertBigIntsToStrings(entity);\n}\n\nexport function serializeUserOperation(entity: UserOperation): SerializedUserOperation {\n return convertBigIntsToStrings(entity);\n}\n\n/**\n * ========================================\n * DESERIALIZATION FUNCTIONS (Updated)\n * ========================================\n */\n\nexport function deserializePaymasterContract(entity: SerializedPaymasterContract): PaymasterContract {\n return convertStringsToBigInts(entity, BIGINT_FIELDS.paymasterContract);\n}\n\nexport function deserializeActivity(entity: SerializedActivity): Activity {\n return convertStringsToBigInts(entity, BIGINT_FIELDS.activity);\n}\n\nexport function deserializeUserOperation(entity: SerializedUserOperation): UserOperation {\n return convertStringsToBigInts(entity, BIGINT_FIELDS.userOperation);\n}\n\n/**\n * ========================================\n * CONVENIENCE EXPORTS (Updated)\n * ========================================\n */\n\n/**\n * All serialization functions\n */\nexport const serializers = {\n paymasterContract: serializePaymasterContract,\n activity: serializeActivity,\n userOperation: serializeUserOperation,\n};\n\n/**\n * All deserialization functions\n */\nexport const deserializers = {\n paymasterContract: deserializePaymasterContract,\n activity: deserializeActivity,\n userOperation: deserializeUserOperation,\n};\n\n/**\n * ========================================\n * ACTIVITY-SPECIFIC HELPERS\n * ========================================\n */\n\n/**\n * Serialize activities with type-specific field handling\n *\n * @param activities - Array of activities to serialize\n * @returns Serialized activities with proper BigInt conversion\n */\nexport function serializeActivities(activities: Activity[]): SerializedActivity[] {\n return activities.map(serializeActivity);\n}\n\n/**\n * Deserialize activities with type-specific field handling\n *\n * @param activities - Array of serialized activities\n * @returns Activities with BigInt fields restored\n */\nexport function deserializeActivities(activities: SerializedActivity[]): Activity[] {\n return activities.map(deserializeActivity);\n}\n\n/**\n * ========================================\n * ANALYTICS HELPERS\n * ========================================\n */\n\n/**\n * Calculate analytics from activity data (no BigInt conversion needed)\n *\n * @param activities - Activities to analyze\n * @returns Analytics summary with proper BigInt types\n */\nexport function calculatePaymasterAnalytics(activities: Activity[]): {\n totalDeposits: bigint;\n totalUserOperations: bigint;\n totalGasSponsored: bigint;\n totalRevenueWithdrawn: bigint;\n averageGasPerOperation: bigint;\n} {\n let totalDeposits = 0n;\n let totalUserOperations = 0n;\n let totalGasSponsored = 0n;\n let totalRevenueWithdrawn = 0n;\n\n for (const activity of activities) {\n switch (activity.type) {\n case 'DEPOSIT':\n totalDeposits++;\n break;\n case 'USER_OP_SPONSORED':\n totalUserOperations++;\n if (activity.actualGasCost) {\n totalGasSponsored += activity.actualGasCost;\n }\n break;\n case 'REVENUE_WITHDRAWN':\n if (activity.amount) {\n totalRevenueWithdrawn += activity.amount;\n }\n break;\n }\n }\n\n const averageGasPerOperation = totalUserOperations > 0n ? totalGasSponsored / totalUserOperations : 0n;\n\n return {\n totalDeposits,\n totalUserOperations,\n totalGasSponsored,\n totalRevenueWithdrawn,\n averageGasPerOperation,\n };\n}\n","/**\n * Query builder for PaymasterContract entities (Updated)\n * For the new single-pool-per-contract architecture\n */\n\nimport type { SubgraphClient } from '../../client/subgraph-client.js';\nimport type { PaymasterContract, SerializedPaymasterContract } from '../../types/subgraph.js';\nimport { BaseQueryBuilder } from './base-query-builder.js';\nimport { PaymasterContractFields, PaymasterContractWhereInput, PaymasterContractOrderBy } from '../types.js';\nimport { serializePaymasterContract } from '../../transformers/index.js';\nimport { PaymasterType } from '@prepaid-gas/constants';\n\n/**\n * Query builder for PaymasterContract entities\n *\n * Provides a fluent interface for building complex paymaster queries\n * Updated for the new single-pool-per-contract schema.\n */\nexport class PaymasterContractQueryBuilder extends BaseQueryBuilder<\n PaymasterContract,\n SerializedPaymasterContract,\n PaymasterContractFields,\n PaymasterContractWhereInput,\n PaymasterContractOrderBy\n> {\n constructor(private subgraphClient: SubgraphClient) {\n super(subgraphClient, 'paymasterContracts', 'deployedTimestamp', 'desc');\n }\n\n protected buildDynamicQuery(): string {\n const fields = this.config.selectedFields?.join('\\n ') || this.getDefaultFields();\n const variables = this.getVariableDeclarations();\n const whereClause = this.buildWhereClauseString();\n const orderByClause = this.config.orderBy ? `orderBy: ${this.config.orderBy}` : '';\n const orderDirectionClause = this.config.orderDirection ? `orderDirection: ${this.config.orderDirection}` : '';\n\n const args = [whereClause, orderByClause, orderDirectionClause, 'first: $first', 'skip: $skip']\n .filter(Boolean)\n .join(', ');\n\n const queryName = `GetPaymasterContracts`;\n\n return `\n query ${queryName}(${variables}) {\n paymasterContracts(${args}) {\n ${fields}\n }\n }\n `;\n }\n\n protected buildVariables(): Record<string, any> {\n const variables: Record<string, any> = {\n first: this.config.first || 100,\n skip: this.config.skip || 0,\n };\n\n if (this.config.where) {\n this.addWhereVariables(this.config.where, variables);\n }\n\n return variables;\n }\n\n protected buildWhereClauseString(): string {\n if (!this.config.where || Object.keys(this.config.where).length === 0) {\n return '';\n }\n\n const conditions = this.buildWhereConditions(this.config.where);\n return conditions.length > 0 ? `where: { ${conditions.join(', ')} }` : '';\n }\n\n protected getSerializer(): (entity: PaymasterContract) => SerializedPaymasterContract {\n return serializePaymasterContract;\n }\n\n private getVariableDeclarations(): string {\n const declarations = ['$first: Int!', '$skip: Int!'];\n\n if (this.config.where) {\n this.addVariableDeclarations(this.config.where, declarations);\n }\n\n return declarations.join(', ');\n }\n\n private addVariableDeclarations(where: Partial<PaymasterContractWhereInput>, declarations: string[]): void {\n for (const [key] of Object.entries(where)) {\n switch (key) {\n case 'network':\n declarations.push('$network: String');\n break;\n case 'contractType':\n declarations.push('$contractType: String');\n break;\n case 'address':\n declarations.push('$address: String');\n break;\n case 'id':\n declarations.push('$id: ID');\n break;\n case 'revenue_gte':\n declarations.push('$revenue_gte: String');\n break;\n case 'revenue_lte':\n declarations.push('$revenue_lte: String');\n break;\n case 'revenue_gt':\n declarations.push('$revenue_gt: String');\n break;\n case 'totalDeposit_gte':\n declarations.push('$totalDeposit_gte: String');\n break;\n case 'totalDeposit_lte':\n declarations.push('$totalDeposit_lte: String');\n break;\n case 'currentDeposit_gte':\n declarations.push('$currentDeposit_gte: String');\n break;\n case 'currentDeposit_lte':\n declarations.push('$currentDeposit_lte: String');\n break;\n case 'treeSize_gte':\n declarations.push('$treeSize_gte: String');\n break;\n case 'treeSize_lte':\n declarations.push('$treeSize_lte: String');\n break;\n case 'treeSize_gt':\n declarations.push('$treeSize_gt: String');\n break;\n case 'deployedTimestamp_gte':\n declarations.push('$deployedTimestamp_gte: String');\n break;\n case 'deployedTimestamp_lte':\n declarations.push('$deployedTimestamp_lte: String');\n break;\n case 'isDead':\n declarations.push('$isDead: Boolean');\n break;\n }\n }\n }\n\n private addWhereVariables(where: Partial<PaymasterContractWhereInput>, variables: Record<string, any>): void {\n for (const [key, value] of Object.entries(where)) {\n switch (key) {\n case 'network':\n case 'contractType':\n case 'address':\n case 'id':\n case 'revenue_gte':\n case 'revenue_lte':\n case 'revenue_gt':\n case 'totalDeposit_gte':\n case 'totalDeposit_lte':\n case 'currentDeposit_gte':\n case 'currentDeposit_lte':\n case 'treeSize_gte':\n case 'treeSize_lte':\n case 'treeSize_gt':\n case 'deployedTimestamp_gte':\n case 'deployedTimestamp_lte':\n case 'isDead':\n variables[key] = value;\n break;\n }\n }\n }\n\n private buildWhereConditions(where: Partial<PaymasterContractWhereInput>): string[] {\n const conditions: string[] = [];\n\n for (const [key] of Object.entries(where)) {\n conditions.push(`${key}: $${key}`);\n }\n\n return conditions;\n }\n\n /**\n * Override default fields for PaymasterContract entity.\n */\n protected getDefaultFields(): string {\n return `\n id\n contractType\n address\n network\n chainId\n joiningAmount\n scope\n verifier\n totalDeposit\n currentDeposit\n revenue\n root\n rootIndex\n treeDepth\n treeSize\n isDead\n deployedBlock\n deployedTransaction\n deployedTimestamp\n lastBlock\n lastTimestamp\n `;\n }\n\n /**\n * Get fields with activities included\n */\n private getFieldsWithActivities(): string {\n return `\n ${this.getDefaultFields()}\n activities(orderBy:timestamp, orderDirection:desc) {\n id\n type\n network\n chainId\n block\n transaction\n timestamp\n \n # Deposit-specific fields\n depositor\n commitment\n memberIndex\n newRoot\n \n # UserOp-specific fields\n sender\n userOpHash\n actualGasCost\n \n # Revenue-specific fields\n withdrawAddress\n amount\n }\n `;\n }\n\n /**\n * ========================================\n * FILTERING METHODS\n * ========================================\n */\n\n /**\n * Filter by network\n */\n byNetwork(network: string): this {\n this.where({ network: network });\n return this;\n }\n\n /**\n * Filter by contract type\n */\n byContractType(type: PaymasterType): this {\n this.where({ contractType: type });\n return this;\n }\n\n /**\n * Filter by contract address\n */\n byAddress(address: string): this {\n this.where({ address: address });\n return this;\n }\n\n /**\n * Filter by composite ID (network-address)\n */\n byId(network: string, address: string): this {\n this.where({ id: `${network}-${address}` });\n return this;\n }\n\n /**\n * Filter by deployment date (after)\n */\n deployedAfter(timestamp: string | number): this {\n this.where({ deployedTimestamp_gte: timestamp.toString() });\n return this;\n }\n\n /**\n * Filter by deployment date (before)\n */\n deployedBefore(timestamp: string | number): this {\n this.where({ deployedTimestamp_lte: timestamp.toString() });\n return this;\n }\n\n /**\n * Filter only active paymasters (positive revenue)\n */\n onlyActive(): this {\n this.where({ revenue_gt: '0' });\n return this;\n }\n\n /**\n * Filter only alive paymasters (not dead)\n */\n onlyAlive(): this {\n this.where({ isDead: false });\n return this;\n }\n\n /**\n * Include activities in the query results\n * This replaces the need to manually select activities fields\n */\n withActivities(): this {\n // Set the fields to include activities\n const fieldsArray = this.getFieldsWithActivities()\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n\n this.select(fieldsArray as PaymasterContractFields[]);\n return this;\n }\n\n /**\n * ========================================\n * ORDERING METHODS\n * ========================================\n */\n\n /**\n * Order by revenue (highest first)\n */\n orderByRevenue(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('revenue', direction);\n return this;\n }\n\n /**\n * Order by total deposit\n */\n orderByTotalDeposit(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('totalDeposit', direction);\n return this;\n }\n\n /**\n * Order by current deposit\n */\n orderByDeposit(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('currentDeposit', direction);\n return this;\n }\n\n /**\n * Order by number of members (tree size)\n */\n orderByMembers(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('treeSize', direction);\n return this;\n }\n\n /**\n * Order by deployment date\n */\n orderByDeployment(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('deployedTimestamp', direction);\n return this;\n }\n\n /**\n * Order by last activity\n */\n orderByActivity(direction: 'asc' | 'desc' = 'desc'): this {\n this.orderBy('lastTimestamp', direction);\n return this;\n }\n}\n","/**\n * Query builder for Activity entities\n * For the unified timeline of all paymaster events\n * Supports type-safe filtering by activity type\n */\n\nimport type { SubgraphClient } from '../../client/subgraph-client.js';\nimport type {\n Activity,\n ActivityType,\n SerializedActivity,\n DepositActivity,\n RevenueActivity,\n SerializedDepositActivity,\n SerializedRevenueActivity,\n ActivityTypeMap,\n SerializedActivityTypeMap,\n} from '../../types/subgraph.js';\nimport { BaseQueryBuilder } from './base-query-builder.js';\nimport { ActivityFields, ActivityWhereInput, ActivityOrderBy } from '../types.js';\nimport { serializeActivity } from '../../transformers/index.js';\nimport { PaymasterType } from '@prepaid-gas/constants';\n\n/**\n * Base Activity Query Builder\n * Generic type T allows for typed results based on activity type\n */\nexport class ActivityQueryBuilder<\n T extends Activity = Activity,\n S extends SerializedActivity = SerializedActivity,\n> extends BaseQueryBuilder<T, S, ActivityFields, ActivityWhereInput, ActivityOrderBy> {\n constructor(private subgraphClient: SubgraphClient) {\n super(subgraphClient, 'activities', 'timestamp', 'desc');\n }\n\n protected buildDynamicQuery(): string {\n const fields = this.config.selectedFields?.join('\\n ') || this.getDefaultFields();\n const variables = this.getVariableDeclarations();\n const whereClause = this.buildWhereClauseString();\n const orderByClause = this.config.orderBy ? `orderBy: ${this.config.orderBy}` : '';\n const orderDirectionClause = this.config.orderDirection ? `orderDirection: ${this.config.orderDirection}` : '';\n\n const args = [whereClause, orderByClause, orderDirectionClause, 'first: $first', 'skip: $skip']\n .filter(Boolean)\n .join(', ');\n\n const queryName = `GetActivities`;\n\n return `\n query ${queryName}(${variables}) {\n activities(${args}) {\n ${fields}\n }\n }\n `;\n }\n\n protected buildVariables(): Record<string, any> {\n const variables: Record<string, any> = {\n first: this.config.first || 100,\n skip: this.config.skip || 0,\n };\n\n if (this.config.where) {\n this.addWhereVariables(this.config.where, variables);\n }\n\n return variables;\n }\n\n protected buildWhereClauseString(): string {\n if (!this.config.where || Object.keys(this.config.where).length === 0) {\n return '';\n }\n\n const conditions = this.buildWhereConditions(this.config.where);\n return conditions.length > 0 ? `where: { ${conditions.join(', ')} }` : '';\n }\n\n protected getSerializer(): (entity: T) => S {\n // The generic serializeActivity function handles all activity types correctly\n // TypeScript casting is safe here since all our activity types extend the base Activity\n return serializeActivity as unknown as (entity: T) => S;\n }\n\n private getVariableDeclarations(): string {\n const declarations = ['$first: Int!', '$skip: Int!'];\n\n if (this.config.where) {\n this.addVariableDeclarations(this.config.where, declarations);\n }\n\n return declarations.join(', ');\n }\n\n private addVariableDeclarations(where: Partial<ActivityWhereInput>, declarations: string[]): void {\n for (const [key] of Object.entries(where)) {\n switch (key) {\n case 'id':\n declarations.push('$id: ID');\n break;\n case 'type':\n declarations.push('$type: String');\n break;\n case 'network':\n declarations.push('$network: String');\n break;\n case 'paymaster':\n declarations.push('$paymaster: String');\n break;\n case 'block_gte':\n case 'block_lte':\n declarations.push(`$${key}: String`);\n break;\n case 'timestamp_gte':\n case 'timestamp_lte':\n declarations.push(`$${key}: String`);\n break;\n case 'depositor':\n declarations.push('$depositor: String');\n break;\n case 'commitment':\n declarations.push('$commitment: String');\n break;\n case 'memberIndex_gte':\n case 'memberIndex_lte':\n declarations.push(`$${key}: String`);\n break;\n case 'sender':\n declarations.push('$sender: String');\n break;\n case 'userOpHash':\n declarations.push('$userOpHash: String');\n break;\n case 'actualGasCost_gte':\n case 'actualGasCost_lte':\n declarations.push(`$${key}: String`);\n break;\n case 'withdrawAddress':\n declarations.push('$withdrawAddress: String');\n break;\n case 'amount_gte':\n case 'amount_lte':\n declarations.push(`$${key}: String`);\n break;\n case 'paymaster_':\n // Nested paymaster filtering\n declarations.push('$paymasterAddress: String');\n declarations.push('$paymasterContractType: String');\n declarations.push('$paymasterNetwork: String');\n break;\n }\n }\n }\n\n private addWhereVariables(where: Partial<ActivityWhereInput>, variables: Record<string, any>): void {\n for (const [key, value] of Object.entries(where)) {\n switch (key) {\n case 'id':\n case 'type':\n case 'network':\n case 'paymaster':\n case 'block_gte':\n case 'block_lte':\n case 'timestamp_gte':\n case 'timestamp_lte':\n case 'depositor':\n case 'commitment':\n case 'memberIndex_gte':\n case 'memberIndex_lte':\n case 'sender':\n case 'userOpHash':\n case 'actualGasCost_gte':\n case 'actualGasCost_lte':\n case 'withdrawAddress':\n case 'amount_gte':\n case 'amount_lte':\n variables[key] = value;\n break;\n case 'paymaster_':\n // Handle nested paymaster filtering\n if (value && typeof value === 'object') {\n if (value.address) variables.paymasterAddress = value.address;\n if (value.contractType) variables.paymasterContractType = value.contractType;\n if (value.network) variables.paymasterNetwork = value.network;\n }\n break;\n }\n }\n }\n\n private buildWhereConditions(where: Partial<ActivityWhereInput>): string[] {\n const conditions: string[] = [];\n\n for (const [key, value] of Object.entries(where)) {\n switch (key) {\n case 'paymaster_':\n // Handle nested paymaster filtering\n if (value && typeof value === 'object') {\n const nestedConditions: string[] = [];\n if (value.address) nestedConditions.push('address: $paymasterAddress');\n if (value.contractType) nestedConditions.push('contractType: $paymasterContractType');\n if (value.network) nestedConditions.push('network: $paymasterNetwork');\n if (nestedConditions.length > 0) {\n conditions.push(`paymaster_: { ${nestedConditions.join(', ')} }`);\n }\n }\n break;\n default:\n conditions.push(`${key}: $${key}`);\n break;\n }\n }\n\n return conditions;\n }\n\n /**\n * Override default fields for Activity entity.\n */\n protected getDefaultFields(): string {\n return `\n id\n type\n network\n chainId\n block\n transaction\n timestamp\n \n # Deposit-specific fields\n depositor\n commitment\n memberIndex\n newRoot\n \n # UserOp-specific fields\n sender\n userOpHash\n actualGasCost\n \n # Revenue-specific fields\n withdrawAddress\n amount\n \n # Paymaster relation\n paymaster {\n id\n address\n contractType\n network\n }\n `;\n }\n\n /**\n * ========================================\n * FILTERING METHODS\n * ========================================\n */\n\n /**\n * Filter by activity type (generic version)\n */\n byType(type: ActivityType): this {\n this.where({ type: type });\n return this;\n }\n\n /**\n * ========================================\n * TYPED FILTERING METHODS\n * ========================================\n */\n\n /**\n * Filter by activity type with type narrowing\n * Returns a typed query builder for the specific activity type\n */\n byTyped<K extends keyof ActivityTypeMap>(\n type: K\n ): ActivityQueryBuilder<ActivityTypeMap[K], SerializedActivityTypeMap[K]> {\n this.where({ type: type });\n return this as unknown as ActivityQueryBuilder<ActivityTypeMap[K], SerializedActivityTypeMap[K]>;\n }\n\n /**\n * Filter by network\n */\n byNetwork(network: string): this {\n this.where({ network: network });\n return this;\n }\n\n /**\n * Filter by paymaster address\n */\n byPaymasterAddress(address: string): this {\n this.where({ paymaster_: { address: address } });\n return this;\n }\n\n /**\n * Filter by paymaster contract type\n */\n byPaymasterType(contractType: PaymasterType): this {\n this.where({ paymaster_: { contractType: contractType } });\n return this;\n }\n\n /**\n * Filter activities after timestamp\n */\n afterTimestamp(timestamp: string | number): this {\n this.where({ timestamp_gte: timestamp.toString() });\n return this;\n }\n\n /**\n * Filter activities before timestamp\n */\n beforeTimestamp(timestamp: string | number): this {\n this.where({ timestamp_lte: timestamp.toString() });\n return this;\n }\n\n /**\n * Filter activities after block\n */\n afterBlock(block: string | number): this {\n this.where({ block_gte: block.toString() });\n return this;\n }\n\n /**\n * Filter activities before block\n */\n beforeBlock(block: string | number): this {\n this.where({ block_lte: block.toString() });\n return this;\n }\n\n /**\n * ========================================\n * DEPOSIT-SPECIFIC FILTERING\n * ========================================\n */\n\n /**\n * Filter deposit activities by depositor\n */\n byDepositor(depositor: string): this {\n this.where({ depositor: depositor });\n return this;\n }\n\n /**\n * Filter deposit activities by commitment\n */\n byCommitment(commitment: string): this {\n this.where({ commitment: commitment });\n return this;\n }\n\n /**\n * Filter deposit activities by minimum member index\n */\n withMinMemberIndex(minIndex: string): this {\n this.where({ memberIndex_gte: minIndex });\n return this;\n }\n\n /**\n * Filter deposit activities by maximum member index\n */\n withMaxMemberIndex(maxIndex: string): this {\n this.where({ memberIndex_lte: maxIndex });\n return this;\n }\n\n /**\n * ========================================\n * USER_OP-SPECIFIC FILTERING\n * ========================================\n */\n\n /**\n * Filter user operation activities by sender\n */\n bySender(sender: string): this {\n this.where({ sender: sender });\n return this;\n }\n\n /**\n * Filter user operation activities by user op hash\n */\n byUserOpHash(userOpHash: string): this {\n this.where({ userOpHash: userOpHash });\n return this;\n }\n\n /**\n * Filter user operation activities by minimum gas cost\n */\n withMinGasCost(minGasCost: string): this {\n this.where({ actualGasCost_gte: minGasCost });\n return this;\n }\n\n /**\n * Filter user operation activities by maximum gas cost\n */\n withMaxGasCost(maxGasCost: string): this {\n this.where({ actualGasCost_lte: maxGasCost });\n return this;\n }\n\n /**\n * ========================================\n * REVENUE-SPECIFIC FILTERING\n * ========================================\n */\n\n /**\n * Filter revenue withdrawal activities by withdraw address\n */\n byWithdrawAddress(withdrawAddress: string): this {\n this.where({ withdrawAddress: withdrawAddress });\n return this;\n }\n\n /**\n * Filter revenue withdrawal activities by minimum amount\n */\n withMinAmount(minAmount: string): this {\n this.where({ amount_gte: minAmount });\n return this;\n }\n\n /**\n * Filter revenue withdrawal activities by maximum amount\n */\n withMaxAmount(maxAmount: string): this {\n this.where({ amount_lte: maxAmount });\n return this;\n }\n\n /**\n * ========================================\n * TYPED CONVENIENCE FILTERS\n * ========================================\n */\n\n /**\n * Filter only deposit activities (returns typed DepositActivity[])\n */\n onlyDeposits(): ActivityQueryBuilder<DepositActivity, SerializedDepositActivity> {\n return this.byTyped('DEPOSIT');\n }\n\n /**\n * Filter only revenue withdrawal activities (returns typed RevenueActivity[])\n */\n onlyRevenueWithdrawals(): ActivityQueryBuilder<RevenueActivity, SerializedRevenueActivity> {\n return this.byTyped('REVENUE_WITHDRAWN');\n }\n\n /**\n * Filter only user operation activities\n * Note: For detailed user operations, use userOperations() query builder instead\n */\n onlyUserOps(): this {\n return this.byType('USER_OP_SPONSORED');\n }\n\n /**\n * Filter activities within a time range\n */\n betweenTimestamps(startTimestamp: string | number, endTimestamp: string | number): this