@stableflow/pay-widgets 0.0.1-beta-82ede460a5-20260923

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.
@@ -0,0 +1,1467 @@
1
+ // src/token-select/context.tsx
2
+ import { createContext, useContext, useEffect as useEffect2, useMemo } from "react";
3
+
4
+ // src/token-select/balances.ts
5
+ import { useSyncExternalStore } from "react";
6
+
7
+ // src/token-select/config.ts
8
+ var TOKEN_BALANCE_POLL_MS = 6e4;
9
+ var CONFIG_STALE_MS = 30 * 60 * 1e3;
10
+ var ALL_CHAIN_FILTER = "all";
11
+ var DEFAULT_API_HOST = "https://api.stableflow.ai";
12
+ var POPULAR_SECTION_TITLE = "Popular on your network";
13
+ var TOKEN_SELECT_PREFS_STORAGE_KEY = "stableflow-pay:token-select-prefs:v1";
14
+ var TOKEN_SELECT_PREFS_VERSION = 2;
15
+ var MAX_RECENT_BLOCKCHAINS = 16;
16
+ var MAX_RECENT_ASSETS = 3;
17
+ var DISCONNECTED_CHAIN_ORDER = [
18
+ "near",
19
+ "sol",
20
+ "tron",
21
+ "eth",
22
+ "bsc",
23
+ "arb",
24
+ "base",
25
+ "pol",
26
+ "avax",
27
+ "op",
28
+ "bera",
29
+ "gnosis",
30
+ "xlayer",
31
+ "scroll"
32
+ ];
33
+ var DEFAULT_CHAIN_KINDS = {
34
+ eth: "evm",
35
+ base: "evm",
36
+ arb: "evm",
37
+ op: "evm",
38
+ pol: "evm",
39
+ bsc: "evm",
40
+ avax: "evm",
41
+ gnosis: "evm",
42
+ scroll: "evm",
43
+ xlayer: "evm",
44
+ bera: "evm",
45
+ near: "near",
46
+ sol: "solana",
47
+ tron: "tron",
48
+ zec: "zec"
49
+ };
50
+
51
+ // src/token-select/balances.ts
52
+ var listeners = /* @__PURE__ */ new Set();
53
+ var balances = {};
54
+ var reader = null;
55
+ var chainInflight = /* @__PURE__ */ new Map();
56
+ function emit() {
57
+ balances = { ...balances };
58
+ for (const listener of listeners) listener();
59
+ }
60
+ function subscribe(listener) {
61
+ listeners.add(listener);
62
+ return () => listeners.delete(listener);
63
+ }
64
+ function subscribePayTokenBalances(listener) {
65
+ return subscribe(listener);
66
+ }
67
+ function getPayTokenBalances() {
68
+ return balances;
69
+ }
70
+ function ownerKey(owner, chainKind) {
71
+ return chainKind === "solana" || chainKind === "zec" ? owner : owner.toLowerCase();
72
+ }
73
+ function balanceKey(owner, assetId, chainKind) {
74
+ return `${ownerKey(owner, chainKind)}:${assetId}`;
75
+ }
76
+ function isFresh(entry, now) {
77
+ return Boolean(
78
+ entry && entry.status === "success" && entry.updatedAt != null && now - entry.updatedAt < TOKEN_BALANCE_POLL_MS
79
+ );
80
+ }
81
+ function loadingEntry(prev) {
82
+ if (!prev) return { raw: null, formatted: null, status: "loading", error: null, updatedAt: null };
83
+ return { ...prev, status: "loading", error: null };
84
+ }
85
+ function withStaleOnError(prev, next) {
86
+ if (next.status !== "error") return next;
87
+ if (prev?.raw == null && prev?.formatted == null) return next;
88
+ return { ...next, raw: prev.raw, formatted: prev.formatted, updatedAt: prev.updatedAt };
89
+ }
90
+ function setBalanceReader(next) {
91
+ reader = next;
92
+ }
93
+ function clearPayTokenBalances() {
94
+ balances = {};
95
+ emit();
96
+ }
97
+ function getPayTokenBalance(owner, assetId) {
98
+ if (!owner || !assetId) return void 0;
99
+ return balances[`${owner}:${assetId}`] ?? balances[`${owner.toLowerCase()}:${assetId}`];
100
+ }
101
+ function usePayTokenBalance(owner, assetId) {
102
+ const current = useSyncExternalStore(subscribe, getPayTokenBalances, getPayTokenBalances);
103
+ if (!owner || !assetId) return void 0;
104
+ return current[`${owner}:${assetId}`] ?? current[`${owner.toLowerCase()}:${assetId}`];
105
+ }
106
+ function writeResult(owner, token, result) {
107
+ const key = balanceKey(owner, token.assetId, token.chain.chainKind);
108
+ const prev = balances[key];
109
+ const next = "error" in result ? { raw: null, formatted: null, status: "error", error: result.error, updatedAt: Date.now() } : { raw: result.raw ?? null, formatted: result.formatted, status: "success", error: null, updatedAt: Date.now() };
110
+ balances[key] = withStaleOnError(prev, next);
111
+ }
112
+ async function refreshPayTokenBalance(owner, token) {
113
+ const key = balanceKey(owner, token.assetId, token.chain.chainKind);
114
+ balances[key] = loadingEntry(balances[key]);
115
+ emit();
116
+ if (!reader) {
117
+ balances[key] = withStaleOnError(balances[key], {
118
+ raw: null,
119
+ formatted: null,
120
+ status: "error",
121
+ error: "Balance reader is not configured",
122
+ updatedAt: Date.now()
123
+ });
124
+ emit();
125
+ return balances[key] ?? null;
126
+ }
127
+ try {
128
+ const results = await reader({
129
+ owner,
130
+ chainKind: token.chain.chainKind,
131
+ blockchain: token.blockchain,
132
+ tokens: [token]
133
+ });
134
+ const result = results.find((item) => item.assetId === token.assetId) ?? {
135
+ assetId: token.assetId,
136
+ error: "Balance reader returned no result"
137
+ };
138
+ writeResult(owner, token, result);
139
+ } catch (cause) {
140
+ writeResult(owner, token, {
141
+ assetId: token.assetId,
142
+ error: cause instanceof Error ? cause.message : "Failed to read balance"
143
+ });
144
+ }
145
+ emit();
146
+ return balances[key] ?? null;
147
+ }
148
+ function groupByChain(tokens) {
149
+ const groups = /* @__PURE__ */ new Map();
150
+ for (const token of tokens) {
151
+ const list = groups.get(token.blockchain) ?? [];
152
+ list.push(token);
153
+ groups.set(token.blockchain, list);
154
+ }
155
+ return groups;
156
+ }
157
+ async function fetchChain(owner, tokens, force) {
158
+ const blockchain = tokens[0]?.blockchain;
159
+ const kind = tokens[0]?.chain.chainKind;
160
+ if (!blockchain || !kind) return;
161
+ const now = Date.now();
162
+ const stale = tokens.filter((token) => force || !isFresh(balances[balanceKey(owner, token.assetId, kind)], now));
163
+ if (stale.length === 0) return;
164
+ const flightKey = `${ownerKey(owner, kind)}:${blockchain}`;
165
+ const pending = chainInflight.get(flightKey);
166
+ if (pending) return pending;
167
+ for (const token of stale) {
168
+ const key = balanceKey(owner, token.assetId, kind);
169
+ balances[key] = loadingEntry(balances[key]);
170
+ }
171
+ emit();
172
+ const run = (async () => {
173
+ if (!reader) {
174
+ for (const token of stale) {
175
+ writeResult(owner, token, { assetId: token.assetId, error: "Balance reader is not configured" });
176
+ }
177
+ emit();
178
+ return;
179
+ }
180
+ try {
181
+ const results = await reader({ owner, chainKind: kind, blockchain, tokens: stale });
182
+ const byId = new Map(results.map((item) => [item.assetId, item]));
183
+ for (const token of stale) {
184
+ writeResult(owner, token, byId.get(token.assetId) ?? {
185
+ assetId: token.assetId,
186
+ error: "Balance reader returned no result"
187
+ });
188
+ }
189
+ } catch (cause) {
190
+ const message = cause instanceof Error ? cause.message : "Failed to read balance";
191
+ for (const token of stale) writeResult(owner, token, { assetId: token.assetId, error: message });
192
+ }
193
+ emit();
194
+ })();
195
+ chainInflight.set(flightKey, run);
196
+ return run.finally(() => {
197
+ if (chainInflight.get(flightKey) === run) chainInflight.delete(flightKey);
198
+ });
199
+ }
200
+ async function ensurePayTokenBalances(owners, tokens, opts) {
201
+ const force = Boolean(opts?.force);
202
+ const groups = groupByChain(tokens);
203
+ await Promise.all(Array.from(groups.values()).map((group) => {
204
+ const kind = group[0]?.chain.chainKind;
205
+ const owner = kind ? owners[kind] : void 0;
206
+ if (!owner) return Promise.resolve();
207
+ return fetchChain(owner, group, force);
208
+ }));
209
+ }
210
+
211
+ // src/token-select/pay-config.ts
212
+ import { useCallback, useEffect, useSyncExternalStore as useSyncExternalStore2 } from "react";
213
+
214
+ // src/token-select/logos.ts
215
+ var LOGO_HOST = "https://assets.dapdap.net";
216
+ var CHAIN_LOGO_ALIAS = {
217
+ eth: "ethereum",
218
+ ethereum: "ethereum",
219
+ base: "base",
220
+ arb: "arbitrum",
221
+ arbitrum: "arbitrum",
222
+ op: "optimism",
223
+ optimism: "optimism",
224
+ pol: "polygon",
225
+ polygon: "polygon",
226
+ bsc: "bsc",
227
+ avax: "avalanche",
228
+ avalanche: "avalanche",
229
+ gnosis: "gnosis",
230
+ scroll: "scroll",
231
+ xlayer: "xlayer",
232
+ bera: "berachain",
233
+ berachain: "berachain",
234
+ near: "near",
235
+ sol: "solana",
236
+ solana: "solana",
237
+ tron: "tron",
238
+ zec: "zcash",
239
+ zcash: "zcash"
240
+ };
241
+ function logoPath(path) {
242
+ const normalized = path.startsWith("/") ? path : `/${path}`;
243
+ return `${LOGO_HOST}${normalized}`;
244
+ }
245
+ function chainLogoUrl(blockchainOrNetwork) {
246
+ const key = String(blockchainOrNetwork || "").toLowerCase();
247
+ const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
248
+ return logoPath(`/stableflow/networks/${file}.png`);
249
+ }
250
+ function tokenLogoUrl(symbol) {
251
+ const upper = String(symbol || "").toUpperCase();
252
+ const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
253
+ return logoPath(`/stableflow/tokens/${file}.png`);
254
+ }
255
+
256
+ // src/token-select/pay-config.ts
257
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
258
+ var WRAP_NEAR_CONTRACT = "wrap.near";
259
+ var listeners2 = /* @__PURE__ */ new Set();
260
+ var chainKindOverrides = {};
261
+ var snapshot = {
262
+ apiHost: DEFAULT_API_HOST,
263
+ chains: [],
264
+ tokens: [],
265
+ source: null,
266
+ status: "idle",
267
+ error: null,
268
+ fetchedAt: null
269
+ };
270
+ var inflight = null;
271
+ function emit2(next) {
272
+ snapshot = next;
273
+ for (const listener of listeners2) listener();
274
+ }
275
+ function subscribe2(listener) {
276
+ listeners2.add(listener);
277
+ return () => listeners2.delete(listener);
278
+ }
279
+ function getPayConfigSnapshot() {
280
+ return snapshot;
281
+ }
282
+ function setChainKindOverrides(overrides) {
283
+ chainKindOverrides = overrides ?? {};
284
+ if (snapshot.source) {
285
+ const normalized = normalizePayConfig(snapshot.source, chainKindOverrides);
286
+ emit2({ ...snapshot, chains: normalized.chains, tokens: normalized.tokens });
287
+ }
288
+ }
289
+ function asRecord(value) {
290
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
291
+ return value;
292
+ }
293
+ function apiText(value) {
294
+ if (typeof value === "string") return value.trim();
295
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
296
+ return "";
297
+ }
298
+ function apiNumber(value) {
299
+ if (typeof value === "number" && Number.isFinite(value)) return value;
300
+ if (typeof value === "string" && value.trim()) {
301
+ const parsed = Number(value);
302
+ if (Number.isFinite(parsed)) return parsed;
303
+ }
304
+ return null;
305
+ }
306
+ function asBoolean(value, fallback = true) {
307
+ if (typeof value === "boolean") return value;
308
+ if (typeof value === "string") {
309
+ const lower = value.trim().toLowerCase();
310
+ if (lower === "true") return true;
311
+ if (lower === "false") return false;
312
+ }
313
+ return fallback;
314
+ }
315
+ function mapSourceChain(raw) {
316
+ const row = asRecord(raw) ?? {};
317
+ return {
318
+ network: apiText(row.network),
319
+ chainId: apiText(row.chain_id ?? row.chainId),
320
+ chainName: apiText(row.chain_name ?? row.chainName),
321
+ logo: apiText(row.logo),
322
+ explorer: apiText(row.explorer)
323
+ };
324
+ }
325
+ function mapSourceToken(raw) {
326
+ const row = asRecord(raw) ?? {};
327
+ return {
328
+ symbol: apiText(row.symbol),
329
+ network: apiText(row.network),
330
+ decimals: apiNumber(row.decimals) ?? 0,
331
+ contractAddress: apiText(row.contract_address ?? row.contractAddress),
332
+ price: apiText(row.price),
333
+ supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
334
+ supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
335
+ };
336
+ }
337
+ function configBody(raw) {
338
+ const row = asRecord(raw) ?? {};
339
+ const data = asRecord(row.data);
340
+ if (typeof row.code === "number" && data) return data;
341
+ return row;
342
+ }
343
+ function mapPayConfigSource(raw) {
344
+ const row = configBody(raw);
345
+ const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
346
+ const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
347
+ return { chains, tokens };
348
+ }
349
+ function chainKindFor(network, overrides = chainKindOverrides) {
350
+ const key = network.trim().toLowerCase();
351
+ if (!key) return null;
352
+ return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
353
+ }
354
+ function normalizeSymbol(symbol) {
355
+ const upper = symbol.trim().toUpperCase();
356
+ if (!upper) return null;
357
+ if (upper === "USDT0") return "USDT";
358
+ if (upper === "WNEAR") return "NEAR";
359
+ return upper;
360
+ }
361
+ function tokenAssetId(network, symbol, contractAddress) {
362
+ const addr = String(contractAddress || "").trim();
363
+ return `${network}:${symbol}:${addr || "native"}`;
364
+ }
365
+ function isNativeAddress(contractAddress) {
366
+ const addr = String(contractAddress || "").trim();
367
+ if (!addr) return true;
368
+ const lower = addr.toLowerCase();
369
+ return lower === "native" || lower === ZERO_ADDRESS;
370
+ }
371
+ function normalizePayConfig(source, overrides = chainKindOverrides) {
372
+ const chains = [];
373
+ for (const chain of source.chains) {
374
+ const chainKind = chainKindFor(chain.network, overrides);
375
+ if (!chainKind || !chain.network) continue;
376
+ chains.push({
377
+ blockchain: chain.network,
378
+ chainName: chain.chainName || chain.network,
379
+ chainKind,
380
+ chainId: chain.chainId || void 0,
381
+ logo: chain.logo || chainLogoUrl(chain.network),
382
+ explorer: chain.explorer
383
+ });
384
+ }
385
+ const chainByCode = new Map(chains.map((chain) => [chain.blockchain, chain]));
386
+ const tokens = [];
387
+ for (const token of source.tokens) {
388
+ const chain = chainByCode.get(token.network);
389
+ if (!chain) continue;
390
+ const symbol = normalizeSymbol(token.symbol);
391
+ if (!symbol) continue;
392
+ if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
393
+ const contractAddress = token.contractAddress.trim() || null;
394
+ tokens.push({
395
+ assetId: tokenAssetId(token.network, symbol, contractAddress),
396
+ symbol,
397
+ providerSymbol: token.symbol,
398
+ blockchain: token.network,
399
+ decimals: token.decimals,
400
+ contractAddress,
401
+ price: Number(token.price || 1),
402
+ logo: tokenLogoUrl(symbol),
403
+ supportPayment: token.supportPayment,
404
+ supportReceive: token.supportReceive,
405
+ chain
406
+ });
407
+ }
408
+ const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
409
+ const visible = hasNearWrap ? tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress))) : tokens;
410
+ return { chains, tokens: visible };
411
+ }
412
+ function configUrl(apiHost) {
413
+ return `${apiHost.replace(/\/$/, "")}/v1/pay/config`;
414
+ }
415
+ async function load(apiHost, force) {
416
+ const fresh = snapshot.apiHost === apiHost && snapshot.status === "success" && snapshot.fetchedAt != null && Date.now() - snapshot.fetchedAt < CONFIG_STALE_MS;
417
+ if (!force && fresh) return;
418
+ if (inflight && !force) return inflight;
419
+ emit2({ ...snapshot, apiHost, status: snapshot.tokens.length > 0 ? snapshot.status : "loading", error: null });
420
+ const run = (async () => {
421
+ try {
422
+ const response = await fetch(configUrl(apiHost));
423
+ if (!response.ok) throw new Error(`Config request failed (${response.status})`);
424
+ const source = mapPayConfigSource(await response.json());
425
+ const normalized = normalizePayConfig(source, chainKindOverrides);
426
+ emit2({
427
+ apiHost,
428
+ source,
429
+ chains: normalized.chains,
430
+ tokens: normalized.tokens,
431
+ status: "success",
432
+ error: null,
433
+ fetchedAt: Date.now()
434
+ });
435
+ } catch (cause) {
436
+ const message = cause instanceof Error ? cause.message : "Failed to load config";
437
+ emit2({
438
+ ...snapshot,
439
+ apiHost,
440
+ status: snapshot.source ? "success" : "error",
441
+ error: message
442
+ });
443
+ } finally {
444
+ inflight = null;
445
+ }
446
+ })();
447
+ inflight = run;
448
+ return run;
449
+ }
450
+ function refetchPayConfig(apiHost = snapshot.apiHost) {
451
+ return load(apiHost, true);
452
+ }
453
+ function usePayConfigSnapshot() {
454
+ return useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
455
+ }
456
+ function usePayConfig(options) {
457
+ const apiHost = options?.apiHost?.trim() || DEFAULT_API_HOST;
458
+ const current = useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
459
+ const refetch = useCallback(() => {
460
+ void refetchPayConfig(apiHost);
461
+ }, [apiHost]);
462
+ useEffect(() => {
463
+ void load(apiHost, false);
464
+ }, [apiHost]);
465
+ return {
466
+ chains: current.apiHost === apiHost ? current.chains : [],
467
+ tokens: current.apiHost === apiHost ? current.tokens : [],
468
+ source: current.apiHost === apiHost ? current.source : null,
469
+ status: current.apiHost === apiHost ? current.status : "loading",
470
+ error: current.apiHost === apiHost ? current.error : null,
471
+ refetch
472
+ };
473
+ }
474
+
475
+ // src/token-select/context.tsx
476
+ import { jsx } from "react/jsx-runtime";
477
+ var PayWidgetsContext = createContext(null);
478
+ function PayWidgetsProvider(props) {
479
+ const {
480
+ apiHost,
481
+ popularTokens = [],
482
+ wallet,
483
+ readBalances,
484
+ onConfig,
485
+ onCopyAddress,
486
+ chainKinds,
487
+ children
488
+ } = props;
489
+ const config = usePayConfig({ apiHost });
490
+ useEffect2(() => {
491
+ setChainKindOverrides(chainKinds);
492
+ }, [chainKinds]);
493
+ useEffect2(() => {
494
+ setBalanceReader(readBalances);
495
+ return () => setBalanceReader(null);
496
+ }, [readBalances]);
497
+ useEffect2(() => {
498
+ if (config.status === "success" && config.source) onConfig?.(config.source);
499
+ }, [config.source, config.status, onConfig]);
500
+ const connected = Object.values(wallet.accounts).some((account) => Boolean(account?.address?.trim()));
501
+ useEffect2(() => {
502
+ if (!connected) clearPayTokenBalances();
503
+ }, [connected]);
504
+ const value = useMemo(
505
+ () => ({ wallet, popularTokens, onCopyAddress }),
506
+ [wallet, popularTokens, onCopyAddress]
507
+ );
508
+ return /* @__PURE__ */ jsx(PayWidgetsContext.Provider, { value, children });
509
+ }
510
+ function usePayWidgets() {
511
+ const value = useContext(PayWidgetsContext);
512
+ if (!value) {
513
+ throw new Error("Token select must be rendered inside PayWidgetsProvider");
514
+ }
515
+ return value;
516
+ }
517
+
518
+ // src/token-select/TokenSelectDialog.tsx
519
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef, useState, useSyncExternalStore as useSyncExternalStore4 } from "react";
520
+ import { Dialog } from "@stableflow/pay-ui/dialog";
521
+ import { SearchInput as SearchInput2 } from "@stableflow/pay-ui/search-input";
522
+
523
+ // src/token-select/prefs.ts
524
+ import { useSyncExternalStore as useSyncExternalStore3 } from "react";
525
+ var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
526
+ function rememberRecentAsset(recent, assetId) {
527
+ const id = assetId.trim();
528
+ if (!id) return recent;
529
+ return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
530
+ }
531
+ function rememberRecentBlockchain(recent, blockchain) {
532
+ const code = blockchain.trim();
533
+ if (!code) return recent;
534
+ return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
535
+ }
536
+ function migrateTokenSelectPrefs(persisted, version) {
537
+ const state = persisted ?? {};
538
+ const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
539
+ const recentAssetIds = version >= TOKEN_SELECT_PREFS_VERSION && Array.isArray(state.recentAssetIds) ? state.recentAssetIds.filter((id) => typeof id === "string" && id.trim()).slice(0, MAX_RECENT_ASSETS) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
540
+ return { recentAssetIds, recentBlockchains };
541
+ }
542
+ function readStorage() {
543
+ if (typeof localStorage === "undefined") return EMPTY_PREFS;
544
+ try {
545
+ const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
546
+ if (!raw) return EMPTY_PREFS;
547
+ const parsed = JSON.parse(raw);
548
+ const version = typeof parsed.version === "number" ? parsed.version : 0;
549
+ const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
550
+ return migrateTokenSelectPrefs(payload, version);
551
+ } catch {
552
+ return EMPTY_PREFS;
553
+ }
554
+ }
555
+ function writeStorage(state) {
556
+ if (typeof localStorage === "undefined") return;
557
+ localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
558
+ state,
559
+ version: TOKEN_SELECT_PREFS_VERSION
560
+ }));
561
+ }
562
+ var prefs = readStorage();
563
+ var listeners3 = /* @__PURE__ */ new Set();
564
+ function emit3(next) {
565
+ prefs = next;
566
+ writeStorage(next);
567
+ for (const listener of listeners3) listener();
568
+ }
569
+ function subscribe3(listener) {
570
+ listeners3.add(listener);
571
+ return () => listeners3.delete(listener);
572
+ }
573
+ function getTokenSelectPrefs() {
574
+ return prefs;
575
+ }
576
+ function setLastToken(assetId, blockchain) {
577
+ emit3({
578
+ recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
579
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
580
+ });
581
+ }
582
+ function setLastBlockchain(blockchain) {
583
+ emit3({
584
+ ...prefs,
585
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
586
+ });
587
+ }
588
+ function useTokenSelectPrefs() {
589
+ return useSyncExternalStore3(subscribe3, getTokenSelectPrefs, getTokenSelectPrefs);
590
+ }
591
+
592
+ // src/token-select/format.ts
593
+ import { clsx } from "clsx";
594
+ import { twMerge } from "tailwind-merge";
595
+ var fontSans = "[font-family:Montserrat,PingFang_SC,system-ui,sans-serif]";
596
+ function cx(...inputs) {
597
+ return twMerge(clsx(inputs));
598
+ }
599
+ function formatAddress(address, prefix = 4, suffix = 5) {
600
+ if (!address) return "";
601
+ if (!address.startsWith("0x") && address.length <= 32) return address;
602
+ if (address.length <= prefix + suffix) return address;
603
+ return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
604
+ }
605
+ function chainKindLabel(kind) {
606
+ if (kind === "evm") return "EVM";
607
+ if (kind === "near") return "Near";
608
+ if (kind === "solana") return "Solana";
609
+ if (kind === "tron") return "Tron";
610
+ if (kind === "zec") return "Zcash";
611
+ return kind;
612
+ }
613
+ function formatDisplayAmount(value, options = {}) {
614
+ const maxDecimals = options.maxDecimals ?? 2;
615
+ const prefix = options.prefix ?? "";
616
+ const text = String(value).trim();
617
+ const negative = text.startsWith("-");
618
+ const unsigned = negative ? text.slice(1) : text;
619
+ if (!/^\d+(\.\d+)?$/.test(unsigned)) return `${prefix}0`;
620
+ const numeric = Number(unsigned);
621
+ const minUnit = 10 ** -maxDecimals;
622
+ if (options.showDust && numeric > 0 && numeric < minUnit) {
623
+ const dust = `<${minUnit.toFixed(maxDecimals)}`;
624
+ return prefix ? `${prefix} ${dust}` : dust;
625
+ }
626
+ const [intPart, fracPart = ""] = unsigned.split(".");
627
+ const fraction = fracPart.slice(0, maxDecimals).replace(/0+$/, "");
628
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
629
+ const body = fraction.length > 0 ? `${grouped}.${fraction}` : grouped;
630
+ return `${prefix}${negative ? "-" : ""}${body}`;
631
+ }
632
+
633
+ // src/token-select/chain-pane.tsx
634
+ import { Tooltip } from "@stableflow/pay-ui/tooltip";
635
+
636
+ // src/token-select/chain-wallet-status.tsx
637
+ import { IconLogout } from "@stableflow/pay-ui/icons/logout";
638
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
639
+ function stop(event) {
640
+ event.preventDefault();
641
+ event.stopPropagation();
642
+ }
643
+ function ChainWalletStatus({ kind }) {
644
+ const { wallet, onCopyAddress } = usePayWidgets();
645
+ const account = wallet.accounts[kind];
646
+ const address = account?.address?.trim() || "";
647
+ const hideDisconnect = account?.canDisconnect === false;
648
+ if (!address) {
649
+ return /* @__PURE__ */ jsx2(
650
+ "button",
651
+ {
652
+ type: "button",
653
+ onClick: (event) => {
654
+ stop(event);
655
+ void wallet.connect(kind);
656
+ },
657
+ className: cx("shrink-0 cursor-pointer border-0 bg-transparent p-0 text-xs font-medium text-[#3F8AFB] hover:underline", fontSans),
658
+ children: account?.connecting ? "Connecting\u2026" : "Connect"
659
+ }
660
+ );
661
+ }
662
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", onClick: stop, children: [
663
+ /* @__PURE__ */ jsx2(
664
+ "button",
665
+ {
666
+ type: "button",
667
+ onClick: async (event) => {
668
+ stop(event);
669
+ if (!onCopyAddress) return;
670
+ onCopyAddress(address);
671
+ },
672
+ className: cx("truncate border-0 bg-transparent p-0 text-xs text-[#606060] hover:text-black", fontSans),
673
+ children: formatAddress(address)
674
+ }
675
+ ),
676
+ hideDisconnect ? null : /* @__PURE__ */ jsx2(
677
+ "button",
678
+ {
679
+ type: "button",
680
+ "aria-label": "Disconnect",
681
+ onClick: (event) => {
682
+ stop(event);
683
+ void wallet.disconnect(kind);
684
+ },
685
+ className: "inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-[#ff5656]",
686
+ children: /* @__PURE__ */ jsx2(IconLogout, { className: "size-3" })
687
+ }
688
+ )
689
+ ] });
690
+ }
691
+
692
+ // src/token-select/utils.ts
693
+ function tokenBalanceUsd(token, formatted) {
694
+ if (formatted == null || formatted === "") return -1;
695
+ const amount = Number(formatted);
696
+ if (!Number.isFinite(amount)) return -1;
697
+ const price = Number(token.price);
698
+ return amount * (Number.isFinite(price) ? price : 0);
699
+ }
700
+ function tokenMatchesSearch(token, query) {
701
+ const q = query.trim().toLowerCase();
702
+ if (!q) return true;
703
+ if (token.symbol.toLowerCase().includes(q)) return true;
704
+ if (token.providerSymbol.toLowerCase().includes(q)) return true;
705
+ return Boolean(token.contractAddress?.toLowerCase().includes(q));
706
+ }
707
+ function matchesChainFilter(token, chainFilter) {
708
+ if (chainFilter === ALL_CHAIN_FILTER) return true;
709
+ return token.blockchain === chainFilter;
710
+ }
711
+ function formattedBalancePositive(formatted) {
712
+ if (formatted == null || formatted === "") return false;
713
+ const amount = Number(formatted);
714
+ return Number.isFinite(amount) && amount > 0;
715
+ }
716
+ function chainHasBalance(blockchain, tokens, getBalance) {
717
+ return tokens.some(
718
+ (token) => token.blockchain === blockchain && formattedBalancePositive(getBalance(token))
719
+ );
720
+ }
721
+ function chainBalanceUsd(blockchain, tokens, getBalanceUsd) {
722
+ let total = 0;
723
+ for (const token of tokens) {
724
+ if (token.blockchain !== blockchain) continue;
725
+ const usd = getBalanceUsd(token);
726
+ if (usd > 0) total += usd;
727
+ }
728
+ return total;
729
+ }
730
+ var DISCONNECTED_CHAIN_RANK = new Map(
731
+ DISCONNECTED_CHAIN_ORDER.map((code, index) => [code, index])
732
+ );
733
+ function parsePopularTokens(raw) {
734
+ if (!raw?.trim()) return [];
735
+ const refs = [];
736
+ for (const part of raw.split(",")) {
737
+ const trimmed = part.trim();
738
+ const colon = trimmed.indexOf(":");
739
+ if (colon <= 0 || colon >= trimmed.length - 1) continue;
740
+ const blockchain = trimmed.slice(0, colon).trim().toLowerCase();
741
+ const symbol = trimmed.slice(colon + 1).trim();
742
+ if (!blockchain || !symbol || symbol.includes(":")) continue;
743
+ refs.push({ blockchain, symbol: symbol.toUpperCase() });
744
+ }
745
+ return refs;
746
+ }
747
+ function popularTokensForWallets(tokens, configured, isChainConnected) {
748
+ const result = [];
749
+ for (const ref of configured) {
750
+ const token = tokens.find((item) => item.blockchain === ref.blockchain && item.symbol.toUpperCase() === ref.symbol);
751
+ if (!token || !isChainConnected(token.chain.chainKind)) continue;
752
+ result.push(token);
753
+ }
754
+ return result;
755
+ }
756
+ function sortChainsForSidebar(chains, tokens, options) {
757
+ if (!options.walletConnected) {
758
+ const tail = DISCONNECTED_CHAIN_ORDER.length;
759
+ return chains.map((chain, index) => ({ chain, index })).sort((left, right) => {
760
+ const leftRank = DISCONNECTED_CHAIN_RANK.get(left.chain.blockchain) ?? tail;
761
+ const rightRank = DISCONNECTED_CHAIN_RANK.get(right.chain.blockchain) ?? tail;
762
+ if (leftRank !== rightRank) return leftRank - rightRank;
763
+ return left.index - right.index;
764
+ }).map((item) => item.chain);
765
+ }
766
+ if (!options.showBalances) return chains.slice();
767
+ return chains.slice().sort((left, right) => {
768
+ const leftUsd = chainBalanceUsd(left.blockchain, tokens, options.getBalanceUsd);
769
+ const rightUsd = chainBalanceUsd(right.blockchain, tokens, options.getBalanceUsd);
770
+ if (rightUsd !== leftUsd) return rightUsd - leftUsd;
771
+ return left.chainName.localeCompare(right.chainName);
772
+ });
773
+ }
774
+ function initialChainFilter(lockChainKind, recentBlockchains, chains) {
775
+ if (!lockChainKind) return ALL_CHAIN_FILTER;
776
+ const matching = chains.filter((chain) => chain.chainKind === lockChainKind);
777
+ if (matching.length === 0) return ALL_CHAIN_FILTER;
778
+ for (const code of recentBlockchains) {
779
+ if (matching.some((chain) => chain.blockchain === code)) return code;
780
+ }
781
+ return matching[0].blockchain;
782
+ }
783
+ function isBlockchainDisabled(blockchain, disabledBlockchains) {
784
+ if (!disabledBlockchains?.length) return false;
785
+ const code = blockchain.toLowerCase();
786
+ return disabledBlockchains.some((item) => item.toLowerCase() === code);
787
+ }
788
+ function isChainKindLocked(chainKind, lockChainKind) {
789
+ return Boolean(lockChainKind && chainKind !== lockChainKind);
790
+ }
791
+ var BALANCE_RANK_FUNDED = 0;
792
+ var BALANCE_RANK_LOADING = 1;
793
+ var BALANCE_RANK_EMPTY = 2;
794
+ function tokenBalanceRank(usd, loading) {
795
+ if (loading) return BALANCE_RANK_LOADING;
796
+ if (usd > 0) return BALANCE_RANK_FUNDED;
797
+ return BALANCE_RANK_EMPTY;
798
+ }
799
+ function compareTokensBySymbol(left, right) {
800
+ return left.symbol.localeCompare(right.symbol) || left.chain.chainName.localeCompare(right.chain.chainName);
801
+ }
802
+ function sortTokensByBalance(tokens, getBalanceUsd, isLoading) {
803
+ return tokens.slice().sort((left, right) => {
804
+ const leftRank = tokenBalanceRank(getBalanceUsd(left), isLoading(left));
805
+ const rightRank = tokenBalanceRank(getBalanceUsd(right), isLoading(right));
806
+ if (leftRank !== rightRank) return leftRank - rightRank;
807
+ if (leftRank === BALANCE_RANK_FUNDED) {
808
+ const byUsd = getBalanceUsd(right) - getBalanceUsd(left);
809
+ if (byUsd !== 0) return byUsd;
810
+ }
811
+ return compareTokensBySymbol(left, right);
812
+ });
813
+ }
814
+ function sortTokensBySymbol(tokens) {
815
+ return tokens.slice().sort(compareTokensBySymbol);
816
+ }
817
+ function positiveBalanceTokens(tokens, getBalanceUsd) {
818
+ return sortTokensByBalance(
819
+ tokens.filter((token) => getBalanceUsd(token) > 0),
820
+ getBalanceUsd,
821
+ () => false
822
+ );
823
+ }
824
+ function recentTokensInOrder(tokens, recentAssetIds) {
825
+ const byId = new Map(tokens.map((token) => [token.assetId, token]));
826
+ const result = [];
827
+ for (const assetId of recentAssetIds) {
828
+ const token = byId.get(assetId);
829
+ if (token) result.push(token);
830
+ }
831
+ return result;
832
+ }
833
+
834
+ // src/token-select/chain-pane.tsx
835
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
836
+ function lockReason(lockChainKind) {
837
+ return `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain`;
838
+ }
839
+ function ChainPane(props) {
840
+ const {
841
+ chains,
842
+ fundedBlockchains,
843
+ showWalletStatus = true,
844
+ onSelectFilter,
845
+ lockChainKind = null,
846
+ disabledBlockchains = null,
847
+ disabledReason
848
+ } = props;
849
+ function wrapDisabled(disabled, reason, content) {
850
+ if (!disabled || !reason) return content;
851
+ return /* @__PURE__ */ jsx3(Tooltip, { side: "right", triggerClassName: "min-w-0 flex-1", content: reason, children: content });
852
+ }
853
+ return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", children: [
854
+ /* @__PURE__ */ jsx3("p", { className: cx("shrink-0 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
855
+ /* @__PURE__ */ jsx3("div", { className: "mt-3 flex min-h-0 flex-1 flex-col overflow-y-auto", children: chains.map((chain) => {
856
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
857
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
858
+ const disabled = locked || chainDisabled;
859
+ const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind) : void 0;
860
+ return /* @__PURE__ */ jsxs2(
861
+ "div",
862
+ {
863
+ className: cx(
864
+ "flex min-h-14 w-full items-center gap-2 rounded-[12px] px-2 md:min-h-[66px] md:gap-3 md:px-3",
865
+ disabled ? "opacity-40" : "hover:bg-[#F6F6F6]"
866
+ ),
867
+ children: [
868
+ wrapDisabled(
869
+ disabled,
870
+ reason,
871
+ /* @__PURE__ */ jsxs2(
872
+ "button",
873
+ {
874
+ type: "button",
875
+ disabled,
876
+ onClick: () => {
877
+ if (disabled) return;
878
+ onSelectFilter(chain.blockchain);
879
+ },
880
+ className: cx(
881
+ "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
882
+ disabled ? "cursor-not-allowed" : "cursor-pointer"
883
+ ),
884
+ children: [
885
+ /* @__PURE__ */ jsxs2("span", { className: "relative size-8 shrink-0", children: [
886
+ /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
887
+ fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx3("span", { className: "absolute -top-0.5 -right-0.5 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
888
+ ] }),
889
+ /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: chain.chainName })
890
+ ]
891
+ }
892
+ )
893
+ ),
894
+ showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : null
895
+ ]
896
+ },
897
+ chain.blockchain
898
+ );
899
+ }) })
900
+ ] });
901
+ }
902
+
903
+ // src/token-select/network-rail.tsx
904
+ import { IconAllNetworks } from "@stableflow/pay-ui/icons/all-networks";
905
+ import { IconWallet } from "@stableflow/pay-ui/icons/wallet";
906
+ import { Tooltip as Tooltip2 } from "@stableflow/pay-ui/tooltip";
907
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
908
+ function chipClass(selected, disabled) {
909
+ return cx(
910
+ "relative flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-solid md:size-[50px]",
911
+ selected ? "border-[#06f] bg-[rgba(0,102,255,0.1)]" : "border-transparent bg-transparent",
912
+ disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer hover:bg-black/5"
913
+ );
914
+ }
915
+ function NetworkRail(props) {
916
+ const {
917
+ view,
918
+ chainFilter,
919
+ chips,
920
+ fundedBlockchains,
921
+ walletConnected,
922
+ showAllLabel = false,
923
+ lockChainKind = null,
924
+ disabledBlockchains = null,
925
+ disabledReason,
926
+ onSelectFilter,
927
+ onOpenNetworks,
928
+ className
929
+ } = props;
930
+ function wrapDisabled(disabled, reason, content) {
931
+ if (!disabled || !reason) return content;
932
+ return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
933
+ }
934
+ return /* @__PURE__ */ jsxs3("div", { className: cx("flex h-full w-[52px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pr-2 md:w-[72px] md:pr-3", className), children: [
935
+ showAllLabel ? /* @__PURE__ */ jsx4(
936
+ "button",
937
+ {
938
+ type: "button",
939
+ "aria-label": "All",
940
+ onClick: () => onSelectFilter(ALL_CHAIN_FILTER),
941
+ className: chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false),
942
+ children: /* @__PURE__ */ jsx4("span", { className: cx("text-xs font-medium text-black md:text-sm", fontSans), children: "All" })
943
+ }
944
+ ) : /* @__PURE__ */ jsx4(
945
+ "button",
946
+ {
947
+ type: "button",
948
+ "aria-label": "Wallet",
949
+ disabled: !walletConnected,
950
+ onClick: () => {
951
+ if (!walletConnected) return;
952
+ onSelectFilter(ALL_CHAIN_FILTER);
953
+ },
954
+ className: walletConnected ? chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false) : "relative flex size-10 shrink-0 cursor-not-allowed items-center justify-center rounded-[12px] border border-solid border-transparent md:size-[50px]",
955
+ children: walletConnected ? /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-black" }) : /* @__PURE__ */ jsx4("span", { className: "flex size-8 items-center justify-center rounded-[8px] bg-[#EFEFEF]", children: /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-[#9FA7BA]" }) })
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsx4("div", { className: "mt-2 flex min-h-0 w-full flex-1 flex-col items-center gap-2 overflow-y-auto", children: chips.map((chain) => {
959
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
960
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
961
+ const disabled = locked || chainDisabled;
962
+ const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain` : void 0;
963
+ const selected = view === "token" && chainFilter === chain.blockchain;
964
+ return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
965
+ disabled,
966
+ reason,
967
+ /* @__PURE__ */ jsxs3(
968
+ "button",
969
+ {
970
+ type: "button",
971
+ "aria-label": chain.chainName,
972
+ disabled,
973
+ onClick: () => {
974
+ if (disabled) return;
975
+ onSelectFilter(chain.blockchain);
976
+ },
977
+ className: chipClass(selected, disabled),
978
+ children: [
979
+ /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-6 object-cover md:size-8" }),
980
+ fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
981
+ ]
982
+ }
983
+ )
984
+ ) }, chain.blockchain);
985
+ }) }),
986
+ /* @__PURE__ */ jsx4("div", { className: "mt-2 flex w-full shrink-0 justify-center border-t border-[#e3e3e3] pt-2", children: /* @__PURE__ */ jsx4(
987
+ "button",
988
+ {
989
+ type: "button",
990
+ "aria-label": "All networks",
991
+ onClick: onOpenNetworks,
992
+ className: chipClass(view === "network", false),
993
+ children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-4 text-black" })
994
+ }
995
+ ) })
996
+ ] });
997
+ }
998
+
999
+ // src/token-select/token-pane.tsx
1000
+ import { SearchInput } from "@stableflow/pay-ui/search-input";
1001
+ import { Skeleton } from "@stableflow/pay-ui/skeleton";
1002
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1003
+ function TokenPane(props) {
1004
+ const {
1005
+ sections,
1006
+ selectedAssetId,
1007
+ loading,
1008
+ showBalances = false,
1009
+ showWalletStatus = true,
1010
+ getBalance,
1011
+ isBalanceLoading,
1012
+ onSelectToken,
1013
+ isTokenDisabled,
1014
+ search = "",
1015
+ onSearchChange,
1016
+ showSearch = false,
1017
+ searchClassName,
1018
+ rowClassName
1019
+ } = props;
1020
+ const visibleSections = sections.filter((section) => section.title || section.tokens.length > 0);
1021
+ const hasTokens = sections.some((section) => section.tokens.length > 0);
1022
+ return /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1023
+ showSearch && onSearchChange ? /* @__PURE__ */ jsx5(
1024
+ SearchInput,
1025
+ {
1026
+ value: search,
1027
+ onChange: onSearchChange,
1028
+ placeholder: "search or paste address",
1029
+ className: "mb-3 shrink-0",
1030
+ inputClassName: cx(
1031
+ "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30",
1032
+ searchClassName
1033
+ )
1034
+ }
1035
+ ) : null,
1036
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: [
1037
+ visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
1038
+ section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-2 md:px-3.5", children: [
1039
+ /* @__PURE__ */ jsx5("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
1040
+ showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx5(ChainWalletStatus, { kind: section.walletKind }) : null
1041
+ ] }) : null,
1042
+ section.tokens.map((token) => /* @__PURE__ */ jsx5(
1043
+ TokenRow,
1044
+ {
1045
+ token,
1046
+ selected: token.assetId === selectedAssetId,
1047
+ recent: token.assetId === section.badgeAssetId,
1048
+ showBalances,
1049
+ formatted: showBalances ? getBalance(token) : null,
1050
+ loadingBalance: showBalances && isBalanceLoading(token),
1051
+ disabled: Boolean(isTokenDisabled?.(token)),
1052
+ onSelect: () => onSelectToken(token),
1053
+ className: rowClassName
1054
+ },
1055
+ `${section.id}-${token.assetId}`
1056
+ ))
1057
+ ] }, section.id)),
1058
+ loading && !hasTokens ? /* @__PURE__ */ jsx5(TokenListSkeleton, {}) : null,
1059
+ !loading && !hasTokens ? /* @__PURE__ */ jsx5("p", { className: cx("px-1 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1060
+ ] })
1061
+ ] });
1062
+ }
1063
+ function TokenListSkeleton() {
1064
+ return /* @__PURE__ */ jsx5("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2.5 px-2 py-3 md:px-3.5", children: [
1065
+ /* @__PURE__ */ jsx5(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
1066
+ /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
1067
+ /* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-24" }),
1068
+ /* @__PURE__ */ jsx5(Skeleton, { className: "h-3 w-16" })
1069
+ ] })
1070
+ ] }, index)) });
1071
+ }
1072
+ function TokenRow(props) {
1073
+ const { token, selected, recent, showBalances, formatted, loadingBalance, disabled, onSelect, className } = props;
1074
+ const usd = showBalances && !loadingBalance && formatted != null ? tokenBalanceUsd(token, formatted) : -1;
1075
+ return /* @__PURE__ */ jsxs4(
1076
+ "button",
1077
+ {
1078
+ type: "button",
1079
+ disabled,
1080
+ onClick: onSelect,
1081
+ className: cx(
1082
+ "flex w-full items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-2 py-3 text-left hover:bg-[#F6F6F6] md:px-3.5",
1083
+ selected && "bg-[#F6F6F6]",
1084
+ disabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
1085
+ className
1086
+ ),
1087
+ children: [
1088
+ /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1089
+ /* @__PURE__ */ jsxs4("span", { className: "relative size-8 shrink-0", children: [
1090
+ /* @__PURE__ */ jsx5("img", { src: token.logo, alt: "", className: "size-8 rounded-full object-cover" }),
1091
+ /* @__PURE__ */ jsx5(
1092
+ "img",
1093
+ {
1094
+ src: token.chain.logo,
1095
+ alt: "",
1096
+ className: "absolute -right-0.5 -bottom-0.5 size-3.5 rounded-[4px] border border-white object-cover"
1097
+ }
1098
+ )
1099
+ ] }),
1100
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1101
+ /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1102
+ /* @__PURE__ */ jsx5("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1103
+ recent ? /* @__PURE__ */ jsx5("span", { className: cx("shrink-0 rounded-[9px] bg-[#06f]/10 px-1.5 text-[10px] font-medium leading-[18px] text-[#06f]", fontSans), children: "Recently Used" }) : null
1104
+ ] }),
1105
+ /* @__PURE__ */ jsx5("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1106
+ ] })
1107
+ ] }),
1108
+ showBalances ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx5(
1109
+ "span",
1110
+ {
1111
+ className: "inline-block size-3.5 animate-spin rounded-full border-2 border-[#606060] border-r-transparent",
1112
+ "aria-label": "Loading balance"
1113
+ }
1114
+ ) : formatted != null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1115
+ /* @__PURE__ */ jsx5("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1116
+ usd >= 0 ? /* @__PURE__ */ jsx5("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1117
+ ] }) : /* @__PURE__ */ jsx5("span", { className: cx("text-sm text-[#606060]", fontSans), children: "\u2014" }) }) : null
1118
+ ]
1119
+ }
1120
+ );
1121
+ }
1122
+
1123
+ // src/token-select/TokenSelectDialog.tsx
1124
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1125
+ var SEARCH_INPUT_CLASS = "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
1126
+ function chainsFromTokens(tokens) {
1127
+ const byCode = /* @__PURE__ */ new Map();
1128
+ for (const token of tokens) {
1129
+ if (!byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1130
+ }
1131
+ return Array.from(byCode.values());
1132
+ }
1133
+ function ownerForToken(owners, token) {
1134
+ return owners[token.chain.chainKind] ?? null;
1135
+ }
1136
+ function hasAnyOwner(owners) {
1137
+ return Object.values(owners).some((address) => Boolean(address));
1138
+ }
1139
+ function isNativeToken(token) {
1140
+ const addr = String(token.contractAddress || "").trim().toLowerCase();
1141
+ return !addr || addr === "native" || addr === "0x0000000000000000000000000000000000000000";
1142
+ }
1143
+ function TokenSelectDialog(props) {
1144
+ const {
1145
+ open,
1146
+ onClose,
1147
+ onSelect,
1148
+ role = "payer",
1149
+ title = "Select Token",
1150
+ selectedAssetId,
1151
+ balanceOwners,
1152
+ allowedBlockchains = null,
1153
+ lockChainKind = null,
1154
+ excludeNative = false,
1155
+ disabledBlockchains = null,
1156
+ disabledReason,
1157
+ className,
1158
+ titleClassName,
1159
+ closeClassName,
1160
+ contentClassName,
1161
+ searchClassName,
1162
+ railClassName,
1163
+ rowClassName
1164
+ } = props;
1165
+ const payer = role === "payer";
1166
+ const showBalances = payer;
1167
+ const { wallet, popularTokens: popularRefs } = usePayWidgets();
1168
+ const { tokens, status } = usePayConfigSnapshot();
1169
+ const prefs2 = useTokenSelectPrefs();
1170
+ const balanceEntries = useSyncExternalStore4(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
1171
+ const connectedOwners = useMemo2(() => {
1172
+ const owners2 = {};
1173
+ for (const [kind, account] of Object.entries(wallet.accounts)) {
1174
+ const address = account?.address?.trim();
1175
+ if (address) owners2[kind] = address;
1176
+ }
1177
+ return owners2;
1178
+ }, [wallet.accounts]);
1179
+ const owners = showBalances ? { ...connectedOwners, ...balanceOwners } : {};
1180
+ const walletConnected = hasAnyOwner(connectedOwners);
1181
+ const walletWasConnected = useRef(null);
1182
+ const [search, setSearch] = useState("");
1183
+ const [chainFilter, setChainFilter] = useState(ALL_CHAIN_FILTER);
1184
+ const [view, setView] = useState("token");
1185
+ const loading = status === "loading" || status === "idle";
1186
+ useEffect3(() => {
1187
+ if (!open) return;
1188
+ setSearch("");
1189
+ if (lockChainKind) {
1190
+ setView("token");
1191
+ return;
1192
+ }
1193
+ if (!payer) {
1194
+ setView("token");
1195
+ setChainFilter(ALL_CHAIN_FILTER);
1196
+ }
1197
+ }, [open, lockChainKind, payer]);
1198
+ useEffect3(() => {
1199
+ if (!open || !lockChainKind || tokens.length === 0) return;
1200
+ setChainFilter(initialChainFilter(
1201
+ lockChainKind,
1202
+ getTokenSelectPrefs().recentBlockchains,
1203
+ chainsFromTokens(tokens)
1204
+ ));
1205
+ }, [open, lockChainKind, tokens]);
1206
+ useEffect3(() => {
1207
+ if (!open) {
1208
+ walletWasConnected.current = null;
1209
+ return;
1210
+ }
1211
+ if (lockChainKind || !payer) {
1212
+ walletWasConnected.current = walletConnected;
1213
+ return;
1214
+ }
1215
+ const previous = walletWasConnected.current;
1216
+ walletWasConnected.current = walletConnected;
1217
+ if (previous !== null && previous === walletConnected) return;
1218
+ setView(walletConnected ? "token" : "network");
1219
+ setChainFilter(ALL_CHAIN_FILTER);
1220
+ }, [open, lockChainKind, payer, walletConnected]);
1221
+ const allowed = useMemo2(() => {
1222
+ if (!allowedBlockchains || allowedBlockchains.length === 0) return null;
1223
+ return new Set(allowedBlockchains.map((code) => code.toLowerCase()));
1224
+ }, [allowedBlockchains]);
1225
+ const scopedTokens = useMemo2(() => {
1226
+ return tokens.filter((token) => {
1227
+ if (allowed && !allowed.has(token.blockchain.toLowerCase())) return false;
1228
+ if (excludeNative && isNativeToken(token)) return false;
1229
+ if (payer && !token.supportPayment) return false;
1230
+ if (!payer && !token.supportReceive) return false;
1231
+ return true;
1232
+ });
1233
+ }, [tokens, allowed, excludeNative, payer]);
1234
+ const ownerKey2 = ["evm", "near", "solana", "tron", "zec"].map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
1235
+ const tokenKey = useMemo2(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1236
+ const ownersRef = useRef(owners);
1237
+ const tokensRef = useRef(scopedTokens);
1238
+ ownersRef.current = owners;
1239
+ tokensRef.current = scopedTokens;
1240
+ useEffect3(() => {
1241
+ if (!showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1242
+ void ensurePayTokenBalances(ownersRef.current, tokensRef.current);
1243
+ const id = window.setInterval(() => {
1244
+ void ensurePayTokenBalances(ownersRef.current, tokensRef.current, { force: true });
1245
+ }, TOKEN_BALANCE_POLL_MS);
1246
+ return () => window.clearInterval(id);
1247
+ }, [showBalances, open, ownerKey2, tokenKey]);
1248
+ function balanceOf(token) {
1249
+ return getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted;
1250
+ }
1251
+ function usdOf(token) {
1252
+ if (!showBalances) return -1;
1253
+ return tokenBalanceUsd(token, balanceOf(token));
1254
+ }
1255
+ function loadingOf(token) {
1256
+ if (!showBalances) return false;
1257
+ const owner = ownerForToken(owners, token);
1258
+ if (!owner) return false;
1259
+ const entry = getPayTokenBalance(owner, token.assetId);
1260
+ return entry?.formatted == null && (!entry || entry.status === "loading");
1261
+ }
1262
+ const availableChains = useMemo2(() => {
1263
+ const codes = new Set(scopedTokens.map((token) => token.blockchain));
1264
+ const byCode = /* @__PURE__ */ new Map();
1265
+ for (const token of scopedTokens) {
1266
+ if (codes.has(token.blockchain) && !byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1267
+ }
1268
+ return Array.from(byCode.values());
1269
+ }, [scopedTokens]);
1270
+ const sortedChains = useMemo2(
1271
+ () => sortChainsForSidebar(availableChains, scopedTokens, {
1272
+ showBalances,
1273
+ walletConnected,
1274
+ getBalanceUsd: (token) => tokenBalanceUsd(token, getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)
1275
+ }),
1276
+ [availableChains, scopedTokens, showBalances, walletConnected, owners, balanceEntries]
1277
+ );
1278
+ const fundedBlockchains = useMemo2(() => {
1279
+ const funded = /* @__PURE__ */ new Set();
1280
+ if (!showBalances) return funded;
1281
+ for (const chain of availableChains) {
1282
+ if (chainHasBalance(chain.blockchain, scopedTokens, (token) => getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)) {
1283
+ funded.add(chain.blockchain);
1284
+ }
1285
+ }
1286
+ return funded;
1287
+ }, [availableChains, owners, scopedTokens, showBalances, balanceEntries]);
1288
+ const popularTokens = useMemo2(
1289
+ () => popularTokensForWallets(scopedTokens, popularRefs, (kind) => Boolean(connectedOwners[kind])),
1290
+ [scopedTokens, popularRefs, connectedOwners]
1291
+ );
1292
+ const newestAssetId = showBalances ? prefs2.recentAssetIds[0] ?? null : null;
1293
+ const searchingAll = chainFilter === ALL_CHAIN_FILTER && search.trim().length > 0;
1294
+ const walletKind = chainFilter === ALL_CHAIN_FILTER ? null : availableChains.find((chain) => chain.blockchain === chainFilter)?.chainKind ?? null;
1295
+ const sections = useMemo2(() => {
1296
+ if (showBalances && chainFilter === ALL_CHAIN_FILTER && searchingAll) {
1297
+ const matched2 = scopedTokens.filter((token) => tokenMatchesSearch(token, search));
1298
+ return [{
1299
+ id: "search",
1300
+ tokens: sortTokensByBalance(matched2, usdOf, loadingOf),
1301
+ badgeAssetId: newestAssetId
1302
+ }];
1303
+ }
1304
+ if (showBalances && chainFilter === ALL_CHAIN_FILTER) {
1305
+ const next = [];
1306
+ const recent = recentTokensInOrder(scopedTokens, prefs2.recentAssetIds);
1307
+ if (recent.length > 0) {
1308
+ next.push({
1309
+ id: "recent",
1310
+ title: "Recently Used",
1311
+ tokens: recent,
1312
+ badgeAssetId: newestAssetId
1313
+ });
1314
+ }
1315
+ const yours = positiveBalanceTokens(scopedTokens, usdOf);
1316
+ if (yours.length > 0) {
1317
+ next.push({ id: "yours", title: "Your Tokens", tokens: yours });
1318
+ }
1319
+ if (popularTokens.length > 0) {
1320
+ next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1321
+ }
1322
+ return next;
1323
+ }
1324
+ const matched = scopedTokens.filter((token) => matchesChainFilter(token, chainFilter) && tokenMatchesSearch(token, search));
1325
+ if (!showBalances && chainFilter === ALL_CHAIN_FILTER && !search.trim()) {
1326
+ const next = [];
1327
+ if (popularTokens.length > 0) {
1328
+ next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1329
+ }
1330
+ next.push({ id: "all", tokens: sortTokensBySymbol(matched) });
1331
+ return next;
1332
+ }
1333
+ const chainName = availableChains.find((chain) => chain.blockchain === chainFilter)?.chainName ?? chainFilter;
1334
+ return [{
1335
+ id: chainFilter,
1336
+ title: chainName,
1337
+ walletKind,
1338
+ tokens: showBalances ? sortTokensByBalance(matched, usdOf, loadingOf) : sortTokensBySymbol(matched),
1339
+ badgeAssetId: showBalances ? newestAssetId : null
1340
+ }];
1341
+ }, [
1342
+ showBalances,
1343
+ chainFilter,
1344
+ searchingAll,
1345
+ search,
1346
+ scopedTokens,
1347
+ prefs2.recentAssetIds,
1348
+ newestAssetId,
1349
+ walletKind,
1350
+ balanceEntries,
1351
+ owners,
1352
+ popularTokens,
1353
+ availableChains
1354
+ ]);
1355
+ function handleSelectFilter(filter) {
1356
+ setChainFilter(filter);
1357
+ if (filter !== ALL_CHAIN_FILTER) setLastBlockchain(filter);
1358
+ setView("token");
1359
+ }
1360
+ function handleSelectToken(token) {
1361
+ if (isBlockchainDisabled(token.blockchain, disabledBlockchains)) return;
1362
+ if (isChainKindLocked(token.chain.chainKind, lockChainKind)) return;
1363
+ if (payer) setLastToken(token.assetId, token.blockchain);
1364
+ else setLastBlockchain(token.blockchain);
1365
+ onSelect({ token });
1366
+ onClose();
1367
+ }
1368
+ const awaitingBalances = showBalances && chainFilter === ALL_CHAIN_FILTER && !searchingAll && scopedTokens.some((token) => loadingOf(token));
1369
+ return /* @__PURE__ */ jsx6(
1370
+ Dialog,
1371
+ {
1372
+ open,
1373
+ onClose,
1374
+ title,
1375
+ titleClassName: cx(
1376
+ "min-w-0 flex-1 truncate text-base font-medium text-[#606060] md:flex-none",
1377
+ fontSans,
1378
+ titleClassName
1379
+ ),
1380
+ closeClassName: cx(view === "token" ? "ml-0" : void 0, closeClassName),
1381
+ contentClassName,
1382
+ headerAction: view === "token" ? /* @__PURE__ */ jsx6(
1383
+ SearchInput2,
1384
+ {
1385
+ value: search,
1386
+ onChange: setSearch,
1387
+ placeholder: "search or paste address",
1388
+ className: "ml-auto hidden w-[215px] shrink-0 md:block",
1389
+ inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1390
+ }
1391
+ ) : null,
1392
+ cardClassName: cx("w-full max-h-[90vh] md:w-[474px]", className),
1393
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1394
+ view === "token" ? /* @__PURE__ */ jsx6(
1395
+ SearchInput2,
1396
+ {
1397
+ value: search,
1398
+ onChange: setSearch,
1399
+ placeholder: "search or paste address",
1400
+ className: "mb-3 shrink-0 md:hidden",
1401
+ inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1402
+ }
1403
+ ) : null,
1404
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-1", children: [
1405
+ /* @__PURE__ */ jsx6(
1406
+ NetworkRail,
1407
+ {
1408
+ view,
1409
+ chainFilter,
1410
+ chips: sortedChains,
1411
+ fundedBlockchains,
1412
+ walletConnected,
1413
+ showAllLabel: !payer,
1414
+ lockChainKind,
1415
+ disabledBlockchains,
1416
+ disabledReason,
1417
+ onSelectFilter: handleSelectFilter,
1418
+ onOpenNetworks: () => setView("network"),
1419
+ className: railClassName
1420
+ }
1421
+ ),
1422
+ view === "network" ? /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
1423
+ ChainPane,
1424
+ {
1425
+ chains: sortedChains,
1426
+ fundedBlockchains,
1427
+ showWalletStatus: payer,
1428
+ onSelectFilter: handleSelectFilter,
1429
+ lockChainKind,
1430
+ disabledBlockchains,
1431
+ disabledReason
1432
+ }
1433
+ ) }) : /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
1434
+ TokenPane,
1435
+ {
1436
+ sections,
1437
+ selectedAssetId,
1438
+ loading: loading || awaitingBalances,
1439
+ showBalances,
1440
+ showWalletStatus: payer,
1441
+ getBalance: balanceOf,
1442
+ isBalanceLoading: loadingOf,
1443
+ onSelectToken: handleSelectToken,
1444
+ isTokenDisabled: (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind),
1445
+ searchClassName,
1446
+ rowClassName
1447
+ }
1448
+ ) })
1449
+ ] })
1450
+ ] })
1451
+ }
1452
+ );
1453
+ }
1454
+ export {
1455
+ DEFAULT_API_HOST,
1456
+ PayWidgetsProvider,
1457
+ TokenSelectDialog,
1458
+ clearPayTokenBalances,
1459
+ mapPayConfigSource,
1460
+ normalizePayConfig,
1461
+ parsePopularTokens,
1462
+ refreshPayTokenBalance,
1463
+ usePayConfig,
1464
+ usePayConfigSnapshot,
1465
+ usePayTokenBalance
1466
+ };
1467
+ //# sourceMappingURL=index.js.map