UNPKG

cdp-wallet-onramp-kit

Version:

Complete toolkit for Coinbase Developer Platform (CDP) Embedded Wallets and Onramp integration with reusable components, utilities, and documentation

270 lines (239 loc) 6.96 kB
// Dynamic import to avoid build-time CDP SDK type checking /** * Configuration for JWT generation */ export interface JWTConfig { method?: string; host?: string; path?: string; expiresIn?: number; } /** * Address configuration for session tokens */ export interface AddressConfig { address: string; blockchains: string[]; } /** * Session token request parameters */ export interface SessionTokenParams { addresses: AddressConfig[]; assets?: string[]; } /** * Onramp URL generation parameters */ export interface OnrampUrlParams { addresses: AddressConfig[]; assets?: string[]; defaultAsset?: string; defaultNetwork?: string; presetFiatAmount?: number; presetCryptoAmount?: number; redirectUrl?: string; partnerUserId?: string; } /** * Generate a JSON Web Token (JWT) for CDP API authentication. * Uses the CDP SDK to handle all key formats automatically. * * @param config - JWT configuration options * @returns Promise<string> - The generated JWT token * * @example * ```typescript * const jwt = await generateJWT({ * method: 'POST', * path: '/onramp/v1/token' * }); * ``` */ export async function generateJWT(config: JWTConfig = {}): Promise<string> { const privateKeyString = process.env.CDP_API_KEY_PRIVATE_KEY || process.env.CDP_SECRET_KEY; const keyName = process.env.CDP_API_KEY_NAME || process.env.CDP_SECRET_KEY_NAME; if (!privateKeyString || !keyName) { throw new Error( 'CDP API credentials are required. Set CDP_API_KEY_PRIVATE_KEY and CDP_API_KEY_NAME environment variables.' ); } const { method = 'POST', host = 'api.developer.coinbase.com', path = '/onramp/v1/token', expiresIn = 120 } = config; try { // Use eval to avoid TypeScript trying to resolve the module during build time const cdpSdkAuth = await eval('import("@coinbase/cdp-sdk/auth")'); // Use CDP SDK to generate JWT - handles all key formats correctly const jwt = await cdpSdkAuth.generateJwt({ apiKeyId: keyName, apiKeySecret: privateKeyString, requestMethod: method, requestHost: host, requestPath: path, expiresIn }); return jwt; } catch (error) { console.error('JWT generation failed:', error); console.error('Key format detected:', privateKeyString.startsWith('-----BEGIN') ? 'PEM' : 'Base64'); throw new Error( 'Failed to generate JWT. Make sure you have @coinbase/cdp-sdk installed:\n' + 'npm install @coinbase/cdp-sdk\n\n' + 'Original error: ' + (error instanceof Error ? error.message : 'Unknown error') ); } } /** * Create a session token for initializing the onramp widget. * Session tokens are single-use and expire after 5 minutes. * * @param jwt - The JWT token for authentication * @param params - Session token parameters * @returns Promise<string> - The session token * * @example * ```typescript * const jwt = await generateJWT(); * const sessionToken = await createSessionToken(jwt, { * addresses: [{ * address: '0x1234...5678', * blockchains: ['base', 'ethereum'] * }], * assets: ['USDC', 'ETH'] * }); * ``` */ export async function createSessionToken( jwt: string, params: SessionTokenParams ): Promise<string> { const response = await fetch('https://api.developer.coinbase.com/onramp/v1/token', { method: 'POST', headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json' }, body: JSON.stringify(params) }); if (!response.ok) { const errorText = await response.text(); let errorMessage = `Failed to create session token: ${response.status}`; try { const errorData = JSON.parse(errorText); errorMessage = errorData.message || errorData.error || errorMessage; } catch { errorMessage += ` - ${errorText}`; } throw new Error(errorMessage); } const data = await response.json(); return data.token || data.data?.token; } /** * Generate a complete onramp URL with session token and parameters. * This is a convenience function that combines JWT generation, session token creation, and URL building. * * @param params - Onramp URL parameters * @returns Promise<string> - The complete onramp URL * * @example * ```typescript * const onrampUrl = await generateOnrampUrl({ * addresses: [{ * address: '0x1234...5678', * blockchains: ['base'] * }], * defaultAsset: 'USDC', * presetFiatAmount: 50 * }); * ``` */ export async function generateOnrampUrl(params: OnrampUrlParams): Promise<string> { // Generate JWT and session token const jwt = await generateJWT(); const sessionToken = await createSessionToken(jwt, { addresses: params.addresses, assets: params.assets }); // Build URL with session token and optional parameters const url = new URL('https://pay.coinbase.com/buy/select-asset'); url.searchParams.set('sessionToken', sessionToken); if (params.defaultAsset) { url.searchParams.set('defaultAsset', params.defaultAsset); } if (params.defaultNetwork) { url.searchParams.set('defaultNetwork', params.defaultNetwork); } if (params.presetFiatAmount) { url.searchParams.set('presetFiatAmount', params.presetFiatAmount.toString()); } if (params.presetCryptoAmount) { url.searchParams.set('presetCryptoAmount', params.presetCryptoAmount.toString()); } if (params.redirectUrl) { url.searchParams.set('redirectUrl', params.redirectUrl); } if (params.partnerUserId) { url.searchParams.set('partnerUserId', params.partnerUserId); } return url.toString(); } /** * Guest checkout limits as defined by Coinbase */ export const GUEST_CHECKOUT_LIMITS = { minimumAmount: 5, // $5 minimum weeklyLimit: 500, // $500 weekly limit country: 'US' // US residents only } as const; /** * Supported blockchain networks */ export const SUPPORTED_NETWORKS = { BASE: 'base', ETHEREUM: 'ethereum', ARBITRUM: 'arbitrum', OPTIMISM: 'optimism', POLYGON: 'polygon' } as const; /** * Common asset symbols */ export const SUPPORTED_ASSETS = { ETHEREUM: 'ETH', USD_COIN: 'USDC', BITCOIN: 'BTC', TETHER: 'USDT', DAI: 'DAI' } as const; /** * Validate wallet address format */ export function isValidEthereumAddress(address: string): boolean { return /^0x[a-fA-F0-9]{40}$/.test(address); } /** * Validate amount within guest checkout limits */ export function validateGuestCheckoutAmount(amount: number): { isValid: boolean; error?: string; } { if (amount < GUEST_CHECKOUT_LIMITS.minimumAmount) { return { isValid: false, error: `Amount must be at least $${GUEST_CHECKOUT_LIMITS.minimumAmount}` }; } if (amount > GUEST_CHECKOUT_LIMITS.weeklyLimit) { return { isValid: false, error: `Amount cannot exceed $${GUEST_CHECKOUT_LIMITS.weeklyLimit} (weekly limit)` }; } return { isValid: true }; }