UNPKG

@prepaid-gas/data

Version:

Data layer for Prepaid Gas with fluent query builders

734 lines (729 loc) 22.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/query/builders/user-operation-query-builder.ts var user_operation_query_builder_exports = {}; __export(user_operation_query_builder_exports, { UserOperationQueryBuilder: () => UserOperationQueryBuilder, getExpensiveUserOperations: () => getExpensiveUserOperations, getRecentUserOperations: () => getRecentUserOperations, getUserOperationByHash: () => getUserOperationByHash, getUserOperationsByPaymaster: () => getUserOperationsByPaymaster, getUserOperationsBySender: () => getUserOperationsBySender }); module.exports = __toCommonJS(user_operation_query_builder_exports); // src/query/builders/base-query-builder.ts var BaseQueryBuilder = class _BaseQueryBuilder { constructor(client, entityName, defaultOrderBy, defaultOrderDirection = "desc") { this.client = client; this.entityName = entityName; if (!entityName || typeof entityName !== "string") { throw new Error("Entity name must be a non-empty string"); } this.entityName = entityName; this.config.orderBy = defaultOrderBy; this.config.orderDirection = defaultOrderDirection; } config = {}; // Safety limits static MAX_SAFE_LIMIT = 1e3; static DEFAULT_LIMIT = 100; // ======================================== // COMMON FLUENT API METHODS // ======================================== /** * Select specific fields to be returned * @param fields - Array of field names to select * @returns The query builder instance for chaining */ select(fields) { if (!Array.isArray(fields)) { throw new Error("Fields must be an array"); } if (fields.length === 0) { throw new Error("Fields array cannot be empty"); } this.config.selectedFields = fields; return this; } /** * Add where conditions to the query * @param where - Object representing where conditions * @returns The query builder instance for chaining */ where(where) { if (typeof where !== "object" || where === null) { throw new Error("Where conditions must be an object"); } this.config.where = this.deepMergeWhereConditions(this.config.where || {}, where); return this; } deepMergeWhereConditions(existing, newWhere) { const result = { ...existing }; for (const [key, value] of Object.entries(newWhere)) { if (key.endsWith("_") && typeof value === "object" && value !== null) { if (typeof result[key] === "object" && result[key] !== null) { result[key] = { ...result[key], ...value }; } else { result[key] = value; } } else { result[key] = value; } } return result; } /** * Limit the number of results * @param limit - Maximum number of results (capped at 1000 for safety) * @returns The query builder instance for chaining */ limit(limit) { if (!Number.isInteger(limit) || limit <= 0) { throw new Error("Limit must be a positive integer"); } const safeLimit = Math.min(limit, _BaseQueryBuilder.MAX_SAFE_LIMIT); if (safeLimit < limit) { console.warn(`Limit ${limit} exceeds maximum ${_BaseQueryBuilder.MAX_SAFE_LIMIT}, using ${safeLimit}`); } this.config.first = safeLimit; return this; } /** * Skip a number of results for pagination * @param skip - Number of results to skip * @returns The query builder instance for chaining */ skip(skip) { if (!Number.isInteger(skip) || skip < 0) { throw new Error("Skip must be a non-negative integer"); } this.config.skip = skip; return this; } /** * Set the ordering for the results * @param orderBy - Field to order by * @param direction - Order direction ("asc" or "desc") * @returns The query builder instance for chaining */ orderBy(orderBy, direction = "desc") { if (!orderBy || typeof orderBy !== "string") { throw new Error("OrderBy field must be a non-empty string"); } if (direction !== "asc" && direction !== "desc") { throw new Error('Order direction must be "asc" or "desc"'); } this.config.orderBy = orderBy; this.config.orderDirection = direction; return this; } // ======================================== // EXECUTION METHODS // ======================================== /** * Execute the query and return raw entities * @returns Promise resolving to an array of TEntity */ async execute() { if (!this.config.first) { this.config.first = _BaseQueryBuilder.DEFAULT_LIMIT; } const query = this.buildDynamicQuery(); const variables = this.buildVariables(); const result = await this.client.executeQuery(query, variables); const data = result[this.entityName] || []; if (data.length === this.config.first) { console.warn(`Query returned maximum results (${this.config.first}). Consider using pagination.`); } return data; } /** * Execute the query and return properly typed serialized entities * @returns Promise resolving to an array of TSerializedEntity (not any!) */ async executeAndSerialize() { const rawResults = await this.execute(); const serializer = this.getSerializer(); return rawResults.map((entity) => serializer(entity)); } /** * Execute the query and return the first result * @returns Promise resolving to the first TEntity or null */ async first() { const originalFirst = this.config.first; this.config.first = 1; try { const results = await this.execute(); return results.length > 0 ? results[0] ?? null : null; } finally { this.config.first = originalFirst; } } /** * Execute the query and return the first result as serialized entity * @returns Promise resolving to the first TSerializedEntity or null */ async firstSerialized() { const entity = await this.first(); if (!entity) return null; const serializer = this.getSerializer(); return serializer(entity); } /** * Execute the query and check if any results exist * @returns Promise resolving to true if any results exist */ async exists() { const result = await this.first(); return result !== null; } /** * Execute the query and return the count of results * Note: This fetches all results and counts them * @returns Promise resolving to the count of matching entities */ async count() { const results = await this.execute(); return results.length; } /** * Clone the current query builder instance * @returns A new instance of the query builder with the same configuration */ clone() { const cloned = new this.constructor( this.client, this.entityName, this.config.orderBy, this.config.orderDirection ); cloned.config = { ...this.config, where: this.config.where ? { ...this.config.where } : void 0, selectedFields: this.config.selectedFields ? [...this.config.selectedFields] : void 0 }; return cloned; } // ======================================== // UTILITY METHODS // ======================================== /** * Get current query configuration (for debugging) * @returns Current configuration object */ getConfig() { return { ...this.config }; } /** * Reset the query builder to default state * @returns The query builder instance for chaining */ reset() { this.config = { orderBy: this.config.orderBy, // Keep default orderBy orderDirection: this.config.orderDirection // Keep default direction }; return this; } }; // src/transformers/index.ts function convertBigIntsToStrings(obj) { if (typeof obj === "bigint") { return obj.toString(); } if (obj === null || obj === void 0) { return obj; } if (Array.isArray(obj)) { return obj.map((item) => convertBigIntsToStrings(item)); } if (typeof obj === "object") { const result = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { result[key] = convertBigIntsToStrings(obj[key]); } } return result; } return obj; } function serializeUserOperation(entity) { return convertBigIntsToStrings(entity); } // src/query/builders/user-operation-query-builder.ts var UserOperationQueryBuilder = class extends BaseQueryBuilder { constructor(subgraphClient) { super(subgraphClient, "userOperations", "timestamp", "desc"); this.subgraphClient = subgraphClient; } buildDynamicQuery() { const fields = this.config.selectedFields?.join("\n ") || this.getDefaultFields(); const variables = this.getVariableDeclarations(); const whereClause = this.buildWhereClauseString(); const orderByClause = this.config.orderBy ? `orderBy: ${this.config.orderBy}` : ""; const orderDirectionClause = this.config.orderDirection ? `orderDirection: ${this.config.orderDirection}` : ""; const args = [whereClause, orderByClause, orderDirectionClause, "first: $first", "skip: $skip"].filter(Boolean).join(", "); const queryName = `GetUserOperations`; return ` query ${queryName}(${variables}) { userOperations(${args}) { ${fields} } } `; } buildVariables() { const variables = { first: this.config.first || 100, skip: this.config.skip || 0 }; if (this.config.where) { this.addWhereVariables(this.config.where, variables); } return variables; } buildWhereClauseString() { if (!this.config.where || Object.keys(this.config.where).length === 0) { return ""; } const conditions = this.buildWhereConditions(this.config.where); return conditions.length > 0 ? `where: { ${conditions.join(", ")} }` : ""; } getSerializer() { return serializeUserOperation; } getVariableDeclarations() { const declarations = ["$first: Int!", "$skip: Int!"]; if (this.config.where) { this.addVariableDeclarations(this.config.where, declarations); } return declarations.join(", "); } addVariableDeclarations(where, declarations) { for (const [key, value] of Object.entries(where)) { switch (key) { case "id": declarations.push("$id: ID"); break; case "hash": declarations.push("$hash: String"); break; case "network": declarations.push("$network: String"); break; case "paymaster": declarations.push("$paymaster: String"); break; case "sender": declarations.push("$sender: String"); break; case "nullifier": declarations.push("$nullifier: String"); break; case "actualGasCost_gte": case "actualGasCost_lte": declarations.push(`$${key}: String`); break; case "block_gte": case "block_lte": declarations.push(`$${key}: String`); break; case "timestamp_gte": case "timestamp_lte": declarations.push(`$${key}: String`); break; case "paymaster_": if (typeof value === "object" && value) { if ("address" in value) { declarations.push("$paymasterAddress: String"); } if ("contractType" in value) { declarations.push("$paymasterContractType: String"); } if ("network" in value) { declarations.push("$paymasterNetwork: String"); } } break; } } } addWhereVariables(where, variables) { for (const [key, value] of Object.entries(where)) { switch (key) { case "id": case "hash": case "network": case "paymaster": case "sender": case "nullifier": case "actualGasCost_gte": case "actualGasCost_lte": case "block_gte": case "block_lte": case "timestamp_gte": case "timestamp_lte": variables[key] = value; break; case "paymaster_": if (typeof value === "object" && value) { if ("address" in value) { variables.paymasterAddress = value.address; } if ("contractType" in value) { variables.paymasterContractType = value.contractType; } if ("network" in value) { variables.paymasterNetwork = value.network; } } break; } } } buildWhereConditions(where) { const conditions = []; for (const [key, value] of Object.entries(where)) { switch (key) { case "paymaster_": if (typeof value === "object" && value) { const nestedConditions = []; if ("address" in value) { nestedConditions.push("address: $paymasterAddress"); } if ("contractType" in value) { nestedConditions.push("contractType: $paymasterContractType"); } if ("network" in value) { nestedConditions.push("network: $paymasterNetwork"); } if (nestedConditions.length > 0) { conditions.push(`paymaster_: { ${nestedConditions.join(", ")} }`); } } break; default: conditions.push(`${key}: $${key}`); break; } } return conditions; } /** * Override default fields for UserOperation entity. */ getDefaultFields() { return ` id hash network chainId sender actualGasCost nullifier block transaction timestamp # Paymaster relation paymaster { id address contractType network } `; } /** * ======================================== * FILTERING METHODS * ======================================== */ /** * Filter by network */ byNetwork(network) { this.where({ network }); return this; } /** * Filter by user operation hash */ byHash(hash) { this.where({ hash }); return this; } /** * Filter by composite ID (network-contractAddress-userOpHash) */ byId(id) { this.where({ id }); return this; } /** * Filter by paymaster ID */ byPaymaster(paymasterId) { this.where({ paymaster: paymasterId }); return this; } /** * Filter by paymaster address */ byPaymasterAddress(address) { this.where({ paymaster_: { address } }); return this; } /** * Filter by paymaster contract type */ byPaymasterType(contractType) { this.where({ paymaster_: { contractType } }); return this; } /** * Filter by sender address */ bySender(sender) { this.where({ sender }); return this; } /** * Filter by nullifier */ byNullifier(nullifier) { this.where({ nullifier }); return this; } /** * Filter by minimum gas cost */ withMinGasCost(minGasCost) { this.where({ actualGasCost_gte: minGasCost }); return this; } /** * Filter by maximum gas cost */ withMaxGasCost(maxGasCost) { this.where({ actualGasCost_lte: maxGasCost }); return this; } /** * Filter user operations after timestamp */ afterTimestamp(timestamp) { this.where({ timestamp_gte: timestamp.toString() }); return this; } /** * Filter user operations before timestamp */ beforeTimestamp(timestamp) { this.where({ timestamp_lte: timestamp.toString() }); return this; } /** * Filter user operations after block */ afterBlock(block) { this.where({ block_gte: block.toString() }); return this; } /** * Filter user operations before block */ beforeBlock(block) { this.where({ block_lte: block.toString() }); return this; } /** * Filter user operations within a time range */ betweenTimestamps(startTimestamp, endTimestamp) { this.afterTimestamp(startTimestamp); this.beforeTimestamp(endTimestamp); return this; } /** * Filter user operations within a block range */ betweenBlocks(startBlock, endBlock) { this.afterBlock(startBlock); this.beforeBlock(endBlock); return this; } /** * ======================================== * ORDERING METHODS * ======================================== */ /** * Order by timestamp (most recent first by default) */ orderByTimestamp(direction = "desc") { this.orderBy("timestamp", direction); return this; } /** * Order by block number */ orderByBlock(direction = "desc") { this.orderBy("block", direction); return this; } /** * Order by gas cost (highest first by default) */ orderByGasCost(direction = "desc") { this.orderBy("actualGasCost", direction); return this; } /** * Order by sender address */ orderBySender(direction = "asc") { this.orderBy("sender", direction); return this; } /** * Order by nullifier */ orderByNullifier(direction = "asc") { this.orderBy("nullifier", direction); return this; } /** * ======================================== * ANALYTICS METHODS * ======================================== */ /** * Get gas statistics for user operations */ async getGasStatistics() { const operations = await this.execute(); const totalOperations = operations.length; const totalGasCost = operations.reduce((sum, op) => sum + BigInt(op.actualGasCost), 0n); const averageGasCost = totalOperations > 0 ? totalGasCost / BigInt(totalOperations) : 0n; const gasCosts = operations.map((op) => BigInt(op.actualGasCost)); const minGasCost = gasCosts.length > 0 ? gasCosts.reduce((min, cost) => cost < min ? cost : min) : 0n; const maxGasCost = gasCosts.length > 0 ? gasCosts.reduce((max, cost) => cost > max ? cost : max) : 0n; const sortedGasCosts = gasCosts.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); const medianGasCost = sortedGasCosts.length > 0 ? sortedGasCosts[Math.floor(sortedGasCosts.length / 2)] : 0n; return { totalOperations, totalGasCost: totalGasCost.toString(), averageGasCost: averageGasCost.toString(), minGasCost: minGasCost.toString(), maxGasCost: maxGasCost.toString(), medianGasCost: medianGasCost?.toString() ?? "0" }; } /** * Get user operation timeline */ async getOperationTimeline(days = 30) { const endTime = Math.floor(Date.now() / 1e3); const startTime = endTime - days * 24 * 60 * 60; const operations = await this.afterTimestamp(startTime).orderByTimestamp("asc").execute(); const timeline = {}; for (const op of operations) { const date = new Date(Number(op.timestamp) * 1e3).toISOString().split("T")[0]; if (!timeline[date]) { timeline[date] = { operations: 0, totalGasCost: 0n, senders: /* @__PURE__ */ new Set() }; } timeline[date].operations += 1; timeline[date].totalGasCost += BigInt(op.actualGasCost); timeline[date].senders.add(op.sender); } return Object.entries(timeline).map(([date, stats]) => ({ date, operations: stats.operations, totalGasCost: stats.totalGasCost.toString(), averageGasCost: stats.operations > 0 ? (stats.totalGasCost / BigInt(stats.operations)).toString() : "0", uniqueSenders: stats.senders.size })); } /** * Get sender analysis */ async getSenderAnalysis() { const operations = await this.execute(); const senderStats = {}; for (const op of operations) { const currentSenderStats = senderStats[op.sender] || { operationCount: 0, totalGasCost: 0n, timestamps: [] }; currentSenderStats.operationCount += 1; currentSenderStats.totalGasCost += BigInt(op.actualGasCost); const timestamp = Number(op.timestamp); if (!isNaN(timestamp)) { currentSenderStats.timestamps.push(timestamp); } senderStats[op.sender] = currentSenderStats; } return Object.entries(senderStats).map(([sender, stats]) => { const sortedTimestamps = (stats.timestamps ?? []).sort((a, b) => a - b); const averageGasCost = stats.operationCount > 0 ? stats.totalGasCost / BigInt(stats.operationCount) : 0n; return { sender, operationCount: stats.operationCount, totalGasCost: stats.totalGasCost.toString(), averageGasCost: averageGasCost.toString(), firstOperation: sortedTimestamps.length > 0 ? sortedTimestamps[0]?.toString() ?? "0" : "0", lastOperation: sortedTimestamps.length > 0 ? sortedTimestamps[sortedTimestamps.length - 1]?.toString() ?? "0" : "0" }; }).sort((a, b) => b.operationCount - a.operationCount); } }; async function getUserOperationByHash(client, hash, network) { return new UserOperationQueryBuilder(client).byNetwork(network).byHash(hash).first(); } async function getRecentUserOperations(client, network, limit = 10) { return new UserOperationQueryBuilder(client).byNetwork(network).orderByTimestamp().limit(limit).execute(); } async function getUserOperationsBySender(client, sender, network) { return new UserOperationQueryBuilder(client).byNetwork(network).bySender(sender).orderByTimestamp().execute(); } async function getUserOperationsByPaymaster(client, paymasterAddress, network) { return new UserOperationQueryBuilder(client).byNetwork(network).byPaymasterAddress(paymasterAddress).orderByTimestamp().execute(); } async function getExpensiveUserOperations(client, network, minGasCost, limit = 10) { return new UserOperationQueryBuilder(client).byNetwork(network).withMinGasCost(minGasCost).orderByGasCost().limit(limit).execute(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { UserOperationQueryBuilder, getExpensiveUserOperations, getRecentUserOperations, getUserOperationByHash, getUserOperationsByPaymaster, getUserOperationsBySender }); //# sourceMappingURL=activity-query-builder.js.map