UNPKG

@coinbase/cdp-sdk

Version:

SDK for interacting with the Coinbase Developer Platform Wallet API

181 lines 9.12 kB
// 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"; import { UserInputValidationError } from "../errors.js"; let axiosInstance; export let config = undefined; /** * Returns the configured axios instance. `configure()` must be called first. * * @returns {AxiosInstance} The configured axios instance. */ export const getAxiosInstance = () => axiosInstance; /** * 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) { if (error instanceof UserInputValidationError) { throw 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