154 lines
4.9 KiB
TypeScript
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);
|
|
}
|