UNPKG

permamind

Version:

An MCP server that provides an immortal memory layer for AI agents and clients

525 lines (524 loc) 17.5 kB
export class ArweaveGraphQLService { debugMode = process.env.DEBUG === "true"; endpoints = [ { name: "Goldsky", priority: 1, url: "https://arweave-search.goldsky.com/graphql", }, { name: "Arweave.net", priority: 2, url: "https://arweave.net/graphql", }, ]; fetchTimeoutMs = 30000; // 30 seconds maxRetries = 2; /** * Execute a custom GraphQL query with automatic endpoint fallback */ async executeCustomQuery(query, variables) { return this.executeGraphQLRequest(query, variables); } /** * Get detailed information for a specific block */ async getBlockData(blockId) { try { const response = await this.executeGraphQLRequest(` query GetBlock($id: String!) { block(id: $id) { id timestamp height previous } } `, { id: blockId }); if (response.errors) { throw new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`); } const block = response.data?.block; return block ? this.formatBlock(block) : null; } catch (error) { throw new Error(`Failed to get block data for ${blockId}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Get detailed information for a specific transaction */ async getTransactionData(txId) { try { const response = await this.executeGraphQLRequest(` query GetTransaction($id: ID!) { transaction(id: $id) { id anchor signature recipient owner { address key } fee { winston ar } quantity { winston ar } data { size type } tags { name value } block { id timestamp height previous } bundledIn { id } ingested_at } } `, { id: txId }); if (response.errors) { throw new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`); } const transaction = response.data?.transaction; return transaction ? this.formatTransaction(transaction) : null; } catch (error) { throw new Error(`Failed to get transaction data for ${txId}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Query AO process messages using process-specific filters */ async queryAOProcessMessages(processQuery) { try { const variables = { first: processQuery.first || 10, sort: processQuery.sort || "INGESTED_AT_DESC", }; if (processQuery.after) { variables.after = processQuery.after; } // Build tags array for AO process message filtering const tags = [ { name: "Data-Protocol", values: ["ao"] }, ]; if (processQuery.fromProcessId) { tags.push({ name: "From-Process", values: [processQuery.fromProcessId], }); } if (processQuery.toProcessId) { tags.push({ name: "To-Process", values: [processQuery.toProcessId] }); } if (processQuery.msgRefs && processQuery.msgRefs.length > 0) { tags.push({ name: "Reference", values: processQuery.msgRefs }); } if (processQuery.action) { tags.push({ name: "Action", values: [processQuery.action] }); } variables.tags = tags; const query = ` query GetAOMessages( $tags: [TagFilter!] $first: Int $after: String $sort: SortOrder ) { transactions( tags: $tags first: $first after: $after sort: $sort ) { pageInfo { hasNextPage } edges { cursor node { id anchor signature recipient owner { address key } fee { winston ar } quantity { winston ar } data { size type } tags { name value } block { id timestamp height previous } bundledIn { id } ingested_at } } } } `; const response = await this.executeGraphQLRequest(query, variables); if (response.errors) { throw new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`); } const transactionData = response.data?.transactions; if (!transactionData) { return { pageInfo: { hasNextPage: false }, transactions: [] }; } return { pageInfo: transactionData.pageInfo, transactions: transactionData.edges.map((edge) => this.formatTransaction(edge.node)), }; } catch (error) { throw new Error(`Failed to query AO process messages: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Query blocks with filtering and pagination */ async queryBlocks(blockQuery) { try { const variables = { first: blockQuery.first || 10, sort: blockQuery.sort || "HEIGHT_DESC", }; if (blockQuery.ids) { variables.ids = blockQuery.ids; } if (blockQuery.height) { variables.height = blockQuery.height; } if (blockQuery.after) { variables.after = blockQuery.after; } const query = ` query GetBlocks( $ids: [ID!] $height: BlockFilter $first: Int $after: String $sort: SortOrder ) { blocks( ids: $ids height: $height first: $first after: $after sort: $sort ) { pageInfo { hasNextPage } edges { cursor node { id timestamp height previous } } } } `; const response = await this.executeGraphQLRequest(query, variables); if (response.errors) { throw new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`); } const blockData = response.data?.blocks; if (!blockData) { return { blocks: [], pageInfo: { hasNextPage: false } }; } return { blocks: blockData.edges.map((edge) => this.formatBlock(edge.node)), pageInfo: blockData.pageInfo, }; } catch (error) { throw new Error(`Failed to query blocks: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Query transactions with comprehensive filtering and pagination */ async queryTransactions(transactionQuery) { try { // Build the query dynamically based on provided parameters const queryParts = []; const variables = {}; // Add pagination if (transactionQuery.first) { queryParts.push("first: $first"); variables.first = transactionQuery.first; } else { queryParts.push("first: 10"); // Default to 10 } // Add cursor for pagination if (transactionQuery.after) { queryParts.push("after: $after"); variables.after = transactionQuery.after; } // Add sort order if (transactionQuery.sort) { queryParts.push("sort: $sort"); variables.sort = transactionQuery.sort; } // Add filters if (transactionQuery.ids) { queryParts.push("ids: $ids"); variables.ids = transactionQuery.ids; } if (transactionQuery.owners) { queryParts.push("owners: $owners"); variables.owners = transactionQuery.owners; } if (transactionQuery.recipients) { queryParts.push("recipients: $recipients"); variables.recipients = transactionQuery.recipients; } if (transactionQuery.tags) { queryParts.push("tags: $tags"); variables.tags = transactionQuery.tags; } if (transactionQuery.block) { queryParts.push("block: $block"); variables.block = transactionQuery.block; } if (transactionQuery.ingested_at) { queryParts.push("ingested_at: $ingested_at"); variables.ingested_at = transactionQuery.ingested_at; } // Build the complete query using the exact structure from documentation const query = ` query GetTransactions${Object.keys(variables).length > 0 ? `(${Object.keys(variables) .map((key) => { switch (key) { case "after": return "$after: String"; case "block": return "$block: BlockFilter"; case "first": return "$first: Int"; case "ids": return "$ids: [ID!]"; case "ingested_at": return "$ingested_at: IngestedAtFilter"; case "owners": return "$owners: [String!]"; case "recipients": return "$recipients: [String!]"; case "sort": return "$sort: SortOrder"; case "tags": return "$tags: [TagFilter!]"; default: return `$${key}: String`; } }) .join(", ")})` : ""} { transactions${queryParts.length > 0 ? `(${queryParts.join(", ")})` : ""} { pageInfo { hasNextPage } edges { cursor node { id anchor signature recipient owner { address key } fee { winston ar } quantity { winston ar } data { size type } tags { name value } block { id timestamp height previous } parent { id } } } } } `; const response = await this.executeGraphQLRequest(query, variables); if (response.errors) { throw new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`); } const transactionData = response.data?.transactions; if (!transactionData) { return { pageInfo: { hasNextPage: false }, transactions: [] }; } return { pageInfo: transactionData.pageInfo, transactions: transactionData.edges.map((edge) => this.formatTransaction(edge.node)), }; } catch (error) { throw new Error(`Failed to query transactions: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Execute GraphQL request with automatic endpoint fallback and retry logic */ async executeGraphQLRequest(query, variables) { let lastError = new Error("No endpoints available"); // Try each endpoint with retry logic for (const endpoint of this.endpoints.sort((a, b) => a.priority - b.priority)) { for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { const response = await this.executeRequest(endpoint.url, query, variables); return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry on timeout errors or GraphQL errors - they're likely to fail again const isTimeout = lastError.message.includes("timed out") || lastError.message.includes("aborted"); const isGraphQLError = lastError.message.includes("GraphQL errors"); if (isTimeout || isGraphQLError) { break; // Try next endpoint } if (attempt < this.maxRetries) { const delayMs = Math.pow(2, attempt) * 1000; await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } } throw new Error(`All GraphQL endpoints failed. Last error: ${lastError.message}`); } /** * Execute a single GraphQL request to a specific endpoint */ async executeRequest(url, query, variables) { // Create AbortController for timeout const abortController = new AbortController(); const timeoutId = setTimeout(() => { abortController.abort(); }, this.fetchTimeoutMs); try { const requestBody = { query, ...(variables && { variables }), }; const response = await fetch(url, { body: JSON.stringify(requestBody), headers: { Accept: "application/json", "Content-Type": "application/json", }, method: "POST", signal: abortController.signal, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = (await response.json()); // Check for GraphQL errors but don't throw immediately - let caller handle // GraphQL errors are handled by the caller return result; } catch (error) { if (error instanceof Error && error.name === "AbortError") { throw new Error(`Request timed out after ${this.fetchTimeoutMs}ms`); } throw error; } finally { clearTimeout(timeoutId); } } /** * Format block data with human-readable timestamps */ formatBlock(block) { return { ...block, timestampFormatted: this.formatTimestamp(block.timestamp), }; } /** * Format Unix timestamp to UTC string */ formatTimestamp(timestamp) { return new Date(timestamp * 1000).toISOString(); } /** * Format transaction data with human-readable timestamps */ formatTransaction(transaction) { const formatted = { ...transaction }; // Format block timestamp if present if (formatted.block?.timestamp) { formatted.block = { ...formatted.block, timestampFormatted: this.formatTimestamp(formatted.block.timestamp), }; } // Format ingested_at timestamp if present if (formatted.ingested_at) { formatted.ingested_atFormatted = this.formatTimestamp(formatted.ingested_at); } return formatted; } } // Export singleton instance export const arweaveGraphQLService = new ArweaveGraphQLService();