@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
171 lines • 8.77 kB
JavaScript
// eslint-disable-next-line import/no-named-as-default
import Axios from "axios";
import axiosRetry, { exponentialDelay } from "axios-retry";
import { withAuth } from "../auth/hooks/axios/index.js";
import { ERROR_DOCS_PAGE_URL } from "../constants.js";
import { isOpenAPIError, APIError, UnknownError, NetworkError } from "./errors.js";
let axiosInstance;
export let config = undefined;
/**
* Configures the CDP client with the given options.
*
* @param {CdpOptions} options - The CDP options.
*/
export const configure = (options) => {
const baseURL = options.basePath || "https://api.cdp.coinbase.com/platform";
config = {
...options,
basePath: baseURL,
};
axiosInstance = Axios.create({
baseURL,
});
axiosRetry(axiosInstance, {
retryDelay: exponentialDelay,
});
axiosInstance = withAuth(axiosInstance, {
apiKeyId: options.apiKeyId,
apiKeySecret: options.apiKeySecret,
source: options.source || "sdk-openapi-client",
sourceVersion: options.sourceVersion,
walletSecret: options.walletSecret,
expiresIn: options.expiresIn,
debug: options.debugging,
});
};
/**
* Adds an idempotency key to request config if provided
*
* @param config - The Axios request configuration.
* @param idempotencyKey - The idempotency key.
* @returns The Axios request configuration with the idempotency key.
*/
const addIdempotencyKey = (config, idempotencyKey) => {
if (!idempotencyKey) {
return config;
}
return {
...config,
headers: {
...(config.headers || {}),
"X-Idempotency-Key": idempotencyKey,
},
};
};
/**
* Mutates the given Axios request configuration to add the CDP API key signature
* to the request headers.
*
* @param {AxiosRequestConfig} config - The Axios request configuration.
* @param idempotencyKey - The idempotency key.
* @returns {Promise<T>} A promise that resolves to the response data.
* @throws {APIError} If the request fails.
*/
export const cdpApiClient = async (config, idempotencyKey) => {
validateCall(config);
// Add idempotency key to the request headers if provided
const configWithIdempotencyKey = addIdempotencyKey(config, idempotencyKey);
try {
const response = await axiosInstance(configWithIdempotencyKey);
return response.data;
}
catch (error) {
// eslint-disable-next-line import/no-named-as-default-member
if (Axios.isAxiosError(error) && !error.response) {
// Network-level errors (no response received)
const errorMessage = (error.message || "").toLowerCase();
const errorCode = error.code?.toLowerCase();
// Categorize network errors based on error messages and codes
if (errorCode === "econnrefused" || errorMessage.includes("connection refused")) {
throw new NetworkError("network_connection_failed", "Unable to connect to CDP service. The service may be unavailable.", { code: error.code, message: error.message, retryable: true }, error.cause);
}
else if (errorCode === "etimedout" ||
errorCode === "econnaborted" ||
errorMessage.includes("timeout")) {
throw new NetworkError("network_timeout", "Request timed out. Please try again.", { code: error.code, message: error.message, retryable: true }, error.cause);
}
else if (errorCode === "enotfound" || errorMessage.includes("getaddrinfo")) {
throw new NetworkError("network_dns_failure", "DNS resolution failed. Please check your network connection.", { code: error.code, message: error.message, retryable: false }, error.cause);
}
else if (errorMessage.includes("network error") || errorMessage.includes("econnreset")) {
throw new NetworkError("network_connection_failed", "Network error occurred. Please check your connection and try again.", { code: error.code, message: error.message, retryable: true }, error.cause);
}
else {
// Generic network error
throw new NetworkError("unknown", error.cause instanceof Error ? error.cause.message : error.message, { code: error.code, message: error.message, retryable: true }, error.cause);
}
}
// eslint-disable-next-line import/no-named-as-default-member
if (Axios.isAxiosError(error) && error.response) {
if (isOpenAPIError(error.response.data)) {
throw new APIError(error.response.status, error.response.data.errorType, error.response.data.errorMessage, error.response.data.correlationId, error.response.data.errorLink, error.cause);
}
else {
const statusCode = error.response.status;
const responseData = error.response.data;
// Check for gateway-level errors that might indicate network issues
const isGatewayError = responseData &&
typeof responseData === "string" &&
(responseData.toLowerCase().includes("forbidden") ||
responseData.toLowerCase().includes("ip") ||
responseData.toLowerCase().includes("blocked") ||
responseData.toLowerCase().includes("gateway"));
switch (statusCode) {
case 401:
throw new APIError(statusCode, "unauthorized", "Unauthorized.", undefined, `${ERROR_DOCS_PAGE_URL}#unauthorized`, error.cause);
case 403:
// Special handling for IP blocklist and other gateway-level 403s
if (isGatewayError) {
throw new NetworkError("network_ip_blocked", "Access denied. Your IP address may be blocked or restricted.", {
code: "IP_BLOCKED",
message: typeof responseData === "string" ? responseData : JSON.stringify(responseData),
retryable: false,
}, error.cause);
}
// Regular 403 forbidden error
throw new APIError(statusCode, "unauthorized", "Forbidden. You don't have permission to access this resource.", undefined, `${ERROR_DOCS_PAGE_URL}#forbidden`, error.cause);
case 404:
throw new APIError(statusCode, "not_found", "API not found.", undefined, `${ERROR_DOCS_PAGE_URL}#not_found`, error.cause);
case 502:
throw new APIError(statusCode, "bad_gateway", "Bad gateway.", undefined, `${ERROR_DOCS_PAGE_URL}`, error.cause);
case 503:
throw new APIError(statusCode, "service_unavailable", "Service unavailable. Please try again later.", undefined, `${ERROR_DOCS_PAGE_URL}`, error.cause);
default: {
let errorText = "";
if (error.response.data) {
try {
errorText = JSON.stringify(error.response.data);
}
catch {
errorText = String(error.response.data);
}
}
const errorMessage = errorText
? `An unexpected error occurred: ${errorText}`
: "An unexpected error occurred.";
throw new APIError(statusCode, "unexpected_error", errorMessage, undefined, `${ERROR_DOCS_PAGE_URL}`, error.cause);
}
}
}
}
throw new UnknownError("Something went wrong. Please reach out at https://discord.com/channels/1220414409550336183/1271495764580896789 for help.", error instanceof Error ? error : undefined);
}
};
/**
* Validates the call to the cdpApiClient.
*
* @param {AxiosRequestConfig} config - The Axios request configuration.
* @throws {Error} If the call is not valid.
*/
const validateCall = (config) => {
if (!axiosInstance.getUri() || axiosInstance.getUri() === "") {
throw new Error("CDP client URI not configured. Call configure() first.");
}
if (!config.url || config.url === "") {
throw new Error("AxiosRequestConfig URL is empty. This should never happen.");
}
if (!config.method || config.method === "") {
throw new Error("AxiosRequestConfig method is empty. This should never happen.");
}
};
//# sourceMappingURL=cdpApiClient.js.map