/** * Coinbase Onramp Provider * * Generates session tokens for the Coinbase-hosted Onramp widget. * Uses Ed25519 JWT auth against the CDP API to get a session token, * then constructs a one-click-buy URL for the iframe. * * Docs: https://docs.cdp.coinbase.com/onramp/docs/welcome */ import { SignJWT } from 'jose'; import { randomBytes, createPrivateKey } from 'crypto'; const CDP_API_HOST = 'api.developer.coinbase.com'; const TOKEN_PATH = '/onramp/v1/token'; const ONRAMP_BASE_URL = 'https://pay.coinbase.com/buy'; export interface CoinbaseOnrampConfig { apiKeyId: string; apiKeySecret: string; // Ed25519 private key (PEM or base64) projectId: string; environment?: 'sandbox' | 'production'; } export interface CoinbaseSessionOpts { walletAddress: string; fiatAmount: number; fiatCurrency: string; network?: string; asset?: string; partnerUserRef?: string; redirectUrl?: string; } export interface CoinbaseSessionResult { sessionToken: string; onrampUrl: string; } export class CoinbaseOnrampProvider { private config: CoinbaseOnrampConfig; constructor(config: CoinbaseOnrampConfig) { this.config = config; } /** * Generate a CDP JWT for authenticating against the onramp token endpoint. * Uses EdDSA (Ed25519) signing per CDP docs. */ private async generateJwt(): Promise { const now = Math.floor(Date.now() / 1000); const nonce = randomBytes(16).toString('hex'); const keySecret = this.config.apiKeySecret.replace(/\\n/g, '\n').trim(); let privateKey: import('crypto').KeyObject; if (keySecret.includes('-----BEGIN')) { privateKey = createPrivateKey({ key: keySecret, format: 'pem' }); } else { // Raw base64 key from CDP portal (64 bytes: 32-byte seed + 32-byte pubkey) const rawBytes = Buffer.from(keySecret, 'base64'); const seed = rawBytes.subarray(0, 32); // Wrap seed in PKCS8 DER envelope for Ed25519 const pkcs8Header = Buffer.from('302e020100300506032b657004220420', 'hex'); const pkcs8Der = Buffer.concat([pkcs8Header, seed]); privateKey = createPrivateKey({ key: pkcs8Der, format: 'der', type: 'pkcs8' }); } const jwt = await new SignJWT({ sub: this.config.apiKeyId, iss: 'cdp', aud: ['cdp_service'], uri: `POST ${CDP_API_HOST}${TOKEN_PATH}`, }) .setProtectedHeader({ alg: 'EdDSA', typ: 'JWT', kid: this.config.apiKeyId, nonce, }) .setIssuedAt(now) .setNotBefore(now) .setExpirationTime(now + 120) .sign(privateKey); return jwt; } /** * Request a session token from the CDP Onramp API. * Returns a short-lived token used to construct the widget URL. */ async createSession(opts: CoinbaseSessionOpts): Promise { const network = opts.network ?? 'base'; const asset = opts.asset ?? 'USDC'; // Build partnerUserRef — prefix with "sandbox-" for test mode let partnerUserRef = opts.partnerUserRef ?? ''; if (this.config.environment === 'sandbox' && !partnerUserRef.startsWith('sandbox-')) { partnerUserRef = `sandbox-${partnerUserRef}`; } const jwt = await this.generateJwt(); const body = { addresses: [{ address: opts.walletAddress, blockchains: [network], }], assets: [asset], }; console.log(`[coinbase-onramp] Creating session: wallet=${opts.walletAddress} network=${network} asset=${asset} fiat=${opts.fiatAmount}`); const res = await fetch(`https://${CDP_API_HOST}${TOKEN_PATH}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwt}`, }, body: JSON.stringify(body), }); if (!res.ok) { const errBody = await res.text(); console.error(`[coinbase-onramp] CDP session token request failed: ${res.status} ${errBody}`); throw new Error(`Coinbase Onramp session failed (${res.status}): ${errBody}`); } const data = await res.json() as { token: string; channel_id?: string }; const sessionToken = data.token; // Construct the one-click-buy URL const url = new URL(ONRAMP_BASE_URL); url.searchParams.set('sessionToken', sessionToken); url.searchParams.set('appId', this.config.projectId); url.searchParams.set('defaultAsset', asset); url.searchParams.set('defaultNetwork', network); url.searchParams.set('defaultPaymentMethod', 'CARD'); url.searchParams.set('fiatCurrency', opts.fiatCurrency.toUpperCase()); url.searchParams.set('presetFiatAmount', opts.fiatAmount.toString()); if (opts.redirectUrl) { url.searchParams.set('redirectUrl', opts.redirectUrl); } if (partnerUserRef) { url.searchParams.set('partnerUserId', partnerUserRef); } const onrampUrl = url.toString(); console.log(`[coinbase-onramp] URL generated: ${onrampUrl.slice(0, 80)}...`); return { sessionToken, onrampUrl }; } }