rspace-online/shared/moonpay.ts

154 lines
4.9 KiB
TypeScript

/**
* MoonPay API utilities — fiat-to-crypto on-ramp.
*
* MoonPay widget URLs are built with query params and signed server-side
* with HMAC-SHA256 using the API secret. No session API needed.
*
* Environment: MOONPAY_ENV controls sandbox vs production.
* Docs: https://dev.moonpay.com/
*/
import { createHmac } from 'crypto';
export type MoonPayEnv = 'sandbox' | 'production';
const WIDGET_URLS = {
sandbox: 'https://buy-sandbox.moonpay.com',
production: 'https://buy.moonpay.com',
} as const;
/** Get current MoonPay environment */
export function getMoonPayEnv(): MoonPayEnv {
const env = process.env.MOONPAY_ENV?.toLowerCase();
return env === 'production' ? 'production' : 'sandbox';
}
/** Get MoonPay publishable API key */
export function getMoonPayApiKey(): string {
const env = getMoonPayEnv();
return (env === 'production'
? process.env.MOONPAY_API_KEY_PRODUCTION
: process.env.MOONPAY_API_KEY_SANDBOX
) || process.env.MOONPAY_API_KEY || '';
}
/** Get MoonPay secret key (for URL signing) */
export function getMoonPaySecretKey(): string {
const env = getMoonPayEnv();
return (env === 'production'
? process.env.MOONPAY_SECRET_KEY_PRODUCTION
: process.env.MOONPAY_SECRET_KEY_SANDBOX
) || process.env.MOONPAY_SECRET_KEY || '';
}
/** Chain ID to MoonPay network name */
const NETWORK_MAP: Record<number, string> = {
8453: 'base',
84532: 'base',
1: 'ethereum',
137: 'polygon',
42161: 'arbitrum',
};
/** Token symbol to MoonPay currency code */
const CURRENCY_MAP: Record<string, Record<string, string>> = {
USDC: {
base: 'usdc_base',
ethereum: 'usdc',
polygon: 'usdc_polygon',
arbitrum: 'usdc_arbitrum',
},
ETH: {
base: 'eth_base',
ethereum: 'eth',
arbitrum: 'eth_arbitrum',
},
};
export interface MoonPayWidgetParams {
walletAddress: string;
email?: string;
currencyCode?: string; // e.g. 'usdc_base'
baseCurrencyCode?: string; // e.g. 'usd'
baseCurrencyAmount?: number; // fiat amount
quoteCurrencyAmount?: number; // crypto amount
externalTransactionId?: string;
colorCode?: string; // hex without # (e.g. '6366f1')
redirectURL?: string;
showWalletAddressForm?: boolean;
}
/**
* Build a signed MoonPay widget URL.
* The URL is signed with HMAC-SHA256 using the secret key.
*/
export function createMoonPayWidgetUrl(params: MoonPayWidgetParams): string {
const env = getMoonPayEnv();
const apiKey = getMoonPayApiKey();
const secretKey = getMoonPaySecretKey();
if (!apiKey) throw new Error('MoonPay API key not configured');
if (!secretKey) throw new Error('MoonPay secret key not configured');
const base = WIDGET_URLS[env];
const url = new URL(base);
url.searchParams.set('apiKey', apiKey);
url.searchParams.set('walletAddress', params.walletAddress);
if (params.currencyCode) url.searchParams.set('currencyCode', params.currencyCode);
if (params.baseCurrencyCode) url.searchParams.set('baseCurrencyCode', params.baseCurrencyCode);
if (params.baseCurrencyAmount) url.searchParams.set('baseCurrencyAmount', String(params.baseCurrencyAmount));
if (params.quoteCurrencyAmount) url.searchParams.set('quoteCurrencyAmount', String(params.quoteCurrencyAmount));
if (params.email) url.searchParams.set('email', params.email);
if (params.externalTransactionId) url.searchParams.set('externalTransactionId', params.externalTransactionId);
if (params.colorCode) url.searchParams.set('colorCode', params.colorCode);
if (params.redirectURL) url.searchParams.set('redirectURL', params.redirectURL);
if (params.showWalletAddressForm === false) url.searchParams.set('showWalletAddressForm', 'false');
// Sign the URL: HMAC-SHA256 of the query string (including the leading '?')
const signature = createHmac('sha256', secretKey)
.update(url.search)
.digest('base64url');
url.searchParams.set('signature', signature);
return url.toString();
}
/**
* Build a MoonPay widget URL for a payment request.
* Convenience wrapper that maps token/chainId to MoonPay currency codes.
*/
export function createMoonPayPaymentUrl(opts: {
walletAddress: string;
token: string;
chainId: number;
amount?: string;
fiatAmount?: string;
fiatCurrency?: string;
email?: string;
paymentId?: string;
}): string {
const network = NETWORK_MAP[opts.chainId] || 'base';
const currencyCode = CURRENCY_MAP[opts.token]?.[network] || 'usdc_base';
const params: MoonPayWidgetParams = {
walletAddress: opts.walletAddress,
currencyCode,
email: opts.email,
externalTransactionId: opts.paymentId ? `pay-${opts.paymentId}` : undefined,
colorCode: '6366f1',
showWalletAddressForm: false,
};
// Prefer fiat amount if available, otherwise use crypto amount
if (opts.fiatAmount && parseFloat(opts.fiatAmount) > 0) {
params.baseCurrencyAmount = parseFloat(opts.fiatAmount);
params.baseCurrencyCode = (opts.fiatCurrency || 'USD').toLowerCase();
} else if (opts.amount && parseFloat(opts.amount) > 0) {
params.quoteCurrencyAmount = parseFloat(opts.amount);
}
return createMoonPayWidgetUrl(params);
}