@stableflow/pay-widgets 0.1.0 → 0.2.0-beta-90dffc3d2f-20261012

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.
@@ -1,35 +1,26 @@
1
1
  // src/token-select/context.tsx
2
- import { createContext, useContext, useEffect as useEffect2, useMemo } from "react";
2
+ import { useEffect } from "react";
3
+ import { createContext, useContext } from "react";
3
4
 
4
5
  // src/token-select/balances.ts
5
6
  import { useSyncExternalStore } from "react";
6
7
 
7
8
  // src/token-select/config.ts
8
9
  var TOKEN_BALANCE_POLL_MS = 6e4;
9
- var CONFIG_STALE_MS = 30 * 60 * 1e3;
10
10
  var ALL_CHAIN_FILTER = "all";
11
- var DEFAULT_API_HOST = "https://api.stableflow.ai";
11
+ var DEFAULT_STABLE_SYMBOLS = ["USDT", "USDC"];
12
12
  var POPULAR_SECTION_TITLE = "Popular on your network";
13
13
  var TOKEN_SELECT_PREFS_STORAGE_KEY = "stableflow-pay:token-select-prefs:v1";
14
14
  var TOKEN_SELECT_PREFS_VERSION = 2;
15
15
  var MAX_RECENT_BLOCKCHAINS = 16;
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
- ];
17
+ var CHAIN_KIND_LABELS = {
18
+ evm: "EVM",
19
+ near: "Near",
20
+ solana: "Solana",
21
+ tron: "Tron",
22
+ zec: "Zcash"
23
+ };
33
24
  var DEFAULT_CHAIN_KINDS = {
34
25
  eth: "evm",
35
26
  base: "evm",
@@ -52,7 +43,23 @@ var DEFAULT_CHAIN_KINDS = {
52
43
  var listeners = /* @__PURE__ */ new Set();
53
44
  var balances = {};
54
45
  var reader = null;
46
+ var syncReader = null;
47
+ var tokensById = /* @__PURE__ */ new Map();
55
48
  var chainInflight = /* @__PURE__ */ new Map();
49
+ function configureBalanceReaders(options) {
50
+ reader = options.readBalances ?? null;
51
+ if (!options.readBalances && options.getBalance) {
52
+ syncReader = {
53
+ getBalance: options.getBalance,
54
+ isBalanceLoading: options.isBalanceLoading ?? (() => false)
55
+ };
56
+ return;
57
+ }
58
+ syncReader = null;
59
+ }
60
+ function setBalanceTokens(tokens) {
61
+ tokensById = new Map(tokens.map((token) => [token.assetId, token]));
62
+ }
56
63
  function emit() {
57
64
  balances = { ...balances };
58
65
  for (const listener of listeners) listener();
@@ -87,15 +94,25 @@ function withStaleOnError(prev, next) {
87
94
  if (prev?.raw == null && prev?.formatted == null) return next;
88
95
  return { ...next, raw: prev.raw, formatted: prev.formatted, updatedAt: prev.updatedAt };
89
96
  }
90
- function setBalanceReader(next) {
91
- reader = next;
92
- }
93
97
  function clearPayTokenBalances() {
94
98
  balances = {};
95
99
  emit();
96
100
  }
101
+ function syncBalance(owner, assetId) {
102
+ if (!syncReader) return void 0;
103
+ const token = tokensById.get(assetId);
104
+ if (!token) return void 0;
105
+ const loading = syncReader.isBalanceLoading(token, owner);
106
+ const formatted = syncReader.getBalance(token, owner);
107
+ if (loading && (formatted == null || formatted === "")) {
108
+ return { raw: null, formatted: null, status: "loading", error: null, updatedAt: null };
109
+ }
110
+ if (formatted == null || formatted === "") return void 0;
111
+ return { raw: null, formatted, status: "success", error: null, updatedAt: null };
112
+ }
97
113
  function getPayTokenBalance(owner, assetId) {
98
114
  if (!owner || !assetId) return void 0;
115
+ if (syncReader) return syncBalance(owner, assetId);
99
116
  return balances[`${owner}:${assetId}`] ?? balances[`${owner.toLowerCase()}:${assetId}`];
100
117
  }
101
118
  function usePayTokenBalance(owner, assetId) {
@@ -198,6 +215,7 @@ async function fetchChain(owner, tokens, force) {
198
215
  });
199
216
  }
200
217
  async function ensurePayTokenBalances(owners, tokens, opts) {
218
+ if (syncReader || !reader) return;
201
219
  const force = Boolean(opts?.force);
202
220
  const groups = groupByChain(tokens);
203
221
  await Promise.all(Array.from(groups.values()).map((group) => {
@@ -208,306 +226,66 @@ async function ensurePayTokenBalances(owners, tokens, opts) {
208
226
  }));
209
227
  }
210
228
 
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 evmTypeLogoUrl() {
246
- return logoPath("/stableflow/networks/type-evm.svg");
247
- }
248
- function chainLogoUrl(blockchainOrNetwork) {
249
- const key = String(blockchainOrNetwork || "").toLowerCase();
250
- const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
251
- return logoPath(`/stableflow/networks/${file}.png`);
252
- }
253
- function tokenLogoUrl(symbol) {
254
- const upper = String(symbol || "").toUpperCase();
255
- const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
256
- return logoPath(`/stableflow/tokens/${file}.png`);
257
- }
258
-
259
- // src/token-select/pay-config.ts
260
- var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
261
- var WRAP_NEAR_CONTRACT = "wrap.near";
262
- var listeners2 = /* @__PURE__ */ new Set();
263
- var chainKindOverrides = {};
264
- var snapshot = {
265
- apiHost: DEFAULT_API_HOST,
266
- chains: [],
267
- tokens: [],
268
- source: null,
269
- status: "idle",
270
- error: null,
271
- fetchedAt: null
272
- };
273
- var inflight = null;
274
- function emit2(next) {
275
- snapshot = next;
276
- for (const listener of listeners2) listener();
277
- }
278
- function subscribe2(listener) {
279
- listeners2.add(listener);
280
- return () => listeners2.delete(listener);
281
- }
282
- function getPayConfigSnapshot() {
283
- return snapshot;
284
- }
285
- function setChainKindOverrides(overrides) {
286
- chainKindOverrides = overrides ?? {};
287
- if (snapshot.source) {
288
- const normalized = normalizePayConfig(snapshot.source, chainKindOverrides);
289
- emit2({ ...snapshot, chains: normalized.chains, tokens: normalized.tokens });
290
- }
291
- }
292
- function asRecord(value) {
293
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
294
- return value;
295
- }
296
- function apiText(value) {
297
- if (typeof value === "string") return value.trim();
298
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
299
- return "";
300
- }
301
- function apiNumber(value) {
302
- if (typeof value === "number" && Number.isFinite(value)) return value;
303
- if (typeof value === "string" && value.trim()) {
304
- const parsed = Number(value);
305
- if (Number.isFinite(parsed)) return parsed;
306
- }
307
- return null;
308
- }
309
- function asBoolean(value, fallback = true) {
310
- if (typeof value === "boolean") return value;
311
- if (typeof value === "string") {
312
- const lower = value.trim().toLowerCase();
313
- if (lower === "true") return true;
314
- if (lower === "false") return false;
315
- }
316
- return fallback;
317
- }
318
- function mapSourceChain(raw) {
319
- const row = asRecord(raw) ?? {};
320
- return {
321
- network: apiText(row.network),
322
- chainId: apiText(row.chain_id ?? row.chainId),
323
- chainName: apiText(row.chain_name ?? row.chainName),
324
- logo: apiText(row.logo),
325
- explorer: apiText(row.explorer)
326
- };
327
- }
328
- function mapSourceToken(raw) {
329
- const row = asRecord(raw) ?? {};
330
- return {
331
- symbol: apiText(row.symbol),
332
- network: apiText(row.network),
333
- decimals: apiNumber(row.decimals) ?? 0,
334
- contractAddress: apiText(row.contract_address ?? row.contractAddress),
335
- price: apiText(row.price),
336
- supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
337
- supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
338
- };
339
- }
340
- function configBody(raw) {
341
- const row = asRecord(raw) ?? {};
342
- const data = asRecord(row.data);
343
- if (typeof row.code === "number" && data) return data;
344
- return row;
345
- }
346
- function mapPayConfigSource(raw) {
347
- const row = configBody(raw);
348
- const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
349
- const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
350
- return { chains, tokens };
351
- }
352
- function chainKindFor(network, overrides = chainKindOverrides) {
353
- const key = network.trim().toLowerCase();
354
- if (!key) return null;
355
- return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
356
- }
357
- function normalizeSymbol(symbol) {
358
- const upper = symbol.trim().toUpperCase();
359
- if (!upper) return null;
360
- if (upper === "USDT0") return "USDT";
361
- if (upper === "WNEAR") return "NEAR";
362
- return upper;
363
- }
364
- function tokenAssetId(network, symbol, contractAddress) {
365
- const addr = String(contractAddress || "").trim();
366
- return `${network}:${symbol}:${addr || "native"}`;
367
- }
368
- function isNativeAddress(contractAddress) {
369
- const addr = String(contractAddress || "").trim();
370
- if (!addr) return true;
371
- const lower = addr.toLowerCase();
372
- return lower === "native" || lower === ZERO_ADDRESS;
373
- }
374
- function normalizePayConfig(source, overrides = chainKindOverrides) {
375
- const chains = [];
376
- for (const chain of source.chains) {
377
- const chainKind = chainKindFor(chain.network, overrides);
378
- if (!chainKind || !chain.network) continue;
379
- chains.push({
380
- blockchain: chain.network,
381
- chainName: chain.chainName || chain.network,
382
- chainKind,
383
- chainId: chain.chainId || void 0,
384
- logo: chain.logo || chainLogoUrl(chain.network),
385
- explorer: chain.explorer
386
- });
387
- }
388
- const chainByCode = new Map(chains.map((chain) => [chain.blockchain, chain]));
389
- const tokens = [];
390
- for (const token of source.tokens) {
391
- const chain = chainByCode.get(token.network);
392
- if (!chain) continue;
393
- const symbol = normalizeSymbol(token.symbol);
394
- if (!symbol) continue;
395
- if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
396
- const contractAddress = token.contractAddress.trim() || null;
397
- tokens.push({
398
- assetId: tokenAssetId(token.network, symbol, contractAddress),
399
- symbol,
400
- providerSymbol: token.symbol,
401
- blockchain: token.network,
402
- decimals: token.decimals,
403
- contractAddress,
404
- price: Number(token.price || 1),
405
- logo: tokenLogoUrl(symbol),
406
- supportPayment: token.supportPayment,
407
- supportReceive: token.supportReceive,
408
- chain
409
- });
410
- }
411
- const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
412
- const visible = hasNearWrap ? tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress))) : tokens;
413
- return { chains, tokens: visible };
414
- }
415
- function configUrl(apiHost) {
416
- return `${apiHost.replace(/\/$/, "")}/v1/pay/config`;
417
- }
418
- async function load(apiHost, force) {
419
- const fresh = snapshot.apiHost === apiHost && snapshot.status === "success" && snapshot.fetchedAt != null && Date.now() - snapshot.fetchedAt < CONFIG_STALE_MS;
420
- if (!force && fresh) return;
421
- if (inflight && !force) return inflight;
422
- emit2({ ...snapshot, apiHost, status: snapshot.tokens.length > 0 ? snapshot.status : "loading", error: null });
423
- const run = (async () => {
424
- try {
425
- const response = await fetch(configUrl(apiHost));
426
- if (!response.ok) throw new Error(`Config request failed (${response.status})`);
427
- const source = mapPayConfigSource(await response.json());
428
- const normalized = normalizePayConfig(source, chainKindOverrides);
429
- emit2({
430
- apiHost,
431
- source,
432
- chains: normalized.chains,
433
- tokens: normalized.tokens,
434
- status: "success",
435
- error: null,
436
- fetchedAt: Date.now()
437
- });
438
- } catch (cause) {
439
- const message = cause instanceof Error ? cause.message : "Failed to load config";
440
- emit2({
441
- ...snapshot,
442
- apiHost,
443
- status: snapshot.source ? "success" : "error",
444
- error: message
445
- });
446
- } finally {
447
- inflight = null;
448
- }
449
- })();
450
- inflight = run;
451
- return run;
452
- }
453
- function refetchPayConfig(apiHost = snapshot.apiHost) {
454
- return load(apiHost, true);
455
- }
456
- function usePayConfigSnapshot() {
457
- return useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
458
- }
459
- function usePayConfig(options) {
460
- const apiHost = options?.apiHost?.trim() || DEFAULT_API_HOST;
461
- const current = useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
462
- const refetch = useCallback(() => {
463
- void refetchPayConfig(apiHost);
464
- }, [apiHost]);
465
- useEffect(() => {
466
- void load(apiHost, false);
467
- }, [apiHost]);
468
- return {
469
- chains: current.apiHost === apiHost ? current.chains : [],
470
- tokens: current.apiHost === apiHost ? current.tokens : [],
471
- source: current.apiHost === apiHost ? current.source : null,
472
- status: current.apiHost === apiHost ? current.status : "loading",
473
- error: current.apiHost === apiHost ? current.error : null,
474
- refetch
475
- };
476
- }
477
-
478
229
  // src/token-select/context.tsx
