@prepaid-gas/data
Version:
Data layer for Prepaid Gas with fluent query builders
1,775 lines (1,758 loc) • 50.9 kB
JavaScript
"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