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
text/typescript
// 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 };
}