UNPKG

@noves/noves-sdk

Version:
1,281 lines (1,266 loc) 151 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; 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); // node_modules/tsup/assets/esm_shims.js import { fileURLToPath } from "url"; import path from "path"; var init_esm_shims = __esm({ "node_modules/tsup/assets/esm_shims.js"() { "use strict"; } }); // src/errors/ErrorTypes.ts var ErrorType, ERROR_MESSAGES, ERROR_STATUS_CODES; var init_ErrorTypes = __esm({ "src/errors/ErrorTypes.ts"() { "use strict"; init_esm_shims(); ErrorType = /* @__PURE__ */ ((ErrorType2) => { ErrorType2["UNAUTHORIZED"] = "UNAUTHORIZED"; ErrorType2["INVALID_API_KEY"] = "INVALID_API_KEY"; ErrorType2["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED"; ErrorType2["JOB_NOT_FOUND"] = "JOB_NOT_FOUND"; ErrorType2["JOB_PROCESSING"] = "JOB_PROCESSING"; ErrorType2["JOB_NOT_READY"] = "JOB_NOT_READY"; ErrorType2["INVALID_REQUEST"] = "INVALID_REQUEST"; ErrorType2["INVALID_RESPONSE_FORMAT"] = "INVALID_RESPONSE_FORMAT"; ErrorType2["NETWORK_ERROR"] = "NETWORK_ERROR"; ErrorType2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; ErrorType2["VALIDATION_ERROR"] = "VALIDATION_ERROR"; return ErrorType2; })(ErrorType || {}); ERROR_MESSAGES = { ["UNAUTHORIZED" /* UNAUTHORIZED */]: "Unauthorized", ["INVALID_API_KEY" /* INVALID_API_KEY */]: "Invalid API Key", ["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */]: "Rate limit exceeded", ["JOB_NOT_FOUND" /* JOB_NOT_FOUND */]: "Job does not exist", ["JOB_PROCESSING" /* JOB_PROCESSING */]: "Job is still processing. Please try again in a few moments.", ["JOB_NOT_READY" /* JOB_NOT_READY */]: "Job not ready yet", ["INVALID_REQUEST" /* INVALID_REQUEST */]: "Invalid request", ["INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */]: "Invalid response format from API", ["NETWORK_ERROR" /* NETWORK_ERROR */]: "Network error occurred", ["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: "An unknown error occurred", ["VALIDATION_ERROR" /* VALIDATION_ERROR */]: "Validation error occurred" }; ERROR_STATUS_CODES = { ["UNAUTHORIZED" /* UNAUTHORIZED */]: 401, ["INVALID_API_KEY" /* INVALID_API_KEY */]: 401, ["RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */]: 429, ["JOB_NOT_FOUND" /* JOB_NOT_FOUND */]: 404, ["JOB_PROCESSING" /* JOB_PROCESSING */]: 425, ["JOB_NOT_READY" /* JOB_NOT_READY */]: 425, ["INVALID_REQUEST" /* INVALID_REQUEST */]: 400, ["INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */]: 500, ["NETWORK_ERROR" /* NETWORK_ERROR */]: 500, ["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: 500, ["VALIDATION_ERROR" /* VALIDATION_ERROR */]: 400 }; } }); // src/errors/TransactionError.ts var TransactionError; var init_TransactionError = __esm({ "src/errors/TransactionError.ts"() { "use strict"; init_esm_shims(); init_ErrorTypes(); TransactionError = class extends Error { errors; errorType; httpStatusCode; details; /** * Creates an instance of TransactionError. * @param {Record<string, string[]>} errors - The validation errors. * @param {ErrorType} [errorType=ErrorType.UNKNOWN_ERROR] - The type of error. * @param {number} [httpStatusCode] - The HTTP status code associated with the error. * @param {any} [details] - Additional error details. */ constructor(errors, errorType = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */, httpStatusCode, details) { super(ERROR_MESSAGES[errorType] || "Transaction validation error"); this.name = "TransactionError"; this.errors = errors; this.errorType = errorType; this.httpStatusCode = httpStatusCode; this.details = details; } /** * Check if this error is of a specific type. * @param {ErrorType} type - The error type to check. * @returns {boolean} True if the error matches the specified type. */ isErrorType(type) { return this.errorType === type; } /** * Check if this error indicates a job not found. * @returns {boolean} True if the error indicates a job not found. */ isJobNotFound() { return this.errorType === "JOB_NOT_FOUND" /* JOB_NOT_FOUND */; } /** * Check if this error indicates a job is still processing. * @returns {boolean} True if the error indicates a job is processing. */ isJobProcessing() { return this.errorType === "JOB_PROCESSING" /* JOB_PROCESSING */ || this.errorType === "JOB_NOT_READY" /* JOB_NOT_READY */; } /** * Check if this error indicates rate limiting. * @returns {boolean} True if the error indicates rate limiting. */ isRateLimited() { return this.errorType === "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */; } /** * Check if this error indicates authorization issues. * @returns {boolean} True if the error indicates authorization issues. */ isUnauthorized() { return this.errorType === "UNAUTHORIZED" /* UNAUTHORIZED */ || this.errorType === "INVALID_API_KEY" /* INVALID_API_KEY */; } }; } }); // src/utils/apiUtils.ts async function retryFetch(url, options, retries = MAX_RETRIES) { try { return await fetch(url, options); } catch (error) { if (retries > 0) { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return retryFetch(url, options, retries - 1); } throw error; } } function createTranslateClient(ecosystem, apiKey) { return async function request(endpoint, method = "GET", options = {}) { const url = `${TRANSLATE_URL}/${ecosystem}/${endpoint}`; const response = await retryFetch(url, { ...options, method, headers: { ...options.headers, "apiKey": apiKey, "Content-Type": "application/json" } }); if (response.status === 429) { return { succeeded: false, response: { message: "Rate limit exceeded" }, httpStatusCode: 429, errorType: "RATE_LIMIT_EXCEEDED" }; } if (response.status === 401) { return { succeeded: false, response: { message: "Unauthorized" }, httpStatusCode: 401, errorType: "UNAUTHORIZED" }; } if (response.status === 425) { const responseData2 = await response.json(); return { succeeded: false, response: { message: "Job not ready yet", detail: responseData2.detail }, httpStatusCode: 425, errorType: "JOB_NOT_READY" }; } let responseData; try { responseData = await response.json(); } catch (error) { return { succeeded: false, response: { message: "Invalid response format" }, httpStatusCode: response.status, errorType: "INVALID_RESPONSE_FORMAT" }; } if (!response.ok) { return { succeeded: false, response: responseData, httpStatusCode: response.status }; } return { succeeded: true, response: responseData, httpStatusCode: response.status }; }; } function createForesightClient(apiKey) { return async function request(endpoint, method = "GET", options = {}) { const response = await retryFetch(`${FORESIGHT_URL}/${endpoint}`, { ...options, method, headers: { ...options.headers, "apiKey": apiKey, "Content-Type": "application/json" } }); if (response.status === 429) { return { succeeded: false, response: { message: "Rate limit exceeded" }, httpStatusCode: 429, errorType: "RATE_LIMIT_EXCEEDED" }; } if (response.status === 401) { return { succeeded: false, response: { message: "Unauthorized" }, httpStatusCode: 401, errorType: "UNAUTHORIZED" }; } let responseData; try { responseData = await response.json(); } catch (error) { return { succeeded: false, response: { message: "Invalid response format" }, httpStatusCode: response.status, errorType: "INVALID_RESPONSE_FORMAT" }; } if (!response.ok) { return { succeeded: false, response: responseData, httpStatusCode: response.status }; } return { succeeded: true, response: responseData, httpStatusCode: response.status }; }; } function createPricingClient(ecosystem, apiKey) { return async function request(endpoint, method = "GET", options = {}) { const response = await retryFetch(`${PRICING_URL}/${ecosystem}/${endpoint}`, { ...options, method, headers: { ...options.headers, "apiKey": apiKey, "Content-Type": "application/json" } }); const responseData = await response.json(); return { succeeded: response.ok, response: responseData, httpStatusCode: response.status }; }; } var TRANSLATE_URL, FORESIGHT_URL, PRICING_URL, MAX_RETRIES, RETRY_DELAY; var init_apiUtils = __esm({ "src/utils/apiUtils.ts"() { "use strict"; init_esm_shims(); TRANSLATE_URL = "https://translate.noves.fi"; FORESIGHT_URL = "https://foresight.noves.fi"; PRICING_URL = "https://pricing.noves.fi"; MAX_RETRIES = 3; RETRY_DELAY = 1e3; } }); // src/foresight/foresight.ts var foresight_exports = {}; __export(foresight_exports, { Foresight: () => Foresight }); var Foresight; var init_foresight = __esm({ "src/foresight/foresight.ts"() { "use strict"; init_esm_shims(); init_TransactionError(); init_apiUtils(); Foresight = class { request; /** * Create a Foresight instance. * @param {string} apiKey - The API key to authenticate requests. * @throws Will throw an error if the API key is not provided. */ constructor(apiKey) { if (!apiKey) { throw new Error("API key is required"); } this.request = createForesightClient(apiKey); } /** * Returns a list with the names of the EVM blockchains currently supported by this API. * Use the provided chain name when calling other methods. * @returns {Promise<EVMForesightChains>} A promise that resolves to an array of chains. */ async getChains() { const result = await this.request("evm/chains"); return result.response; } /** * Takes an unsigned transaction object and returns a fully classified transaction, * including an enriched English description of the action that is about to take place, and all relevant asset transfers tagged. * * Optionally, it takes a stateOverrides object, which allows you to customize the state of the chain before the transaction is previewed. * Useful for more advanced applications. You can skip this object to preview the transaction in the "real" state of the chain. * @param {string} chain - The chain name. * @param {EVMTranslateUnsignedTransaction} unsignedTransaction - The unsigned transaction object, modeled after the standard format used by multiple EVM wallets. * @param {EVMTranslateStateOverrides} stateOverrides - OPTIONAL. The state overrides object allows you to customize the state of the chain before the transaction is previewed. * @param {string} viewAsAccountAddress - OPTIONAL The account address from which perspective the transaction will be previewed. Leave blank to use the raw 'from' of the transaction object. * @param {number} block - OPTIONAL. The block number to preview the transaction at. Leave blank to use the latest block number. * @returns {Promise<EVMForesightPreviewResponse>} A promise that resolves to the transaction preview details. * @throws {TransactionError} If there are validation errors in the request. */ async preview(chain, unsignedTransaction, stateOverrides, viewAsAccountAddress, block) { try { let endpoint = `evm/${chain}/preview`; const queryParams = new URLSearchParams(); if (block) { queryParams.append("block", block.toString()); } if (viewAsAccountAddress) { queryParams.append("viewAsAccountAddress", viewAsAccountAddress); } if (queryParams.toString()) { endpoint += `?${queryParams.toString()}`; } const body = { transaction: unsignedTransaction, stateOverrides: stateOverrides || {} }; const result = await this.request(endpoint, "POST", { body: JSON.stringify(body) }); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } /** * Takes an ERC-4337 userOp object, and returns a classified transaction previewing what will happen if the userOp is executed. * * It includes an English description plus all relevant asset transfers tagged from the perspective of the userOp's 'sender' (the user). * @param {string} chain - The chain name. * @param {EVMTranslateUserOperation} userOperation - The ERC-4337 userOp object, in exactly the same format that would be submitted to a bundler for transaction execution. * @param {number} block - OPTIONAL. The block number to preview the userOp at. Leave blank to preview the userOp in the current state of the chain. * @returns {Promise<EVMForesightPreview4337Response>} A promise that resolves to the transaction preview details. * @throws {TransactionError} If there are validation errors in the request. */ async preview4337(chain, userOperation, block) { try { let endpoint = `evm/${chain}/preview4337`; endpoint += block ? `?block=${block}` : ""; const result = await this.request(endpoint, "POST", { body: JSON.stringify({ userOp: userOperation }) }); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } /** * Returns a description of the action that will take place if the transaction executes. * * @param {string} chain - The chain name. * @param {EVMTranslateUnsignedTransaction} unsignedTransaction - The unsigned transaction object, modeled after the standard format used by multiple EVM wallets. * @returns {Promise<EVMForesightDescribeResponse>} A promise that resolves to the transaction description. * @throws {TransactionError} If there are validation errors in the request. */ async describe(chain, unsignedTransaction) { try { let endpoint = `evm/${chain}/describe`; const result = await this.request(endpoint, "POST", { body: JSON.stringify({ transaction: unsignedTransaction }) }); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } /** * Validates that the response contains all required fields. * @param {any} response - The response to validate. * @param {string[]} requiredFields - Array of required field names. * @returns {boolean} True if all required fields are present. */ validateResponse(response, requiredFields) { return requiredFields.every((field) => response && typeof response[field] === "string"); } /** * Returns a description of what will happen if the ERC-4337 userOp object executes. * * @param {string} chain - The chain name. * @param {EVMTranslateUserOperation} userOperation - The ERC-4337 userOp object, in exactly the same format that would be submitted to a bundler for transaction execution. * @returns {Promise<EVMForesightDescribe4337Response>} A promise that resolves to the transaction description and type. * @throws {TransactionError} If there are validation errors in the request. */ async describe4337(chain, userOperation) { try { let endpoint = `evm/${chain}/describe4337`; const result = await this.request(endpoint, "POST", { body: JSON.stringify({ userOp: userOperation }) }); if (!this.validateResponse(result.response, ["description", "type"])) { throw new TransactionError({ message: ["Invalid response format"] }); } return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } if (error instanceof TransactionError) { throw error; } throw new TransactionError({ message: ["Failed to describe user operation"] }); } } /** * Screens a transaction for potential risks and provides detailed analysis. * @param {string} chain - The chain name. * @param {EVMTranslateUnsignedTransaction} unsignedTransaction - The unsigned transaction object to screen. * @returns {Promise<EVMForesightScreenResponse>} A promise that resolves to the transaction screening results. * @throws {TransactionError} If there are validation errors in the request. */ async screen(chain, unsignedTransaction) { try { const endpoint = `evm/${chain}/screen`; const result = await this.request(endpoint, "POST", { body: JSON.stringify({ transaction: unsignedTransaction }) }); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } /** * Screens an ERC-4337 user operation for potential risks and provides detailed analysis. * @param {string} chain - The chain name. * @param {EVMTranslateUserOperation} userOperation - The ERC-4337 userOp object to screen. * @returns {Promise<EVMForesightScreen4337Response>} A promise that resolves to the user operation screening results. * @throws {TransactionError} If there are validation errors in the request. */ async screen4337(chain, userOperation) { try { const endpoint = `evm/${chain}/screen4337`; const result = await this.request(endpoint, "POST", { body: JSON.stringify({ userOp: userOperation }) }); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } /** * Screens a URL for potential risks and provides detailed analysis. * @param {string} url - The URL to screen. * @returns {Promise<ForesightUrlScreenResponse>} A promise that resolves to the URL screening results. * @throws {TransactionError} If there are validation errors in the request. */ async screenUrl(url) { try { const endpoint = `url/screen?url=${encodeURIComponent(url)}`; const result = await this.request(endpoint); return result.response; } catch (error) { if (error instanceof Response) { const errorResponse = await error.json(); if (errorResponse.status === 400 && errorResponse.errors) { throw new TransactionError(errorResponse.errors); } } throw error; } } }; } }); // src/index.ts init_esm_shims(); // src/translate/translateEVM.ts init_esm_shims(); // src/translate/transactionsPage.ts init_esm_shims(); // src/translate/pagination.ts init_esm_shims(); var Pagination = class _Pagination { translate; walletAddress; chain; transactions; currentPageKeys; nextPageKeys; previousPageKeys; pageKeys; /** * Maximum number of pages to keep in navigation history to prevent cursor growth. * This limits backward navigation but prevents 413 errors on deep pagination. * Users can still navigate forward indefinitely. */ static DEFAULT_MAX_NAVIGATION_HISTORY = 10; /** * Warning threshold for cursor size in KB. * If cursor exceeds this size, a warning will be logged. */ static CURSOR_SIZE_WARNING_THRESHOLD_KB = 5; constructor(translate, initialData) { this.translate = translate; this.walletAddress = initialData.walletAddress; this.chain = initialData.chain; this.transactions = initialData.transactions || []; this.currentPageKeys = initialData.currentPageKeys; this.nextPageKeys = initialData.nextPageKeys; this.previousPageKeys = null; this.pageKeys = [initialData.currentPageKeys]; } /** * Get the current page of transactions. * @returns {T[]} The current page of transactions. */ getTransactions() { return this.transactions || []; } getCurrentPageKeys() { return this.currentPageKeys; } /** * Get the next page keys. * @returns {PageOptions | null} The next page keys or null if there is no next page. */ getNextPageKeys() { return this.nextPageKeys; } getPreviousPageKeys() { return this.previousPageKeys; } getPageKeys() { return this.pageKeys; } getPageKeyByIndex(index) { return this.pageKeys[index]; } /** * Fetch the previous page of transactions and update internal state. * @returns {Promise<boolean>} A promise that resolves to true if the previous page was fetched successfully, false otherwise. */ async previous() { const currentIndex = this.pageKeys.findIndex( (keys) => JSON.stringify(keys) === JSON.stringify(this.currentPageKeys) ); if (currentIndex <= 0) { return false; } const previousKeys = this.pageKeys[currentIndex - 1]; try { let response; if ("getTransactions" in this.translate) { response = await this.translate.getTransactions(this.chain, this.walletAddress, previousKeys); } else { response = await this.translate.Transactions(this.chain, this.walletAddress, previousKeys); } if (!response || !response.getTransactions) { return false; } const transactions = response.getTransactions(); if (!Array.isArray(transactions)) { return false; } this.transactions = transactions; this.previousPageKeys = currentIndex > 1 ? this.pageKeys[currentIndex - 2] : null; this.currentPageKeys = previousKeys; this.nextPageKeys = response.getNextPageKeys(); return true; } catch (error) { return false; } } /** * Check if there's a next page available. * @returns {boolean} True if there's a next page, false otherwise. */ hasNext() { return !!this.nextPageKeys; } /** * Check if there's a previous page available. * @returns {boolean} True if there's a previous page, false otherwise. */ hasPrevious() { const currentIndex = this.pageKeys.findIndex( (keys) => JSON.stringify(keys) === JSON.stringify(this.currentPageKeys) ); return currentIndex > 0; } /** * Get cursor information for external pagination systems. * @returns {object} Cursor information including next/previous availability and cursor strings. */ getCursorInfo() { return { hasNextPage: !!this.nextPageKeys, hasPreviousPage: this.hasPrevious(), nextCursor: this.getNextCursor(), previousCursor: this.getPreviousCursor() }; } /** * Get the next page cursor as an enhanced encoded string with navigation metadata. * @returns {string | null} Base64 encoded enhanced cursor for the next page, or null if no next page. */ getNextCursor() { if (!this.nextPageKeys) return null; const currentIndex = this.getCurrentPageIndex(); const enhancedCursorData = this.createEnhancedCursor( this.nextPageKeys, currentIndex + 1 ); return this.encodeEnhancedCursor(enhancedCursorData); } /** * Get the previous page cursor as an enhanced encoded string with navigation metadata. * @returns {string | null} Base64 encoded enhanced cursor for the previous page, or null if no previous page. */ getPreviousCursor() { if (!this.hasPrevious()) return null; const currentIndex = this.getCurrentPageIndex(); if (currentIndex <= 0) return null; const previousPageOptions = this.pageKeys[currentIndex - 1]; const enhancedCursorData = this.createEnhancedCursor( previousPageOptions, currentIndex - 1 ); return this.encodeEnhancedCursor(enhancedCursorData); } /** * Create an enhanced cursor with navigation metadata. * @param {PageOptions} targetPageOptions - The page options for the target page * @param {number} targetPageIndex - The index of the target page in navigation history * @returns {EnhancedCursorData} Enhanced cursor data with navigation metadata */ createEnhancedCursor(targetPageOptions, targetPageIndex) { const maxHistorySize = this.getMaxNavigationHistorySize(); const startIndex = Math.max(0, targetPageIndex + 1 - maxHistorySize); const navigationHistory = this.pageKeys.slice(startIndex, targetPageIndex + 1); const adjustedTargetIndex = targetPageIndex - startIndex; if (navigationHistory.length <= adjustedTargetIndex) { navigationHistory[adjustedTargetIndex] = targetPageOptions; } const cursorMeta = { currentPageIndex: adjustedTargetIndex, navigationHistory: [...navigationHistory], canGoBack: targetPageIndex > 0, canGoForward: targetPageIndex < this.pageKeys.length - 1 || !!this.nextPageKeys, previousPageOptions: adjustedTargetIndex > 0 ? navigationHistory[adjustedTargetIndex - 1] : null, nextPageOptions: targetPageIndex === this.getCurrentPageIndex() ? this.nextPageKeys : targetPageIndex < this.pageKeys.length - 1 ? this.pageKeys[targetPageIndex + 1] : null, // Store the original page index for proper navigation originalPageIndex: targetPageIndex, // Store the start index so we can reconstruct the full context if needed historyStartIndex: startIndex }; return { ...targetPageOptions, _cursorMeta: cursorMeta }; } /** * Get the maximum navigation history size. * @returns {number} Maximum number of pages to keep in navigation history */ getMaxNavigationHistorySize() { const currentPageMaxHistory = this.currentPageKeys.maxNavigationHistory; if (typeof currentPageMaxHistory === "number" && currentPageMaxHistory > 0) { return currentPageMaxHistory; } return _Pagination.DEFAULT_MAX_NAVIGATION_HISTORY; } /** * Get the current page index in the navigation history. * @returns {number} Current page index (0-based) */ getCurrentPageIndex() { return this.pageKeys.findIndex( (keys) => JSON.stringify(keys) === JSON.stringify(this.currentPageKeys) ); } /** * Encode enhanced cursor data as a Base64 string. * @param {EnhancedCursorData} enhancedCursorData - The enhanced cursor data to encode * @returns {string} Base64 encoded cursor string */ encodeEnhancedCursor(enhancedCursorData) { const jsonString = JSON.stringify(enhancedCursorData); const base64String = Buffer.from(jsonString).toString("base64"); const sizeKB = Buffer.byteLength(base64String, "utf8") / 1024; if (sizeKB > _Pagination.CURSOR_SIZE_WARNING_THRESHOLD_KB) { console.warn(`[Noves SDK] Cursor size is ${sizeKB.toFixed(2)}KB (${enhancedCursorData._cursorMeta?.navigationHistory?.length || 0} pages in history). Consider reducing maxNavigationHistory to prevent potential 413 errors.`); } return base64String; } /** * Encode PageOptions as a cursor string (legacy method for backward compatibility). * @param {PageOptions} pageKeys - The page options to encode. * @returns {string} Base64 encoded cursor string. */ encodeCursor(pageKeys) { return Buffer.from(JSON.stringify(pageKeys)).toString("base64"); } /** * Decode a cursor string back to PageOptions or EnhancedCursorData. * @param {string} cursor - The Base64 encoded cursor string. * @returns {PageOptions | EnhancedCursorData} The decoded cursor data. */ static decodeCursor(cursor) { try { const decoded = JSON.parse(Buffer.from(cursor, "base64").toString()); return decoded; } catch (error) { throw new Error("Invalid cursor format"); } } /** * Check if a decoded cursor is an enhanced cursor with navigation metadata. * @param {any} decodedCursor - The decoded cursor data * @returns {boolean} True if the cursor is enhanced, false otherwise */ static isEnhancedCursor(decodedCursor) { return decodedCursor && typeof decodedCursor === "object" && "_cursorMeta" in decodedCursor; } }; // src/translate/transactionsPage.ts var TransactionsPage = class _TransactionsPage extends Pagination { currentIndex = 0; blockNumber; tokenAddress; constructor(translate, initialData) { super(translate, initialData); this.blockNumber = initialData.blockNumber; this.tokenAddress = initialData.tokenAddress; if (initialData.navigationHistory && initialData.navigationHistory.length > 0) { this.pageKeys = [...initialData.navigationHistory]; } } /** * Get the next page keys. * @returns {PageOptions | null} The next page keys or null if there is no next page. */ getNextPageKeys() { return this.nextPageKeys; } /** * Fetch the next page of transactions and update internal state. * @returns {Promise<boolean>} A promise that resolves to true if the next page was fetched successfully, false otherwise. */ async next() { if (!this.nextPageKeys) { return false; } try { let response; if (this.walletAddress) { response = await this.translate.getTransactions(this.chain, this.walletAddress, this.nextPageKeys); } else if (this.blockNumber !== void 0) { response = await this.translate.getBlockTransactions(this.chain, this.blockNumber, this.nextPageKeys); } else if (this.tokenAddress) { response = await this.translate.getTokenHolders(this.chain, this.tokenAddress, this.nextPageKeys); } else { return false; } if (!response || !response.getTransactions) { return false; } const transactions = response.getTransactions(); if (!Array.isArray(transactions)) { return false; } this.transactions = transactions; this.previousPageKeys = this.currentPageKeys; this.currentPageKeys = this.nextPageKeys; this.nextPageKeys = response.getNextPageKeys(); this.pageKeys.push(this.currentPageKeys); this.currentIndex = 0; const maxHistorySize = this.getMaxNavigationHistorySize(); if (this.pageKeys.length > maxHistorySize) { this.pageKeys = this.pageKeys.slice(-maxHistorySize); } return true; } catch (error) { return false; } } /** * Get the maximum navigation history size from current page options. * @returns {number} Maximum number of pages to keep in navigation history */ getMaxNavigationHistorySize() { const currentPageMaxHistory = this.currentPageKeys.maxNavigationHistory; if (typeof currentPageMaxHistory === "number" && currentPageMaxHistory > 0) { return currentPageMaxHistory; } return 10; } /** * Implements the async iterator protocol. * @returns {AsyncIterator<T>} An async iterator for the transactions. */ async *[Symbol.asyncIterator]() { while (true) { if (this.currentIndex < this.transactions.length) { yield this.transactions[this.currentIndex++]; continue; } if (this.nextPageKeys) { const hasNext = await this.next(); if (!hasNext) { break; } continue; } break; } } /** * Create a TransactionsPage from a cursor string with enhanced navigation support. * This is useful for cursor-based pagination where the cursor is passed between API calls. * Enhanced cursors will preserve navigation history for proper backward navigation. * @param {any} translate - The translate instance (EVM, SVM, COSMOS, etc.) * @param {string} chain - The chain name * @param {string} address - The wallet address * @param {string} cursor - The Base64 encoded cursor string (regular or enhanced) * @returns {Promise<TransactionsPage<T>>} A new TransactionsPage instance for the cursor position */ static async fromCursor(translate, chain, address, cursor) { const decodedCursor = _TransactionsPage.decodeCursor(cursor); if (Pagination.isEnhancedCursor(decodedCursor)) { return await _TransactionsPage.fromEnhancedCursor(translate, chain, address, decodedCursor); } else { const pageOptions = decodedCursor; return await translate.getTransactions(chain, address, pageOptions); } } /** * Create a TransactionsPage from an enhanced cursor with full navigation context. * @param {any} translate - The translate instance * @param {string} chain - The chain name * @param {string} address - The wallet address * @param {EnhancedCursorData} enhancedCursor - The decoded enhanced cursor data * @returns {Promise<TransactionsPage<T>>} A new TransactionsPage instance with navigation context */ static async fromEnhancedCursor(translate, chain, address, enhancedCursor) { const { _cursorMeta, ...pageOptions } = enhancedCursor; const response = await translate.getTransactions(chain, address, pageOptions); const transactions = response.getTransactions(); const nextPageKeys = response.getNextPageKeys(); const enhancedPage = new _TransactionsPage(translate, { chain, walletAddress: address, transactions, currentPageKeys: pageOptions, nextPageKeys, navigationHistory: _cursorMeta ? _cursorMeta.navigationHistory : [pageOptions] }); if (_cursorMeta) { if (_cursorMeta.nextPageOptions) { enhancedPage.nextPageKeys = _cursorMeta.nextPageOptions; } const currentIndex = _cursorMeta.currentPageIndex; const originalIndex = _cursorMeta.originalPageIndex ?? currentIndex; const historyStartIndex = _cursorMeta.historyStartIndex ?? 0; if (currentIndex > 0 && _cursorMeta.navigationHistory.length > currentIndex) { enhancedPage.previousPageKeys = _cursorMeta.navigationHistory[currentIndex - 1]; } const reconstructedPageKeys = []; for (let i = 0; i < historyStartIndex; i++) { reconstructedPageKeys.push({}); } reconstructedPageKeys.push(..._cursorMeta.navigationHistory); enhancedPage.pageKeys = reconstructedPageKeys; const currentPageWithApiParams = response.getCurrentPageKeys ? response.getCurrentPageKeys() : pageOptions; enhancedPage.currentPageKeys = currentPageWithApiParams; const adjustedCurrentIndex = historyStartIndex + currentIndex; if (adjustedCurrentIndex < enhancedPage.pageKeys.length) { enhancedPage.pageKeys[adjustedCurrentIndex] = currentPageWithApiParams; } } return enhancedPage; } /** * Decode a cursor string back to PageOptions or EnhancedCursorData. * @param {string} cursor - The Base64 encoded cursor string. * @returns {PageOptions | EnhancedCursorData} The decoded cursor data. */ static decodeCursor(cursor) { try { const decoded = JSON.parse(Buffer.from(cursor, "base64").toString()); return decoded; } catch (error) { throw new Error("Invalid cursor format"); } } }; // src/translate/translateEVM.ts init_TransactionError(); // src/utils/urlUtils.ts init_esm_shims(); function constructUrl(endpoint, params) { if (!params || Object.keys(params).length === 0) { return endpoint; } const queryParams = new URLSearchParams(); Object.keys(params).forEach((key) => { const value = params[key]; if (value !== void 0) { queryParams.append(key, value.toString()); } }); if (params.sort === void 0) { queryParams.append("sort", "desc"); } const queryString = queryParams.toString(); return queryString ? `${endpoint}?${queryString}` : endpoint; } function parseUrl(urlString) { const baseUrl = "https://translate.noves.fi"; const fullUrl = urlString.startsWith("http") ? urlString : `${baseUrl}${urlString}`; const url = new URL(fullUrl); const params = {}; const keys = [ "startBlock", "endBlock", "startTimestamp", "endTimestamp", "sort", "viewAsAccountAddress", "liveData", "pageSize", "viewAsTransactionSender", "v5Format", "numberOfEpochs", "includePrices", "excludeZeroPrices", "ignoreTransactions", "pageKey", "pageNumber", "ascending", "marker" ]; keys.forEach((key) => { const value = url.searchParams.get(key); if (value !== null) { if (key === "sort") { params[key] = value; } else if (key === "liveData" || key === "v5Format" || key === "includePrices" || key === "excludeZeroPrices" || key === "ascending") { params[key] = value === "true"; } else if (key === "viewAsAccountAddress" || key === "ignoreTransactions" || key === "pageKey" || key === "marker") { params[key] = value; } else { params[key] = isNaN(Number(value)) ? value : Number(value); } } }); return params; } // src/translate/historyPage.ts init_esm_shims(); var HistoryPage = class extends Pagination { /** * Get the current page of transactions. * @returns {T[]} The current page of transactions. */ getTransactions() { return this.transactions || []; } /** * Get the next page keys. * @returns {PageOptions | null} The next page keys or null if there is no next page. */ getNextPageKeys() { return this.nextPageKeys; } /** * Fetch the next page of transactions and update internal state. * @returns {Promise<boolean>} A promise that resolves to true if the next page was fetched successfully, false otherwise. */ async next() { if (!this.nextPageKeys) { return false; } try { const response = await this.translate.getHistory(this.chain, this.walletAddress, this.nextPageKeys); if (!response || !response.getTransactions) { return false; } const transactions = response.getTransactions(); if (!Array.isArray(transactions)) { return false; } this.transactions = transactions; this.previousPageKeys = this.currentPageKeys; this.currentPageKeys = this.nextPageKeys; this.nextPageKeys = response.getNextPageKeys(); this.pageKeys.push(this.currentPageKeys); const maxHistorySize = this.getMaxNavigationHistorySize(); if (this.pageKeys.length > maxHistorySize) { this.pageKeys = this.pageKeys.slice(-maxHistorySize); } return true; } catch (error) { return false; } } /** * Get the previous page keys. * @returns {PageOptions | null} The previous page keys or null if there is no previous page. */ getPreviousPageKeys() { return this.previousPageKeys; } /** * Get all page keys. * @returns {PageOptions[]} All page keys. */ getPageKeys() { return this.pageKeys; } }; // src/translate/baseTranslate.ts init_esm_shims(); init_apiUtils(); init_TransactionError(); init_ErrorTypes(); var BaseTranslate = class { ecosystem; client; /** * Create a BaseTranslate instance. * @param {string} ecosystem - The blockchain ecosystem identifier. * @param {string} apiKey - The API key to authenticate requests. * @throws Will throw an error if the API key is not provided. */ constructor(ecosystem, apiKey) { if (!apiKey) { throw new Error("API key is required"); } this.ecosystem = ecosystem; this.client = createTranslateClient(ecosystem, apiKey); } /** * Make a request to the API. * @param {string} endpoint - The API endpoint to request. * @param {string} [method='GET'] - The HTTP method to use. * @param {RequestInit} [options={}] - Additional request options. * @returns {Promise<any>} The response from the API. * @throws Will throw an error if the request fails or returns an error response. */ async makeRequest(endpoint, method = "GET", options = {}) { const result = await this.client(endpoint, method, options); if (!result.succeeded) { if (result.errorType) { const errorType = result.errorType; const errors = result.response?.errors || { message: [result.response?.message || "Request failed"] }; throw new TransactionError(errors, errorType, result.httpStatusCode, result.response); } if (result.response?.message === "Unauthorized") { throw new TransactionError({ message: ["Invalid API Key"] }, "INVALID_API_KEY" /* INVALID_API_KEY */, result.httpStatusCode); } if (result.response?.message === "Rate limit exceeded") { throw new TransactionError({ message: ["Rate limit exceeded"] }, "RATE_LIMIT_EXCEEDED" /* RATE_LIMIT_EXCEEDED */, result.httpStatusCode); } if (result.response?.message === "Job not ready yet") { throw new TransactionError({ message: ["Job is still processing. Please try again in a few moments."] }, "JOB_NOT_READY" /* JOB_NOT_READY */, result.httpStatusCode); } if (result.response?.message === "Invalid response format") { throw new TransactionError({ message: ["Invalid response format from API"] }, "INVALID_RESPONSE_FORMAT" /* INVALID_RESPONSE_FORMAT */, result.httpStatusCode); } if (result.response?.message && typeof result.response.message === "string" && result.response.message.includes("does not exist")) { throw new TransactionError({ message: [result.response.message] }, "JOB_NOT_FOUND" /* JOB_NOT_FOUND */, result.httpStatusCode); } if (result.response?.message && typeof result.response.message === "string" && result.response.message.includes("Job is still processing")) { throw new TransactionError({ message: [result.response.message] }, "JOB_PROCESSING" /* JOB_PROCESSING */, result.httpStatusCode); } if (result.response?.errors) { throw new TransactionError(result.response.errors, "VALIDATION_ERROR" /* VALIDATION_ERROR */, result.httpStatusCode); } if (result.response?.status === 400 && result.response?.errors) { throw new TransactionError({ message: [result.response.title || "Request failed"] }, "INVALID_REQUEST" /* INVALID_REQUEST */, result.httpStatusCode); } if (result.response?.detail) { throw new TransactionError({ message: [result.response.detail] }, "UNKNOWN_ERROR" /* UNKNOWN_ERROR */, result.httpStatusCode); } throw new TransactionError({ message: [result.response?.message || "Request failed"] }, "UNKNOWN_ERROR" /* UNKNOWN_ERROR */, result.httpStatusCode); } if (!result.response) { return Array.isArray(result.response) ? [] : {}; } return result.response; } /** * Validate the response structure. * @param {any} response - The response to validate. * @param {string[]} requiredFields - The required fields in the response. * @returns {boolean} True if the response is valid, false otherwise. */ validateResponse(response, requiredFields) { if (!response) { return false; } for (const field of requiredFields) { if (!(field in response)) { return false; } } return true; } }; // src/translate/translateEVM.ts var ECOSYSTEM = "evm"; var TranslateEVM = class extends BaseTranslate { /** * Create a TranslateEVM instance. * @param {string} apiKey - The API key to authenticate requests. * @throws Will throw an error if the API key is not provided. */ constructor(apiKey) { super(ECOSYSTEM, apiKey); } /** * Returns a list with the names of the EVM blockchains currently supported by this API. * Use the provided chain name when calling other methods. * @returns {Promise<EVMTranslateChains>} A promise that resolves to an array of chains. */ async getChains() { try { const result = await this.makeRequest("chains"); if (!Array.isArray(result)) { throw new TransactionError({ message: ["Invalid response format"] }); } return result; } catch (error) { if (error instanceof TransactionError) { throw error; } throw new TransactionError({ message: ["Failed to get chains"] }); } } /** * For any given transaction, it returns only the description and the type. * Useful in cases where you're pulling a large number of transactions but only need this data for purposes of displaying on a UI or similar. * @param {string} chain - The chain name. * @param {string} txHash - The transaction hash. * @param {string} viewAsAccountAddress - OPTIONAL - Results are returned with the view/perspective of this wallet address. * @returns {Promise<EVMTranslateDescribeTransaction>} A promise that resolves to the transaction description. * @throws {TransactionError} If there are validation errors in the request. */ async describeTransaction(chain, txHash, viewAsAccountAddress) { try { const validatedChain = chain.toLowerCase() === "ethereum" ? "eth" : chain.toLowerCase(); let endpoint = `${validatedChain}/describeTx/${txHash}`; if (viewAsAccountAddress) { endpoint += `?viewAsAccountAddress=${encodeURIComponent(viewAsAccountAddress)}`; } const result = await this.makeRequest(endpoint); if (!this.validateResponse(result, ["description", "type"])) { throw new TransactionError({ message: ["Invalid response format"] }); } return result; } catch (error) { if (error instanceof TransactionError) { throw error; } throw new TransactionError({ message: ["Failed to describe transaction"] }); } } /** * For a list of transactions, returns their descriptions and types. * Useful in cases where you need to describe multiple transactions at once. * @param {string} chain - The chain name. * @param {string[]} txHashes - Array of transaction hashes. * @param {string} viewAsAccountAddress - OPTIONAL - Results are returned with the view/perspective of this wallet address. * @returns {Promise<EVMTranslateDescribeTransactions[]>} A promise that resolves to an array of transaction descriptions with txHash. * @throws {TransactionError} If there are validation errors in the request. */ async describeTransactions(chain, txHashes, viewAsAccountAddress) { try { const validatedChain = chain.toLowerCase() === "ethereum" ? "eth" : chain.toLowerCase(); let endpoint = `${validatedChain}/describeTxs`; if (viewAsAccountAddress) { endpoint += `?viewAsAccountAddress=${encodeURIComponent(viewAsAccountAddress)}`; } const result = await this.makeRequest(endpoint, "POST", { body: JSON.stringify(txHashes) }); if (!Array.isArray(result)) { throw new TransactionError({ message: ["Invalid response format"] }); } for (const item of result) { if (!this.validateResponse(item, ["txHash", "type", "description"])) { throw new TransactionError({ message: ["Invalid transaction description format"] }); } } return result; } catch (error) { if (error instanceof TransactionError) { throw error; } throw new TransactionError({ message: ["Failed to describe transactions"] }); } } /** * Returns all of the available transaction information for