@noves/noves-sdk
Version:
Noves Developer Kit
1,281 lines (1,266 loc) • 151 kB
JavaScript
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