@ensofinance/checkout-widget 0.0.18 → 0.0.19

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.
package/src/store.ts CHANGED
@@ -30,6 +30,10 @@ type Store = {
30
30
  slippage?: number;
31
31
  setSlippage: (slippage: number) => void;
32
32
 
33
+ // Override recipient address (different from connected wallet)
34
+ recipient?: string;
35
+ setRecipient: (recipient: string | undefined) => void;
36
+
33
37
  // Mesh integration
34
38
  meshAccessToken: AccessTokenPayload | null;
35
39
  setMeshAccessToken: (meshAccessToken: AccessTokenPayload) => void;
@@ -63,6 +67,10 @@ export const useAppStore = create<Store>((set) => ({
63
67
  slippage: DEFAULT_SLIPPAGE,
64
68
  setSlippage: (slippage: number) => set({ slippage }),
65
69
 
70
+ // Override recipient address
71
+ recipient: undefined,
72
+ setRecipient: (recipient: string | undefined) => set({ recipient }),
73
+
66
74
  // Mesh integration
67
75
  meshAccessToken: null,
68
76
  setMeshAccessToken: (meshAccessToken: AccessTokenPayload) =>
@@ -16,6 +16,8 @@ export type CheckoutConfig = {
16
16
  enableExchange?: SupportedExchanges[];
17
17
  /** Override the default CEX bridge chain mapping (maps target chains to intermediate chains for withdrawal + bridge) */
18
18
  cexBridgeChainMapping?: Record<number, number>;
19
+ /** Override recipient address (defaults to connected wallet's smart account) */
20
+ recipient?: string;
19
21
  };
20
22
 
21
23
  export type CheckoutModalProps = {
@@ -18,7 +18,7 @@ export const EXCHANGE_MIN_LIMIT = {
18
18
  [MESH_SYMBOLS.USDT]: 20,
19
19
  [MESH_SYMBOLS.WBTC]: 0.00000001,
20
20
  };
21
- export const EXCHANGE_MAX_LIMIT_GAP_USD = 5
21
+ export const EXCHANGE_MAX_LIMIT_GAP_USD = 5;
22
22
 
23
23
  export const ETH_TOKEN: Token = {
24
24
  address: ETH_ADDRESS,
@@ -197,14 +197,6 @@ export const DEFAULT_CEX_BRIDGE_CHAIN_MAPPING: Record<number, number> = {
197
197
  [SupportedChainId.HYPERLIQUID]: SupportedChainId.ARBITRUM_ONE, // 999 → 42161
198
198
  };
199
199
 
200
- /** @deprecated Use the context-aware version with custom mapping parameter */
201
- export const CEX_BRIDGE_CHAIN_MAPPING = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING;
202
-
203
- export const isCexBridgeRequired = (
204
- chainId: number,
205
- mapping: Record<number, number> = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
206
- ): boolean => chainId in mapping;
207
-
208
200
  export const getCexIntermediateChain = (
209
201
  chainId: number,
210
202
  mapping: Record<number, number> = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
@@ -14,9 +14,9 @@ import {
14
14
  import { getWalletDisplayName, getWalletIcon } from "@/util/wallet";
15
15
  import { formatUSD, normalizeValue } from "@/util";
16
16
  import { useAppStore } from "@/store";
17
- import { VITALIK_ADDRESS } from "./constants";
17
+ import { SupportedChainId, VITALIK_ADDRESS } from "./constants";
18
18
 
19
- export function getERC4337CloneFactory(chainId: ChainIds): AddressArg {
19
+ export function getERC4337CloneFactory(chainId: SupportedChainId): Address {
20
20
  return "0x1a59347d28f64091079fa04a2cbd03da63dff154";
21
21
  }
22
22
 
@@ -84,6 +84,7 @@ export const useAppDetails = () => {
84
84
  const selectedIntegration = useAppStore(
85
85
  (state) => state.selectedIntegration,
86
86
  );
87
+ const recipient = useAppStore((state) => state.recipient);
87
88
 
88
89
  const {
89
90
  data: [tokenInData],
@@ -115,7 +116,8 @@ export const useAppDetails = () => {
115
116
  }),
116
117
  );
117
118
 
118
- const protocolName = protocolData?.name || nontokenizedData.protocol;
119
+ const protocolName =
120
+ protocolData?.name || nontokenizedData.protocol;
119
121
 
120
122
  // Return first underlying token as primary, with all underlyingTokens available
121
123
  return {
@@ -153,6 +155,7 @@ export const useAppDetails = () => {
153
155
  effectiveTokenOutData,
154
156
  effectivePrice,
155
157
  isNontokenized,
158
+ recipient,
156
159
  };
157
160
  };
158
161
 
@@ -262,12 +265,16 @@ export const useRouteData = () => {
262
265
  tokenOut,
263
266
  selectedIntegration,
264
267
  isNontokenized,
268
+ recipient,
265
269
  } = useAppDetails();
266
270
 
271
+ // Use recipient override if provided, otherwise use connected wallet address
272
+ const receiver = recipient || address;
273
+
267
274
  const standardRoute = useTokenizedRouteData({
268
275
  routingStrategy: selectedIntegration?.type ? "checkout" : "router",
269
276
  fromAddress: address,
270
- receiver: address,
277
+ receiver,
271
278
  spender: address,
272
279
  amountIn,
273
280
  tokenIn,
@@ -281,7 +288,7 @@ export const useRouteData = () => {
281
288
  const nontokenizedRoute = useNontokenizedRouteData({
282
289
  routingStrategy: selectedIntegration?.type ? "checkout" : "router",
283
290
  fromAddress: address,
284
- receiver: address,
291
+ receiver,
285
292
  spender: address,
286
293
  amountIn,
287
294
  tokenIn,
@@ -0,0 +1,319 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { useCallback, useState, useEffect } from "react";
3
+ import type {
4
+ MeldQuote,
5
+ MeldQuotesResponse,
6
+ MeldSessionRequest,
7
+ MeldSessionResponse,
8
+ MeldTransaction,
9
+ MeldTransactionStatus,
10
+ } from "@/types";
11
+
12
+ // BFF URL - proxies requests to MELD API with authentication
13
+ const MELD_BFF_URL = "https://meld-bff.enso-checkout.workers.dev";
14
+
15
+ // Chain ID to MELD chain code mapping
16
+ export const MELD_CHAIN_CODES: Record<number, string> = {
17
+ 1: "ETH", // Ethereum
18
+ 8453: "BASE", // Base
19
+ 42161: "ARBITRUM_ONE", // Arbitrum
20
+ 137: "POLYGON", // Polygon
21
+ 10: "OPTIMISM", // Optimism
22
+ 43114: "AVAX_CCHAIN", // Avalanche
23
+ 56: "BSC", // BNB Chain
24
+ 146: "SONIC", // Sonic
25
+ };
26
+
27
+ // Native token symbols by chain
28
+ export const NATIVE_TOKEN_SYMBOLS: Record<number, string> = {
29
+ 1: "ETH",
30
+ 8453: "ETH",
31
+ 42161: "ETH",
32
+ 137: "MATIC",
33
+ 10: "ETH",
34
+ 43114: "AVAX",
35
+ 56: "BNB",
36
+ 146: "S",
37
+ };
38
+
39
+ // Common tokens across chains
40
+ export const COMMON_TOKENS = ["ETH", "USDC", "USDT", "DAI"];
41
+
42
+ export const MELD_SUPPORTED_CHAINS = Object.keys(MELD_CHAIN_CODES).map(Number);
43
+
44
+ /**
45
+ * Check if a chain is supported by MELD
46
+ */
47
+ export function isMeldSupportedChain(chainId: number): boolean {
48
+ return chainId in MELD_CHAIN_CODES;
49
+ }
50
+
51
+ /**
52
+ * Get the destination crypto code for MELD
53
+ * Format: SYMBOL_CHAINCODE (e.g., ETH_BASE, USDC_POLYGON)
54
+ */
55
+ export function getMeldCryptoCode(symbol: string, chainId: number): string {
56
+ const chainCode = MELD_CHAIN_CODES[chainId];
57
+ if (!chainCode) {
58
+ throw new Error(`Chain ${chainId} not supported by MELD`);
59
+ }
60
+ return `${symbol}_${chainCode}`;
61
+ }
62
+
63
+ /**
64
+ * Hook to fetch quotes from MELD
65
+ */
66
+ export function useMeldQuotes(params: {
67
+ sourceCurrency: string;
68
+ destinationCurrency: string; // Should be in format SYMBOL_CHAINCODE
69
+ amount: number;
70
+ countryCode: string;
71
+ enabled?: boolean;
72
+ }) {
73
+ const { sourceCurrency, destinationCurrency, amount, countryCode, enabled = true } = params;
74
+
75
+ return useQuery({
76
+ queryKey: ["meld-quotes", sourceCurrency, destinationCurrency, amount, countryCode],
77
+ queryFn: async (): Promise<MeldQuote[]> => {
78
+ const searchParams = new URLSearchParams({
79
+ sourceCurrencyCode: sourceCurrency,
80
+ destinationCurrencyCode: destinationCurrency,
81
+ sourceAmount: String(amount),
82
+ countryCode,
83
+ paymentMethodType: "CREDIT_DEBIT_CARD",
84
+ });
85
+
86
+ const res = await fetch(`${MELD_BFF_URL}/quotes?${searchParams}`);
87
+
88
+ if (!res.ok) {
89
+ const error = await res.json().catch(() => ({ error: "Failed to fetch quotes" }));
90
+ throw new Error(error.error || error.message || "Failed to fetch quotes");
91
+ }
92
+
93
+ const data: MeldQuotesResponse = await res.json();
94
+
95
+ if (data.error) {
96
+ throw new Error(data.error);
97
+ }
98
+
99
+ // Sort by best rate (highest destination amount)
100
+ return (data.quotes || []).sort((a, b) => b.destinationAmount - a.destinationAmount);
101
+ },
102
+ enabled: enabled && amount > 0 && !!destinationCurrency && !!countryCode,
103
+ staleTime: 30000, // 30 seconds
104
+ refetchInterval: 60000, // Refresh every minute
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Hook to create a MELD session
110
+ */
111
+ export function useCreateMeldSession() {
112
+ const queryClient = useQueryClient();
113
+
114
+ return useMutation({
115
+ mutationFn: async (params: MeldSessionRequest): Promise<MeldSessionResponse> => {
116
+ const res = await fetch(`${MELD_BFF_URL}/session`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify(params),
120
+ });
121
+
122
+ if (!res.ok) {
123
+ const error = await res.json().catch(() => ({ error: "Failed to create session" }));
124
+ throw new Error(error.error || error.message || "Failed to create session");
125
+ }
126
+
127
+ return res.json();
128
+ },
129
+ onSuccess: (data) => {
130
+ // Store session for tracking
131
+ if (data.sessionId) {
132
+ queryClient.setQueryData(["meld-session", data.sessionId], data);
133
+ }
134
+ },
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Hook to track a MELD transaction
140
+ */
141
+ export function useMeldTransaction(sessionId: string | null) {
142
+ return useQuery({
143
+ queryKey: ["meld-transaction", sessionId],
144
+ queryFn: async (): Promise<MeldTransaction | null> => {
145
+ if (!sessionId) return null;
146
+
147
+ const res = await fetch(`${MELD_BFF_URL}/transaction/${sessionId}`);
148
+
149
+ if (!res.ok) {
150
+ if (res.status === 404) {
151
+ return null; // Transaction not found yet
152
+ }
153
+ const error = await res.json().catch(() => ({ error: "Failed to fetch transaction" }));
154
+ throw new Error(error.error || error.message || "Failed to fetch transaction");
155
+ }
156
+
157
+ return res.json();
158
+ },
159
+ enabled: !!sessionId,
160
+ refetchInterval: (query) => {
161
+ const data = query.state.data;
162
+ // Stop polling when transaction is complete or failed
163
+ if (
164
+ data?.status === "COMPLETED" ||
165
+ data?.status === "FAILED" ||
166
+ data?.status === "REFUNDED" ||
167
+ data?.status === "CANCELLED"
168
+ ) {
169
+ return false;
170
+ }
171
+ return 3000; // Poll every 3 seconds
172
+ },
173
+ staleTime: 0, // Always refetch
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Terminal status check
179
+ */
180
+ export function isTerminalStatus(status: MeldTransactionStatus): boolean {
181
+ return ["COMPLETED", "FAILED", "REFUNDED", "CANCELLED"].includes(status);
182
+ }
183
+
184
+ /**
185
+ * Success status check
186
+ */
187
+ export function isSuccessStatus(status: MeldTransactionStatus): boolean {
188
+ return status === "COMPLETED";
189
+ }
190
+
191
+ /**
192
+ * Hook for detecting user's country code
193
+ */
194
+ export function useCountryCode() {
195
+ const [countryCode, setCountryCode] = useState<string>("US");
196
+ const [isLoading, setIsLoading] = useState(true);
197
+
198
+ useEffect(() => {
199
+ // Try to detect country from timezone
200
+ const detectCountry = async () => {
201
+ try {
202
+ // Simple timezone-based detection (can be enhanced)
203
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
204
+
205
+ // Basic mapping of common timezones
206
+ const tzToCountry: Record<string, string> = {
207
+ "America/New_York": "US",
208
+ "America/Los_Angeles": "US",
209
+ "America/Chicago": "US",
210
+ "Europe/London": "GB",
211
+ "Europe/Paris": "FR",
212
+ "Europe/Berlin": "DE",
213
+ "Asia/Tokyo": "JP",
214
+ "Asia/Singapore": "SG",
215
+ "Australia/Sydney": "AU",
216
+ };
217
+
218
+ const detected = tzToCountry[timezone];
219
+ if (detected) {
220
+ setCountryCode(detected);
221
+ }
222
+ } catch {
223
+ // Keep default
224
+ } finally {
225
+ setIsLoading(false);
226
+ }
227
+ };
228
+
229
+ detectCountry();
230
+ }, []);
231
+
232
+ return { countryCode, setCountryCode, isLoading };
233
+ }
234
+
235
+ /**
236
+ * Hook to manage the full MELD card buy flow state
237
+ */
238
+ export function useMeldCardBuyFlow() {
239
+ const [sessionId, setSessionId] = useState<string | null>(null);
240
+ const [widgetUrl, setWidgetUrl] = useState<string | null>(null);
241
+ const [widgetOpen, setWidgetOpen] = useState(false);
242
+
243
+ const createSession = useCreateMeldSession();
244
+ const { data: transaction, isLoading: isTrackingLoading } = useMeldTransaction(sessionId);
245
+
246
+ const startSession = useCallback(
247
+ async (params: MeldSessionRequest) => {
248
+ const result = await createSession.mutateAsync(params);
249
+ setSessionId(result.sessionId);
250
+ setWidgetUrl(result.widgetUrl);
251
+ return result;
252
+ },
253
+ [createSession]
254
+ );
255
+
256
+ const openWidget = useCallback(() => {
257
+ if (widgetUrl) {
258
+ setWidgetOpen(true);
259
+ // Open in new window/tab
260
+ window.open(widgetUrl, "_blank", "width=500,height=700");
261
+ }
262
+ }, [widgetUrl]);
263
+
264
+ const reset = useCallback(() => {
265
+ setSessionId(null);
266
+ setWidgetUrl(null);
267
+ setWidgetOpen(false);
268
+ }, []);
269
+
270
+ return {
271
+ // Session management
272
+ sessionId,
273
+ widgetUrl,
274
+ widgetOpen,
275
+ startSession,
276
+ openWidget,
277
+ reset,
278
+
279
+ // Session creation state
280
+ isCreatingSession: createSession.isPending,
281
+ sessionError: createSession.error,
282
+
283
+ // Transaction tracking
284
+ transaction,
285
+ isTrackingLoading,
286
+ isComplete: transaction ? isSuccessStatus(transaction.status) : false,
287
+ isFailed: transaction
288
+ ? ["FAILED", "REFUNDED", "CANCELLED"].includes(transaction.status)
289
+ : false,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Format MELD provider name for display
295
+ */
296
+ export function formatProviderName(provider: string): string {
297
+ const names: Record<string, string> = {
298
+ TRANSAK: "Transak",
299
+ MOONPAY: "MoonPay",
300
+ BANXA: "Banxa",
301
+ RAMP: "Ramp",
302
+ SARDINE: "Sardine",
303
+ STRIPE: "Stripe",
304
+ };
305
+ return names[provider] || provider;
306
+ }
307
+
308
+ /**
309
+ * Get provider icon URL
310
+ */
311
+ export function getProviderIcon(provider: string): string {
312
+ const icons: Record<string, string> = {
313
+ TRANSAK: "https://assets.transak.com/images/website/transak-logo.svg",
314
+ MOONPAY: "https://www.moonpay.com/assets/logo-full-white.svg",
315
+ BANXA: "https://banxa.com/wp-content/uploads/2022/07/banxa-logo.svg",
316
+ RAMP: "https://ramp.network/assets/images/Logo.svg",
317
+ };
318
+ return icons[provider] || "";
319
+ }