UNPKG

@coinbase/cdp-sdk

Version:

SDK for interacting with the Coinbase Developer Platform Wallet API

387 lines • 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CdpX402Client = void 0; /* * CDP-powered x402 payment client. * * CdpX402Client is a drop-in extension of x402Client that auto-provisions * CDP-managed wallets (EVM EOA or Smart Contract Wallet + Solana), registers * payment schemes and wires spend controls. * Credentials fall back to environment variables; wallet configuration and * RPC URLs are supplied explicitly via config. */ const client_1 = require("@x402/core/client"); const client_2 = require("@x402/evm/auth-capture/client"); const client_3 = require("@x402/evm/batch-settlement/client"); const client_4 = require("@x402/evm/exact/client"); const client_5 = require("@x402/evm/exact/v1/client"); const client_6 = require("@x402/evm/upto/client"); const builder_code_1 = require("@x402/extensions/builder-code"); const client_7 = require("@x402/svm/exact/client"); const client_8 = require("@x402/svm/exact/v1/client"); const client_9 = require("@x402/svm/upto/client"); const account_signers_js_1 = require("./account-signers.js"); const builder_code_js_1 = require("./builder-code.js"); const constants_js_1 = require("./constants.js"); const cdp_js_1 = require("../client/cdp.js"); const apply_js_1 = require("./guardrails/apply.js"); const normalize_js_1 = require("./guardrails/normalize.js"); const smart_account_js_1 = require("./smart-account.js"); const DEFAULT_ACCOUNT_NAME = "x402-client-wallet-1"; const resolveWalletType = (type) => { if (!type) return "eoa"; if (type === "eoa" || type === "smart") return type; throw new Error(`Unsupported wallet type "${type}". Supported values: "eoa", "smart".`); }; const resolveAccountName = (config) => config?.accountName ?? DEFAULT_ACCOUNT_NAME; /** * Resolves the deployment environment, falling back from explicit config to * the `CDP_X402_CLIENT_ENVIRONMENT` env var. Mirrors the server-side * `CDP_X402_SERVER_ENVIRONMENT` resolution in `server.ts`. * * @param config - Optional explicit environment override. * @returns `"development"` only when explicitly requested; `"production"` otherwise. */ const resolveEnvironment = (config) => { const rawEnv = config?.environment ?? process.env.CDP_X402_CLIENT_ENVIRONMENT; return rawEnv === "development" ? "development" : "production"; }; const buildEvmRpcUrlsByChainId = (rpcUrlsByCaip2) => { const result = {}; for (const [caip2, cfg] of Object.entries(rpcUrlsByCaip2)) { const [namespace, chainId] = caip2.split(":"); if (namespace !== "eip155" || !chainId) continue; const numericChainId = Number(chainId); if (!Number.isNaN(numericChainId)) { result[numericChainId] = cfg; } } return result; }; const setupCdpSigners = async (client, config) => { const apiKeyId = config?.apiKeyId ?? process.env.CDP_API_KEY_ID; const apiKeySecret = config?.apiKeySecret ?? process.env.CDP_API_KEY_SECRET; const walletSecret = config?.walletSecret ?? process.env.CDP_WALLET_SECRET; const missing = []; if (!apiKeyId) missing.push("CDP_API_KEY_ID"); if (!apiKeySecret) missing.push("CDP_API_KEY_SECRET"); if (!walletSecret) missing.push("CDP_WALLET_SECRET"); if (missing.length > 0) { throw new Error(`Missing required CDP credentials: ${missing.join(", ")}. ` + "Provide them via config options or set the corresponding environment variables."); } const cdpClient = new cdp_js_1.CdpClient({ apiKeyId: apiKeyId, apiKeySecret: apiKeySecret, walletSecret: walletSecret, }); const walletConfigInput = config?.walletConfig; const walletType = resolveWalletType(walletConfigInput?.type); const accountName = resolveAccountName(walletConfigInput); const svmAccount = await cdpClient.solana.getOrCreateAccount({ name: accountName }); let evmAddress; let ownerWallet; let evmSigner; if (walletType === "smart") { const ownerAccountName = walletConfigInput?.type === "smart" ? walletConfigInput.ownerAccountName : undefined; if (!ownerAccountName) { throw new Error('Missing required owner account name for wallet type "smart". ' + "Provide it via walletConfig.ownerAccountName."); } const ownerAccount = await cdpClient.evm.getOrCreateAccount({ name: ownerAccountName }); let smartAccount; try { smartAccount = await cdpClient.evm.getOrCreateSmartAccount({ name: accountName, owner: ownerAccount, }); } catch (error) { if ((0, smart_account_js_1.isOwnerAlreadyHasSmartWalletError)(error)) { const existingAddress = await (0, smart_account_js_1.findSmartAccountByOwner)(cdpClient, ownerAccount.address); if (!existingAddress) throw error; smartAccount = await cdpClient.evm.getSmartAccount({ address: existingAddress, owner: ownerAccount, }); } else { throw error; } } evmAddress = smartAccount.address; ownerWallet = ownerAccountName; evmSigner = (0, account_signers_js_1.fromCdpSmartWallet)(smartAccount); } else { const evmAccount = await cdpClient.evm.getOrCreateAccount({ name: accountName }); evmAddress = evmAccount.address; evmSigner = (0, account_signers_js_1.fromCdpEvmAccount)(evmAccount); } const svmSigner = (0, account_signers_js_1.cdpSolanaAccountToSvmSigner)(svmAccount); // Keyed by CAIP-2 identifier (e.g. "eip155:8453"), not by network name. const defaultEvmRpcUrls = await (0, constants_js_1.getDefaultEvmRpcUrls)(); const defaultBaseRpcUrl = defaultEvmRpcUrls[constants_js_1.baseMainnetCaip2]?.rpcUrl; const defaultBaseSepoliaRpcUrl = defaultEvmRpcUrls[constants_js_1.baseSepoliaCaip2]?.rpcUrl; // `environment` "development" prescribes Base Sepolia; "production" (default) prescribes Base mainnet. const environment = resolveEnvironment(config); const defaultNetworkSchemes = environment === "development" ? [ { network: "base-sepolia", rpcUrl: defaultBaseSepoliaRpcUrl, scheme: { exact: true, upto: true, authCapture: true }, }, ] : [ { network: "base", rpcUrl: defaultBaseRpcUrl, scheme: { exact: true, upto: true, authCapture: true }, }, ]; /* * `networkSchemes` merges into the default baseline per network: a partial `scheme` override * (e.g. `{ authCapture: false }`) only touches the fields it sets — it doesn't disable the * other schemes the network defaulted to (or a prior override already set). */ const networksByName = new Map(defaultNetworkSchemes.map(config => [config.network, config])); for (const override of config?.networkSchemes ?? []) { const existing = networksByName.get(override.network); /* * `existing` (and thus `existing?.rpcUrl`) is only populated for the Base * network matching `environment` — the one already in the default * baseline. An override for the *other* Base network (e.g. `"base"` in a * `"development"` environment) has no `existing` entry, so fall back to * the pre-fetched CDP-hosted default RPC by name to honor the documented * contract that Base and Base Sepolia always get a default RPC injected. */ const defaultRpcUrl = override.network === "base" ? defaultBaseRpcUrl : override.network === "base-sepolia" ? defaultBaseSepoliaRpcUrl : undefined; const rpcUrl = override.rpcUrl ?? existing?.rpcUrl ?? defaultRpcUrl; const scheme = { ...existing?.scheme, ...override.scheme }; networksByName.set(override.network, { ...override, rpcUrl, scheme }); } // `normalizeNetwork` converts the v1-style plain names used here (e.g. "base") into the CAIP-2 form v2's `register` needs (e.g. "eip155:8453"); `registerV1` keeps using the plain name. const evmRpcUrlsByCaip2 = {}; for (const [network, netConfig] of networksByName) { if (network === "solana" || network === "solana-devnet") continue; if (netConfig.rpcUrl) evmRpcUrlsByCaip2[(0, normalize_js_1.normalizeNetwork)(network)] = { rpcUrl: netConfig.rpcUrl }; } const evmRpcUrlsByChainId = buildEvmRpcUrlsByChainId(evmRpcUrlsByCaip2); for (const [network, netConfig] of networksByName) { const caip2Network = (0, normalize_js_1.normalizeNetwork)(network); const isSolana = network === "solana" || network === "solana-devnet"; if (netConfig.scheme.exact) { if (isSolana) { client.register(caip2Network, new client_7.ExactSvmScheme(svmSigner, { rpcUrl: netConfig.rpcUrl })); client.registerV1(network, new client_8.ExactSvmSchemeV1(svmSigner, { rpcUrl: netConfig.rpcUrl })); } else { client.register(caip2Network, new client_4.ExactEvmScheme(evmSigner, evmRpcUrlsByChainId)); client.registerV1(network, new client_5.ExactEvmSchemeV1(evmSigner)); } } if (netConfig.scheme.upto) { if (isSolana) { client.register(caip2Network, new client_9.UptoSvmScheme(svmSigner, { rpcUrl: netConfig.rpcUrl })); /* * `upto`'s EVM Permit2 transfer method requires an initial approval * transaction that cannot be sponsored; disabled for smart accounts at * this time. Solana `upto` has no such restriction — CDP Solana * accounts are never smart-wallet-typed, so it's unaffected either way. */ } else if (walletType === "smart") { // eslint-disable-next-line no-console console.warn(`CdpX402Client: skipping network "${network}": upto scheme is not supported for Smart Contract Wallets (Permit2 approval cannot be sponsored).`); } else { client.register(caip2Network, new client_6.UptoEvmScheme(evmSigner, evmRpcUrlsByChainId)); } } if (netConfig.scheme.batchSettlement) { if (isSolana) { // eslint-disable-next-line no-console console.warn(`CdpX402Client: skipping network "${network}": Solana Batch Settlement scheme is not yet supported.`); } else { client.register(caip2Network, new client_3.BatchSettlementEvmScheme(evmSigner, { rpcUrl: netConfig.rpcUrl })); } } if (netConfig.scheme.authCapture) { if (isSolana) { // eslint-disable-next-line no-console console.warn(`CdpX402Client: skipping network "${network}": Solana Auth Capture scheme is not supported (EVM-only upstream).`); } else if (walletType === "smart") { /* * `fromCdpSmartWallet` produces an ERC-1271/ERC-6492-wrapped signature. * `AuthCaptureEvmScheme`'s settlement path (and this SDK's * `signEvmSmartAccountX402Payment` capability, which only supports * `exact`) has no verified support for that signature shape yet, so * registering it here would let a smart-wallet client select an * auth-capture requirement and produce an unsettleable payload. */ // eslint-disable-next-line no-console console.warn(`CdpX402Client: skipping network "${network}": auth-capture is not supported for Smart Contract Wallets.`); } else { client.register(caip2Network, new client_2.AuthCaptureEvmScheme(evmSigner)); } } } if (config?.spendControls) { (0, apply_js_1.applySpendControls)(client, config.spendControls); } return { cdpClient, evmAddress, svmAddress: svmAccount.address, ownerWallet }; }; /** * A Coinbase CDP-powered x402 client that initializes lazily on first payment. * * Extends `x402Client` with automatic wallet provisioning and scheme registration. * Credentials fall back to environment variables; wallet configuration and * RPC URLs are supplied explicitly via config. * * The account name/address used for payments is resolved internally. Use * {@link CdpX402Client.getAddresses} to retrieve it — for example, to fund * the wallet before making a payment. * * @example * ```typescript * import { CdpX402Client } from "@coinbase/cdp-sdk/x402"; * import { wrapFetchWithPayment } from "@x402/fetch"; * * const client = new CdpX402Client(); * const { evmAddress, svmAddress } = await client.getAddresses(); * console.log("Fund this address before paying:", evmAddress, svmAddress); * * const fetchWithPayment = wrapFetchWithPayment(fetch, client); * const response = await fetchWithPayment("https://api.example.com/report"); * ``` * * @example * ```typescript * const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; * const client = new CdpX402Client({ * spendControls: { * maxAmountPerPayment: { atomic: 10_000n, asset: USDC_BASE }, * maxCumulativeSpend: { atomic: 50_000n, asset: USDC_BASE }, * maxCumulativeSpendWindow: "24h", * allowedNetworks: ["eip155:8453"], * }, * }); * ``` * * @example * ```typescript * // Attribute payments to this client via the builder-code extension. * const client = new CdpX402Client({ builderCode: "my_client" }); * ``` */ class CdpX402Client extends client_1.x402Client { _config; _serviceBuilderCodes; _initPromise = null; _addresses = null; /** * Creates a CdpX402Client that initializes lazily on first payment. * * @param config - Optional configuration. Credentials fall back to environment variables. * @throws If `config.builderCode` is empty, has more than four codes, or * contains a code that is not 1-32 lowercase alphanumeric / underscore characters. */ constructor(config) { super(); /* * Disable x402Client's own default $1-per-payment cap so the only active * spend policy is this SDK's own, set via `config.spendControls`. That * leaves no cap at all when `config.spendControls` is unset, so warn * instead of failing silently. */ this.setSpendControls(false); if (!config?.spendControls) { // eslint-disable-next-line no-console console.warn("CdpX402Client: spendControls not configured. No default spend cap is applied. " + "Set `spendControls` to bound payment size."); } this._serviceBuilderCodes = [ ...(config?.builderCode !== undefined ? (0, builder_code_js_1.toServiceBuilderCodes)(config.builderCode) : []), builder_code_js_1.CDP_SDK_CLIENT_BUILDER_CODE, ]; /* * Registered here rather than during lazy initialization so that a malformed * code throws before any CDP I/O, and so a `registerExtension("builder-code")` * call by the caller takes precedence over the config. */ this.registerExtension(new builder_code_1.BuilderCodeClientExtension(this._serviceBuilderCodes)); this._config = config; } /** * The CDP account name that this client provisions (or has already * provisioned) for payment signing. Resolved synchronously from config * and defaults — does not perform any CDP account lookups. * * @returns The resolved account name, e.g. `"x402-client-wallet-1"`. */ get accountName() { return resolveAccountName(this._config?.walletConfig); } /** * Provisions the CDP wallet (if not already provisioned) and returns its * addresses. Call this eagerly — for example, to display or fund the * wallet — instead of waiting for the lazy initialization on first payment. * * @returns The EVM and Solana addresses backing this client's payments. */ async getAddresses() { await this._ensureInitialized(); return this._addresses; } /** * Creates the payment payload, initializing the CDP wallet lazily on first call. * * @param paymentRequired - The x402 payment requirements from the resource server. * @returns The signed payment payload. */ async createPaymentPayload(paymentRequired) { await this._ensureInitialized(); return super.createPaymentPayload(paymentRequired); } /** * Provisions CDP accounts and registers payment schemes. */ async _initialize() { const { evmAddress, svmAddress, ownerWallet } = await setupCdpSigners(this, this._config); this._addresses = { evmAddress, svmAddress, ownerWallet }; } /** * Returns a shared initialization promise, starting initialization if needed. * * @returns Promise that resolves when the client is ready to make payments. */ _ensureInitialized() { if (!this._initPromise) { this._initPromise = this._initialize().catch(error => { this._initPromise = null; throw error; }); } return this._initPromise; } } exports.CdpX402Client = CdpX402Client; //# sourceMappingURL=client.js.map