UNPKG

@aptos-labs/ts-sdk

Version:
223 lines 9.76 kB
// Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 import { VERSION } from "../version.js"; import { MimeType, } from "../types/index.js"; import { AptosApiType } from "../utils/index.js"; import { getRuntimePlatformTag } from "../utils/runtime.js"; import { AptosApiError } from "../errors/index.js"; /** * Sends a request using the specified options and returns the response. * * @param options - The options for the request. * @param options.url - The URL to send the request to. * @param options.method - The HTTP method to use for the request. * @param options.body - The body of the request. * @param options.contentType - The content type of the request. * @param options.params - The query parameters to include in the request. * @param options.overrides - Additional overrides for the request. * @param options.overrides.HEADERS - Custom headers to include in the request. * @param options.overrides.AUTH_TOKEN - The authorization token for the request. * @param options.overrides.API_KEY - The API key for the request. * @param options.overrides.http2 - Whether to use HTTP/2 for the request. * @param options.originMethod - The origin method for the request. * @param client - The client used to make the request. * * @returns The response from the request. * @group Implementation * @category Client */ export async function request(options, client) { const { url, method, body, contentType, params, overrides, originMethod } = options; const headers = { ...overrides?.HEADERS, "x-aptos-client": `aptos-typescript-sdk/${VERSION}; platform=${getRuntimePlatformTag()}`, "content-type": contentType ?? MimeType.JSON, "x-aptos-typescript-sdk-origin-method": originMethod, }; if (overrides?.AUTH_TOKEN) { headers.Authorization = `Bearer ${overrides?.AUTH_TOKEN}`; } if (overrides?.API_KEY) { headers.Authorization = `Bearer ${overrides?.API_KEY}`; } /* * make a call using the @aptos-labs/aptos-client package * {@link https://www.npmjs.com/package/@aptos-labs/aptos-client} */ return client.provider({ url, method, body, params, headers, overrides, http2: overrides?.http2, }); } const HTTP_STATUS_GONE = 410; const CREDENTIAL_HEADERS = new Set(["authorization", "cookie", "x-api-key"]); const warnedArchivalEndpoints = new Set(); /** * Registrable domain, approximated as the last two labels. * Hosts under a multi-part suffix like `co.uk` compare equal. */ function siteOf(hostname) { return hostname.toLowerCase().split(".").slice(-2).join("."); } /** * Validates the archival endpoint advertised in a pruning error, returning undefined if it is unusable. * * The endpoint is chosen by the node rather than the caller, so it must be an absolute http(s) URL that does * not downgrade an https request, and credentials are only forwarded to it when it is same-site. */ function resolveArchivalRetryTarget(originalUrl, responseData) { const advertisedEndpoint = responseData?.archival_endpoint; if (typeof advertisedEndpoint !== "string" || advertisedEndpoint.length === 0) { return undefined; } let archivalUrl; let originUrl; try { archivalUrl = new URL(advertisedEndpoint); originUrl = new URL(originalUrl); } catch { return undefined; } if (archivalUrl.protocol !== "https:" && archivalUrl.protocol !== "http:") { return undefined; } if (originUrl.protocol === "https:" && archivalUrl.protocol !== "https:") { return undefined; } return { url: advertisedEndpoint.replace(/\/+$/, ""), forwardCredentials: siteOf(archivalUrl.hostname) === siteOf(originUrl.hostname), }; } function hasCredentials(overrides) { if (!overrides) { return false; } if (overrides.API_KEY || overrides.AUTH_TOKEN) { return true; } return Object.keys(overrides.HEADERS ?? {}).some((name) => CREDENTIAL_HEADERS.has(name.toLowerCase())); } function withoutCredentials(overrides) { if (!overrides) { return overrides; } const { API_KEY, AUTH_TOKEN, HEADERS, ...rest } = overrides; if (!HEADERS) { return rest; } return { ...rest, HEADERS: Object.fromEntries(Object.entries(HEADERS).filter(([name]) => !CREDENTIAL_HEADERS.has(name.toLowerCase()))), }; } function warnOnceAboutDroppedCredentials(originalUrl, archivalUrl) { if (warnedArchivalEndpoints.has(archivalUrl)) { return; } warnedArchivalEndpoints.add(archivalUrl); console.warn(`[Aptos SDK] ${originalUrl} reported pruned data and advertised the archival endpoint ${archivalUrl}. ` + "Retrying there without credentials because it is not on the same site. If that endpoint requires " + "authentication, set it as your `fullnode` URL, or disable this retry with `archivalFallback: false`."); } /** Sends a request and shapes the client response into an `AptosResponse`, without interpreting the status. */ async function sendAptosRequest(aptosRequestOpts, aptosConfig) { const { url, path } = aptosRequestOpts; const fullUrl = path ? `${url}/${path}` : url; const clientResponse = await request({ ...aptosRequestOpts, url: fullUrl }, aptosConfig.client); return { status: clientResponse.status, statusText: clientResponse.statusText ?? "No status text provided", data: clientResponse.data, headers: clientResponse.headers, config: clientResponse.config, request: clientResponse.request, url: fullUrl, }; } /** Returns the response on success, or throws an `AptosApiError` describing the failure. */ function handleAptosResponse(aptosResponse, aptosRequestOpts, apiType) { // Handle case for `Unauthorized` error (i.e. API_KEY error) if (aptosResponse.status === 401) { throw new AptosApiError({ apiType, aptosRequest: aptosRequestOpts, aptosResponse }); } // to support both fullnode and indexer responses, // check if it is an indexer query, and adjust response.data if (apiType === AptosApiType.INDEXER) { const indexerResponse = aptosResponse.data; // Only probe for `errors`/`data` keys on actual objects; `in` throws a // `TypeError` on primitives/null and would mask the underlying API error. if (typeof indexerResponse === "object" && indexerResponse !== null) { // Handle Indexer general errors if ("errors" in indexerResponse && indexerResponse.errors) { throw new AptosApiError({ apiType, aptosRequest: aptosRequestOpts, aptosResponse, }); } if ("data" in indexerResponse) { aptosResponse.data = indexerResponse.data; } } } else if (apiType === AptosApiType.PEPPER || apiType === AptosApiType.PROVER) { if (aptosResponse.status >= 400) { throw new AptosApiError({ apiType, aptosRequest: aptosRequestOpts, aptosResponse }); } } if (aptosResponse.status >= 200 && aptosResponse.status < 300) { return aptosResponse; } // We have to explicitly check for all request types, because if the error is a non-indexer error, but // comes from an indexer request (e.g. 404), we'll need to mention it appropriately throw new AptosApiError({ apiType, aptosRequest: aptosRequestOpts, aptosResponse }); } /** * The main function to use when making an API request, returning the response or throwing an AptosApiError on failure. * * A Node API read of pruned data answers `410 Gone` with an `archival_endpoint`. Such requests are replayed * once against that endpoint and the result returned transparently. Opt out with `archivalFallback: false`. * * @param aptosRequestOpts - Options for the Aptos request, including the URL and path. * @param aptosConfig - The configuration information for the SDK client instance. * @param apiType - The type of API being accessed, which determines how the response is handled. * @returns The response from the API request or throws an AptosApiError if the request fails. * @group Implementation * @category Client */ export async function aptosRequest(aptosRequestOpts, aptosConfig, apiType) { const aptosResponse = await sendAptosRequest(aptosRequestOpts, aptosConfig); const archivalTarget = apiType === AptosApiType.FULLNODE && aptosResponse.status === HTTP_STATUS_GONE && aptosConfig.archivalFallback ? resolveArchivalRetryTarget(aptosResponse.url, aptosResponse.data) : undefined; if (archivalTarget === undefined) { return handleAptosResponse(aptosResponse, aptosRequestOpts, apiType); } if (!archivalTarget.forwardCredentials && hasCredentials(aptosRequestOpts.overrides)) { warnOnceAboutDroppedCredentials(aptosResponse.url, archivalTarget.url); } const archivalRequestOpts = { ...aptosRequestOpts, url: archivalTarget.url, overrides: archivalTarget.forwardCredentials ? aptosRequestOpts.overrides : withoutCredentials(aptosRequestOpts.overrides), }; let archivalResponse; try { archivalResponse = await sendAptosRequest(archivalRequestOpts, aptosConfig); } catch { // Archival was unreachable, so the pruning error remains the more useful one to surface. throw new AptosApiError({ apiType, aptosRequest: aptosRequestOpts, aptosResponse }); } return handleAptosResponse(archivalResponse, archivalRequestOpts, apiType); } //# sourceMappingURL=core.js.map