@aptos-labs/ts-sdk
Version:
Aptos TypeScript SDK
223 lines • 9.76 kB
JavaScript
// 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