UNPKG

@prepaid-gas/data

Version:

Data layer for Prepaid Gas with fluent query builders

1,775 lines (1,758 loc) 50.9 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/index.ts var src_exports = {}; __export(src_exports, { BaseQueryBuilder: () => BaseQueryBuilder, PaymasterContractQueryBuilder: () => PaymasterContractQueryBuilder, QueryBuilder: () => QueryBuilder, SubgraphClient: () => SubgraphClient, UserOperationQueryBuilder: () => UserOperationQueryBuilder, VERSION: () => VERSION }); module.exports = __toCommonJS(src_exports); // src/client/subgraph-client.ts var import_graphql_request = require("graphql-request"); var import_constants = require("@prepaid-gas/constants"); // 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 serializePaymasterContract(entity) { return convertBigIntsToStrings(entity); } function serializeActivity(entity) { return convertBigIntsToStrings(entity); } function serializeUserOperation(entity) { return convertBigIntsToStrings(entity); } // src/query/builders/paymaster-query-builder.ts var PaymasterContractQueryBuilder = class extends BaseQueryBuilder { constructor(subgraphClient) { super(subgraphClient, "paymasterContracts", "deployedTimestamp", "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 = `GetPaymasterContracts`; return ` query ${queryName}(${variables}) { paymasterContracts(${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 serializePaymasterContract; } 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] of Object.entries(where)) { switch (key) { case "network": declarations.push("$network: String"); break; case "contractType": declarations.push("$contractType: String"); break; case "address": declarations.push("$address: String"); break; case "id": declarations.push("$id: ID"); break; case "revenue_gte": declarations.push("$revenue_gte: String"); break; case "revenue_lte": declarations.push("$revenue_lte: String"); break; case "revenue_gt": declarations.push("$revenue_gt: String"); break; case "totalDeposit_gte": declarations.push("$totalDeposit_gte: String"); break; case "totalDeposit_lte": declarations.push("$totalDeposit_lte: String"); break; case "currentDeposit_gte": declarations.push("$currentDeposit_gte: String"); break; case "currentDeposit_lte": declarations.push("$currentDeposit_lte: String"); break; case "treeSize_gte": declarations.push("$treeSize_gte: String"); break; case "treeSize_lte": declarations.push("$treeSize_lte: String"); break; case "treeSize_gt": declarations.push("$treeSize_gt: String"); break; case "deployedTimestamp_gte": declarations.push("$deployedTimestamp_gte: String"); break; case "deployedTimestamp_lte": declarations.push("$deployedTimestamp_lte: String"); break; case "isDead": declarations.push("$isDead: Boolean"); break; } } } addWhereVariables(where, variables) { for (const [key, value] of Object.entries(where)) { switch (key) { case "network": case "contractType": case "address": case "id": case "revenue_gte": case "revenue_lte": case "revenue_gt": case "totalDeposit_gte": case "totalDeposit_lte": case "currentDeposit_gte": case "currentDeposit_lte": case "treeSize_gte": case "treeSize_lte": case "treeSize_gt": case "deployedTimestamp_gte": case "deployedTimestamp_lte": case "isDead": variables[key] = value; break; } } } buildWhereConditions(where) { const conditions = []; for (const [key] of Object.entries(where)) { conditions.push(`${key}: $${key}`); } return conditions; } /** * Override default fields for PaymasterContract entity. */ getDefaultFields() { return ` id contractType address network chainId joiningAmount scope verifier totalDeposit currentDeposit revenue root rootIndex treeDepth treeSize isDead deployedBlock deployedTransaction deployedTimestamp lastBlock lastTimestamp `; } /** * Get fields with activities included */ getFieldsWithActivities() { return ` ${this.getDefaultFields()} activities(orderBy:timestamp, orderDirection:desc) { id type network chainId block transaction timestamp # Deposit-specific fields depositor commitment memberIndex newRoot # UserOp-specific fields sender userOpHash actualGasCost # Revenue-specific fields withdrawAddress amount } `; } /** * ======================================== * FILTERING METHODS * ======================================== */ /** * Filter by network */ byNetwork(network) { this.where({ network }); return this; } /** * Filter by contract type */ byContractType(type) { this.where({ contractType: type }); return this; } /** * Filter by contract address */ byAddress(address) { this.where({ address }); return this; } /** * Filter by composite ID (network-address) */ byId(network, address) { this.where({ id: `${network}-${address}` }); return this; } /** * Filter by deployment date (after) */ deployedAfter(timestamp) { this.where({ deployedTimestamp_gte: timestamp.toString() }); return this; } /** * Filter by deployment date (before) */ deployedBefore(timestamp) { this.where({ deployedTimestamp_lte: timestamp.toString() }); return this; } /** * Filter only active paymasters (positive revenue) */ onlyActive() { this.where({ revenue_gt: "0" }); return this; } /** * Filter only alive paymasters (not dead) */ onlyAlive() { this.where({ isDead: false }); return this; } /** * Include activities in the query results * This replaces the need to manually select activities fields */ withActivities() { const fieldsArray = this.getFieldsWithActivities().split("\n").map((line) => line.trim()).filter((line) => line.length > 0); this.select(fieldsArray); return this; } /** * ======================================== * ORDERING METHODS * ======================================== */ /** * Order by revenue (highest first) */ orderByRevenue(direction = "desc") { this.orderBy("revenue", direction); return this; } /** * Order by total deposit */ orderByTotalDeposit(direction = "desc") { this.orderBy("totalDeposit", direction); return this; } /** * Order by current deposit */ orderByDeposit(direction = "desc") { this.orderBy("currentDeposit", direction); return this; } /** * Order by number of members (tree size) */ orderByMembers(direction = "desc") { this.orderBy("treeSize", direction); return this; } /** * Order by deployment date */ orderByDeployment(direction = "desc") { this.orderBy("deployedTimestamp", direction); return this; } /** * Order by last activity */ orderByActivity(direction = "desc") { this.orderBy("lastTimestamp", direction); return this; } }; // src/query/builders/activity-query-builder.ts var ActivityQueryBuilder = class extends BaseQueryBuilder { constructor(subgraphClient) { super(subgraphClient, "activities", "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 = `GetActivities`; return ` query ${queryName}(${variables}) { activities(${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 serializeActivity; } 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] of Object.entries(where)) { switch (key) { case "id": declarations.push("$id: ID"); break; case "type": declarations.push("$type: String"); break; case "network": declarations.push("$network: String"); break; case "paymaster": declarations.push("$paymaster: 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 "depositor": declarations.push("$depositor: String"); break; case "commitment": declarations.push("$commitment: String"); break; case "memberIndex_gte": case "memberIndex_lte": declarations.push(`$${key}: String`); break; case "sender": declarations.push("$sender: String"); break; case "userOpHash": declarations.push("$userOpHash: String"); break; case "actualGasCost_gte": case "actualGasCost_lte": declarations.push(`$${key}: String`); break; case "withdrawAddress": declarations.push("$withdrawAddress: String"); break; case "amount_gte": case "amount_lte": declarations.push(`$${key}: String`); break; case "paymaster_": declarations.push("$paymasterAddress: String"); declarations.push("$paymasterContractType: String"); declarations.push("$paymasterNetwork: String"); break; } } } addWhereVariables(where, variables) { for (const [key, value] of Object.entries(where)) { switch (key) { case "id": case "type": case "network": case "paymaster": case "block_gte": case "block_lte": case "timestamp_gte": case "timestamp_lte": case "depositor": case "commitment": case "memberIndex_gte": case "memberIndex_lte": case "sender": case "userOpHash": case "actualGasCost_gte": case "actualGasCost_lte": case "withdrawAddress": case "amount_gte": case "amount_lte": variables[key] = value; break; case "paymaster_": if (value && typeof value === "object") { if (value.address) variables.paymasterAddress = value.address; if (value.contractType) variables.paymasterContractType = value.contractType; if (value.network) variables.paymasterNetwork = value.network; } break; } } } buildWhereConditions(where) { const conditions = []; for (const [key, value] of Object.entries(where)) { switch (key) { case "paymaster_": if (value && typeof value === "object") { const nestedConditions = []; if (value.address) nestedConditions.push("address: $paymasterAddress"); if (value.contractType) nestedConditions.push("contractType: $paymasterContractType"); if (value.network) 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 Activity entity. */ getDefaultFields() { return ` id type network chainId block transaction timestamp # Deposit-specific fields depositor commitment memberIndex newRoot # UserOp-specific fields sender userOpHash actualGasCost # Revenue-specific fields withdrawAddress amount # Paymaster relation paymaster { id address contractType network } `; } /** * ======================================== * FILTERING METHODS * ======================================== */ /** * Filter by activity type (generic version) */ byType(type) { this.where({ type }); return this; } /** * ======================================== * TYPED FILTERING METHODS * ======================================== */ /** * Filter by activity type with type narrowing * Returns a typed query builder for the specific activity type */ byTyped(type) { this.where({ type }); return this; } /** * Filter by network */ byNetwork(network) { this.where({ network }); 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 activities after timestamp */ afterTimestamp(timestamp) { this.where({ timestamp_gte: timestamp.toString() }); return this; } /** * Filter activities before timestamp */ beforeTimestamp(timestamp) { this.where({ timestamp_lte: timestamp.toString() }); return this; } /** * Filter activities after block */ afterBlock(block) { this.where({ block_gte: block.toString() }); return this; } /** * Filter activities before block */ beforeBlock(block) { this.where({ block_lte: block.toString() }); return this; } /** * ======================================== * DEPOSIT-SPECIFIC FILTERING * ======================================== */ /** * Filter deposit activities by depositor */ byDepositor(depositor) { this.where({ depositor }); return this; } /** * Filter deposit activities by commitment */ byCommitment(commitment) { this.where({ commitment }); return this; } /** * Filter deposit activities by minimum member index */ withMinMemberIndex(minIndex) { this.where({ memberIndex_gte: minIndex }); return this; } /** * Filter deposit activities by maximum member index */ withMaxMemberIndex(maxIndex) { this.where({ memberIndex_lte: maxIndex }); return this; } /** * ======================================== * USER_OP-SPECIFIC FILTERING * ======================================== */ /** * Filter user operation activities by sender */ bySender(sender) { this.where({ sender }); return this; } /** * Filter user operation activities by user op hash */ byUserOpHash(userOpHash) { this.where({ userOpHash }); return this; } /** * Filter user operation activities by minimum gas cost */ withMinGasCost(minGasCost) { this.where({ actualGasCost_gte: minGasCost }); return this; } /** * Filter user operation activities by maximum gas cost */ withMaxGasCost(maxGasCost) { this.where({ actualGasCost_lte: maxGasCost }); return this; } /** * ======================================== * REVENUE-SPECIFIC FILTERING * ======================================== */ /** * Filter revenue withdrawal activities by withdraw address */ byWithdrawAddress(withdrawAddress) { this.where({ withdrawAddress }); return this; } /** * Filter revenue withdrawal activities by minimum amount */ withMinAmount(minAmount) { this.where({ amount_gte: minAmount }); return this; } /** * Filter revenue withdrawal activities by maximum amount */ withMaxAmount(maxAmount) { this.where({ amount_lte: maxAmount }); return this; } /** * ======================================== * TYPED CONVENIENCE FILTERS * ======================================== */ /** * Filter only deposit activities (returns typed DepositActivity[]) */ onlyDeposits() { return this.byTyped("DEPOSIT"); } /** * Filter only revenue withdrawal activities (returns typed RevenueActivity[]) */ onlyRevenueWithdrawals() { return this.byTyped("REVENUE_WITHDRAWN"); } /** * Filter only user operation activities * Note: For detailed user operations, use userOperations() query builder instead */ onlyUserOps() { return this.byType("USER_OP_SPONSORED"); } /** * Filter activities within a time range */ betweenTimestamps(startTimestamp, endTimestamp) { this.afterTimestamp(startTimestamp); this.beforeTimestamp(endTimestamp); return this; } /** * Filter activities 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 activity type */ orderByType(direction = "asc") { this.orderBy("type", direction); return this; } /** * Order by gas cost (for USER_OP_SPONSORED activities) */ orderByGasCost(direction = "desc") { this.orderBy("actualGasCost", direction); return this; } /** * Order by amount (for REVENUE_WITHDRAWN activities) */ orderByAmount(direction = "desc") { this.orderBy("amount", direction); return this; } }; function createDepositActivityQueryBuilder(client) { return new ActivityQueryBuilder(client).onlyDeposits(); } function createRevenueActivityQueryBuilder(client) { return new ActivityQueryBuilder(client).onlyRevenueWithdrawals(); } // 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); } }; // src/query/query-builder.ts var QueryBuilder = class { constructor(client) { this.client = client; } /** * ======================================== * ENTITY-SPECIFIC QUERY BUILDERS * ======================================== */ /** * Start building a query for paymaster contracts * Each contract IS a pool in the new architecture * * @returns PaymasterContractQueryBuilder for fluent query building * * @example * ```typescript * const paymasters = await client.query() * .paymasters() * .byNetwork("base-sepolia") * .byContractType("OneTimeUse") * .withMinRevenue("1000000000000000000") * .orderByRevenue() * .limit(10) * .execute(); * ``` */ paymasters() { return new PaymasterContractQueryBuilder(this.client); } /** * Start building a query for activities * Unified timeline of all events (deposits, user ops, revenue withdrawals) * * @returns ActivityQueryBuilder for fluent query building * * @example * ```typescript * // Mixed timeline view - all activity types together * const activities = await client.query() * .activities() * .byNetwork("base-sepolia") * .byPaymaster("0x456...") * .afterTimestamp("1640995200") * .orderByTimestamp() * .limit(50) * .execute(); * * // For detailed user operation data with nullifier, use userOperations() instead * ``` */ activities() { return new ActivityQueryBuilder(this.client); } /** * Start building a query for user operations * Detailed tracking for specialized analytics including nullifier data * * @returns UserOperationQueryBuilder for fluent query building * * @example * ```typescript * const userOps = await client.query() * .userOperations() * .byNetwork("base-sepolia") * .byPaymaster("0x456...") * .bySender("0x789...") * .withMinGasCost("1000000000000000") * .orderByTimestamp() * .limit(25) * .execute(); * ``` */ userOperations() { return new UserOperationQueryBuilder(this.client); } /** * ======================================== * TYPED ACTIVITY QUERY BUILDERS * ======================================== */ /** * Start building a query for deposit activities only * Returns typed DepositActivity[] results * * @returns ActivityQueryBuilder<DepositActivity> for fluent query building * * @example * ```typescript * const deposits = await client.query() * .depositActivities() * .byNetwork("base-sepolia") * .byDepositor("0x123...") * .orderByTimestamp() * .limit(20) * .execute(); * ``` */ depositActivities() { return createDepositActivityQueryBuilder(this.client); } /** * Start building a query for revenue withdrawal activities only * Returns typed RevenueActivity[] results * * @returns ActivityQueryBuilder<RevenueActivity> for fluent query building * * @example * ```typescript * const revenues = await client.query() * .revenueActivities() * .byNetwork("base-sepolia") * .byWithdrawAddress("0x789...") * .withMinAmount("5000000000000000000") * .orderByAmount() * .limit(10) * .execute(); * ``` */ revenueActivities() { return createRevenueActivityQueryBuilder(this.client); } }; // src/client/subgraph-client.ts var SubgraphClient = class _SubgraphClient { client; chainId; options; requestMap = /* @__PURE__ */ new Map(); maxPendingRequests = 100; constructor(chainId, options = {}) { const preset = (0, import_constants.getNetworkPreset)(chainId); this.chainId = chainId; this.options = options; const finalSubgraphUrl = options.subgraphUrl || preset?.defaultSubgraphUrl; if (!finalSubgraphUrl) { throw new Error( `No subgraph URL available for network(chainId: ${chainId}). Please provide one in options.subgraphUrl` ); } this.client = new import_graphql_request.GraphQLClient(finalSubgraphUrl); } /** * Generate a unique key for a query/variables combination * * @param query - GraphQL query string * @param variables - Query variables * @returns Unique key for the request */ generateRequestKey(query, variables) { const normalizedQuery = query.replace(/\s+/g, " ").trim(); const sortedVariables = Object.keys(variables).sort().reduce( (sorted, key) => { sorted[key] = variables[key]; return sorted; }, {} ); return `${normalizedQuery}|${JSON.stringify(sortedVariables)}`; } /** * Clean up completed request from the map * * @param requestKey - Key of the request to clean up */ cleanupRequest(requestKey) { this.requestMap.delete(requestKey); } /** * Clean up old requests if map gets too large */ cleanupOldRequests() { if (this.requestMap.size > this.maxPendingRequests) { const keysToDelete = Array.from(this.requestMap.keys()).slice(0, this.maxPendingRequests / 2); keysToDelete.forEach((key) => this.requestMap.delete(key)); } } /** * Create SubgraphClient for a specific network using presets * * @param chainId - The chain ID to create client for * @param options - Optional configuration overrides * @returns Configured SubgraphClient instance * * @example * ```typescript * // Create for Base Sepolia using preset * const client = SubgraphClient.createForNetwork(84532); * * // Create with custom subgraph URL override * const client = SubgraphClient.createForNetwork(84532, { * subgraphUrl: "https://custom-subgraph.com", * timeout: 60000 * }); * ``` */ static createForNetwork(chainId, options = {}) { return new _SubgraphClient(chainId, options); } /** * ✨ Create a fluent query builder instance * * This is the main entry point for the new query builder API. * Provides a fluent interface for building complex queries with type safety. * * @returns QueryBuilder for fluent query building * * @example * ```typescript * // Simple paymaster query * const paymasters = await client * .query() * .paymasters() * .byType("GasLimited") * .withMinRevenue("1000000000000000000") * .orderByRevenue() * .limit(10) * .execute(); * * // Complex pool query with members * const pools = await client * .query() * .pools() * .byPaymaster("0x456...") * .withMinMembers(10) * .withMembers(50) * .orderByPopularity() * .limit(20) * .execute(); * * // Analytics query * const analytics = await client * .query() * .dailyGlobalStats() * .forDateRange("2024-01-01", "2024-01-31") * .withMinNewPools(2) * .orderByNewest() * .execute(); * ``` */ query() { return new QueryBuilder(this); } /** * Execute a raw GraphQL query with request deduplication * * This method provides a generic interface for executing any GraphQL query, * with built-in deduplication to prevent duplicate requests for identical queries. * * @template T - The expected response type * @param query - GraphQL query string * @param variables - Query variables object * @returns Promise resolving to the query response data * * @example * ```typescript * const response = await client.executeQuery<{ pools: Pool[] }>( * 'query GetPools($first: Int!) { pools(first: $first) { id poolId } }', * { first: 10 } * ); * console.log(response.pools); * ``` */ async executeQuery(query, variables = {}) { const requestKey = this.generateRequestKey(query, variables); const existingRequest = this.requestMap.get(requestKey); if (existingRequest) { return existingRequest; } this.cleanupOldRequests(); const requestPromise = this.client.request(query, variables).then((response) => { this.cleanupRequest(requestKey); return response; }).catch((error) => { this.cleanupRequest(requestKey); throw error; }); this.requestMap.set(requestKey, requestPromise); return requestPromise; } /** * Execute multiple queries in parallel with deduplica