479
230
  import { jsx } from "react/jsx-runtime";
480
231
  var PayWidgetsContext = createContext(null);
232
+ function isProduction() {
233
+ const env = globalThis.process?.env?.NODE_ENV;
234
+ return env === "production";
235
+ }
236
+ function warnBalanceMode(readBalances, getBalance) {
237
+ if (isProduction()) return;
238
+ if (readBalances && getBalance) {
239
+ console.warn("PayWidgetsProvider: both readBalances and getBalance were passed; using readBalances.");
240
+ return;
241
+ }
242
+ if (!readBalances && !getBalance) {
243
+ console.warn("PayWidgetsProvider: pass readBalances or getBalance so token balances can load.");
244
+ }
245
+ }
481
246
  function PayWidgetsProvider(props) {
482
247
  const {
483
- apiHost,
248
+ tokens,
249
+ loading = false,
484
250
  popularTokens = [],
485
251
  wallet,
486
252
  readBalances,
487
- onConfig,
253
+ getBalance,
254
+ isBalanceLoading,
488
255
  onCopyAddress,
489
- chainKinds,
256
+ stableSymbols = [...DEFAULT_STABLE_SYMBOLS],
257
+ chainKindLabels = {},
490
258
  children
491
259
  } = props;
492
- const config = usePayConfig({ apiHost });
493
- useEffect2(() => {
494
- setChainKindOverrides(chainKinds);
495
- }, [chainKinds]);
496
- useEffect2(() => {
497
- setBalanceReader(readBalances);
498
- return () => setBalanceReader(null);
499
- }, [readBalances]);
500
- useEffect2(() => {
501
- if (config.status === "success" && config.source) onConfig?.(config.source);
502
- }, [config.source, config.status, onConfig]);
260
+ useEffect(() => {
261
+ warnBalanceMode(readBalances, getBalance);
262
+ configureBalanceReaders({
263
+ readBalances: readBalances ?? null,
264
+ getBalance: getBalance ?? null,
265
+ isBalanceLoading: isBalanceLoading ?? null
266
+ });
267
+ return () => configureBalanceReaders({});
268
+ }, [readBalances, getBalance, isBalanceLoading]);
269
+ useEffect(() => {
270
+ setBalanceTokens(tokens);
271
+ }, [tokens]);
503
272
  const connected = Object.values(wallet.accounts).some((account) => Boolean(account?.address?.trim()));
504
- useEffect2(() => {
273
+ useEffect(() => {
505
274
  if (!connected) clearPayTokenBalances();
506
275
  }, [connected]);
507
- const value = useMemo(
508
- () => ({ wallet, popularTokens, onCopyAddress }),
509
- [wallet, popularTokens, onCopyAddress]
510
- );
276
+ const balanceMode = readBalances ? "async" : getBalance ? "sync" : "none";
277
+ const value = {
278
+ tokens,
279
+ loading,
280
+ wallet,
281
+ popularTokens,
282
+ stableSymbols,
283
+ chainKindLabels,
284
+ onCopyAddress,
285
+ balanceMode,
286
+ getBalance: balanceMode === "sync" ? getBalance ?? null : null,
287
+ isBalanceLoading: balanceMode === "sync" ? isBalanceLoading ?? null : null
288
+ };
511
289
  return /* @__PURE__ */ jsx(PayWidgetsContext.Provider, { value, children });
512
290
  }
513
291
  function usePayWidgets() {
@@ -519,83 +297,23 @@ function usePayWidgets() {
519
297
  }
520
298
 
521
299
  // src/token-select/TokenSelectDialog.tsx
522
- import { useEffect as useEffect3, useMemo as useMemo2, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore4 } from "react";
300
+ import { useEffect as useEffect3, useLayoutEffect, useMemo, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore3 } from "react";
523
301
  import { Dialog } from "@stableflow/pay-ui/dialog";
524
- import { SearchInput as SearchInput2 } from "@stableflow/pay-ui/search-input";
302
+ import { SearchInput } from "@stableflow/pay-ui/search-input";
525
303
 
526
- // src/token-select/prefs.ts
527
- import { useSyncExternalStore as useSyncExternalStore3 } from "react";
528
- var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
529
- function rememberRecentAsset(recent, assetId) {
530
- const id = assetId.trim();
531
- if (!id) return recent;
532
- return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
533
- }
534
- function rememberRecentBlockchain(recent, blockchain) {
535
- const code = blockchain.trim();
536
- if (!code) return recent;
537
- return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
538
- }
539
- function migrateTokenSelectPrefs(persisted, version) {
540
- const state = persisted ?? {};
541
- const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
542
- 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] : [];
543
- return { recentAssetIds, recentBlockchains };
544
- }
545
- function readStorage() {
546
- if (typeof localStorage === "undefined") return EMPTY_PREFS;
547
- try {
548
- const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
549
- if (!raw) return EMPTY_PREFS;
550
- const parsed = JSON.parse(raw);
551
- const version = typeof parsed.version === "number" ? parsed.version : 0;
552
- const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
553
- return migrateTokenSelectPrefs(payload, version);
554
- } catch {
555
- return EMPTY_PREFS;
556
- }
557
- }
558
- function writeStorage(state) {
559
- if (typeof localStorage === "undefined") return;
560
- localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
561
- state,
562
- version: TOKEN_SELECT_PREFS_VERSION
563
- }));
564
- }
565
- var prefs = readStorage();
566
- var listeners3 = /* @__PURE__ */ new Set();
567
- function emit3(next) {
568
- prefs = next;
569
- writeStorage(next);
570
- for (const listener of listeners3) listener();
571
- }
572
- function subscribe3(listener) {
573
- listeners3.add(listener);
574
- return () => listeners3.delete(listener);
575
- }
576
- function getTokenSelectPrefs() {
577
- return prefs;
578
- }
579
- function setLastToken(assetId, blockchain) {
580
- emit3({
581
- recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
582
- recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
583
- });
584
- }
585
- function setLastBlockchain(blockchain) {
586
- emit3({
587
- ...prefs,
588
- recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
589
- });
590
- }
591
- function useTokenSelectPrefs() {
592
- return useSyncExternalStore3(subscribe3, getTokenSelectPrefs, getTokenSelectPrefs);
593
- }
304
+ // src/token-select/chain-pane.tsx
305
+ import { IconChevron } from "@stableflow/pay-ui/icons/chevron";
306
+ import { Tooltip } from "@stableflow/pay-ui/tooltip";
307
+
308
+ // src/token-select/chain-wallet-status.tsx
309
+ import { IconCopy } from "@stableflow/pay-ui/icons/copy";
310
+ import { IconLogout } from "@stableflow/pay-ui/icons/logout";
594
311
 
595
312
  // src/token-select/format.ts
596
313
  import { clsx } from "clsx";
597
314
  import { twMerge } from "tailwind-merge";
598
315
  var fontSans = "[font-family:Montserrat,PingFang_SC,system-ui,sans-serif]";
316
+ var thinScrollbar = "[&::-webkit-scrollbar]:w-[4px] [&::-webkit-scrollbar-thumb]:rounded-[12px] [&::-webkit-scrollbar-thumb]:bg-[#d9d9d9] [&::-webkit-scrollbar-track]:bg-transparent supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] [scrollbar-color:#d9d9d9_transparent]";
599
317
  function cx(...inputs) {
600
318
  return twMerge(clsx(inputs));
601
319
  }
@@ -605,13 +323,8 @@ function formatAddress(address, prefix = 4, suffix = 5) {
605
323
  if (address.length <= prefix + suffix) return address;
606
324
  return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
607
325
  }
608
- function chainKindLabel(kind) {
609
- if (kind === "evm") return "EVM";
610
- if (kind === "near") return "Near";
611
- if (kind === "solana") return "Solana";
612
- if (kind === "tron") return "Tron";
613
- if (kind === "zec") return "Zcash";
614
- return kind;
326
+ function chainKindLabel(kind, labels) {
327
+ return labels?.[kind] ?? CHAIN_KIND_LABELS[kind] ?? kind;
615
328
  }
616
329
  function formatDisplayAmount(value, options = {}) {
617
330
  const maxDecimals = options.maxDecimals ?? 2;
@@ -633,14 +346,7 @@ function formatDisplayAmount(value, options = {}) {
633
346
  return `${prefix}${negative ? "-" : ""}${body}`;
634
347
  }
635
348
 
636
- // src/token-select/chain-pane.tsx
637
- import { useState } from "react";
638
- import { IconArrowDown } from "@stableflow/pay-ui/icons/arrow-down";
639
- import { Tooltip } from "@stableflow/pay-ui/tooltip";
640
-
641
349
  // src/token-select/chain-wallet-status.tsx
642
- import { IconCopy } from "@stableflow/pay-ui/icons/copy";
643
- import { IconLogout } from "@stableflow/pay-ui/icons/logout";
644
350
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
645
351
  function stop(event) {
646
352
  event.preventDefault();
@@ -710,22 +416,14 @@ function ChainWalletStatus({ kind }) {
710
416
  }
711
417
 
712
418
  // src/token-select/utils.ts
713
- function groupEvmChains(chains) {
714
- const evm = chains.filter((chain) => chain.chainKind === "evm");
715
- if (evm.length === 0) return chains.map((chain) => ({ kind: "chain", chain }));
716
- let placed = false;
717
- const entries = [];
718
- for (const chain of chains) {
719
- if (chain.chainKind === "evm") {
720
- if (!placed) {
721
- entries.push({ kind: "evm", chains: evm });
722
- placed = true;
723
- }
724
- continue;
725
- }
726
- entries.push({ kind: "chain", chain });
419
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
420
+ function chainsFromTokens(tokens) {
421
+ const byCode = /* @__PURE__ */ new Map();
422
+ for (const token of tokens) {
423
+ const code = token.chain.blockchain || token.blockchain;
424
+ if (!byCode.has(code)) byCode.set(code, token.chain);
727
425
  }
728
- return entries;
426
+ return Array.from(byCode.values());
729
427
  }
730
428
  function tokenBalanceUsd(token, formatted) {
731
429
  if (formatted == null || formatted === "") return -1;
@@ -741,10 +439,6 @@ function tokenMatchesSearch(token, query) {
741
439
  if (token.providerSymbol.toLowerCase().includes(q)) return true;
742
440
  return Boolean(token.contractAddress?.toLowerCase().includes(q));
743
441
  }
744
- function matchesChainFilter(token, chainFilter) {
745
- if (chainFilter === ALL_CHAIN_FILTER) return true;
746
- return token.blockchain === chainFilter;
747
- }
748
442
  function formattedBalancePositive(formatted) {
749
443
  if (formatted == null || formatted === "") return false;
750
444
  const amount = Number(formatted);
@@ -764,9 +458,6 @@ function chainBalanceUsd(blockchain, tokens, getBalanceUsd) {
764
458
  }
765
459
  return total;
766
460
  }
767
- var DISCONNECTED_CHAIN_RANK = new Map(
768
- DISCONNECTED_CHAIN_ORDER.map((code, index) => [code, index])
769
- );
770
461
  function parsePopularTokens(raw) {
771
462
  if (!raw?.trim()) return [];
772
463
  const refs = [];
@@ -790,16 +481,10 @@ function popularTokensForWallets(tokens, configured, isChainConnected) {
790
481
  }
791
482
  return result;
792
483
  }
484
+ function sortChainsAlpha(chains) {
485
+ return chains.slice().sort((left, right) => left.chainName.localeCompare(right.chainName));
486
+ }
793
487
  function sortChainsForSidebar(chains, tokens, options) {
794
- if (!options.walletConnected) {
795
- const tail = DISCONNECTED_CHAIN_ORDER.length;
796
- return chains.map((chain, index) => ({ chain, index })).sort((left, right) => {
797
- const leftRank = DISCONNECTED_CHAIN_RANK.get(left.chain.blockchain) ?? tail;
798
- const rightRank = DISCONNECTED_CHAIN_RANK.get(right.chain.blockchain) ?? tail;
799
- if (leftRank !== rightRank) return leftRank - rightRank;
800
- return left.index - right.index;
801
- }).map((item) => item.chain);
802
- }
803
488
  if (!options.showBalances) return chains.slice();
804
489
  return chains.slice().sort((left, right) => {
805
490
  const leftUsd = chainBalanceUsd(left.blockchain, tokens, options.getBalanceUsd);
@@ -808,6 +493,13 @@ function sortChainsForSidebar(chains, tokens, options) {
808
493
  return left.chainName.localeCompare(right.chainName);
809
494
  });
810
495
  }
496
+ function payerRailChains(chains, owners, fundedSet) {
497
+ return chains.filter((chain) => {
498
+ if (!owners[chain.chainKind]) return false;
499
+ if (chain.chainKind === "evm" && !fundedSet.has(chain.blockchain)) return false;
500
+ return true;
501
+ });
502
+ }
811
503
  function initialChainFilter(lockChainKind, recentBlockchains, chains) {
812
504
  if (!lockChainKind) return ALL_CHAIN_FILTER;
813
505
  const matching = chains.filter((chain) => chain.chainKind === lockChainKind);
@@ -848,9 +540,6 @@ function sortTokensByBalance(tokens, getBalanceUsd, isLoading) {
848
540
  return compareTokensBySymbol(left, right);
849
541
  });
850
542
  }
851
- function sortTokensBySymbol(tokens) {
852
- return tokens.slice().sort(compareTokensBySymbol);
853
- }
854
543
  function positiveBalanceTokens(tokens, getBalanceUsd) {
855
544
  return sortTokensByBalance(
856
545
  tokens.filter((token) => getBalanceUsd(token) > 0),
@@ -858,6 +547,90 @@ function positiveBalanceTokens(tokens, getBalanceUsd) {
858
547
  () => false
859
548
  );
860
549
  }
550
+ function isGasToken(token) {
551
+ const addr = String(token.contractAddress || "").trim().toLowerCase();
552
+ if (!addr || addr === "native" || addr === ZERO_ADDRESS) return true;
553
+ return token.blockchain === "near" && token.symbol.toUpperCase() === "NEAR";
554
+ }
555
+ function stableSymbolIndex(symbol, stableSymbols) {
556
+ const upper = symbol.toUpperCase();
557
+ return stableSymbols.findIndex((item) => item.toUpperCase() === upper);
558
+ }
559
+ function tokenPriority(token, stableSymbols = DEFAULT_STABLE_SYMBOLS) {
560
+ if (stableSymbolIndex(token.symbol, stableSymbols) >= 0) return 0;
561
+ if (isGasToken(token)) return 1;
562
+ return 2;
563
+ }
564
+ function compareTokensByPriority(left, right, stableSymbols = DEFAULT_STABLE_SYMBOLS) {
565
+ const leftPriority = tokenPriority(left, stableSymbols);
566
+ const rightPriority = tokenPriority(right, stableSymbols);
567
+ if (leftPriority !== rightPriority) return leftPriority - rightPriority;
568
+ if (leftPriority === 0) {
569
+ const leftIndex = stableSymbolIndex(left.symbol, stableSymbols);
570
+ const rightIndex = stableSymbolIndex(right.symbol, stableSymbols);
571
+ if (leftIndex !== rightIndex) return leftIndex - rightIndex;
572
+ }
573
+ return left.symbol.localeCompare(right.symbol) || left.chain.chainName.localeCompare(right.chain.chainName);
574
+ }
575
+ function sortTokensForChain(tokens, options) {
576
+ const stableSymbols = options.stableSymbols ?? DEFAULT_STABLE_SYMBOLS;
577
+ if (!options.withBalances) {
578
+ return tokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
579
+ }
580
+ return tokens.slice().sort((left, right) => {
581
+ const leftRank = tokenBalanceRank(options.usdOf(left), options.loadingOf(left));
582
+ const rightRank = tokenBalanceRank(options.usdOf(right), options.loadingOf(right));
583
+ if (leftRank !== rightRank) return leftRank - rightRank;
584
+ if (leftRank === BALANCE_RANK_FUNDED) {
585
+ const byUsd = options.usdOf(right) - options.usdOf(left);
586
+ if (byUsd !== 0) return byUsd;
587
+ }
588
+ return compareTokensByPriority(left, right, stableSymbols);
589
+ });
590
+ }
591
+ var EVM_SEARCH_GROUP = "evm";
592
+ var EVM_SEARCH_GROUP_NAME = "EVM-based";
593
+ function groupSearchResults(tokens, query, options) {
594
+ const matched = tokens.filter((token) => tokenMatchesSearch(token, query));
595
+ const groups = /* @__PURE__ */ new Map();
596
+ for (const token of matched) {
597
+ const key = token.chain.chainKind === EVM_SEARCH_GROUP ? EVM_SEARCH_GROUP : token.blockchain;
598
+ const list = groups.get(key) ?? [];
599
+ list.push(token);
600
+ groups.set(key, list);
601
+ }
602
+ const stableSymbols = options.stableSymbols ?? DEFAULT_STABLE_SYMBOLS;
603
+ const built = Array.from(groups.entries()).map(([key, groupTokens]) => {
604
+ const chain = groupTokens[0].chain;
605
+ const folded = key === EVM_SEARCH_GROUP;
606
+ const sorted = options.role === "payer" ? sortTokensForChain(groupTokens, {
607
+ withBalances: options.hasOwner(chain.chainKind),
608
+ usdOf: options.usdOf,
609
+ loadingOf: options.loadingOf,
610
+ stableSymbols
611
+ }) : groupTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
612
+ return {
613
+ blockchain: folded ? EVM_SEARCH_GROUP : key,
614
+ chainName: folded ? EVM_SEARCH_GROUP_NAME : chain.chainName,
615
+ chainKind: chain.chainKind,
616
+ tokens: sorted,
617
+ usd: sorted.reduce((total, token) => {
618
+ const usd = options.usdOf(token);
619
+ return usd > 0 ? total + usd : total;
620
+ }, 0)
621
+ };
622
+ });
623
+ built.sort((left, right) => {
624
+ if (options.role === "payer" && right.usd !== left.usd) return right.usd - left.usd;
625
+ return left.chainName.localeCompare(right.chainName);
626
+ });
627
+ return built.map(({ blockchain, chainName, chainKind, tokens: groupTokens }) => ({
628
+ blockchain,
629
+ chainName,
630
+ chainKind,
631
+ tokens: groupTokens
632
+ }));
633
+ }
861
634
  function recentTokensInOrder(tokens, recentAssetIds) {
862
635
  const byId = new Map(tokens.map((token) => [token.assetId, token]));
863
636
  const result = [];
@@ -870,109 +643,86 @@ function recentTokensInOrder(tokens, recentAssetIds) {
870
643
 
871
644
  // src/token-select/chain-pane.tsx
872
645
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
873
- function lockReason(lockChainKind) {
874
- return `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain`;
646
+ function lockReason(lockChainKind, labels) {
647
+ return `Recipient address is on ${chainKindLabel(lockChainKind, labels)}; edit the recipient to change chain`;
875
648
  }
876
649
  function ChainPane(props) {
877
650
  const {
878
651
  chains,
879
- fundedBlockchains,
880
652
  showWalletStatus = true,
881
- onSelectFilter,
653
+ onSelectChain,
882
654
  lockChainKind = null,
883
655
  disabledBlockchains = null,
884
- disabledReason
656
+ disabledReason,
657
+ scrollRef
885
658
  } = props;
886
- const [evmExpanded, setEvmExpanded] = useState(true);
659
+ const { chainKindLabels } = usePayWidgets();
887
660
  function wrapDisabled(disabled, reason, content) {
888
661
  if (!disabled || !reason) return content;
889
662
  return /* @__PURE__ */ jsx3(Tooltip, { side: "right", triggerClassName: "min-w-0 flex-1", content: reason, children: content });
890
663
  }
891
- function chainRow(chain, nested) {
892
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
893
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
894
- const disabled = locked || chainDisabled;
895
- const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind) : void 0;
896
- return /* @__PURE__ */ jsxs2(
897
- "div",
898
- {
899
- className: cx(
900
- "flex min-h-14 w-full items-center gap-2 rounded-[12px] px-2 md:min-h-[66px] md:gap-3 md:px-3",
901
- disabled ? "opacity-40" : "hover:bg-[#F6F6F6]",
902
- nested ? "min-h-12 md:min-h-14" : ""
903
- ),
904
- children: [
905
- wrapDisabled(
906
- disabled,
907
- reason,
908
- /* @__PURE__ */ jsxs2(
664
+ return /* @__PURE__ */ jsx3("div", { className: "flex h-full min-h-0 flex-col", children: /* @__PURE__ */ jsx3("div", { ref: scrollRef, className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs2("div", { className: "px-2.5 pt-[17px] pb-3", children: [
665
+ /* @__PURE__ */ jsx3("p", { className: cx("px-3.5 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
666
+ /* @__PURE__ */ jsx3("div", { className: "mt-1", children: chains.map((chain) => {
667
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
668
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
669
+ const disabled = locked || chainDisabled;
670
+ const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind, chainKindLabels) : void 0;
671
+ return /* @__PURE__ */ jsxs2(
672
+ "div",
673
+ {
674
+ className: cx(
675
+ "flex h-[66px] w-full items-center gap-4 rounded-[12px] px-3.5",
676
+ disabled ? "opacity-40" : "hover:bg-[#f6f6f6]"
677
+ ),
678
+ children: [
679
+ wrapDisabled(
680
+ disabled,
681
+ reason,
682
+ /* @__PURE__ */ jsxs2(
683
+ "button",
684
+ {
685
+ type: "button",
686
+ disabled,
687
+ onClick: () => {
688
+ if (disabled) return;
689
+ onSelectChain(chain.blockchain);
690
+ },
691
+ className: cx(
692
+ "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
693
+ disabled ? "cursor-not-allowed" : "cursor-pointer"
694
+ ),
695
+ children: [
696
+ /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 shrink-0 object-cover" }),
697
+ /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black", fontSans), children: chain.chainName })
698
+ ]
699
+ }
700
+ )
701
+ ),
702
+ showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" }),
703
+ /* @__PURE__ */ jsx3(
909
704
  "button",
910
705
  {
911
706
  type: "button",
707
+ "aria-label": `Open ${chain.chainName}`,
912
708
  disabled,
913
709
  onClick: () => {
914
710
  if (disabled) return;
915
- onSelectFilter(chain.blockchain);
711
+ onSelectChain(chain.blockchain);
916
712
  },
917
713
  className: cx(
918
- "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
714
+ "inline-flex shrink-0 border-0 bg-transparent p-0 text-[#606060]",
919
715
  disabled ? "cursor-not-allowed" : "cursor-pointer"
920
716
  ),
921
- children: [
922
- /* @__PURE__ */ jsxs2("span", { className: "relative size-8 shrink-0", children: [
923
- /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
924
- 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
925
- ] }),
926
- /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: chain.chainName })
927
- ]
717
+ children: /* @__PURE__ */ jsx3(IconChevron, {})
928
718
  }
929
719
  )
930
- ),
931
- showWalletStatus && !nested ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : null
932
- ]
933
- },
934
- chain.blockchain
935
- );
936
- }
937
- return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", children: [
938
- /* @__PURE__ */ jsx3("p", { className: cx("shrink-0 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
939
- /* @__PURE__ */ jsx3("div", { className: "mt-3 flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto", children: groupEvmChains(chains).map((entry) => {
940
- if (entry.kind === "chain") return chainRow(entry.chain, false);
941
- return /* @__PURE__ */ jsxs2("div", { className: "rounded-[12px] px-2 md:px-3 py-2 hover:bg-[rgba(185,215,255,0.1)]", children: [
942
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
943
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-3", children: [
944
- /* @__PURE__ */ jsx3("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-[10px] bg-[#627EEA]", children: /* @__PURE__ */ jsx3("img", { src: evmTypeLogoUrl(), alt: "", className: "size-5 object-contain" }) }),
945
- /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: "EVM-based" })
946
- ] }),
947
- showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: "evm" }) : /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" })
948
- ] }),
949
- /* @__PURE__ */ jsxs2("div", { className: "mt-2 flex items-center gap-2", children: [
950
- evmExpanded ? /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" }) : /* @__PURE__ */ jsx3("span", { className: "flex min-w-0 flex-1 overflow-hidden", children: entry.chains.map((chain, index) => /* @__PURE__ */ jsx3(
951
- "img",
952
- {
953
- src: chain.logo,
954
- alt: "",
955
- style: { zIndex: entry.chains.length - index },
956
- className: index === 0 ? "relative size-4 shrink-0 rounded-[4px] border border-white object-cover" : "relative -ml-1.5 size-4 shrink-0 rounded-[4px] border border-white object-cover"
957
- },
958
- chain.blockchain
959
- )) }),
960
- /* @__PURE__ */ jsx3(
961
- "button",
962
- {
963
- type: "button",
964
- "aria-expanded": evmExpanded,
965
- "aria-label": evmExpanded ? "Collapse EVM-based" : "Expand EVM-based",
966
- onClick: () => setEvmExpanded((open) => !open),
967
- className: "ml-auto inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-1 text-[#606060]",
968
- children: /* @__PURE__ */ jsx3(IconArrowDown, { className: cx("size-3 transition-transform", evmExpanded ? "rotate-180" : "") })
969
- }
970
- )
971
- ] }),
972
- evmExpanded ? /* @__PURE__ */ jsx3("div", { className: "mt-2 border-t border-[#e3e3e3] pt-1", children: entry.chains.map((chain) => chainRow(chain, true)) }) : null
973
- ] }, "evm");
720
+ ]
721
+ },
722
+ chain.blockchain
723
+ );
974
724
  }) })
975
- ] });
725
+ ] }) }) });
976
726
  }
977
727
 
978
728
  // src/token-select/network-rail.tsx
@@ -982,99 +732,257 @@ import { Tooltip as Tooltip2 } from "@stableflow/pay-ui/tooltip";
982
732
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
983
733
  function chipClass(selected, disabled) {
984
734
  return cx(
985
- "relative flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-solid md:size-[50px]",
735
+ "relative flex size-[50px] shrink-0 items-center justify-center rounded-[12px] border border-solid",
986
736
  selected ? "border-[#06f] bg-[rgba(0,102,255,0.1)]" : "border-transparent bg-transparent",
987
737
  disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer hover:bg-black/5"
988
738
  );
989
739
  }
990
- function NetworkRail(props) {
991
- const {
992
- view,
993
- chainFilter,
994
- chips,
995
- fundedBlockchains,
996
- walletConnected,
997
- showAllLabel = false,
998
- lockChainKind = null,
999
- disabledBlockchains = null,
1000
- disabledReason,
1001
- onSelectFilter,
1002
- onOpenNetworks,
1003
- className
1004
- } = props;
1005
- function wrapDisabled(disabled, reason, content) {
1006
- if (!disabled || !reason) return content;
1007
- return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
1008
- }
1009
- 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: [
1010
- showAllLabel ? /* @__PURE__ */ jsx4(
1011
- "button",
1012
- {
1013
- type: "button",
1014
- "aria-label": "All",
1015
- onClick: () => onSelectFilter(ALL_CHAIN_FILTER),
1016
- className: chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false),
1017
- children: /* @__PURE__ */ jsx4("span", { className: cx("text-xs font-medium text-black md:text-sm", fontSans), children: "All" })
1018
- }
1019
- ) : /* @__PURE__ */ jsx4(
1020
- "button",
1021
- {
1022
- type: "button",
1023
- "aria-label": "Wallet",
1024
- disabled: !walletConnected,
1025
- onClick: () => {
1026
- if (!walletConnected) return;
1027
- onSelectFilter(ALL_CHAIN_FILTER);
1028
- },
1029
- 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]",
1030
- 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]" }) })
1031
- }
1032
- ),
1033
- /* @__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) => {
1034
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
1035
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
1036
- const disabled = locked || chainDisabled;
1037
- const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain` : void 0;
1038
- const selected = view === "token" && chainFilter === chain.blockchain;
1039
- return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
1040
- disabled,
1041
- reason,
1042
- /* @__PURE__ */ jsxs3(
1043
- "button",
1044
- {
1045
- type: "button",
1046
- "aria-label": chain.chainName,
1047
- disabled,
1048
- onClick: () => {
1049
- if (disabled) return;
1050
- onSelectFilter(chain.blockchain);
1051
- },
1052
- className: chipClass(selected, disabled),
1053
- children: [
1054
- /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-6 object-cover md:size-8" }),
1055
- 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
1056
- ]
1057
- }
1058
- )
1059
- ) }, chain.blockchain);
1060
- }) }),
1061
- /* @__PURE__ */ jsx4("div", { className: "mt-2 flex w-full shrink-0 justify-center border-t border-[#e3e3e3] pt-2", children: /* @__PURE__ */ jsx4(
1062
- "button",
1063
- {
1064
- type: "button",
1065
- "aria-label": "All networks",
1066
- onClick: onOpenNetworks,
1067
- className: chipClass(view === "network", false),
1068
- children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-4 text-black" })
1069
- }
1070
- ) })
1071
- ] });
740
+ function NetworkRail(props) {
741
+ const {
742
+ role,
743
+ walletConnected,
744
+ selection,
745
+ chains,
746
+ fundedBlockchains,
747
+ lockChainKind = null,
748
+ disabledBlockchains = null,
749
+ disabledReason,
750
+ onSelectWallet,
751
+ onSelectAll,
752
+ onSelectNetworks,
753
+ onSelectChain,
754
+ className
755
+ } = props;
756
+ const { chainKindLabels } = usePayWidgets();
757
+ const payer = role === "payer";
758
+ const networksSelected = selection.kind === "networks";
759
+ const allSelected = selection.kind === "all";
760
+ const walletSelected = selection.kind === "wallet";
761
+ function wrapDisabled(disabled, reason, content) {
762
+ if (!disabled || !reason) return content;
763
+ return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
764
+ }
765
+ function chainButton(chain) {
766
+ const locked = isChainKindLocked(chain.chainKind, lockChainKind);
767
+ const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
768
+ const disabled = locked || chainDisabled;
769
+ const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind, chainKindLabels)}; edit the recipient to change chain` : void 0;
770
+ const selected = selection.kind === "chain" && selection.blockchain === chain.blockchain;
771
+ return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
772
+ disabled,
773
+ reason,
774
+ /* @__PURE__ */ jsxs3(
775
+ "button",
776
+ {
777
+ type: "button",
778
+ "aria-label": chain.chainName,
779
+ disabled,
780
+ onClick: () => {
781
+ if (disabled) return;
782
+ onSelectChain(chain.blockchain);
783
+ },
784
+ className: chipClass(selected, disabled),
785
+ children: [
786
+ /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
787
+ fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2.5 rounded-full border border-white bg-[#06f]" }) : null
788
+ ]
789
+ }
790
+ )
791
+ ) }, chain.blockchain);
792
+ }
793
+ return /* @__PURE__ */ jsxs3("div", { className: cx("box-border flex h-full w-[76px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pt-3", className), children: [
794
+ payer && walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Wallet", onClick: onSelectWallet, className: chipClass(walletSelected, false), children: /* @__PURE__ */ jsx4(IconWallet, { className: walletSelected ? "size-5 text-[#06f]" : "size-5 text-black" }) }) : null,
795
+ payer && !walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }) : null,
796
+ !payer ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All", onClick: onSelectAll, className: chipClass(allSelected, false), children: /* @__PURE__ */ jsx4("span", { className: cx("text-sm font-medium text-black", fontSans), children: "All" }) }) : null,
797
+ payer && walletConnected ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
798
+ chains.map((chain) => chainButton(chain)),
799
+ /* @__PURE__ */ jsx4("div", { className: "h-px w-8 shrink-0 bg-[#e3e3e3]" }),
800
+ /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }),
801
+ /* @__PURE__ */ jsx4("div", { className: "pb-3" })
802
+ ] }) : null,
803
+ !payer ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
804
+ chains.map((chain) => chainButton(chain)),
805
+ /* @__PURE__ */ jsx4("div", { className: "pb-3" })
806
+ ] }) : null
807
+ ] });
808
+ }
809
+
810
+ // src/token-select/pane-transition.tsx
811
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
812
+ import { jsx as jsx5 } from "react/jsx-runtime";
813
+ var EASE = [0.32, 0.72, 0, 1];
814
+ var variants = {
815
+ enter: (direction) => ({ x: direction > 0 ? 24 : -24, opacity: 0 }),
816
+ center: { x: 0, opacity: 1 },
817
+ exit: (direction) => ({ x: direction > 0 ? -24 : 24, opacity: 0 })
818
+ };
819
+ function PaneTransition(props) {
820
+ const reduce = useReducedMotion();
821
+ if (reduce) {
822
+ return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: props.children });
823
+ }
824
+ return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx5(AnimatePresence, { initial: false, custom: props.direction, children: /* @__PURE__ */ jsx5(
825
+ motion.div,
826
+ {
827
+ custom: props.direction,
828
+ variants,
829
+ initial: "enter",
830
+ animate: "center",
831
+ exit: "exit",
832
+ transition: { duration: 0.22, ease: EASE },
833
+ className: "absolute inset-0 flex min-h-0 flex-col",
834
+ children: props.children
835
+ },
836
+ props.paneKey
837
+ ) }) });
838
+ }
839
+
840
+ // src/token-select/prefs.ts
841
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
842
+ var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
843
+ function rememberRecentAsset(recent, assetId) {
844
+ const id = assetId.trim();
845
+ if (!id) return recent;
846
+ return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
847
+ }
848
+ function rememberRecentBlockchain(recent, blockchain) {
849
+ const code = blockchain.trim();
850
+ if (!code) return recent;
851
+ return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
852
+ }
853
+ function migrateTokenSelectPrefs(persisted, version) {
854
+ const state = persisted ?? {};
855
+ const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
856
+ 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] : [];
857
+ return { recentAssetIds, recentBlockchains };
858
+ }
859
+ function readStorage() {
860
+ if (typeof localStorage === "undefined") return EMPTY_PREFS;
861
+ try {
862
+ const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
863
+ if (!raw) return EMPTY_PREFS;
864
+ const parsed = JSON.parse(raw);
865
+ const version = typeof parsed.version === "number" ? parsed.version : 0;
866
+ const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
867
+ return migrateTokenSelectPrefs(payload, version);
868
+ } catch {
869
+ return EMPTY_PREFS;
870
+ }
871
+ }
872
+ function writeStorage(state) {
873
+ if (typeof localStorage === "undefined") return;
874
+ localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
875
+ state,
876
+ version: TOKEN_SELECT_PREFS_VERSION
877
+ }));
878
+ }
879
+ var prefs = readStorage();
880
+ var listeners2 = /* @__PURE__ */ new Set();
881
+ function emit2(next) {
882
+ prefs = next;
883
+ writeStorage(next);
884
+ for (const listener of listeners2) listener();
885
+ }
886
+ function subscribe2(listener) {
887
+ listeners2.add(listener);
888
+ return () => listeners2.delete(listener);
889
+ }
890
+ function getTokenSelectPrefs() {
891
+ return prefs;
892
+ }
893
+ function setLastToken(assetId, blockchain) {
894
+ emit2({
895
+ recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
896
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
897
+ });
898
+ }
899
+ function setLastBlockchain(blockchain) {
900
+ emit2({
901
+ ...prefs,
902
+ recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
903
+ });
904
+ }
905
+ function useTokenSelectPrefs() {
906
+ return useSyncExternalStore2(subscribe2, getTokenSelectPrefs, getTokenSelectPrefs);
907
+ }
908
+
909
+ // src/token-select/token-pane.tsx
910
+ import { IconChevron as IconChevron2 } from "@stableflow/pay-ui/icons/chevron";
911
+ import { Skeleton } from "@stableflow/pay-ui/skeleton";
912
+
913
+ // src/token-select/lazy-image.tsx
914
+ import { useEffect as useEffect2, useRef, useState } from "react";
915
+ import { useInView } from "motion/react";
916
+ import { jsx as jsx6 } from "react/jsx-runtime";
917
+ function LazyImage(props) {
918
+ const { src, fallbackSrc, alt = "", className } = props;
919
+ const ref = useRef(null);
920
+ const inView = useInView(ref, { once: true });
921
+ const [failedSrc, setFailedSrc] = useState(null);
922
+ useEffect2(() => {
923
+ setFailedSrc(null);
924
+ }, [src]);
925
+ const requested = src.trim();
926
+ const useFallback = requested.length === 0 || failedSrc === requested;
927
+ const shown = useFallback ? fallbackSrc : requested;
928
+ return /* @__PURE__ */ jsx6("span", { ref, className: "block size-full", children: inView ? /* @__PURE__ */ jsx6(
929
+ "img",
930
+ {
931
+ src: shown,
932
+ alt,
933
+ className,
934
+ onError: () => {
935
+ if (!useFallback) setFailedSrc(requested);
936
+ }
937
+ }
938
+ ) : null });
939
+ }
940
+
941
+ // src/token-select/logos.ts
942
+ var LOGO_HOST = "https://assets.dapdap.net";
943
+ var CHAIN_LOGO_ALIAS = {
944
+ eth: "ethereum",
945
+ ethereum: "ethereum",
946
+ base: "base",
947
+ arb: "arbitrum",
948
+ arbitrum: "arbitrum",
949
+ op: "optimism",
950
+ optimism: "optimism",
951
+ pol: "polygon",
952
+ polygon: "polygon",
953
+ bsc: "bsc",
954
+ avax: "avalanche",
955
+ avalanche: "avalanche",
956
+ gnosis: "gnosis",
957
+ scroll: "scroll",
958
+ xlayer: "xlayer",
959
+ bera: "berachain",
960
+ berachain: "berachain",
961
+ near: "near",
962
+ sol: "solana",
963
+ solana: "solana",
964
+ tron: "tron",
965
+ zec: "zcash",
966
+ zcash: "zcash"
967
+ };
968
+ var DEFAULT_TOKEN_LOGO = `${LOGO_HOST}/tokens/default_icon.png`;
969
+ function logoPath(path) {
970
+ const normalized = path.startsWith("/") ? path : `/${path}`;
971
+ return `${LOGO_HOST}${normalized}`;
972
+ }
973
+ function chainLogoUrl(blockchainOrNetwork) {
974
+ const key = String(blockchainOrNetwork || "").toLowerCase();
975
+ const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
976
+ return logoPath(`/stableflow/networks/${file}.png`);
977
+ }
978
+ function tokenLogoUrl(symbol) {
979
+ const upper = String(symbol || "").toUpperCase();
980
+ const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
981
+ return logoPath(`/stableflow/tokens/${file}.png`);
1072
982
  }
1073
983
 
1074
984
  // src/token-select/token-pane.tsx
1075
- import { SearchInput } from "@stableflow/pay-ui/search-input";
1076
- import { Skeleton } from "@stableflow/pay-ui/skeleton";
1077
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
985
+ import { Fragment, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1078
986
  function TokenPane(props) {
1079
987
  const {
1080
988
  sections,
@@ -1082,65 +990,67 @@ function TokenPane(props) {
1082
990
  loading,
1083
991
  showBalances = false,
1084
992
  showWalletStatus = true,
993
+ header = null,
1085
994
  getBalance,
1086
995
  isBalanceLoading,
1087
996
  onSelectToken,
1088
997
  isTokenDisabled,
1089
- search = "",
1090
- onSearchChange,
1091
- showSearch = false,
1092
- searchClassName,
1093
998
  rowClassName
1094
999
  } = props;
1095
1000
  const visibleSections = sections.filter((section) => section.title || section.tokens.length > 0);
1096
1001
  const hasTokens = sections.some((section) => section.tokens.length > 0);
1097
- return /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1098
- showSearch && onSearchChange ? /* @__PURE__ */ jsx5(
1099
- SearchInput,
1002
+ return /* @__PURE__ */ jsx7("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx7("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs4("div", { className: "px-2.5 pt-[17px] pb-3", children: [
1003
+ header ? /* @__PURE__ */ jsx7(ChainListHeader, { header, showWalletStatus }) : null,
1004
+ visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
1005
+ section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
1006
+ /* @__PURE__ */ jsx7("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
1007
+ showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx7(ChainWalletStatus, { kind: section.walletKind }) : null
1008
+ ] }) : null,
1009
+ section.tokens.map((token) => /* @__PURE__ */ jsx7(
1010
+ TokenRow,
1011
+ {
1012
+ token,
1013
+ selected: token.assetId === selectedAssetId,
1014
+ recent: token.assetId === section.badgeAssetId,
1015
+ showBalances,
1016
+ formatted: showBalances ? getBalance(token) : null,
1017
+ loadingBalance: showBalances && isBalanceLoading(token),
1018
+ disabled: Boolean(isTokenDisabled?.(token)),
1019
+ onSelect: () => onSelectToken(token),
1020
+ className: rowClassName
1021
+ },
1022
+ `${section.id}-${token.assetId}`
1023
+ ))
1024
+ ] }, section.id)),
1025
+ loading && !hasTokens ? /* @__PURE__ */ jsx7(TokenListSkeleton, {}) : null,
1026
+ !loading && !hasTokens ? /* @__PURE__ */ jsx7("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1027
+ ] }) }) });
1028
+ }
1029
+ function ChainListHeader(props) {
1030
+ const { header, showWalletStatus } = props;
1031
+ const walletKind = header.walletKind;
1032
+ return /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-3 px-3.5", children: [
1033
+ header.kind === "back" ? /* @__PURE__ */ jsxs4(
1034
+ "button",
1100
1035
  {
1101
- value: search,
1102
- onChange: onSearchChange,
1103
- placeholder: "search or paste address",
1104
- className: "mb-3 shrink-0",
1105
- inputClassName: cx(
1106
- "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30",
1107
- searchClassName
1108
- )
1036
+ type: "button",
1037
+ onClick: header.onBack,
1038
+ className: cx("flex min-w-0 cursor-pointer items-center gap-3 border-0 bg-transparent p-0 text-sm font-medium text-[#aaa]", fontSans),
1039
+ children: [
1040
+ /* @__PURE__ */ jsx7(IconChevron2, { className: "rotate-180 text-[#aaa]" }),
1041
+ /* @__PURE__ */ jsx7("span", { className: "truncate", children: header.title })
1042
+ ]
1109
1043
  }
1110
- ) : null,
1111
- /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: [
1112
- visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
1113
- section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-2 md:px-3.5", children: [
1114
- /* @__PURE__ */ jsx5("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
1115
- showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx5(ChainWalletStatus, { kind: section.walletKind }) : null
1116
- ] }) : null,
1117
- section.tokens.map((token) => /* @__PURE__ */ jsx5(
1118
- TokenRow,
1119
- {
1120
- token,
1121
- selected: token.assetId === selectedAssetId,
1122
- recent: token.assetId === section.badgeAssetId,
1123
- showBalances,
1124
- formatted: showBalances ? getBalance(token) : null,
1125
- loadingBalance: showBalances && isBalanceLoading(token),
1126
- disabled: Boolean(isTokenDisabled?.(token)),
1127
- onSelect: () => onSelectToken(token),
1128
- className: rowClassName
1129
- },
1130
- `${section.id}-${token.assetId}`
1131
- ))
1132
- ] }, section.id)),
1133
- loading && !hasTokens ? /* @__PURE__ */ jsx5(TokenListSkeleton, {}) : null,
1134
- !loading && !hasTokens ? /* @__PURE__ */ jsx5("p", { className: cx("px-1 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1135
- ] })
1044
+ ) : /* @__PURE__ */ jsx7("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: header.title }),
1045
+ showWalletStatus && walletKind ? /* @__PURE__ */ jsx7(ChainWalletStatus, { kind: walletKind }) : null
1136
1046
  ] });
1137
1047
  }
1138
1048
  function TokenListSkeleton() {
1139
- 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: [
1140
- /* @__PURE__ */ jsx5(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
1049
+ return /* @__PURE__ */ jsx7("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex h-[66px] items-center gap-2.5 px-3.5", children: [
1050
+ /* @__PURE__ */ jsx7(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
1141
1051
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
1142
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-24" }),
1143
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-3 w-16" })
1052
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-3.5 w-24" }),
1053
+ /* @__PURE__ */ jsx7(Skeleton, { className: "h-3 w-16" })
1144
1054
  ] })
1145
1055
  ] }, index)) });
1146
1056
  }
@@ -1154,16 +1064,23 @@ function TokenRow(props) {
1154
1064
  disabled,
1155
1065
  onClick: onSelect,
1156
1066
  className: cx(
1157
- "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",
1158
- selected && "bg-[#F6F6F6]",
1067
+ "flex h-[66px] w-full cursor-pointer items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-3.5 text-left hover:bg-[#f6f6f6]",
1068
+ selected && "bg-[#f6f6f6]",
1159
1069
  disabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
1160
1070
  className
1161
1071
  ),
1162
1072
  children: [
1163
1073
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1164
1074
  /* @__PURE__ */ jsxs4("span", { className: "relative size-8 shrink-0", children: [
1165
- /* @__PURE__ */ jsx5("img", { src: token.logo, alt: "", className: "size-8 rounded-full object-cover" }),
1166
- /* @__PURE__ */ jsx5(
1075
+ /* @__PURE__ */ jsx7(
1076
+ LazyImage,
1077
+ {
1078
+ src: token.logo,
1079
+ fallbackSrc: DEFAULT_TOKEN_LOGO,
1080
+ className: "size-8 rounded-[12px] object-cover"
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx7(
1167
1084
  "img",
1168
1085
  {
1169
1086
  src: token.chain.logo,
@@ -1174,37 +1091,78 @@ function TokenRow(props) {
1174
1091
  ] }),
1175
1092
  /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1176
1093
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1177
- /* @__PURE__ */ jsx5("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1178
- 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
1094
+ /* @__PURE__ */ jsx7("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1095
+ recent ? /* @__PURE__ */ jsx7("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
1179
1096
  ] }),
1180
- /* @__PURE__ */ jsx5("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1097
+ /* @__PURE__ */ jsx7("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1181
1098
  ] })
1182
1099
  ] }),
1183
- showBalances ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx5(
1100
+ showBalances ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx7(
1184
1101
  "span",
1185
1102
  {
1186
1103
  className: "inline-block size-3.5 animate-spin rounded-full border-2 border-[#606060] border-r-transparent",
1187
1104
  "aria-label": "Loading balance"
1188
1105
  }
1189
1106
  ) : formatted != null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1190
- /* @__PURE__ */ jsx5("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1191
- usd >= 0 ? /* @__PURE__ */ jsx5("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1192
- ] }) : /* @__PURE__ */ jsx5("span", { className: cx("text-sm text-[#606060]", fontSans), children: "\u2014" }) }) : null
1107
+ /* @__PURE__ */ jsx7("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1108
+ usd >= 0 ? /* @__PURE__ */ jsx7("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1109
+ ] }) : /* @__PURE__ */ jsx7("span", { className: cx("text-sm font-medium text-black", fontSans), children: "-" }) }) : null
1193
1110
  ]
1194
1111
  }
1195
1112
  );
1196
1113
  }
1197
1114
 
1115
+ // src/token-select/search-results.tsx
1116
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1117
+ function SearchResults(props) {
1118
+ const {
1119
+ groups,
1120
+ selectedAssetId,
1121
+ loading,
1122
+ showBalances = false,
1123
+ showWalletStatus = true,
1124
+ getBalance,
1125
+ isBalanceLoading,
1126
+ onSelectToken,
1127
+ isTokenDisabled,
1128
+ rowClassName
1129
+ } = props;
1130
+ const hasTokens = groups.some((group) => group.tokens.length > 0);
1131
+ return /* @__PURE__ */ jsx8("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx8("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs5("div", { className: "px-2.5 pt-[17px] pb-3", children: [
1132
+ groups.map((group) => /* @__PURE__ */ jsxs5("section", { className: "mb-2", children: [
1133
+ /* @__PURE__ */ jsxs5("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
1134
+ /* @__PURE__ */ jsx8("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: group.chainName }),
1135
+ showWalletStatus ? /* @__PURE__ */ jsx8(ChainWalletStatus, { kind: group.chainKind }) : null
1136
+ ] }),
1137
+ group.tokens.map((token) => /* @__PURE__ */ jsx8(
1138
+ TokenRow,
1139
+ {
1140
+ token,
1141
+ selected: token.assetId === selectedAssetId,
1142
+ recent: false,
1143
+ showBalances,
1144
+ formatted: showBalances ? getBalance(token) : null,
1145
+ loadingBalance: showBalances && isBalanceLoading(token),
1146
+ disabled: Boolean(isTokenDisabled?.(token)),
1147
+ onSelect: () => onSelectToken(token),
1148
+ className: rowClassName
1149
+ },
1150
+ token.assetId
1151
+ ))
1152
+ ] }, group.blockchain)),
1153
+ !loading && !hasTokens ? /* @__PURE__ */ jsx8("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1154
+ ] }) }) });
1155
+ }
1156
+
1157
+ // src/token-select/types.ts
1158
+ var KNOWN_CHAIN_KINDS = ["evm", "near", "solana", "tron", "zec"];
1159
+ function ownerKey2(owners) {
1160
+ return Object.keys(owners).sort().map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
1161
+ }
1162
+
1198
1163
  // src/token-select/TokenSelectDialog.tsx
1199
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1164
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1200
1165
  var SEARCH_INPUT_CLASS = "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
1201
- function chainsFromTokens(tokens) {
1202
- const byCode = /* @__PURE__ */ new Map();
1203
- for (const token of tokens) {
1204
- if (!byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1205
- }
1206
- return Array.from(byCode.values());
1207
- }
1208
1166
  function ownerForToken(owners, token) {
1209
1167
  return owners[token.chain.chainKind] ?? null;
1210
1168
  }
@@ -1215,6 +1173,15 @@ function isNativeToken(token) {
1215
1173
  const addr = String(token.contractAddress || "").trim().toLowerCase();
1216
1174
  return !addr || addr === "native" || addr === "0x0000000000000000000000000000000000000000";
1217
1175
  }
1176
+ function railSelection(view) {
1177
+ if (view.kind === "wallet") return { kind: "wallet" };
1178
+ if (view.kind === "all") return { kind: "all" };
1179
+ if (view.kind === "rail-chain") return { kind: "chain", blockchain: view.blockchain };
1180
+ return { kind: "networks" };
1181
+ }
1182
+ function chainByCode(chains, blockchain) {
1183
+ return chains.find((chain) => chain.blockchain === blockchain);
1184
+ }
1218
1185
  function TokenSelectDialog(props) {
1219
1186
  const {
1220
1187
  open,
@@ -1231,6 +1198,7 @@ function TokenSelectDialog(props) {
1231
1198
  disabledReason,
1232
1199
  className,
1233
1200
  titleClassName,
1201
+ headerClassName,
1234
1202
  closeClassName,
1235
1203
  contentClassName,
1236
1204
  searchClassName,
@@ -1239,11 +1207,19 @@ function TokenSelectDialog(props) {
1239
1207
  } = props;
1240
1208
  const payer = role === "payer";
1241
1209
  const showBalances = payer;
1242
- const { wallet, popularTokens: popularRefs } = usePayWidgets();
1243
- const { tokens, status } = usePayConfigSnapshot();
1210
+ const {
1211
+ wallet,
1212
+ popularTokens: popularRefs,
1213
+ tokens,
1214
+ loading,
1215
+ stableSymbols,
1216
+ balanceMode,
1217
+ getBalance: syncGetBalance,
1218
+ isBalanceLoading: syncIsBalanceLoading
1219
+ } = usePayWidgets();
1244
1220
  const prefs2 = useTokenSelectPrefs();
1245
- const balanceEntries = useSyncExternalStore4(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
1246
- const connectedOwners = useMemo2(() => {
1221
+ const balanceEntries = useSyncExternalStore3(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
1222
+ const connectedOwners = useMemo(() => {
1247
1223
  const owners2 = {};
1248
1224
  for (const [kind, account] of Object.entries(wallet.accounts)) {
1249
1225
  const address = account?.address?.trim();
@@ -1251,53 +1227,57 @@ function TokenSelectDialog(props) {
1251
1227
  }
1252
1228
  return owners2;
1253
1229
  }, [wallet.accounts]);
1254
- const owners = showBalances ? { ...connectedOwners, ...balanceOwners } : {};
1230
+ const owners = useMemo(
1231
+ () => showBalances ? { ...connectedOwners, ...balanceOwners } : {},
1232
+ [showBalances, connectedOwners, balanceOwners]
1233
+ );
1255
1234
  const walletConnected = hasAnyOwner(connectedOwners);
1256
- const walletWasConnected = useRef(null);
1257
1235
  const [search, setSearch] = useState2("");
1258
- const [chainFilter, setChainFilter] = useState2(ALL_CHAIN_FILTER);
1259
- const [view, setView] = useState2("token");
1260
- const loading = status === "loading" || status === "idle";
1236
+ const [view, setView] = useState2(payer ? { kind: "networks" } : { kind: "all" });
1237
+ const [slideDirection, setSlideDirection] = useState2(1);
1238
+ const networksScrollRef = useRef2(null);
1239
+ const savedNetworksScroll = useRef2(0);
1240
+ const walletConnectedRef = useRef2(walletConnected);
1241
+ const payerRef = useRef2(payer);
1242
+ const lockRef = useRef2(lockChainKind);
1243
+ const tokensRef = useRef2(tokens);
1244
+ walletConnectedRef.current = walletConnected;
1245
+ payerRef.current = payer;
1246
+ lockRef.current = lockChainKind;
1247
+ tokensRef.current = tokens;
1261
1248
  useEffect3(() => {
1262
1249
  if (!open) return;
1263
1250
  setSearch("");
1264
- if (lockChainKind) {
1265
- setView("token");
1251
+ const lock = lockRef.current;
1252
+ if (lock) {
1253
+ const filter = initialChainFilter(
1254
+ lock,
1255
+ getTokenSelectPrefs().recentBlockchains,
1256
+ chainsFromTokens(tokensRef.current)
1257
+ );
1258
+ if (filter !== ALL_CHAIN_FILTER) {
1259
+ setView({ kind: "rail-chain", blockchain: filter });
1260
+ return;
1261
+ }
1266
1262
  return;
1267
1263
  }
1268
- if (!payer) {
1269
- setView("token");
1270
- setChainFilter(ALL_CHAIN_FILTER);
1264
+ if (!payerRef.current) {
1265
+ setView({ kind: "all" });
1266
+ return;
1271
1267
  }
1272
- }, [open, lockChainKind, payer]);
1268
+ setView(walletConnectedRef.current ? { kind: "wallet" } : { kind: "networks" });
1269
+ }, [open]);
1273
1270
  useEffect3(() => {
1274
- if (!open || !lockChainKind || tokens.length === 0) return;
1275
- setChainFilter(initialChainFilter(
1276
- lockChainKind,
1277
- getTokenSelectPrefs().recentBlockchains,
1278
- chainsFromTokens(tokens)
1279
- ));
1271
+ if (!open || !lockChainKind) return;
1272
+ const filter = initialChainFilter(lockChainKind, getTokenSelectPrefs().recentBlockchains, chainsFromTokens(tokens));
1273
+ if (filter === ALL_CHAIN_FILTER) return;
1274
+ setView((current) => current.kind === "rail-chain" ? current : { kind: "rail-chain", blockchain: filter });
1280
1275
  }, [open, lockChainKind, tokens]);
1281
- useEffect3(() => {
1282
- if (!open) {
1283
- walletWasConnected.current = null;
1284
- return;
1285
- }
1286
- if (lockChainKind || !payer) {
1287
- walletWasConnected.current = walletConnected;
1288
- return;
1289
- }
1290
- const previous = walletWasConnected.current;
1291
- walletWasConnected.current = walletConnected;
1292
- if (previous !== null && previous === walletConnected) return;
1293
- setView(walletConnected ? "token" : "network");
1294
- setChainFilter(ALL_CHAIN_FILTER);
1295
- }, [open, lockChainKind, payer, walletConnected]);
1296
- const allowed = useMemo2(() => {
1276
+ const allowed = useMemo(() => {
1297
1277
  if (!allowedBlockchains || allowedBlockchains.length === 0) return null;
1298
1278
  return new Set(allowedBlockchains.map((code) => code.toLowerCase()));
1299
1279
  }, [allowedBlockchains]);
1300
- const scopedTokens = useMemo2(() => {
1280
+ const scopedTokens = useMemo(() => {
1301
1281
  return tokens.filter((token) => {
1302
1282
  if (allowed && !allowed.has(token.blockchain.toLowerCase())) return false;
1303
1283
  if (excludeNative && isNativeToken(token)) return false;
@@ -1306,22 +1286,25 @@ function TokenSelectDialog(props) {
1306
1286
  return true;
1307
1287
  });
1308
1288
  }, [tokens, allowed, excludeNative, payer]);
1309
- const ownerKey2 = ["evm", "near", "solana", "tron", "zec"].map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
1310
- const tokenKey = useMemo2(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1311
- const ownersRef = useRef(owners);
1312
- const tokensRef = useRef(scopedTokens);
1289
+ const ownersKey = ownerKey2(owners);
1290
+ const tokenKey = useMemo(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1291
+ const ownersRef = useRef2(owners);
1292
+ const scopedRef = useRef2(scopedTokens);
1313
1293
  ownersRef.current = owners;
1314
- tokensRef.current = scopedTokens;
1294
+ scopedRef.current = scopedTokens;
1315
1295
  useEffect3(() => {
1316
- if (!showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1317
- void ensurePayTokenBalances(ownersRef.current, tokensRef.current);
1296
+ if (balanceMode !== "async" || !showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1297
+ void ensurePayTokenBalances(ownersRef.current, scopedRef.current);
1318
1298
  const id = window.setInterval(() => {
1319
- void ensurePayTokenBalances(ownersRef.current, tokensRef.current, { force: true });
1299
+ void ensurePayTokenBalances(ownersRef.current, scopedRef.current, { force: true });
1320
1300
  }, TOKEN_BALANCE_POLL_MS);
1321
1301
  return () => window.clearInterval(id);
1322
- }, [showBalances, open, ownerKey2, tokenKey]);
1302
+ }, [balanceMode, showBalances, open, ownersKey, tokenKey]);
1323
1303
  function balanceOf(token) {
1324
- return getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted;
1304
+ const owner = ownerForToken(owners, token);
1305
+ if (!showBalances || !owner) return void 0;
1306
+ if (balanceMode === "sync" && syncGetBalance) return syncGetBalance(token, owner) ?? void 0;
1307
+ return getPayTokenBalance(owner, token.assetId)?.formatted;
1325
1308
  }
1326
1309
  function usdOf(token) {
1327
1310
  if (!showBalances) return -1;
@@ -1331,52 +1314,50 @@ function TokenSelectDialog(props) {
1331
1314
  if (!showBalances) return false;
1332
1315
  const owner = ownerForToken(owners, token);
1333
1316
  if (!owner) return false;
1317
+ const formatted = balanceOf(token);
1318
+ if (formatted != null && formatted !== "") return false;
1319
+ if (balanceMode === "sync") return syncIsBalanceLoading?.(token, owner) ?? false;
1334
1320
  const entry = getPayTokenBalance(owner, token.assetId);
1335
- return entry?.formatted == null && (!entry || entry.status === "loading");
1321
+ return !entry || entry.status === "loading";
1336
1322
  }
1337
- const availableChains = useMemo2(() => {
1338
- const codes = new Set(scopedTokens.map((token) => token.blockchain));
1339
- const byCode = /* @__PURE__ */ new Map();
1340
- for (const token of scopedTokens) {
1341
- if (codes.has(token.blockchain) && !byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
1342
- }
1343
- return Array.from(byCode.values());
1344
- }, [scopedTokens]);
1345
- const sortedChains = useMemo2(
1346
- () => sortChainsForSidebar(availableChains, scopedTokens, {
1347
- showBalances,
1348
- walletConnected,
1349
- getBalanceUsd: (token) => tokenBalanceUsd(token, getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)
1350
- }),
1351
- [availableChains, scopedTokens, showBalances, walletConnected, owners, balanceEntries]
1352
- );
1353
- const fundedBlockchains = useMemo2(() => {
1323
+ const availableChains = useMemo(() => chainsFromTokens(scopedTokens), [scopedTokens]);
1324
+ const fundedBlockchains = useMemo(() => {
1354
1325
  const funded = /* @__PURE__ */ new Set();
1355
1326
  if (!showBalances) return funded;
1356
1327
  for (const chain of availableChains) {
1357
- if (chainHasBalance(chain.blockchain, scopedTokens, (token) => getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)) {
1328
+ if (chainHasBalance(chain.blockchain, scopedTokens, (token) => balanceOf(token))) {
1358
1329
  funded.add(chain.blockchain);
1359
1330
  }
1360
1331
  }
1361
1332
  return funded;
1362
- }, [availableChains, owners, scopedTokens, showBalances, balanceEntries]);
1363
- const popularTokens = useMemo2(
1333
+ }, [availableChains, owners, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1334
+ const railChains = useMemo(() => {
1335
+ if (!payer) return sortChainsAlpha(availableChains);
1336
+ const connected = payerRailChains(availableChains, owners, fundedBlockchains);
1337
+ return sortChainsForSidebar(connected, scopedTokens, {
1338
+ showBalances,
1339
+ getBalanceUsd: (token) => tokenBalanceUsd(token, balanceOf(token))
1340
+ });
1341
+ }, [payer, availableChains, owners, fundedBlockchains, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance]);
1342
+ const networkChains = useMemo(() => sortChainsAlpha(availableChains), [availableChains]);
1343
+ useEffect3(() => {
1344
+ if (!open || !payer || lockChainKind) return;
1345
+ setView((current) => {
1346
+ if (current.kind === "wallet" && !walletConnected) return { kind: "networks" };
1347
+ if (current.kind === "rail-chain" && !railChains.some((chain) => chain.blockchain === current.blockchain)) {
1348
+ return walletConnected ? { kind: "wallet" } : { kind: "networks" };
1349
+ }
1350
+ return current;
1351
+ });
1352
+ }, [open, payer, lockChainKind, walletConnected, railChains]);
1353
+ const popularTokens = useMemo(
1364
1354
  () => popularTokensForWallets(scopedTokens, popularRefs, (kind) => Boolean(connectedOwners[kind])),
1365
1355
  [scopedTokens, popularRefs, connectedOwners]
1366
1356
  );
1367
1357
  const newestAssetId = showBalances ? prefs2.recentAssetIds[0] ?? null : null;
1368
- const searchingAll = chainFilter === ALL_CHAIN_FILTER && search.trim().length > 0;
1369
- const walletKind = chainFilter === ALL_CHAIN_FILTER ? null : availableChains.find((chain) => chain.blockchain === chainFilter)?.chainKind ?? null;
1370
- const sections = useMemo2(() => {
1371
- if (showBalances && chainFilter === ALL_CHAIN_FILTER && searchingAll) {
1372
- const matched2 = scopedTokens.filter((token) => tokenMatchesSearch(token, search));
1373
- return [{
1374
- id: "search",
1375
- tokens: sortTokensByBalance(matched2, usdOf, loadingOf),
1376
- badgeAssetId: newestAssetId
1377
- }];
1378
- }
1379
- if (showBalances && chainFilter === ALL_CHAIN_FILTER) {
1358
+ const searching = search.trim().length > 0;
1359
+ const sections = useMemo(() => {
1360
+ if (view.kind === "wallet") {
1380
1361
  const next = [];
1381
1362
  const recent = recentTokensInOrder(scopedTokens, prefs2.recentAssetIds);
1382
1363
  if (recent.length > 0) {
@@ -1396,42 +1377,87 @@ function TokenSelectDialog(props) {
1396
1377
  }
1397
1378
  return next;
1398
1379
  }
1399
- const matched = scopedTokens.filter((token) => matchesChainFilter(token, chainFilter) && tokenMatchesSearch(token, search));
1400
- if (!showBalances && chainFilter === ALL_CHAIN_FILTER && !search.trim()) {
1380
+ if (view.kind === "all") {
1401
1381
  const next = [];
1402
1382
  if (popularTokens.length > 0) {
1403
1383
  next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1404
1384
  }
1405
- next.push({ id: "all", tokens: sortTokensBySymbol(matched) });
1385
+ const sorted = scopedTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
1386
+ next.push({ id: "all", tokens: sorted });
1406
1387
  return next;
1407
1388
  }
1408
- const chainName = availableChains.find((chain) => chain.blockchain === chainFilter)?.chainName ?? chainFilter;
1409
- return [{
1410
- id: chainFilter,
1411
- title: chainName,
1412
- walletKind,
1413
- tokens: showBalances ? sortTokensByBalance(matched, usdOf, loadingOf) : sortTokensBySymbol(matched),
1414
- badgeAssetId: showBalances ? newestAssetId : null
1415
- }];
1389
+ if (view.kind === "rail-chain" || view.kind === "network-chain") {
1390
+ const matched = scopedTokens.filter((token) => token.blockchain === view.blockchain);
1391
+ if (!payer) {
1392
+ const next = [];
1393
+ const popular = popularTokens.filter((token) => token.blockchain === view.blockchain);
1394
+ if (popular.length > 0) {
1395
+ next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popular });
1396
+ }
1397
+ next.push({
1398
+ id: view.blockchain,
1399
+ tokens: matched.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols))
1400
+ });
1401
+ return next;
1402
+ }
1403
+ const chain = chainByCode(availableChains, view.blockchain);
1404
+ return [{
1405
+ id: view.blockchain,
1406
+ tokens: sortTokensForChain(matched, {
1407
+ withBalances: Boolean(chain && owners[chain.chainKind]),
1408
+ usdOf,
1409
+ loadingOf,
1410
+ stableSymbols
1411
+ }),
1412
+ badgeAssetId: newestAssetId
1413
+ }];
1414
+ }
1415
+ return [];
1416
1416
  }, [
1417
- showBalances,
1418
- chainFilter,
1419
- searchingAll,
1420
- search,
1417
+ view,
1418
+ payer,
1421
1419
  scopedTokens,
1422
1420
  prefs2.recentAssetIds,
1423
1421
  newestAssetId,
1424
- walletKind,
1425
1422
  balanceEntries,
1426
1423
  owners,
1427
1424
  popularTokens,
1428
- availableChains
1425
+ availableChains,
1426
+ stableSymbols,
1427
+ balanceMode,
1428
+ syncGetBalance,
1429
+ syncIsBalanceLoading
1429
1430
  ]);
1430
- function handleSelectFilter(filter) {
1431
- setChainFilter(filter);
1432
- if (filter !== ALL_CHAIN_FILTER) setLastBlockchain(filter);
1433
- setView("token");
1431
+ const searchGroups = useMemo(() => {
1432
+ if (!searching) return [];
1433
+ return groupSearchResults(scopedTokens, search, {
1434
+ role: payer ? "payer" : "receiver",
1435
+ usdOf,
1436
+ loadingOf,
1437
+ hasOwner: (kind) => Boolean(owners[kind]),
1438
+ stableSymbols
1439
+ });
1440
+ }, [searching, scopedTokens, search, payer, owners, stableSymbols, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1441
+ function handleSelectChain(blockchain, fromNetworks) {
1442
+ setLastBlockchain(blockchain);
1443
+ if (fromNetworks) {
1444
+ savedNetworksScroll.current = networksScrollRef.current?.scrollTop ?? 0;
1445
+ setSlideDirection(1);
1446
+ setView({ kind: "network-chain", blockchain });
1447
+ return;
1448
+ }
1449
+ setView({ kind: "rail-chain", blockchain });
1434
1450
  }
1451
+ function handleBackToNetworks() {
1452
+ setSlideDirection(-1);
1453
+ setView({ kind: "networks" });
1454
+ }
1455
+ useLayoutEffect(() => {
1456
+ if (view.kind !== "networks" || slideDirection !== -1) return;
1457
+ const node = networksScrollRef.current;
1458
+ if (!node) return;
1459
+ node.scrollTop = savedNetworksScroll.current;
1460
+ }, [view, slideDirection]);
1435
1461
  function handleSelectToken(token) {
1436
1462
  if (isBlockchainDisabled(token.blockchain, disabledBlockchains)) return;
1437
1463
  if (isChainKindLocked(token.chain.chainKind, lockChainKind)) return;
@@ -1440,8 +1466,28 @@ function TokenSelectDialog(props) {
1440
1466
  onSelect({ token });
1441
1467
  onClose();
1442
1468
  }
1443
- const awaitingBalances = showBalances && chainFilter === ALL_CHAIN_FILTER && !searchingAll && scopedTokens.some((token) => loadingOf(token));
1444
- return /* @__PURE__ */ jsx6(
1469
+ const activeChain = view.kind === "rail-chain" || view.kind === "network-chain" ? chainByCode(availableChains, view.blockchain) : void 0;
1470
+ const chainHeader = view.kind === "network-chain" ? { kind: "back", title: activeChain?.chainName ?? view.blockchain, onBack: handleBackToNetworks, walletKind: activeChain?.chainKind } : view.kind === "rail-chain" ? { kind: "chain", title: activeChain?.chainName ?? view.blockchain, walletKind: activeChain?.chainKind } : null;
1471
+ const awaitingBalances = showBalances && sections.every((section) => section.tokens.length === 0) && scopedTokens.some((token) => loadingOf(token));
1472
+ const tokenDisabled = (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind);
1473
+ const sliding = view.kind === "networks" || view.kind === "network-chain";
1474
+ const tokenPane = /* @__PURE__ */ jsx9(
1475
+ TokenPane,
1476
+ {
1477
+ sections,
1478
+ header: chainHeader,
1479
+ selectedAssetId,
1480
+ loading: loading || awaitingBalances,
1481
+ showBalances,
1482
+ showWalletStatus: payer,
1483
+ getBalance: balanceOf,
1484
+ isBalanceLoading: loadingOf,
1485
+ onSelectToken: handleSelectToken,
1486
+ isTokenDisabled: tokenDisabled,
1487
+ rowClassName
1488
+ }
1489
+ );
1490
+ return /* @__PURE__ */ jsx9(
1445
1491
  Dialog,
1446
1492
  {
1447
1493
  open,
@@ -1452,10 +1498,11 @@ function TokenSelectDialog(props) {
1452
1498
  fontSans,
1453
1499
  titleClassName
1454
1500
  ),
1455
- closeClassName: cx(view === "token" ? "ml-0" : void 0, closeClassName),
1456
- contentClassName,
1457
- headerAction: view === "token" ? /* @__PURE__ */ jsx6(
1458
- SearchInput2,
1501
+ headerClassName: cx("h-14 border-b border-[#e3e3e3] px-[22px] pr-4", headerClassName),
1502
+ closeClassName: cx("md:ml-0", closeClassName),
1503
+ contentClassName: cx("overflow-hidden", contentClassName),
1504
+ headerAction: /* @__PURE__ */ jsx9(
1505
+ SearchInput,
1459
1506
  {
1460
1507
  value: search,
1461
1508
  onChange: setSearch,
@@ -1463,80 +1510,219 @@ function TokenSelectDialog(props) {
1463
1510
  className: "ml-auto hidden w-[215px] shrink-0 md:block",
1464
1511
  inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1465
1512
  }
1466
- ) : null,
1467
- cardClassName: cx("w-full max-h-[90vh] md:w-[474px]", className),
1468
- children: /* @__PURE__ */ jsxs5("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1469
- view === "token" ? /* @__PURE__ */ jsx6(
1470
- SearchInput2,
1513
+ ),
1514
+ cardClassName: cx(
1515
+ "w-full max-h-[90vh] gap-0 rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] p-0 shadow-[0_0_40px_0_rgba(0,0,0,0.06)] md:w-[474px] md:rounded-[12px] md:p-0",
1516
+ className
1517
+ ),
1518
+ children: /* @__PURE__ */ jsxs6("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1519
+ /* @__PURE__ */ jsx9(
1520
+ SearchInput,
1471
1521
  {
1472
1522
  value: search,
1473
1523
  onChange: setSearch,
1474
1524
  placeholder: "search or paste address",
1475
- className: "mb-3 shrink-0 md:hidden",
1525
+ className: "mb-3 shrink-0 px-[22px] md:hidden",
1476
1526
  inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1477
1527
  }
1478
- ) : null,
1479
- /* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-1", children: [
1480
- /* @__PURE__ */ jsx6(
1528
+ ),
1529
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
1530
+ /* @__PURE__ */ jsx9(
1481
1531
  NetworkRail,
1482
1532
  {
1483
- view,
1484
- chainFilter,
1485
- chips: sortedChains,
1486
- fundedBlockchains,
1533
+ role: payer ? "payer" : "receiver",
1487
1534
  walletConnected,
1488
- showAllLabel: !payer,
1535
+ selection: railSelection(view),
1536
+ chains: railChains,
1537
+ fundedBlockchains,
1489
1538
  lockChainKind,
1490
1539
  disabledBlockchains,
1491
1540
  disabledReason,
1492
- onSelectFilter: handleSelectFilter,
1493
- onOpenNetworks: () => setView("network"),
1541
+ onSelectWallet: () => setView({ kind: "wallet" }),
1542
+ onSelectAll: () => setView({ kind: "all" }),
1543
+ onSelectNetworks: () => setView({ kind: "networks" }),
1544
+ onSelectChain: (blockchain) => handleSelectChain(blockchain, false),
1494
1545
  className: railClassName
1495
1546
  }
1496
1547
  ),
1497
- 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(
1498
- ChainPane,
1499
- {
1500
- chains: sortedChains,
1501
- fundedBlockchains,
1502
- showWalletStatus: payer,
1503
- onSelectFilter: handleSelectFilter,
1504
- lockChainKind,
1505
- disabledBlockchains,
1506
- disabledReason
1507
- }
1508
- ) }) : /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
1509
- TokenPane,
1548
+ searching ? /* @__PURE__ */ jsx9(
1549
+ SearchResults,
1510
1550
  {
1511
- sections,
1551
+ groups: searchGroups,
1512
1552
  selectedAssetId,
1513
- loading: loading || awaitingBalances,
1553
+ loading,
1514
1554
  showBalances,
1515
1555
  showWalletStatus: payer,
1516
1556
  getBalance: balanceOf,
1517
1557
  isBalanceLoading: loadingOf,
1518
1558
  onSelectToken: handleSelectToken,
1519
- isTokenDisabled: (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind),
1520
- searchClassName,
1559
+ isTokenDisabled: tokenDisabled,
1521
1560
  rowClassName
1522
1561
  }
1523
- ) })
1562
+ ) : sliding ? /* @__PURE__ */ jsx9(
1563
+ PaneTransition,
1564
+ {
1565
+ paneKey: view.kind === "network-chain" ? `network-chain:${view.blockchain}` : "networks",
1566
+ direction: slideDirection,
1567
+ children: view.kind === "networks" ? /* @__PURE__ */ jsx9(
1568
+ ChainPane,
1569
+ {
1570
+ chains: networkChains,
1571
+ showWalletStatus: payer,
1572
+ onSelectChain: (blockchain) => handleSelectChain(blockchain, true),
1573
+ lockChainKind,
1574
+ disabledBlockchains,
1575
+ disabledReason,
1576
+ scrollRef: networksScrollRef
1577
+ }
1578
+ ) : tokenPane
1579
+ }
1580
+ ) : /* @__PURE__ */ jsx9("div", { className: "relative min-h-0 flex-1", children: tokenPane })
1524
1581
  ] })
1525
1582
  ] })
1526
1583
  }
1527
1584
  );
1528
1585
  }
1586
+
1587
+ // src/token-select/pay-config.ts
1588
+ var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
1589
+ var WRAP_NEAR_CONTRACT = "wrap.near";
1590
+ function asRecord(value) {
1591
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1592
+ return value;
1593
+ }
1594
+ function apiText(value) {
1595
+ if (typeof value === "string") return value.trim();
1596
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1597
+ return "";
1598
+ }
1599
+ function apiNumber(value) {
1600
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1601
+ if (typeof value === "string" && value.trim()) {
1602
+ const parsed = Number(value);
1603
+ if (Number.isFinite(parsed)) return parsed;
1604
+ }
1605
+ return null;
1606
+ }
1607
+ function asBoolean(value, fallback = true) {
1608
+ if (typeof value === "boolean") return value;
1609
+ if (typeof value === "string") {
1610
+ const lower = value.trim().toLowerCase();
1611
+ if (lower === "true") return true;
1612
+ if (lower === "false") return false;
1613
+ }
1614
+ return fallback;
1615
+ }
1616
+ function mapSourceChain(raw) {
1617
+ const row = asRecord(raw) ?? {};
1618
+ return {
1619
+ network: apiText(row.network),
1620
+ chainId: apiText(row.chain_id ?? row.chainId),
1621
+ chainName: apiText(row.chain_name ?? row.chainName),
1622
+ logo: apiText(row.logo),
1623
+ explorer: apiText(row.explorer)
1624
+ };
1625
+ }
1626
+ function mapSourceToken(raw) {
1627
+ const row = asRecord(raw) ?? {};
1628
+ return {
1629
+ symbol: apiText(row.symbol),
1630
+ network: apiText(row.network),
1631
+ decimals: apiNumber(row.decimals) ?? 0,
1632
+ contractAddress: apiText(row.contract_address ?? row.contractAddress),
1633
+ price: apiText(row.price),
1634
+ supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
1635
+ supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
1636
+ };
1637
+ }
1638
+ function configBody(raw) {
1639
+ const row = asRecord(raw) ?? {};
1640
+ const data = asRecord(row.data);
1641
+ if (typeof row.code === "number" && data) return data;
1642
+ return row;
1643
+ }
1644
+ function mapPayConfigSource(raw) {
1645
+ const row = configBody(raw);
1646
+ const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
1647
+ const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
1648
+ return { chains, tokens };
1649
+ }
1650
+ function chainKindFor(network, overrides = {}) {
1651
+ const key = network.trim().toLowerCase();
1652
+ if (!key) return null;
1653
+ return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
1654
+ }
1655
+ function normalizeSymbol(symbol) {
1656
+ const upper = symbol.trim().toUpperCase();
1657
+ if (!upper) return null;
1658
+ if (upper === "USDT0") return "USDT";
1659
+ if (upper === "WNEAR") return "NEAR";
1660
+ return upper;
1661
+ }
1662
+ function tokenAssetId(network, symbol, contractAddress) {
1663
+ const addr = String(contractAddress || "").trim();
1664
+ return `${network}:${symbol}:${addr || "native"}`;
1665
+ }
1666
+ function isNativeAddress(contractAddress) {
1667
+ const addr = String(contractAddress || "").trim();
1668
+ if (!addr) return true;
1669
+ const lower = addr.toLowerCase();
1670
+ return lower === "native" || lower === ZERO_ADDRESS2;
1671
+ }
1672
+ function normalizePayConfig(source, options) {
1673
+ const overrides = options?.chainKinds ?? {};
1674
+ const chains = [];
1675
+ for (const chain of source.chains) {
1676
+ const chainKind = chainKindFor(chain.network, overrides);
1677
+ if (!chainKind || !chain.network) continue;
1678
+ chains.push({
1679
+ blockchain: chain.network,
1680
+ chainName: chain.chainName || chain.network,
1681
+ chainKind,
1682
+ chainId: chain.chainId || void 0,
1683
+ logo: chain.logo || chainLogoUrl(chain.network),
1684
+ explorer: chain.explorer
1685
+ });
1686
+ }
1687
+ const chainByCode2 = new Map(chains.map((chain) => [chain.blockchain, chain]));
1688
+ const tokens = [];
1689
+ for (const token of source.tokens) {
1690
+ const chain = chainByCode2.get(token.network);
1691
+ if (!chain) continue;
1692
+ const symbol = normalizeSymbol(token.symbol);
1693
+ if (!symbol) continue;
1694
+ if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
1695
+ const contractAddress = token.contractAddress.trim() || null;
1696
+ tokens.push({
1697
+ assetId: tokenAssetId(token.network, symbol, contractAddress),
1698
+ symbol,
1699
+ providerSymbol: token.symbol,
1700
+ blockchain: token.network,
1701
+ decimals: token.decimals,
1702
+ contractAddress,
1703
+ price: Number(token.price || 1),
1704
+ logo: tokenLogoUrl(symbol),
1705
+ supportPayment: token.supportPayment,
1706
+ supportReceive: token.supportReceive,
1707
+ chain
1708
+ });
1709
+ }
1710
+ const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
1711
+ if (!hasNearWrap) return tokens;
1712
+ return tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress)));
1713
+ }
1529
1714
  export {
1530
- DEFAULT_API_HOST,
1715
+ KNOWN_CHAIN_KINDS,
1531
1716
  PayWidgetsProvider,
1532
1717
  TokenSelectDialog,
1718
+ chainsFromTokens,
1533
1719
  clearPayTokenBalances,
1534
1720
  mapPayConfigSource,
1535
1721
  normalizePayConfig,
1722
+ ownerKey2 as ownerKey,
1536
1723
  parsePopularTokens,
1537
1724
  refreshPayTokenBalance,
1538
- usePayConfig,
1539
- usePayConfigSnapshot,
1725
+ tokenAssetId,
1540
1726
  usePayTokenBalance
1541
1727
  };
1542
1728
  //# sourceMappingURL=index.js.map