@prepaid-gas/data
Version:
Data layer for Prepaid Gas with fluent query builders
734 lines (729 loc) • 22.8 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/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