paybridge 0.1.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +87 -7
  2. package/dist/circuit-breaker-store.d.ts +27 -0
  3. package/dist/circuit-breaker-store.js +25 -0
  4. package/dist/circuit-breaker.d.ts +30 -0
  5. package/dist/circuit-breaker.js +86 -0
  6. package/dist/crypto/base.d.ts +15 -0
  7. package/dist/crypto/base.js +24 -0
  8. package/dist/crypto/index.d.ts +35 -0
  9. package/dist/crypto/index.js +95 -0
  10. package/dist/crypto/mock.d.ts +15 -0
  11. package/dist/crypto/mock.js +112 -0
  12. package/dist/crypto/moonpay.d.ts +33 -0
  13. package/dist/crypto/moonpay.js +261 -0
  14. package/dist/crypto/router.d.ts +36 -0
  15. package/dist/crypto/router.js +287 -0
  16. package/dist/crypto/types.d.ts +89 -0
  17. package/dist/crypto/types.js +5 -0
  18. package/dist/crypto/yellowcard.d.ts +56 -0
  19. package/dist/crypto/yellowcard.js +311 -0
  20. package/dist/index.d.ts +10 -1
  21. package/dist/index.js +60 -3
  22. package/dist/providers/base.d.ts +5 -0
  23. package/dist/providers/flutterwave.d.ts +36 -0
  24. package/dist/providers/flutterwave.js +339 -0
  25. package/dist/providers/ozow.d.ts +20 -2
  26. package/dist/providers/ozow.js +158 -114
  27. package/dist/providers/payfast.d.ts +40 -0
  28. package/dist/providers/payfast.js +352 -0
  29. package/dist/providers/paystack.d.ts +37 -0
  30. package/dist/providers/paystack.js +336 -0
  31. package/dist/providers/peach.d.ts +50 -0
  32. package/dist/providers/peach.js +302 -0
  33. package/dist/providers/softycomp.d.ts +106 -0
  34. package/dist/providers/softycomp.js +229 -10
  35. package/dist/providers/stripe.d.ts +38 -0
  36. package/dist/providers/stripe.js +367 -0
  37. package/dist/providers/yoco.d.ts +12 -0
  38. package/dist/providers/yoco.js +148 -61
  39. package/dist/router.d.ts +33 -0
  40. package/dist/router.js +282 -0
  41. package/dist/routing-types.d.ts +39 -0
  42. package/dist/routing-types.js +14 -0
  43. package/dist/stores/redis.d.ts +30 -0
  44. package/dist/stores/redis.js +42 -0
  45. package/dist/strategies.d.ts +18 -0
  46. package/dist/strategies.js +44 -0
  47. package/dist/types.d.ts +4 -2
  48. package/dist/utils/fetch.d.ts +24 -0
  49. package/dist/utils/fetch.js +74 -0
  50. package/package.json +7 -4
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ /**
3
+ * MoonPay crypto on/off-ramp provider
4
+ * @see https://dev.moonpay.com
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.MoonPayProvider = void 0;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const base_1 = require("./base");
13
+ const fetch_1 = require("../utils/fetch");
14
+ class MoonPayProvider extends base_1.CryptoRampProvider {
15
+ constructor(config) {
16
+ super();
17
+ this.name = 'moonpay';
18
+ this.apiKey = config.apiKey;
19
+ this.secretKey = config.secretKey;
20
+ this.sandbox = config.sandbox;
21
+ this.webhookSecret = config.webhookSecret;
22
+ // Sandbox vs live is determined by API key prefix (pk_test_* vs pk_live_*)
23
+ this.baseUrl = 'https://api.moonpay.com';
24
+ this.widgetUrl = this.sandbox
25
+ ? 'https://buy-sandbox.moonpay.com'
26
+ : 'https://buy.moonpay.com';
27
+ }
28
+ async getQuote(direction, fiatAmount, fiatCurrency, cryptoAsset, _network) {
29
+ const endpoint = direction === 'on' ? 'quote' : 'sell_quote';
30
+ let params;
31
+ if (direction === 'on') {
32
+ params = new URLSearchParams({
33
+ apiKey: this.apiKey,
34
+ baseCurrencyCode: fiatCurrency.toLowerCase(),
35
+ quoteCurrencyCode: cryptoAsset.toLowerCase(),
36
+ baseCurrencyAmount: fiatAmount.toString(),
37
+ });
38
+ }
39
+ else {
40
+ params = new URLSearchParams({
41
+ apiKey: this.apiKey,
42
+ baseCurrencyCode: cryptoAsset.toLowerCase(),
43
+ quoteCurrencyCode: fiatCurrency.toLowerCase(),
44
+ baseCurrencyAmount: fiatAmount.toString(),
45
+ });
46
+ }
47
+ const url = `${this.baseUrl}/v3/currencies/${cryptoAsset.toLowerCase()}/${endpoint}?${params}`;
48
+ const response = await (0, fetch_1.timedFetch)(url);
49
+ if (!response.ok) {
50
+ throw new Error(`MoonPay quote failed: ${response.status}`);
51
+ }
52
+ const data = await response.json();
53
+ const feeTotal = data.feeAmount || 0;
54
+ const rate = data.quoteCurrencyPrice || 0;
55
+ const cryptoAmount = data.quoteCurrencyAmount || 0;
56
+ return {
57
+ fiatAmount,
58
+ cryptoAmount,
59
+ rate,
60
+ feeFixed: 0,
61
+ feePercent: (feeTotal / fiatAmount) * 100,
62
+ feeTotal,
63
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
64
+ };
65
+ }
66
+ async createOnRamp(params) {
67
+ (0, base_1.validateWalletAddress)(params.destinationWallet, params.network);
68
+ const quote = await this.getQuote('on', params.fiatAmount, params.fiatCurrency, params.asset, params.network);
69
+ const widgetParams = new URLSearchParams({
70
+ apiKey: this.apiKey,
71
+ currencyCode: params.asset.toLowerCase(),
72
+ baseCurrencyCode: params.fiatCurrency.toLowerCase(),
73
+ baseCurrencyAmount: params.fiatAmount.toString(),
74
+ walletAddress: params.destinationWallet,
75
+ email: params.customer.email,
76
+ externalTransactionId: params.reference,
77
+ redirectURL: params.urls.success,
78
+ });
79
+ const signature = this.signWidgetUrl(`?${widgetParams.toString()}`);
80
+ widgetParams.append('signature', signature);
81
+ const checkoutUrl = `${this.widgetUrl}?${widgetParams}`;
82
+ return {
83
+ id: `moonpay_on_${params.reference}`,
84
+ direction: 'on',
85
+ status: 'pending',
86
+ quote,
87
+ checkoutUrl,
88
+ createdAt: new Date().toISOString(),
89
+ expiresAt: quote.expiresAt,
90
+ };
91
+ }
92
+ async createOffRamp(params) {
93
+ if (params.sourceWallet) {
94
+ (0, base_1.validateWalletAddress)(params.sourceWallet, params.network);
95
+ }
96
+ const quote = await this.getQuote('off', params.cryptoAmount, params.fiatCurrency, params.asset, params.network);
97
+ const sellWidgetUrl = this.sandbox
98
+ ? 'https://sell-sandbox.moonpay.com'
99
+ : 'https://sell.moonpay.com';
100
+ const widgetParams = new URLSearchParams({
101
+ apiKey: this.apiKey,
102
+ baseCurrencyCode: params.asset.toLowerCase(),
103
+ quoteCurrencyCode: params.fiatCurrency.toLowerCase(),
104
+ baseCurrencyAmount: params.cryptoAmount.toString(),
105
+ externalTransactionId: params.reference,
106
+ refundWalletAddress: params.sourceWallet || '',
107
+ redirectURL: params.urls?.success || '',
108
+ });
109
+ const signature = this.signWidgetUrl(`?${widgetParams.toString()}`);
110
+ widgetParams.append('signature', signature);
111
+ const checkoutUrl = `${sellWidgetUrl}?${widgetParams}`;
112
+ return {
113
+ id: `moonpay_off_${params.reference}`,
114
+ direction: 'off',
115
+ status: 'pending',
116
+ quote,
117
+ checkoutUrl,
118
+ createdAt: new Date().toISOString(),
119
+ expiresAt: quote.expiresAt,
120
+ };
121
+ }
122
+ async getRamp(id) {
123
+ const response = await (0, fetch_1.timedFetch)(`${this.baseUrl}/v3/transactions/${id}`, {
124
+ headers: {
125
+ Authorization: `Bearer ${this.secretKey}`,
126
+ },
127
+ });
128
+ if (!response.ok) {
129
+ throw new Error(`MoonPay getRamp failed: ${response.status}`);
130
+ }
131
+ const data = await response.json();
132
+ const direction = data.type === 'buy' ? 'on' : 'off';
133
+ const status = this.mapMoonPayStatus(data.status);
134
+ const quote = {
135
+ fiatAmount: data.baseCurrencyAmount || 0,
136
+ cryptoAmount: data.quoteCurrencyAmount || 0,
137
+ rate: data.quoteCurrencyPrice || 0,
138
+ feeFixed: 0,
139
+ feePercent: 0,
140
+ feeTotal: data.feeAmount || 0,
141
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
142
+ };
143
+ const sanitizedRaw = { ...data };
144
+ if (sanitizedRaw.bankAccount)
145
+ delete sanitizedRaw.bankAccount;
146
+ if (sanitizedRaw.bank_account)
147
+ delete sanitizedRaw.bank_account;
148
+ return {
149
+ id: data.id,
150
+ direction,
151
+ status,
152
+ quote,
153
+ txHash: data.cryptoTransactionId,
154
+ createdAt: data.createdAt,
155
+ raw: sanitizedRaw,
156
+ };
157
+ }
158
+ parseWebhook(body, _headers) {
159
+ const event = typeof body === 'string' ? JSON.parse(body) : body;
160
+ return {
161
+ type: event.type,
162
+ data: event.data,
163
+ raw: event,
164
+ };
165
+ }
166
+ verifyWebhook(body, headers) {
167
+ if (!this.webhookSecret)
168
+ return false;
169
+ // Try V2 signature first (recommended)
170
+ const signatureV2 = headers?.['moonpay-signature-v2'];
171
+ if (signatureV2) {
172
+ return this.verifyWebhookV2(body, signatureV2);
173
+ }
174
+ // Fall back to legacy signature
175
+ const signature = headers?.['moonpay-signature'] || headers?.signature;
176
+ if (!signature)
177
+ return false;
178
+ const expectedSignature = crypto_1.default
179
+ .createHmac('sha256', this.webhookSecret)
180
+ .update(body)
181
+ .digest('hex');
182
+ const sigBuf = Buffer.from(signature);
183
+ const expBuf = Buffer.from(expectedSignature);
184
+ if (sigBuf.length !== expBuf.length)
185
+ return false;
186
+ return crypto_1.default.timingSafeEqual(sigBuf, expBuf);
187
+ }
188
+ verifyWebhookV2(body, signatureHeader) {
189
+ if (!this.webhookSecret)
190
+ return false;
191
+ // Parse V2 format: t=<timestamp>,s=<signature>
192
+ const parts = signatureHeader.split(',');
193
+ let timestamp;
194
+ let signature;
195
+ for (const part of parts) {
196
+ const [key, value] = part.split('=');
197
+ if (key === 't')
198
+ timestamp = value;
199
+ if (key === 's')
200
+ signature = value;
201
+ }
202
+ if (!timestamp || !signature)
203
+ return false;
204
+ // Replay protection: timestamp must be within 5 minutes
205
+ const now = Math.floor(Date.now() / 1000);
206
+ const ts = parseInt(timestamp, 10);
207
+ if (isNaN(ts) || Math.abs(now - ts) > 300)
208
+ return false;
209
+ // Verify signature: HMAC-SHA256 of ${timestamp}.${body}
210
+ const bodyStr = Buffer.isBuffer(body) ? body.toString('utf8') : body;
211
+ const payload = `${timestamp}.${bodyStr}`;
212
+ const expectedSignature = crypto_1.default
213
+ .createHmac('sha256', this.webhookSecret)
214
+ .update(payload)
215
+ .digest('hex');
216
+ const sigBuf = Buffer.from(signature);
217
+ const expBuf = Buffer.from(expectedSignature);
218
+ if (sigBuf.length !== expBuf.length)
219
+ return false;
220
+ return crypto_1.default.timingSafeEqual(sigBuf, expBuf);
221
+ }
222
+ getCapabilities() {
223
+ return {
224
+ supportedAssets: ['BTC', 'ETH', 'USDT', 'USDC', 'BNB', 'SOL'],
225
+ supportedNetworks: ['BTC', 'ETH', 'TRON', 'POLYGON', 'BSC', 'SOLANA'],
226
+ supportedFiat: ['USD', 'EUR', 'GBP', 'ZAR'],
227
+ country: 'GLOBAL',
228
+ kycRequired: true,
229
+ onRampLimits: {
230
+ min: 20,
231
+ max: 50000,
232
+ },
233
+ offRampLimits: {
234
+ min: 30,
235
+ max: 50000,
236
+ },
237
+ fees: {
238
+ onRampPercent: 4.5,
239
+ offRampPercent: 1.0,
240
+ },
241
+ };
242
+ }
243
+ signWidgetUrl(queryString) {
244
+ return crypto_1.default
245
+ .createHmac('sha256', this.secretKey)
246
+ .update(queryString)
247
+ .digest('base64');
248
+ }
249
+ mapMoonPayStatus(moonpayStatus) {
250
+ const statusMap = {
251
+ waitingPayment: 'pending',
252
+ pending: 'pending',
253
+ waitingAuthorization: 'pending',
254
+ completed: 'completed',
255
+ failed: 'failed',
256
+ expired: 'expired',
257
+ };
258
+ return statusMap[moonpayStatus] || 'pending';
259
+ }
260
+ }
261
+ exports.MoonPayProvider = MoonPayProvider;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CryptoRampRouter — Multi-provider crypto routing
3
+ */
4
+ import { CryptoRamp } from './index';
5
+ import { OnRampParams, OffRampParams, RampQuote, RampResult } from './types';
6
+ export interface CryptoRampRouterConfig {
7
+ providers: Array<{
8
+ provider: CryptoRamp;
9
+ priority?: number;
10
+ }>;
11
+ strategy?: 'cheapest' | 'priority' | 'round-robin';
12
+ fallback?: {
13
+ enabled: boolean;
14
+ maxAttempts?: number;
15
+ retryDelayMs?: number;
16
+ };
17
+ allowExperimental?: boolean;
18
+ circuitBreakerStore?: import('../circuit-breaker-store').CircuitBreakerStore;
19
+ }
20
+ export declare class CryptoRampRouter {
21
+ private providers;
22
+ private strategy;
23
+ private fallback;
24
+ private allowExperimental;
25
+ private circuitBreakers;
26
+ private rrIndex;
27
+ private config;
28
+ constructor(config: CryptoRampRouterConfig);
29
+ getQuote(direction: 'on' | 'off', fiatAmount: number, fiatCurrency: string, cryptoAsset: string, network: string): Promise<RampQuote>;
30
+ createOnRamp(params: OnRampParams): Promise<RampResult>;
31
+ createOffRamp(params: OffRampParams): Promise<RampResult>;
32
+ getRamp(id: string, provider?: string): Promise<RampResult>;
33
+ private filterProviders;
34
+ private orderProviders;
35
+ private sleep;
36
+ }
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ /**
3
+ * CryptoRampRouter — Multi-provider crypto routing
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CryptoRampRouter = void 0;
7
+ const routing_types_1 = require("../routing-types");
8
+ const circuit_breaker_1 = require("../circuit-breaker");
9
+ function sanitizeErrorMessage(msg) {
10
+ if (!msg)
11
+ return 'unknown error';
12
+ return msg
13
+ .replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
14
+ .replace(/(api[_-]?key|secret|token|password)["':=\s]+\S+/gi, '$1=[REDACTED]')
15
+ .slice(0, 500);
16
+ }
17
+ class CryptoRampRouter {
18
+ constructor(config) {
19
+ this.rrIndex = 0;
20
+ this.config = config;
21
+ this.providers = config.providers.map(p => ({
22
+ instance: p.provider,
23
+ priority: p.priority,
24
+ }));
25
+ this.strategy = config.strategy ?? 'cheapest';
26
+ this.fallback = {
27
+ enabled: config.fallback?.enabled ?? true,
28
+ maxAttempts: config.fallback?.maxAttempts ?? 3,
29
+ retryDelayMs: config.fallback?.retryDelayMs ?? 250,
30
+ };
31
+ this.allowExperimental = config.allowExperimental ?? false;
32
+ this.circuitBreakers = new Map();
33
+ for (const p of this.providers) {
34
+ const name = p.instance.getProviderName();
35
+ this.circuitBreakers.set(name, new circuit_breaker_1.CircuitBreaker(name, {
36
+ store: config.circuitBreakerStore,
37
+ }));
38
+ }
39
+ }
40
+ async getQuote(direction, fiatAmount, fiatCurrency, cryptoAsset, network) {
41
+ const filtered = this.filterProviders({ fiatCurrency, asset: cryptoAsset, network });
42
+ if (filtered.length === 0) {
43
+ throw new Error(`No providers support ${cryptoAsset} on ${network} with ${fiatCurrency}`);
44
+ }
45
+ const ordered = this.orderProviders(filtered, direction);
46
+ const attempts = [];
47
+ let lastError = null;
48
+ for (const providerMeta of ordered) {
49
+ const providerName = providerMeta.instance.getProviderName();
50
+ const breaker = this.circuitBreakers.get(providerName);
51
+ if (breaker && (await breaker.isOpen())) {
52
+ attempts.push({
53
+ provider: providerName,
54
+ status: 'skipped',
55
+ errorMessage: 'Circuit breaker open',
56
+ latencyMs: 0,
57
+ });
58
+ continue;
59
+ }
60
+ const startTime = Date.now();
61
+ try {
62
+ const result = await providerMeta.instance.getQuote(direction, fiatAmount, fiatCurrency, cryptoAsset, network);
63
+ const latencyMs = Date.now() - startTime;
64
+ if (breaker)
65
+ await breaker.recordSuccess();
66
+ attempts.push({
67
+ provider: providerName,
68
+ status: 'success',
69
+ latencyMs,
70
+ });
71
+ return result;
72
+ }
73
+ catch (error) {
74
+ const latencyMs = Date.now() - startTime;
75
+ lastError = error;
76
+ if (breaker)
77
+ await breaker.recordFailure();
78
+ attempts.push({
79
+ provider: providerName,
80
+ status: 'failed',
81
+ errorCode: error.code,
82
+ errorMessage: sanitizeErrorMessage(error.message),
83
+ latencyMs,
84
+ });
85
+ if (!this.fallback.enabled || attempts.length >= this.fallback.maxAttempts) {
86
+ break;
87
+ }
88
+ await this.sleep(this.fallback.retryDelayMs);
89
+ }
90
+ }
91
+ throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
92
+ }
93
+ async createOnRamp(params) {
94
+ if (!Number.isFinite(params.fiatAmount) || params.fiatAmount <= 0) {
95
+ throw new Error('Invalid amount: must be a positive finite number');
96
+ }
97
+ const filtered = this.filterProviders(params);
98
+ if (filtered.length === 0) {
99
+ throw new Error(`No providers support ${params.asset} on ${params.network} with ${params.fiatCurrency}`);
100
+ }
101
+ const ordered = this.orderProviders(filtered, 'on');
102
+ const attempts = [];
103
+ let lastError = null;
104
+ for (const providerMeta of ordered) {
105
+ const providerName = providerMeta.instance.getProviderName();
106
+ const breaker = this.circuitBreakers.get(providerName);
107
+ if (breaker && (await breaker.isOpen())) {
108
+ attempts.push({
109
+ provider: providerName,
110
+ status: 'skipped',
111
+ errorMessage: 'Circuit breaker open',
112
+ latencyMs: 0,
113
+ });
114
+ continue;
115
+ }
116
+ const startTime = Date.now();
117
+ try {
118
+ const result = await providerMeta.instance.createOnRamp(params);
119
+ const latencyMs = Date.now() - startTime;
120
+ if (breaker)
121
+ await breaker.recordSuccess();
122
+ attempts.push({
123
+ provider: providerName,
124
+ status: 'success',
125
+ latencyMs,
126
+ });
127
+ const routingMeta = {
128
+ attempts,
129
+ chosenProvider: providerName,
130
+ strategy: this.strategy,
131
+ };
132
+ return {
133
+ ...result,
134
+ routingMeta,
135
+ };
136
+ }
137
+ catch (error) {
138
+ const latencyMs = Date.now() - startTime;
139
+ lastError = error;
140
+ if (breaker)
141
+ await breaker.recordFailure();
142
+ attempts.push({
143
+ provider: providerName,
144
+ status: 'failed',
145
+ errorCode: error.code,
146
+ errorMessage: sanitizeErrorMessage(error.message),
147
+ latencyMs,
148
+ });
149
+ if (!this.fallback.enabled || attempts.length >= this.fallback.maxAttempts) {
150
+ break;
151
+ }
152
+ await this.sleep(this.fallback.retryDelayMs);
153
+ }
154
+ }
155
+ throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
156
+ }
157
+ async createOffRamp(params) {
158
+ if (!Number.isFinite(params.cryptoAmount) || params.cryptoAmount <= 0) {
159
+ throw new Error('Invalid amount: must be a positive finite number');
160
+ }
161
+ const filtered = this.filterProviders(params);
162
+ if (filtered.length === 0) {
163
+ throw new Error(`No providers support ${params.asset} on ${params.network} with ${params.fiatCurrency}`);
164
+ }
165
+ const ordered = this.orderProviders(filtered, 'off');
166
+ const attempts = [];
167
+ let lastError = null;
168
+ for (const providerMeta of ordered) {
169
+ const providerName = providerMeta.instance.getProviderName();
170
+ const breaker = this.circuitBreakers.get(providerName);
171
+ if (breaker && (await breaker.isOpen())) {
172
+ attempts.push({
173
+ provider: providerName,
174
+ status: 'skipped',
175
+ errorMessage: 'Circuit breaker open',
176
+ latencyMs: 0,
177
+ });
178
+ continue;
179
+ }
180
+ const startTime = Date.now();
181
+ try {
182
+ const result = await providerMeta.instance.createOffRamp(params);
183
+ const latencyMs = Date.now() - startTime;
184
+ if (breaker)
185
+ await breaker.recordSuccess();
186
+ attempts.push({
187
+ provider: providerName,
188
+ status: 'success',
189
+ latencyMs,
190
+ });
191
+ const routingMeta = {
192
+ attempts,
193
+ chosenProvider: providerName,
194
+ strategy: this.strategy,
195
+ };
196
+ return {
197
+ ...result,
198
+ routingMeta,
199
+ };
200
+ }
201
+ catch (error) {
202
+ const latencyMs = Date.now() - startTime;
203
+ lastError = error;
204
+ if (breaker)
205
+ await breaker.recordFailure();
206
+ attempts.push({
207
+ provider: providerName,
208
+ status: 'failed',
209
+ errorCode: error.code,
210
+ errorMessage: sanitizeErrorMessage(error.message),
211
+ latencyMs,
212
+ });
213
+ if (!this.fallback.enabled || attempts.length >= this.fallback.maxAttempts) {
214
+ break;
215
+ }
216
+ await this.sleep(this.fallback.retryDelayMs);
217
+ }
218
+ }
219
+ throw new routing_types_1.RoutingError(`All providers failed: ${lastError?.message || 'Unknown error'}`, attempts);
220
+ }
221
+ async getRamp(id, provider) {
222
+ if (provider) {
223
+ const providerMeta = this.providers.find(p => p.instance.getProviderName() === provider);
224
+ if (!providerMeta) {
225
+ throw new Error(`Provider ${provider} not found in router`);
226
+ }
227
+ return providerMeta.instance.getRamp(id);
228
+ }
229
+ let lastError = null;
230
+ for (const providerMeta of this.providers) {
231
+ try {
232
+ return await providerMeta.instance.getRamp(id);
233
+ }
234
+ catch (error) {
235
+ lastError = error;
236
+ }
237
+ }
238
+ throw new Error(`Ramp ${id} not found in any provider: ${lastError?.message || 'Unknown error'}`);
239
+ }
240
+ filterProviders(params) {
241
+ return this.providers.filter(p => {
242
+ const caps = p.instance.getCapabilities();
243
+ if (!this.allowExperimental && caps.experimental === true) {
244
+ return false;
245
+ }
246
+ if (!caps.supportedAssets.includes(params.asset)) {
247
+ return false;
248
+ }
249
+ if (!caps.supportedNetworks.includes(params.network)) {
250
+ return false;
251
+ }
252
+ if (!caps.supportedFiat.includes(params.fiatCurrency)) {
253
+ return false;
254
+ }
255
+ return true;
256
+ });
257
+ }
258
+ orderProviders(providers, direction) {
259
+ switch (this.strategy) {
260
+ case 'cheapest':
261
+ return [...providers].sort((a, b) => {
262
+ const capsA = a.instance.getCapabilities();
263
+ const capsB = b.instance.getCapabilities();
264
+ const feeA = direction === 'on' ? capsA.fees.onRampPercent : capsA.fees.offRampPercent;
265
+ const feeB = direction === 'on' ? capsB.fees.onRampPercent : capsB.fees.offRampPercent;
266
+ return feeA - feeB;
267
+ });
268
+ case 'priority':
269
+ return [...providers].sort((a, b) => {
270
+ const priorityA = a.priority ?? 0;
271
+ const priorityB = b.priority ?? 0;
272
+ return priorityB - priorityA;
273
+ });
274
+ case 'round-robin': {
275
+ const idx = this.rrIndex;
276
+ this.rrIndex = (this.rrIndex + 1) % providers.length;
277
+ return [...providers.slice(idx), ...providers.slice(0, idx)];
278
+ }
279
+ default:
280
+ return providers;
281
+ }
282
+ }
283
+ sleep(ms) {
284
+ return new Promise(resolve => setTimeout(resolve, ms));
285
+ }
286
+ }
287
+ exports.CryptoRampRouter = CryptoRampRouter;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Crypto on/off-ramp types
3
+ */
4
+ import { Customer, Currency } from '../types';
5
+ import { RoutingMeta } from '../routing-types';
6
+ export type CryptoAsset = 'BTC' | 'ETH' | 'USDT' | 'USDC' | 'BNB' | 'SOL' | string;
7
+ export type CryptoNetwork = 'BTC' | 'ETH' | 'TRON' | 'POLYGON' | 'BSC' | 'SOLANA' | string;
8
+ export type RampDirection = 'on' | 'off';
9
+ export type RampStatus = 'pending' | 'completed' | 'failed' | 'expired';
10
+ export interface OnRampParams {
11
+ fiatAmount: number;
12
+ fiatCurrency: Currency;
13
+ asset: CryptoAsset;
14
+ network: CryptoNetwork;
15
+ destinationWallet: string;
16
+ customer: Customer;
17
+ urls: {
18
+ success: string;
19
+ cancel: string;
20
+ webhook: string;
21
+ };
22
+ reference: string;
23
+ metadata?: Record<string, any>;
24
+ }
25
+ export interface BankAccount {
26
+ accountNumber: string;
27
+ branchCode: string;
28
+ accountHolder: string;
29
+ bankName: string;
30
+ }
31
+ export interface OffRampParams {
32
+ cryptoAmount: number;
33
+ asset: CryptoAsset;
34
+ network: CryptoNetwork;
35
+ sourceWallet?: string;
36
+ fiatCurrency: Currency;
37
+ bankAccount: BankAccount;
38
+ customer: Customer;
39
+ urls?: {
40
+ success: string;
41
+ cancel: string;
42
+ webhook: string;
43
+ };
44
+ reference: string;
45
+ metadata?: Record<string, any>;
46
+ }
47
+ export interface RampQuote {
48
+ fiatAmount: number;
49
+ cryptoAmount: number;
50
+ rate: number;
51
+ feeFixed: number;
52
+ feePercent: number;
53
+ feeTotal: number;
54
+ expiresAt: string;
55
+ }
56
+ export interface RampResult {
57
+ id: string;
58
+ direction: RampDirection;
59
+ status: RampStatus;
60
+ quote: RampQuote;
61
+ checkoutUrl?: string;
62
+ depositAddress?: string;
63
+ depositTag?: string;
64
+ txHash?: string;
65
+ createdAt: string;
66
+ expiresAt?: string;
67
+ raw?: any;
68
+ routingMeta?: RoutingMeta;
69
+ }
70
+ export interface CryptoRampCapabilities {
71
+ supportedAssets: CryptoAsset[];
72
+ supportedNetworks: CryptoNetwork[];
73
+ supportedFiat: Currency[];
74
+ country: string;
75
+ kycRequired: boolean;
76
+ onRampLimits?: {
77
+ min: number;
78
+ max: number;
79
+ };
80
+ offRampLimits?: {
81
+ min: number;
82
+ max: number;
83
+ };
84
+ fees: {
85
+ onRampPercent: number;
86
+ offRampPercent: number;
87
+ };
88
+ experimental?: boolean;
89
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Crypto on/off-ramp types
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });