@rhinestone/deposit-modal 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +68 -0
  2. package/dist/{DepositModalReown-IFN6YLZJ.cjs → DepositModalReown-TVO3ZQQK.cjs} +17 -17
  3. package/dist/{DepositModalReown-R7UROJPJ.mjs → DepositModalReown-VCY65DJX.mjs} +4 -4
  4. package/dist/WithdrawModal-GR3HLYXU.cjs +11 -0
  5. package/dist/{WithdrawModal-XKSSHVEJ.mjs → WithdrawModal-RMCYJJ4S.mjs} +3 -3
  6. package/dist/bearer-transport-CVHNBgoV.d.cts +14 -0
  7. package/dist/bearer-transport-CVHNBgoV.d.ts +14 -0
  8. package/dist/{chunk-24N4KMJX.cjs → chunk-34JVJZEZ.cjs} +71 -71
  9. package/dist/{chunk-3HCCR54R.mjs → chunk-3TEVMP76.mjs} +1 -1
  10. package/dist/chunk-ADEWTBSN.cjs +19 -0
  11. package/dist/{chunk-EA5CE2UZ.mjs → chunk-AIZKGFVA.mjs} +675 -233
  12. package/dist/{chunk-F6ROQ6RJ.mjs → chunk-EKC2IRQG.mjs} +185 -69
  13. package/dist/{chunk-V466XQSG.cjs → chunk-JV5OGG53.cjs} +630 -514
  14. package/dist/{chunk-H4O22O7Y.cjs → chunk-N3AAT6BS.cjs} +826 -384
  15. package/dist/{chunk-5CM3ZQ5U.cjs → chunk-N6JJG57R.cjs} +1 -1
  16. package/dist/chunk-O4JZKKCZ.cjs +41 -0
  17. package/dist/chunk-PP4QE32S.mjs +19 -0
  18. package/dist/{chunk-HMB2XQGD.mjs → chunk-R2B6VD4E.mjs} +1 -1
  19. package/dist/chunk-X4EUXBHJ.mjs +41 -0
  20. package/dist/compliance.cjs +14 -0
  21. package/dist/compliance.d.cts +10 -0
  22. package/dist/compliance.d.ts +10 -0
  23. package/dist/compliance.mjs +14 -0
  24. package/dist/constants.cjs +3 -3
  25. package/dist/constants.d.cts +1 -1
  26. package/dist/constants.d.ts +1 -1
  27. package/dist/constants.mjs +3 -3
  28. package/dist/deposit.cjs +4 -4
  29. package/dist/deposit.d.cts +2 -2
  30. package/dist/deposit.d.ts +2 -2
  31. package/dist/deposit.mjs +3 -3
  32. package/dist/embed.cjs +16 -16
  33. package/dist/embed.d.cts +1 -1
  34. package/dist/embed.d.ts +1 -1
  35. package/dist/embed.mjs +8 -8
  36. package/dist/index.cjs +5 -5
  37. package/dist/index.d.cts +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.mjs +4 -4
  40. package/dist/offramp.cjs +25 -0
  41. package/dist/offramp.d.cts +28 -0
  42. package/dist/offramp.d.ts +28 -0
  43. package/dist/offramp.mjs +25 -0
  44. package/dist/onramp.cjs +27 -0
  45. package/dist/onramp.d.cts +37 -0
  46. package/dist/onramp.d.ts +37 -0
  47. package/dist/onramp.mjs +27 -0
  48. package/dist/ramp-types-C6aUz4SZ.d.cts +43 -0
  49. package/dist/ramp-types-C6aUz4SZ.d.ts +43 -0
  50. package/dist/server.cjs +54 -2
  51. package/dist/server.d.cts +13 -1
  52. package/dist/server.d.ts +13 -1
  53. package/dist/server.mjs +52 -0
  54. package/dist/styles.css +22 -0
  55. package/dist/{types-B9SbZQIG.d.ts → types-CZYypODQ.d.ts} +3 -2
  56. package/dist/{types-SwGGf0eH.d.cts → types-CbUs4boj.d.cts} +3 -2
  57. package/dist/withdraw.cjs +4 -4
  58. package/dist/withdraw.d.cts +2 -2
  59. package/dist/withdraw.d.ts +2 -2
  60. package/dist/withdraw.mjs +3 -3
  61. package/package.json +35 -5
  62. package/dist/WithdrawModal-SZEAI473.cjs +0 -11
@@ -1,7 +1,3 @@
1
- import {
2
- MODAL_VERSION,
3
- serviceHeaders
4
- } from "./chunk-3HCCR54R.mjs";
5
1
  import {
6
2
  CHAIN_DISPLAY_ORDER,
7
3
  HYPERCORE_CHAIN_ID,
@@ -36,6 +32,10 @@ import {
36
32
  toEvmCaip2,
37
33
  tokenIconUrl
38
34
  } from "./chunk-EAGMY3QE.mjs";
35
+ import {
36
+ MODAL_VERSION,
37
+ serviceHeaders
38
+ } from "./chunk-3TEVMP76.mjs";
39
39
 
40
40
  // src/components/ui/Modal.tsx
41
41
  import {
@@ -650,12 +650,37 @@ function parseQuotePreview(raw) {
650
650
  );
651
651
  if (Math.abs(totalUsd - breakdownTotalUsd) > 1e-6) return null;
652
652
  const breakdown = Object.fromEntries(parsedEntries);
653
+ const timing = parseDepositTiming(obj.timing);
653
654
  return {
654
655
  settlementLayer: obj.settlementLayer,
655
656
  expiresAt: obj.expiresAt,
656
657
  estimatedFillTimeSeconds: obj.estimatedFillTimeSeconds,
657
658
  fees: { total: { usd: totalUsd }, breakdown },
658
- output
659
+ output,
660
+ ...timing ? { timing } : {}
661
+ };
662
+ }
663
+ function parseDepositTiming(raw) {
664
+ if (typeof raw !== "object" || raw === null) return void 0;
665
+ const o = raw;
666
+ const seconds = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
667
+ const expectedSeconds = seconds(o.expectedSeconds);
668
+ const softDelaySeconds = seconds(o.softDelaySeconds);
669
+ const escalatedDelaySeconds = seconds(o.escalatedDelaySeconds);
670
+ if (expectedSeconds === void 0 || softDelaySeconds === void 0 || escalatedDelaySeconds === void 0) {
671
+ return void 0;
672
+ }
673
+ return { expectedSeconds, softDelaySeconds, escalatedDelaySeconds };
674
+ }
675
+ function normalizeHistoryTiming(raw) {
676
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
677
+ return raw;
678
+ }
679
+ const { timing: rawTiming, ...row } = raw;
680
+ const timing = parseDepositTiming(rawTiming);
681
+ return {
682
+ ...row,
683
+ ...timing ? { timing } : {}
659
684
  };
660
685
  }
661
686
  function parseQuoteOutput(raw) {
@@ -711,6 +736,31 @@ function resolveAppFeeBps(appFees, sourceChainCaip2, sourceToken) {
711
736
  const bps = override ?? appFees?.feeBps ?? 0;
712
737
  return bps > 0 ? bps : 0;
713
738
  }
739
+ function isRawAmount(value) {
740
+ return typeof value === "string" && /^\d+$/.test(value);
741
+ }
742
+ function isDepositPolicyRefusal(value) {
743
+ return typeof value === "object" && value !== null && "policyRefusal" in value;
744
+ }
745
+ var DEPOSIT_POLICY_REFUSAL_CODES = [
746
+ "TOKEN-3",
747
+ "BALANCE-3",
748
+ "BALANCE-4"
749
+ ];
750
+ var DEPOSIT_PREFLIGHT_TIMEOUT_MS = 1e4;
751
+ async function readPolicyRefusal(response) {
752
+ if (response.status !== 422) return null;
753
+ try {
754
+ const body = await response.json();
755
+ if (!body || typeof body.code !== "string" || !DEPOSIT_POLICY_REFUSAL_CODES.includes(body.code)) {
756
+ return null;
757
+ }
758
+ const message = typeof body.error === "string" && body.error !== "" ? body.error : "This deposit is not accepted.";
759
+ return { policyRefusal: { code: body.code, message } };
760
+ } catch {
761
+ return null;
762
+ }
763
+ }
714
764
  var PermitDepositSubmitError = class extends Error {
715
765
  constructor(message, safeToTransfer) {
716
766
  super(message);
@@ -737,29 +787,60 @@ function createDepositService(baseUrl, options) {
737
787
  if (value.length <= 20) return value;
738
788
  return `${value.slice(0, 10)}...${value.slice(-8)}`;
739
789
  }
790
+ function withPreflightTimeout(label, fallback, run) {
791
+ const controller = new AbortController();
792
+ return new Promise((resolve) => {
793
+ const timer = setTimeout(() => {
794
+ debugLog(debug, scope, `${label}:timeout`, {
795
+ timeoutMs: DEPOSIT_PREFLIGHT_TIMEOUT_MS
796
+ });
797
+ controller.abort();
798
+ resolve(fallback);
799
+ }, DEPOSIT_PREFLIGHT_TIMEOUT_MS);
800
+ run(controller.signal).then(
801
+ (value) => {
802
+ clearTimeout(timer);
803
+ resolve(value);
804
+ },
805
+ () => {
806
+ clearTimeout(timer);
807
+ resolve(fallback);
808
+ }
809
+ );
810
+ });
811
+ }
740
812
  const PORTFOLIO_CACHE_TTL_MS = 3e4;
741
813
  const portfolioCache = /* @__PURE__ */ new Map();
742
814
  let topTokensPromise = null;
743
815
  let chainsPromise = null;
744
- function parseTopTokens(value) {
745
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
816
+ function parseDepositOptions(value) {
817
+ if (typeof value !== "object" || value === null) return null;
818
+ const { restricted, tokens: rawTokens } = value;
819
+ if (typeof restricted !== "boolean") return null;
820
+ if (typeof rawTokens !== "object" || rawTokens === null || Array.isArray(rawTokens)) {
746
821
  return null;
747
822
  }
748
- const parsed = {};
749
- for (const [chainKey, list] of Object.entries(value)) {
823
+ const tokens = {};
824
+ for (const [chainKey, list] of Object.entries(rawTokens)) {
750
825
  if (!Array.isArray(list)) return null;
751
- const tokens = [];
826
+ const parsed = [];
752
827
  for (const entry of list) {
753
828
  if (typeof entry !== "object" || entry === null) return null;
754
- const { symbol, address, decimals } = entry;
829
+ const { symbol, address, decimals, minAmount, maxAmount } = entry;
755
830
  if (typeof symbol !== "string" || typeof address !== "string" || typeof decimals !== "number") {
756
831
  return null;
757
832
  }
758
- tokens.push({ symbol, address, decimals });
833
+ parsed.push({
834
+ symbol,
835
+ address,
836
+ decimals,
837
+ ...isRawAmount(minAmount) && { minAmount },
838
+ ...isRawAmount(maxAmount) && { maxAmount }
839
+ });
759
840
  }
760
- parsed[chainKey] = tokens;
841
+ tokens[chainKey] = parsed;
761
842
  }
762
- return parsed;
843
+ return { restricted, tokens };
763
844
  }
764
845
  async function requestTopTokens() {
765
846
  try {
@@ -771,21 +852,21 @@ function createDepositService(baseUrl, options) {
771
852
  debugLog(debug, scope, "getTopTokens:unavailable", {
772
853
  status: response.status
773
854
  });
774
- return null;
855
+ return { ok: false };
775
856
  }
776
- const data = await response.json();
777
- const tokens = parseTopTokens(data.tokens);
778
- if (!tokens) {
857
+ const options2 = parseDepositOptions(await response.json());
858
+ if (!options2) {
779
859
  debugLog(debug, scope, "getTopTokens:malformed");
780
- return null;
860
+ return { ok: false };
781
861
  }
782
862
  debugLog(debug, scope, "getTopTokens:success", {
783
- chains: Object.keys(tokens).length
863
+ chains: Object.keys(options2.tokens).length,
864
+ restricted: options2.restricted
784
865
  });
785
- return tokens;
866
+ return { ok: true, options: options2 };
786
867
  } catch (error) {
787
868
  debugError(debug, scope, "getTopTokens:error", error);
788
- return null;
869
+ return { ok: false };
789
870
  }
790
871
  }
791
872
  async function requestChains() {
@@ -1197,7 +1278,7 @@ function createDepositService(baseUrl, options) {
1197
1278
  );
1198
1279
  }
1199
1280
  const data = await response.json();
1200
- const deposits = Array.isArray(data?.deposits) ? data.deposits : [];
1281
+ const deposits = Array.isArray(data?.deposits) ? data.deposits.map(normalizeHistoryTiming) : [];
1201
1282
  const nextCursor = data?.nextCursor ?? data?.next_cursor ?? null;
1202
1283
  debugLog(debug, scope, "fetchDepositHistory:success", {
1203
1284
  recipient: shortRef(params.recipient),
@@ -1278,36 +1359,46 @@ function createDepositService(baseUrl, options) {
1278
1359
  sourceToken: params.sourceToken,
1279
1360
  amount: params.amount
1280
1361
  });
1281
- try {
1282
- const response = await fetch(url, {
1283
- method: "POST",
1284
- headers: serviceHeaders(),
1285
- body: JSON.stringify(body),
1286
- cache: "no-store"
1287
- });
1288
- if (!response.ok) {
1289
- debugLog(debug, scope, "getQuotePreview:unavailable", {
1290
- status: response.status
1362
+ return withPreflightTimeout("getQuotePreview", null, async (signal) => {
1363
+ try {
1364
+ const response = await fetch(url, {
1365
+ method: "POST",
1366
+ headers: serviceHeaders(),
1367
+ body: JSON.stringify(body),
1368
+ cache: "no-store",
1369
+ signal
1291
1370
  });
1292
- return null;
1293
- }
1294
- const quote = parseQuotePreview(await response.json());
1295
- if (!quote) {
1296
- debugLog(debug, scope, "getQuotePreview:unavailable", {
1297
- status: response.status,
1298
- reason: "malformed"
1371
+ if (!response.ok) {
1372
+ const refusal = await readPolicyRefusal(response);
1373
+ if (refusal) {
1374
+ debugLog(debug, scope, "getQuotePreview:refused", {
1375
+ code: refusal.policyRefusal.code
1376
+ });
1377
+ return refusal;
1378
+ }
1379
+ debugLog(debug, scope, "getQuotePreview:unavailable", {
1380
+ status: response.status
1381
+ });
1382
+ return null;
1383
+ }
1384
+ const quote = parseQuotePreview(await response.json());
1385
+ if (!quote) {
1386
+ debugLog(debug, scope, "getQuotePreview:unavailable", {
1387
+ status: response.status,
1388
+ reason: "malformed"
1389
+ });
1390
+ return null;
1391
+ }
1392
+ debugLog(debug, scope, "getQuotePreview:success", {
1393
+ settlementLayer: quote.settlementLayer,
1394
+ totalUsd: quote.fees.total.usd
1299
1395
  });
1396
+ return quote;
1397
+ } catch (error) {
1398
+ debugError(debug, scope, "getQuotePreview:error", error);
1300
1399
  return null;
1301
1400
  }
1302
- debugLog(debug, scope, "getQuotePreview:success", {
1303
- settlementLayer: quote.settlementLayer,
1304
- totalUsd: quote.fees.total.usd
1305
- });
1306
- return quote;
1307
- } catch (error) {
1308
- debugError(debug, scope, "getQuotePreview:error", error);
1309
- return null;
1310
- }
1401
+ });
1311
1402
  },
1312
1403
  async preparePermitDeposit(params) {
1313
1404
  const url = apiUrl("/deposits/permit/prepare");
@@ -1318,88 +1409,98 @@ function createDepositService(baseUrl, options) {
1318
1409
  token: params.token,
1319
1410
  amount: params.amount
1320
1411
  });
1321
- try {
1322
- const response = await fetch(url, {
1323
- method: "POST",
1324
- headers: serviceHeaders(),
1325
- body: JSON.stringify({ ...params, kind: "auto" }),
1326
- cache: "no-store"
1327
- });
1328
- if (!response.ok) {
1329
- debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1330
- status: response.status
1331
- });
1332
- return null;
1333
- }
1334
- const data = await response.json();
1335
- if (!data || typeof data.available !== "boolean") {
1336
- debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1337
- status: response.status,
1338
- reason: "malformed"
1412
+ return withPreflightTimeout("preparePermitDeposit", null, async (signal) => {
1413
+ try {
1414
+ const response = await fetch(url, {
1415
+ method: "POST",
1416
+ headers: serviceHeaders(),
1417
+ body: JSON.stringify({ ...params, kind: "auto" }),
1418
+ cache: "no-store",
1419
+ signal
1339
1420
  });
1340
- return null;
1341
- }
1342
- if (data.available === false) {
1343
- const reason = data.reason;
1344
- if (reason !== "permit2-approval-required" && reason !== "unsupported-token" && reason !== "unsupported-owner") {
1421
+ if (!response.ok) {
1422
+ const refusal = await readPolicyRefusal(response);
1423
+ if (refusal) {
1424
+ debugLog(debug, scope, "preparePermitDeposit:refused", {
1425
+ code: refusal.policyRefusal.code
1426
+ });
1427
+ return refusal;
1428
+ }
1429
+ debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1430
+ status: response.status
1431
+ });
1345
1432
  return null;
1346
1433
  }
1347
- debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1348
- status: response.status,
1349
- reason
1350
- });
1351
- const approval = data.approval;
1352
- const parsedApproval = approval && typeof approval.token === "string" && typeof approval.spender === "string" && typeof approval.minimumAmount === "string" ? {
1353
- token: approval.token,
1354
- spender: approval.spender,
1355
- minimumAmount: approval.minimumAmount
1356
- } : void 0;
1357
- return {
1358
- available: false,
1359
- reason,
1360
- ...parsedApproval && { approval: parsedApproval }
1361
- };
1362
- }
1363
- const authorization = data.authorization;
1364
- if (!authorization || typeof authorization.typedData !== "object" || authorization.typedData === null || typeof authorization.nonce !== "string") {
1365
- return null;
1366
- }
1367
- if (authorization.kind === "erc3009" && typeof authorization.validAfter === "string" && typeof authorization.validBefore === "string") {
1368
- debugLog(debug, scope, "preparePermitDeposit:success", {
1369
- kind: authorization.kind,
1370
- nonce: authorization.nonce
1371
- });
1372
- return {
1373
- available: true,
1374
- authorization: {
1375
- kind: "erc3009",
1376
- typedData: authorization.typedData,
1377
- nonce: authorization.nonce,
1378
- validAfter: authorization.validAfter,
1379
- validBefore: authorization.validBefore
1434
+ const data = await response.json();
1435
+ if (!data || typeof data.available !== "boolean") {
1436
+ debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1437
+ status: response.status,
1438
+ reason: "malformed"
1439
+ });
1440
+ return null;
1441
+ }
1442
+ if (data.available === false) {
1443
+ const reason = data.reason;
1444
+ if (reason !== "permit2-approval-required" && reason !== "unsupported-token" && reason !== "unsupported-owner") {
1445
+ return null;
1380
1446
  }
1381
- };
1382
- }
1383
- if ((authorization.kind === "erc2612" || authorization.kind === "permit2") && typeof authorization.deadline === "string") {
1384
- debugLog(debug, scope, "preparePermitDeposit:success", {
1385
- kind: authorization.kind,
1386
- nonce: authorization.nonce
1387
- });
1388
- return {
1389
- available: true,
1390
- authorization: {
1447
+ debugLog(debug, scope, "preparePermitDeposit:unavailable", {
1448
+ status: response.status,
1449
+ reason
1450
+ });
1451
+ const approval = data.approval;
1452
+ const parsedApproval = approval && typeof approval.token === "string" && typeof approval.spender === "string" && typeof approval.minimumAmount === "string" ? {
1453
+ token: approval.token,
1454
+ spender: approval.spender,
1455
+ minimumAmount: approval.minimumAmount
1456
+ } : void 0;
1457
+ return {
1458
+ available: false,
1459
+ reason,
1460
+ ...parsedApproval && { approval: parsedApproval }
1461
+ };
1462
+ }
1463
+ const authorization = data.authorization;
1464
+ if (!authorization || typeof authorization.typedData !== "object" || authorization.typedData === null || typeof authorization.nonce !== "string") {
1465
+ return null;
1466
+ }
1467
+ if (authorization.kind === "erc3009" && typeof authorization.validAfter === "string" && typeof authorization.validBefore === "string") {
1468
+ debugLog(debug, scope, "preparePermitDeposit:success", {
1391
1469
  kind: authorization.kind,
1392
- typedData: authorization.typedData,
1393
- nonce: authorization.nonce,
1394
- deadline: authorization.deadline
1395
- }
1396
- };
1470
+ nonce: authorization.nonce
1471
+ });
1472
+ return {
1473
+ available: true,
1474
+ authorization: {
1475
+ kind: "erc3009",
1476
+ typedData: authorization.typedData,
1477
+ nonce: authorization.nonce,
1478
+ validAfter: authorization.validAfter,
1479
+ validBefore: authorization.validBefore
1480
+ }
1481
+ };
1482
+ }
1483
+ if ((authorization.kind === "erc2612" || authorization.kind === "permit2") && typeof authorization.deadline === "string") {
1484
+ debugLog(debug, scope, "preparePermitDeposit:success", {
1485
+ kind: authorization.kind,
1486
+ nonce: authorization.nonce
1487
+ });
1488
+ return {
1489
+ available: true,
1490
+ authorization: {
1491
+ kind: authorization.kind,
1492
+ typedData: authorization.typedData,
1493
+ nonce: authorization.nonce,
1494
+ deadline: authorization.deadline
1495
+ }
1496
+ };
1497
+ }
1498
+ return null;
1499
+ } catch (error) {
1500
+ debugError(debug, scope, "preparePermitDeposit:error", error);
1501
+ return null;
1397
1502
  }
1398
- return null;
1399
- } catch (error) {
1400
- debugError(debug, scope, "preparePermitDeposit:error", error);
1401
- return null;
1402
- }
1503
+ });
1403
1504
  },
1404
1505
  async submitPermitDeposit(params) {
1405
1506
  const url = apiUrl("/deposits/permit");
@@ -1708,9 +1809,11 @@ function createDepositService(baseUrl, options) {
1708
1809
  },
1709
1810
  getTopTokens() {
1710
1811
  if (!topTokensPromise) {
1711
- const attempt = requestTopTokens().then((tokens) => {
1712
- if (!tokens && topTokensPromise === attempt) topTokensPromise = null;
1713
- return tokens;
1812
+ const attempt = requestTopTokens().then((result) => {
1813
+ if (!result.ok && topTokensPromise === attempt) {
1814
+ topTokensPromise = null;
1815
+ }
1816
+ return result;
1714
1817
  });
1715
1818
  topTokensPromise = attempt;
1716
1819
  }
@@ -1939,6 +2042,7 @@ function useStableRpcUrls(rpcUrls) {
1939
2042
  // src/core/chain-catalog-context.tsx
1940
2043
  import {
1941
2044
  createContext as createContext2,
2045
+ useCallback as useCallback2,
1942
2046
  useContext as useContext2,
1943
2047
  useEffect as useEffect2,
1944
2048
  useMemo as useMemo2,
@@ -1946,6 +2050,59 @@ import {
1946
2050
  useSyncExternalStore
1947
2051
  } from "react";
1948
2052
 
2053
+ // src/core/deposit-bounds.ts
2054
+ import { formatUnits } from "viem";
2055
+ function findListed(options, chain, token) {
2056
+ const normalize2 = (value) => chain === "solana" ? value : value.toLowerCase();
2057
+ return options.tokens[String(chain)]?.find(
2058
+ (listed) => normalize2(listed.address) === normalize2(token)
2059
+ );
2060
+ }
2061
+ function isExcludedByRestrictedList(options, chain, token) {
2062
+ return Boolean(options?.restricted && !findListed(options, chain, token));
2063
+ }
2064
+ function findDepositBounds(options, chain, token) {
2065
+ if (!options?.restricted) return void 0;
2066
+ const entry = findListed(options, chain, token);
2067
+ if (!entry || entry.minAmount === void 0 && entry.maxAmount === void 0) {
2068
+ return void 0;
2069
+ }
2070
+ return {
2071
+ symbol: entry.symbol,
2072
+ decimals: entry.decimals,
2073
+ ...entry.minAmount !== void 0 && { minAmount: BigInt(entry.minAmount) },
2074
+ ...entry.maxAmount !== void 0 && { maxAmount: BigInt(entry.maxAmount) }
2075
+ };
2076
+ }
2077
+ function boundMessage(bounds, bound) {
2078
+ const raw = bounds?.[bound];
2079
+ if (!bounds || raw === void 0) return null;
2080
+ const amount = new Intl.NumberFormat("en-US", {
2081
+ maximumFractionDigits: Math.min(bounds.decimals, 20)
2082
+ }).format(Number(formatUnits(raw, bounds.decimals)));
2083
+ return `${bound === "minAmount" ? "Minimum" : "Maximum"} deposit is ${amount} ${bounds.symbol}`;
2084
+ }
2085
+ function depositBoundsViolation(bounds, units) {
2086
+ if (!bounds || units === null || units <= 0n) return null;
2087
+ if (bounds.minAmount !== void 0 && units < bounds.minAmount) {
2088
+ return boundMessage(bounds, "minAmount");
2089
+ }
2090
+ if (bounds.maxAmount !== void 0 && units > bounds.maxAmount) {
2091
+ return boundMessage(bounds, "maxAmount");
2092
+ }
2093
+ return null;
2094
+ }
2095
+ function describePolicyRefusal(refusal, bounds) {
2096
+ switch (refusal.code) {
2097
+ case "BALANCE-4":
2098
+ return boundMessage(bounds, "minAmount") ?? "This amount is below the minimum deposit";
2099
+ case "BALANCE-3":
2100
+ return boundMessage(bounds, "maxAmount") ?? "This amount is above the maximum deposit";
2101
+ default:
2102
+ return "This token isn't accepted for deposits";
2103
+ }
2104
+ }
2105
+
1949
2106
  // src/core/public-client.ts
1950
2107
  import { createPublicClient, http } from "viem";
1951
2108
  var clientCache = /* @__PURE__ */ new Map();
@@ -2110,6 +2267,9 @@ var EMPTY = {
2110
2267
  viemChains: null,
2111
2268
  isLoading: true,
2112
2269
  shortlist: null,
2270
+ depositOptions: { status: "loading" },
2271
+ retryDepositOptions: () => {
2272
+ },
2113
2273
  clientCapableChainIds: null
2114
2274
  };
2115
2275
  var ChainCatalogContext = createContext2(EMPTY);
@@ -2267,6 +2427,48 @@ function useTargetTokens() {
2267
2427
  };
2268
2428
  }, [catalog, shortlist]);
2269
2429
  }
2430
+ function createDepositOffer(state, retry) {
2431
+ const options = state.status === "ready" ? state.options : null;
2432
+ const restricted = options?.restricted ?? false;
2433
+ const listed = (chain) => options?.tokens[String(chain)];
2434
+ const isChainOffered = (chain) => {
2435
+ if (!options) return false;
2436
+ const entry = listed(chain);
2437
+ return restricted ? (entry?.length ?? 0) > 0 : entry?.length !== 0;
2438
+ };
2439
+ return {
2440
+ status: state.status,
2441
+ restricted,
2442
+ retry,
2443
+ tokensFor: (chain) => {
2444
+ if (!options) return [];
2445
+ const entry = listed(chain);
2446
+ return restricted ? entry ?? [] : entry;
2447
+ },
2448
+ isChainOffered
2449
+ };
2450
+ }
2451
+ function useDepositOffer() {
2452
+ const { depositOptions, retryDepositOptions } = useChainCatalog();
2453
+ return useMemo2(
2454
+ () => createDepositOffer(depositOptions, retryDepositOptions),
2455
+ [depositOptions, retryDepositOptions]
2456
+ );
2457
+ }
2458
+ function useDepositBounds() {
2459
+ const { depositOptions } = useChainCatalog();
2460
+ return useMemo2(() => {
2461
+ const options = depositOptions.status === "ready" ? depositOptions.options : null;
2462
+ return (chain, token) => findDepositBounds(options, chain, token);
2463
+ }, [depositOptions]);
2464
+ }
2465
+ function useRestrictedListExclusion() {
2466
+ const { depositOptions } = useChainCatalog();
2467
+ return useMemo2(() => {
2468
+ const options = depositOptions.status === "ready" ? depositOptions.options : null;
2469
+ return (chain, token) => isExcludedByRestrictedList(options, chain, token);
2470
+ }, [depositOptions]);
2471
+ }
2270
2472
  function useIsDepositChain() {
2271
2473
  const ids = useDepositChainIds();
2272
2474
  const { clientCapableChainIds } = useChainCatalog();
@@ -2359,19 +2561,36 @@ function ChainCatalogProvider({
2359
2561
  const chains = toViemChains(catalog, overrides, viemChains);
2360
2562
  if (chains.length > 0) setRuntimeChains(chains);
2361
2563
  }, [catalog, viemChains, overrides]);
2362
- const [shortlist, setShortlist] = useState(null);
2564
+ const [depositOptions, setDepositOptions] = useState({
2565
+ status: "loading"
2566
+ });
2567
+ const [depositOptionsAttempt, setDepositOptionsAttempt] = useState(0);
2363
2568
  useEffect2(() => {
2364
2569
  let active = true;
2365
- setShortlist(null);
2366
- service.getTopTokens().then((tokens) => {
2367
- if (!active || !tokens) return;
2368
- publishShortlistTokens(tokens);
2369
- setShortlist(tokens);
2370
- });
2570
+ setDepositOptions({ status: "loading" });
2571
+ service.getTopTokens().then(
2572
+ (result) => {
2573
+ if (!active) return;
2574
+ if (!result?.ok) {
2575
+ setDepositOptions({ status: "error" });
2576
+ return;
2577
+ }
2578
+ publishShortlistTokens(result.options.tokens);
2579
+ setDepositOptions({ status: "ready", options: result.options });
2580
+ },
2581
+ () => {
2582
+ if (active) setDepositOptions({ status: "error" });
2583
+ }
2584
+ );
2371
2585
  return () => {
2372
2586
  active = false;
2373
2587
  };
2374
- }, [service]);
2588
+ }, [service, depositOptionsAttempt]);
2589
+ const retryDepositOptions = useCallback2(
2590
+ () => setDepositOptionsAttempt((attempt) => attempt + 1),
2591
+ []
2592
+ );
2593
+ const shortlist = depositOptions.status === "ready" ? depositOptions.options.tokens : null;
2375
2594
  const clientCapableChainIds = useMemo2(() => {
2376
2595
  if (!catalog) return null;
2377
2596
  return new Set(
@@ -2384,9 +2603,19 @@ function ChainCatalogProvider({
2384
2603
  viemChains,
2385
2604
  isLoading,
2386
2605
  shortlist,
2606
+ depositOptions,
2607
+ retryDepositOptions,
2387
2608
  clientCapableChainIds
2388
2609
  }),
2389
- [catalog, viemChains, isLoading, shortlist, clientCapableChainIds]
2610
+ [
2611
+ catalog,
2612
+ viemChains,
2613
+ isLoading,
2614
+ shortlist,
2615
+ depositOptions,
2616
+ retryDepositOptions,
2617
+ clientCapableChainIds
2618
+ ]
2390
2619
  );
2391
2620
  return /* @__PURE__ */ jsx3(ChainCatalogContext.Provider, { value, children });
2392
2621
  }
@@ -2404,7 +2633,7 @@ function useLatestRef(value) {
2404
2633
  // src/core/analytics.tsx
2405
2634
  import {
2406
2635
  createContext as createContext3,
2407
- useCallback as useCallback2,
2636
+ useCallback as useCallback3,
2408
2637
  useContext as useContext3,
2409
2638
  useEffect as useEffect4,
2410
2639
  useMemo as useMemo3,
@@ -2792,7 +3021,7 @@ function AnalyticsSession(props) {
2792
3021
  nowRef.current = props.now ?? (() => /* @__PURE__ */ new Date());
2793
3022
  const modal = props.modal;
2794
3023
  const reportedFailuresRef = useRef3(/* @__PURE__ */ new Set());
2795
- const emitFailure = useCallback2(
3024
+ const emitFailure = useCallback3(
2796
3025
  ({ reason, status, droppedEvents }) => {
2797
3026
  if (reportedFailuresRef.current.has(reason)) return;
2798
3027
  reportedFailuresRef.current.add(reason);
@@ -2842,7 +3071,7 @@ function AnalyticsSession(props) {
2842
3071
  ingestRef.current = createIngest();
2843
3072
  }
2844
3073
  const pendingDisposalRef = useRef3(null);
2845
- const enqueue = useCallback2(
3074
+ const enqueue = useCallback3(
2846
3075
  (event) => {
2847
3076
  try {
2848
3077
  ingestRef.current?.transport?.enqueue(
@@ -2877,7 +3106,7 @@ function AnalyticsSession(props) {
2877
3106
  }, 0);
2878
3107
  };
2879
3108
  }, [emitFailure]);
2880
- const emitDeposit = useCallback2(
3109
+ const emitDeposit = useCallback3(
2881
3110
  (...[payload, fundingMethod]) => {
2882
3111
  if (modal !== "deposit") return;
2883
3112
  const event = {
@@ -2896,7 +3125,7 @@ function AnalyticsSession(props) {
2896
3125
  },
2897
3126
  [enqueue, modal, nowRef, onEventRef]
2898
3127
  );
2899
- const emitWithdraw = useCallback2(
3128
+ const emitWithdraw = useCallback3(
2900
3129
  (payload) => {
2901
3130
  if (modal !== "withdraw") return;
2902
3131
  const event = {
@@ -2912,7 +3141,7 @@ function AnalyticsSession(props) {
2912
3141
  },
2913
3142
  [enqueue, modal, nowRef, onEventRef]
2914
3143
  );
2915
- const emitClaim = useCallback2(
3144
+ const emitClaim = useCallback3(
2916
3145
  (payload) => {
2917
3146
  if (modal !== "claim") return;
2918
3147
  const event = {
@@ -2986,17 +3215,38 @@ function recoveryFailureCause(sourceChainName) {
2986
3215
  }
2987
3216
 
2988
3217
  // src/core/history-status.ts
2989
- function normalizeStatus(raw, isSpam) {
3218
+ var UNKNOWN = ["pending", false, []];
3219
+ var STATUS_FACTS = [
3220
+ // `delayed` is held up but still coming on the corridor's schedule.
3221
+ ["pending", true, ["pending", "delayed"]],
3222
+ // Nothing emits `submitted` or `bridging`; they are here so the badge and
3223
+ // the arrival line cannot diverge should a row ever carry them.
3224
+ ["processing", true, ["processing", "submitted", "bridging"]],
3225
+ ["completed", false, ["completed", "complete", "bridge-complete", "processed"]],
3226
+ // `rejected` is recoverable upstream exactly like `failed`, so it must not
3227
+ // read as Pending — a row offering recovery would otherwise claim to still
3228
+ // be on its way.
3229
+ ["failed", false, ["failed", "error", "bridge-failed", "rejected"]],
3230
+ // Where a recovered deposit lands. Terminal and not recoverable again.
3231
+ ["refunded", false, ["refunded"]],
3232
+ // These read Pending but are not arriving: the deposit is bound for a refund
3233
+ // or for manual reconciliation rather than for the recipient, so an arrival
3234
+ // expectation would be a claim about a corridor it never enters.
3235
+ ["pending", false, ["ignored", "expecting_refund", "reconciliation_required"]]
3236
+ ];
3237
+ function lookup(raw) {
2990
3238
  const lower = raw.toLowerCase();
2991
- if (lower === "completed" || lower === "complete" || lower === "bridge-complete" || lower === "processed")
2992
- return "completed";
2993
- if (lower === "failed" || lower === "error" || lower === "bridge-failed" || lower === "rejected") {
2994
- return isSpam ? "spam" : "failed";
2995
- }
2996
- if (lower === "refunded") return "refunded";
2997
- if (lower === "processing" || lower === "submitted" || lower === "bridging")
2998
- return "processing";
2999
- return "pending";
3239
+ return STATUS_FACTS.find((facts) => facts[2].includes(lower)) ?? UNKNOWN;
3240
+ }
3241
+ function normalizeStatus(raw, isSpam) {
3242
+ const [badge] = lookup(raw);
3243
+ return isSpam && badge === "failed" ? "spam" : badge;
3244
+ }
3245
+ function isInFlight(status) {
3246
+ return status === "pending" || status === "processing";
3247
+ }
3248
+ function expectsArrival(raw) {
3249
+ return lookup(raw)[1];
3000
3250
  }
3001
3251
  var STATUS_LABEL = {
3002
3252
  pending: "Pending",
@@ -3016,20 +3266,37 @@ function filterVisibleDeposits(deposits) {
3016
3266
  }
3017
3267
 
3018
3268
  // src/core/history-badge.ts
3019
- function deriveHistoryBadge(deposits, { recipient, canRecover }) {
3269
+ var RECOVERY_INDICATOR_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
3270
+ function deriveHistoryBadgeState(deposits, {
3271
+ recipient,
3272
+ canRecover,
3273
+ evaluatedAt = Date.now()
3274
+ }) {
3020
3275
  const visible = filterVisibleDeposits(deposits);
3021
3276
  if (canRecover) {
3022
- const needsYou = visible.filter(
3023
- (row) => toRecoverableDeposit(row, { recipient }) !== null
3024
- ).length;
3025
- if (needsYou > 0) return { state: "needs-you", count: needsYou };
3026
- }
3027
- const inProgress = visible.filter((row) => {
3028
- const status = normalizeStatus(row.status, row.isSpam);
3029
- return status === "pending" || status === "processing";
3030
- }).length;
3031
- if (inProgress > 0) return { state: "progress", count: inProgress };
3032
- return null;
3277
+ let count = 0;
3278
+ let nextExpiryAt = null;
3279
+ for (const row of visible) {
3280
+ if (toRecoverableDeposit(row, { recipient }) === null) continue;
3281
+ const expiryAt = Date.parse(row.createdAt ?? "") + RECOVERY_INDICATOR_WINDOW_MS;
3282
+ if (!(evaluatedAt < expiryAt)) continue;
3283
+ count += 1;
3284
+ nextExpiryAt = nextExpiryAt === null ? expiryAt : Math.min(nextExpiryAt, expiryAt);
3285
+ }
3286
+ if (count > 0) {
3287
+ return {
3288
+ badge: { state: "needs-you", count },
3289
+ nextExpiryAt
3290
+ };
3291
+ }
3292
+ }
3293
+ const inProgress = visible.filter(
3294
+ (row) => isInFlight(normalizeStatus(row.status, row.isSpam))
3295
+ ).length;
3296
+ return {
3297
+ badge: inProgress > 0 ? { state: "progress", count: inProgress } : null,
3298
+ nextExpiryAt: null
3299
+ };
3033
3300
  }
3034
3301
  function historyBadgeAriaLabel(badge) {
3035
3302
  if (!badge) return "Deposit history";
@@ -3069,11 +3336,11 @@ function HistoryEntryButton({
3069
3336
  }
3070
3337
 
3071
3338
  // src/components/history/HistoryOverlay.tsx
3072
- import { useCallback as useCallback6, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
3339
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
3073
3340
 
3074
3341
  // src/components/history/DepositHistoryPanel.tsx
3075
3342
  import {
3076
- useCallback as useCallback3,
3343
+ useCallback as useCallback4,
3077
3344
  useEffect as useEffect5,
3078
3345
  useId,
3079
3346
  useMemo as useMemo4,
@@ -3351,8 +3618,83 @@ function TokenIcon({ symbol, className, fallback }) {
3351
3618
  }
3352
3619
  TokenIcon.displayName = "TokenIcon";
3353
3620
 
3621
+ // src/core/deposit-timing.ts
3622
+ var FALLBACK_SOFT_DELAY_MS = {
3623
+ confirming: 90 * 1e3,
3624
+ received: 90 * 1e3,
3625
+ bridging: 4 * 60 * 1e3
3626
+ };
3627
+ var FALLBACK_ESCALATED_DELAY_MS = 10 * 60 * 1e3;
3628
+ var MIN_SOFT_DELAY_MS = 90 * 1e3;
3629
+ function formatExpectedDuration(seconds) {
3630
+ if (!Number.isFinite(seconds) || seconds <= 0) return "a few seconds";
3631
+ if (seconds < 60) {
3632
+ const whole = Math.round(seconds);
3633
+ return `~${whole} second${whole === 1 ? "" : "s"}`;
3634
+ }
3635
+ return `~${Math.max(1, Math.round(seconds / 60))} min`;
3636
+ }
3637
+ function resolveServedThresholds(timing) {
3638
+ const softMs = Math.max(timing.softDelaySeconds * 1e3, MIN_SOFT_DELAY_MS);
3639
+ return {
3640
+ softMs,
3641
+ escalatedMs: Math.max(timing.escalatedDelaySeconds * 1e3, softMs)
3642
+ };
3643
+ }
3644
+ function resolveServedTimingState(timing, totalElapsedMs) {
3645
+ const { softMs, escalatedMs } = resolveServedThresholds(timing);
3646
+ if (totalElapsedMs >= escalatedMs) return "escalated";
3647
+ if (totalElapsedMs >= softMs) return "soft";
3648
+ return "expected";
3649
+ }
3650
+ function resolvePendingTimingState({
3651
+ timing,
3652
+ totalElapsedMs,
3653
+ phaseId,
3654
+ phaseElapsedMs
3655
+ }) {
3656
+ if (timing) return resolveServedTimingState(timing, totalElapsedMs);
3657
+ if (totalElapsedMs >= FALLBACK_ESCALATED_DELAY_MS) return "escalated";
3658
+ if (phaseId && phaseElapsedMs >= FALLBACK_SOFT_DELAY_MS[phaseId]) {
3659
+ return "soft";
3660
+ }
3661
+ return "expected";
3662
+ }
3663
+ var STATE_ORDER = {
3664
+ expected: 0,
3665
+ soft: 1,
3666
+ escalated: 2
3667
+ };
3668
+ function advancePendingTimingState(previous, next) {
3669
+ return STATE_ORDER[next] > STATE_ORDER[previous] ? next : previous;
3670
+ }
3671
+
3672
+ // src/core/history-timing.ts
3673
+ function resolveHistoryTiming({
3674
+ status,
3675
+ timing,
3676
+ createdAt,
3677
+ nowMs
3678
+ }) {
3679
+ if (!expectsArrival(status) || !timing || !createdAt) {
3680
+ return void 0;
3681
+ }
3682
+ const createdAtMs = Date.parse(createdAt);
3683
+ if (!Number.isFinite(createdAtMs)) return void 0;
3684
+ const elapsedMs = Math.max(0, nowMs - createdAtMs);
3685
+ const state = resolveServedTimingState(timing, elapsedMs);
3686
+ const { softMs, escalatedMs } = resolveServedThresholds(timing);
3687
+ const nextTransitionAtMs = state === "expected" ? createdAtMs + softMs : state === "soft" ? createdAtMs + escalatedMs : void 0;
3688
+ return {
3689
+ state,
3690
+ expectedDuration: formatExpectedDuration(timing.expectedSeconds),
3691
+ ...nextTransitionAtMs !== void 0 ? { nextTransitionAtMs } : {}
3692
+ };
3693
+ }
3694
+
3354
3695
  // src/components/history/DepositHistoryPanel.tsx
3355
3696
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
3697
+ var MAX_TIMER_DELAY_MS = 2147e6;
3356
3698
  function shortenHash(hash) {
3357
3699
  if (hash.length <= 14) return hash;
3358
3700
  return `${hash.slice(0, 6)}\u2026${hash.slice(-4)}`;
@@ -3458,7 +3800,7 @@ function DepositHistoryPanel({
3458
3800
  useEffect5(() => {
3459
3801
  rootRef.current?.toggleAttribute("inert", Boolean(obscured));
3460
3802
  }, [obscured]);
3461
- const handleKeyDown = useCallback3(
3803
+ const handleKeyDown = useCallback4(
3462
3804
  (e) => {
3463
3805
  if (obscured) return;
3464
3806
  if (e.key === "Escape") {
@@ -3472,6 +3814,48 @@ function DepositHistoryPanel({
3472
3814
  () => filterVisibleDeposits(deposits),
3473
3815
  [deposits]
3474
3816
  );
3817
+ const [nowMs, setNowMs] = useState3(() => Date.now());
3818
+ const timingPresentations = useMemo4(
3819
+ () => visibleDeposits.map(
3820
+ (deposit) => resolveHistoryTiming({
3821
+ status: deposit.status,
3822
+ timing: deposit.timing,
3823
+ createdAt: deposit.createdAt,
3824
+ nowMs
3825
+ })
3826
+ ),
3827
+ [nowMs, visibleDeposits]
3828
+ );
3829
+ useEffect5(() => {
3830
+ setNowMs(Date.now());
3831
+ }, [visibleDeposits]);
3832
+ useEffect5(() => {
3833
+ const nextBoundary = timingPresentations.reduce(
3834
+ (earliest, presentation) => {
3835
+ const next = presentation?.nextTransitionAtMs;
3836
+ if (next === void 0) return earliest;
3837
+ return earliest === void 0 || next < earliest ? next : earliest;
3838
+ },
3839
+ void 0
3840
+ );
3841
+ if (nextBoundary === void 0) return;
3842
+ let timeout;
3843
+ const updateAtBoundary = () => {
3844
+ const current = Date.now();
3845
+ if (current < nextBoundary) {
3846
+ timeout = setTimeout(
3847
+ updateAtBoundary,
3848
+ Math.min(nextBoundary - current, MAX_TIMER_DELAY_MS)
3849
+ );
3850
+ return;
3851
+ }
3852
+ setNowMs(current);
3853
+ };
3854
+ updateAtBoundary();
3855
+ return () => {
3856
+ if (timeout !== void 0) clearTimeout(timeout);
3857
+ };
3858
+ }, [timingPresentations]);
3475
3859
  return /* @__PURE__ */ jsxs8(
3476
3860
  "div",
3477
3861
  {
@@ -3548,6 +3932,7 @@ function DepositHistoryPanel({
3548
3932
  HistoryCard,
3549
3933
  {
3550
3934
  deposit,
3935
+ timing: timingPresentations[i],
3551
3936
  position: i,
3552
3937
  recipient,
3553
3938
  onRecover,
@@ -3575,6 +3960,7 @@ function DepositHistoryPanel({
3575
3960
  }
3576
3961
  function HistoryCard({
3577
3962
  deposit,
3963
+ timing,
3578
3964
  position,
3579
3965
  recipient,
3580
3966
  onRecover,
@@ -3682,6 +4068,16 @@ function HistoryCard({
3682
4068
  }
3683
4069
  )
3684
4070
  ] }),
4071
+ timing && /* @__PURE__ */ jsx13(
4072
+ "span",
4073
+ {
4074
+ className: "rs-history-card-timing",
4075
+ "data-state": timing.state,
4076
+ "aria-live": "polite",
4077
+ "aria-atomic": "true",
4078
+ children: timing.state === "expected" ? `Usually arrives in ${timing.expectedDuration}` : timing.state === "soft" ? "Taking longer than usual" : "Taking much longer than usual"
4079
+ }
4080
+ ),
3685
4081
  recoverable && /* @__PURE__ */ jsxs8(
3686
4082
  Button,
3687
4083
  {
@@ -3705,7 +4101,7 @@ function HistoryCard({
3705
4101
  DepositHistoryPanel.displayName = "DepositHistoryPanel";
3706
4102
 
3707
4103
  // src/components/history/HistoryFallbackFlow.tsx
3708
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
4104
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
3709
4105
 
3710
4106
  // src/components/ui/ListRow.tsx
3711
4107
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -3889,10 +4285,10 @@ function RecoveryLookupScreen({
3889
4285
  RecoveryLookupScreen.displayName = "RecoveryLookupScreen";
3890
4286
 
3891
4287
  // src/components/recovery/recovery-display.ts
3892
- import { formatUnits as formatUnits2 } from "viem";
4288
+ import { formatUnits as formatUnits3 } from "viem";
3893
4289
 
3894
4290
  // src/core/token-amount.ts
3895
- import { formatUnits } from "viem";
4291
+ import { formatUnits as formatUnits2 } from "viem";
3896
4292
  var STABLECOIN_SYMBOLS = /* @__PURE__ */ new Set([
3897
4293
  "USDC",
3898
4294
  "USDT",
@@ -3956,7 +4352,7 @@ function formatTokenAmount(amount, symbol) {
3956
4352
  function tokenAmountToNumber(rawAmount, decimals) {
3957
4353
  if (decimals === void 0) return void 0;
3958
4354
  try {
3959
- const value = Number(formatUnits(BigInt(rawAmount), decimals));
4355
+ const value = Number(formatUnits2(BigInt(rawAmount), decimals));
3960
4356
  return Number.isFinite(value) ? value : void 0;
3961
4357
  } catch {
3962
4358
  return void 0;
@@ -4043,18 +4439,18 @@ function describeRecoveryDeposit(params) {
4043
4439
  const decimals = getTokenDecimalsByAddress(token, params.chainId);
4044
4440
  const symbol = getTokenSymbol(token, params.chainId);
4045
4441
  const raw = toBigInt(params.amountRaw);
4046
- const human = Number(formatUnits2(raw, decimals));
4442
+ const human = Number(formatUnits3(raw, decimals));
4047
4443
  return {
4048
4444
  symbol,
4049
4445
  decimals,
4050
- amount: formatTokenAmount(human, symbol) ?? formatUnits2(raw, decimals),
4446
+ amount: formatTokenAmount(human, symbol) ?? formatUnits3(raw, decimals),
4051
4447
  chainName: getChainName(params.chainId),
4052
4448
  chainIcon: getChainIcon(params.chainId)
4053
4449
  };
4054
4450
  }
4055
4451
  function formatRecoveryAmount(amountRaw, decimals, symbol) {
4056
4452
  const raw = toBigInt(amountRaw);
4057
- return formatTokenAmount(Number(formatUnits2(raw, decimals)), symbol) ?? formatUnits2(raw, decimals);
4453
+ return formatTokenAmount(Number(formatUnits3(raw, decimals)), symbol) ?? formatUnits3(raw, decimals);
4058
4454
  }
4059
4455
  function toBigInt(value) {
4060
4456
  try {
@@ -4542,7 +4938,7 @@ function HistoryFallbackFlow({
4542
4938
  epochRef.current += 1;
4543
4939
  };
4544
4940
  }, [recipient]);
4545
- const reportSelection = useCallback4(
4941
+ const reportSelection = useCallback5(
4546
4942
  (match, autoSelected) => {
4547
4943
  analytics.selectDeposit({
4548
4944
  depositId: match.depositId,
@@ -4552,7 +4948,7 @@ function HistoryFallbackFlow({
4552
4948
  },
4553
4949
  [analytics]
4554
4950
  );
4555
- const lookup = useCallback4(
4951
+ const lookup2 = useCallback5(
4556
4952
  async (txHash) => {
4557
4953
  const epoch = epochRef.current;
4558
4954
  const isCurrentLookup = () => epochRef.current === epoch;
@@ -4595,7 +4991,7 @@ function HistoryFallbackFlow({
4595
4991
  },
4596
4992
  [analytics, service, recipient, onFound, reportSelection]
4597
4993
  );
4598
- const selectMatch = useCallback4(
4994
+ const selectMatch = useCallback5(
4599
4995
  (match) => {
4600
4996
  analytics.completeStep("select");
4601
4997
  reportSelection(match, false);
@@ -4603,7 +4999,7 @@ function HistoryFallbackFlow({
4603
4999
  },
4604
5000
  [analytics, onFound, reportSelection]
4605
5001
  );
4606
- const handleKeyDown = useCallback4(
5002
+ const handleKeyDown = useCallback5(
4607
5003
  (e) => {
4608
5004
  if (e.key === "Escape") {
4609
5005
  e.stopPropagation();
@@ -4682,7 +5078,7 @@ function HistoryFallbackFlow({
4682
5078
  hint: "The transaction hash from the wallet or exchange you sent from.",
4683
5079
  loading,
4684
5080
  error,
4685
- onSubmit: lookup,
5081
+ onSubmit: lookup2,
4686
5082
  onInvalid: () => analytics.friction("lookup_transaction_hash_invalid", "lookup")
4687
5083
  }
4688
5084
  )
@@ -4698,7 +5094,7 @@ function newestFirst(rows) {
4698
5094
  HistoryFallbackFlow.displayName = "HistoryFallbackFlow";
4699
5095
 
4700
5096
  // src/components/history/HistoryRecoveryFlow.tsx
4701
- import { useCallback as useCallback5, useEffect as useEffect9, useRef as useRef6, useState as useState7 } from "react";
5097
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef6, useState as useState7 } from "react";
4702
5098
 
4703
5099
  // src/components/recovery/RecoveryOutcomeScreen.tsx
4704
5100
  import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
@@ -4902,7 +5298,7 @@ function HistoryRecoveryFlow({
4902
5298
  token: deposit.token,
4903
5299
  amountRaw: deposit.amountRaw
4904
5300
  });
4905
- const submit = useCallback5(
5301
+ const submit = useCallback6(
4906
5302
  async (destination) => {
4907
5303
  setPhase("submitting");
4908
5304
  onSubmittingChange(true);
@@ -4974,7 +5370,7 @@ function HistoryRecoveryFlow({
4974
5370
  symbol
4975
5371
  ]
4976
5372
  );
4977
- const retry = useCallback5(() => {
5373
+ const retry = useCallback6(() => {
4978
5374
  const reason = lastFailureRef.current;
4979
5375
  if (reason) {
4980
5376
  lastFailureRef.current = null;
@@ -5071,7 +5467,7 @@ function HistoryOverlay({
5071
5467
  const [fallbackOpen, setFallbackOpen] = useState8(false);
5072
5468
  const [attempt, setAttempt] = useState8(null);
5073
5469
  const claimControllerRef = useRef7(null);
5074
- const beginAttempt = useCallback6(
5470
+ const beginAttempt = useCallback7(
5075
5471
  (entrySource) => {
5076
5472
  const id = randomUuid();
5077
5473
  const started = {
@@ -5089,7 +5485,7 @@ function HistoryOverlay({
5089
5485
  },
5090
5486
  [analytics]
5091
5487
  );
5092
- const endAttempt = useCallback6(
5488
+ const endAttempt = useCallback7(
5093
5489
  (source, reason) => {
5094
5490
  const claim = claimControllerRef.current;
5095
5491
  claim?.abandon(reason);
@@ -5099,12 +5495,12 @@ function HistoryOverlay({
5099
5495
  },
5100
5496
  []
5101
5497
  );
5102
- const openFallback = useCallback6(() => {
5498
+ const openFallback = useCallback7(() => {
5103
5499
  const started = beginAttempt("history_fallback");
5104
5500
  analytics.fallbackOpened(started.id);
5105
5501
  setFallbackOpen(true);
5106
5502
  }, [analytics, beginAttempt]);
5107
- const closeFallback = useCallback6(
5503
+ const closeFallback = useCallback7(
5108
5504
  (via) => {
5109
5505
  endAttempt(via === "escape" ? "escape" : "history_back", "back");
5110
5506
  setFallbackOpen(false);
@@ -5117,7 +5513,7 @@ function HistoryOverlay({
5117
5513
  setFallbackOpen(false);
5118
5514
  if (fallbackOpen && !recovering) endAttempt("host_controlled", "back");
5119
5515
  }, [canLookup, fallbackOpen, recovering, endAttempt]);
5120
- const startRecoveryFromRow = useCallback6(
5516
+ const startRecoveryFromRow = useCallback7(
5121
5517
  (deposit) => {
5122
5518
  const started = beginAttempt("history_row");
5123
5519
  analytics.recoveryStarted(started.id, "history_row", deposit.depositId);
@@ -5125,7 +5521,7 @@ function HistoryOverlay({
5125
5521
  },
5126
5522
  [analytics, beginAttempt, controller]
5127
5523
  );
5128
- const startRecoveryFromLookup = useCallback6(
5524
+ const startRecoveryFromLookup = useCallback7(
5129
5525
  (deposit) => {
5130
5526
  if (attempt) {
5131
5527
  analytics.recoveryStarted(
@@ -5139,11 +5535,11 @@ function HistoryOverlay({
5139
5535
  },
5140
5536
  [analytics, attempt, controller]
5141
5537
  );
5142
- const cancelRecovery = useCallback6(() => {
5538
+ const cancelRecovery = useCallback7(() => {
5143
5539
  endAttempt("history_back", "back");
5144
5540
  controller.cancelRecovery();
5145
5541
  }, [controller, endAttempt]);
5146
- const closeModal = useCallback6(() => {
5542
+ const closeModal = useCallback7(() => {
5147
5543
  endAttempt("header_button", "modal_close");
5148
5544
  onCloseModal();
5149
5545
  }, [endAttempt, onCloseModal]);
@@ -5234,7 +5630,8 @@ function HistoryOverlay({
5234
5630
  }
5235
5631
 
5236
5632
  // src/components/history/useHistoryPanel.ts
5237
- import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef8, useState as useState9 } from "react";
5633
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef8, useState as useState9 } from "react";
5634
+ var MAX_TIMEOUT_MS = 2147483647;
5238
5635
  function sortByCreatedAtDesc(items) {
5239
5636
  return [...items].sort((a, b) => {
5240
5637
  const at = a.createdAt ? Date.parse(a.createdAt) : 0;
@@ -5262,7 +5659,7 @@ function useHistoryPanel({
5262
5659
  const requestRef = useRef8(0);
5263
5660
  const [recovering, setRecovering] = useState9(null);
5264
5661
  const [recoverySubmitting, setRecoverySubmitting] = useState9(false);
5265
- const fetchHistory = useCallback7(
5662
+ const fetchHistory = useCallback8(
5266
5663
  async (mode = "initial") => {
5267
5664
  if (!recipient) return;
5268
5665
  const isInitial = mode === "initial";
@@ -5312,27 +5709,40 @@ function useHistoryPanel({
5312
5709
  },
5313
5710
  [recipient, service]
5314
5711
  );
5315
- const badge = useMemo7(
5316
- () => deriveHistoryBadge(deposits, { recipient, canRecover }),
5317
- [deposits, recipient, canRecover]
5318
- );
5319
- const openPanel = useCallback7(() => {
5712
+ const badgeState2 = deriveHistoryBadgeState(deposits, {
5713
+ recipient,
5714
+ canRecover
5715
+ });
5716
+ const badge = badgeState2.badge;
5717
+ useEffect11(() => {
5718
+ if (!isOpen || badgeState2.nextExpiryAt === null) return;
5719
+ const delay = Math.min(
5720
+ badgeState2.nextExpiryAt - Date.now(),
5721
+ MAX_TIMEOUT_MS
5722
+ );
5723
+ const timeout = setTimeout(
5724
+ () => setDeposits((current) => current.slice()),
5725
+ delay
5726
+ );
5727
+ return () => clearTimeout(timeout);
5728
+ }, [isOpen, badgeState2.nextExpiryAt]);
5729
+ const openPanel = useCallback8(() => {
5320
5730
  setIsPanelOpen(true);
5321
5731
  analytics?.visitOpened(badge);
5322
5732
  if (!loadedRef.current || staleRef.current) {
5323
5733
  fetchHistory("initial");
5324
5734
  }
5325
5735
  }, [analytics, badge, fetchHistory]);
5326
- const closePanel = useCallback7(() => {
5736
+ const closePanel = useCallback8(() => {
5327
5737
  if (recovering) return;
5328
5738
  setIsPanelOpen(false);
5329
5739
  analytics?.visitEnded();
5330
5740
  }, [analytics, recovering]);
5331
- const loadMore = useCallback7(() => {
5741
+ const loadMore = useCallback8(() => {
5332
5742
  analytics?.loadMore(filterVisibleDeposits(deposits).length);
5333
5743
  fetchHistory("more");
5334
5744
  }, [analytics, deposits, fetchHistory]);
5335
- const markStale = useCallback7(() => {
5745
+ const markStale = useCallback8(() => {
5336
5746
  staleRef.current = true;
5337
5747
  if (isOpen) {
5338
5748
  fetchHistory("initial");
@@ -5368,10 +5778,10 @@ function useHistoryPanel({
5368
5778
  setRecovering(null);
5369
5779
  setRecoverySubmitting(false);
5370
5780
  }, [isOpen, analytics]);
5371
- const invalidate = useCallback7(() => {
5781
+ const invalidate = useCallback8(() => {
5372
5782
  staleRef.current = true;
5373
5783
  }, []);
5374
- const cancelRecovery = useCallback7(() => setRecovering(null), []);
5784
+ const cancelRecovery = useCallback8(() => setRecovering(null), []);
5375
5785
  return useMemo7(
5376
5786
  () => ({
5377
5787
  deposits,
@@ -5805,14 +6215,14 @@ function isSubmissionUncertain(error) {
5805
6215
  }
5806
6216
 
5807
6217
  // src/components/steps/ProcessingStep.tsx
5808
- import { useCallback as useCallback9, useEffect as useEffect14, useRef as useRef10, useState as useState13 } from "react";
6218
+ import { useCallback as useCallback10, useEffect as useEffect14, useRef as useRef10, useState as useState13 } from "react";
5809
6219
 
5810
6220
  // src/components/ui/Tooltip.tsx
5811
6221
  import {
5812
6222
  useState as useState10,
5813
6223
  useRef as useRef9,
5814
6224
  useEffect as useEffect12,
5815
- useCallback as useCallback8
6225
+ useCallback as useCallback9
5816
6226
  } from "react";
5817
6227
  import { createPortal as createPortal2 } from "react-dom";
5818
6228
  import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
@@ -5821,7 +6231,7 @@ function Tooltip({ content, children, className }) {
5821
6231
  const [position, setPosition] = useState10(null);
5822
6232
  const triggerRef = useRef9(null);
5823
6233
  const bubbleRef = useRef9(null);
5824
- const updatePosition = useCallback8(() => {
6234
+ const updatePosition = useCallback9(() => {
5825
6235
  const trigger = triggerRef.current;
5826
6236
  if (!trigger) return;
5827
6237
  const rect = trigger.getBoundingClientRect();
@@ -6325,12 +6735,6 @@ var MAX_POLL_INTERVAL = 3e4;
6325
6735
  var BACKOFF_MULTIPLIER = 1.5;
6326
6736
  var SWAPPED_RECEIPT_STATUS_RETRY_INTERVAL_MS = 1e3;
6327
6737
  var SWAPPED_FIAT_STATUS_MAX_ATTEMPTS = 12;
6328
- var ESCALATED_DELAY_MS = 10 * 60 * 1e3;
6329
- var SOFT_DELAY_MS = {
6330
- confirming: 90 * 1e3,
6331
- received: 90 * 1e3,
6332
- bridging: 4 * 60 * 1e3
6333
- };
6334
6738
  var PHASE_TIMINGS_PREFIX = "rhinestone:phase-timings";
6335
6739
  var PAYMENT_METHOD_LABELS = {
6336
6740
  creditcard: "Card",
@@ -6610,7 +7014,6 @@ function ProcessingStep({
6610
7014
  }
6611
7015
  return { type: "processing" };
6612
7016
  });
6613
- const [elapsedSeconds, setElapsedSeconds] = useState13(0);
6614
7017
  const [phaseTimings, setPhaseTimings] = useState13(() => {
6615
7018
  const saved = loadPhaseTimings(txHash);
6616
7019
  if (saved) {
@@ -6619,8 +7022,16 @@ function ProcessingStep({
6619
7022
  }
6620
7023
  return { startedAt: startTimeRef.current };
6621
7024
  });
6622
- const [hasEscalatedDelay, setHasEscalatedDelay] = useState13(false);
6623
- const updatePhaseTimings = useCallback9(
7025
+ const [elapsedSeconds, setElapsedSeconds] = useState13(
7026
+ () => Math.max(0, Math.floor((Date.now() - startTimeRef.current) / 1e3))
7027
+ );
7028
+ const [latchedTiming, setLatchedTiming] = useState13(quote?.timing);
7029
+ if (latchedTiming === void 0 && quote?.timing !== void 0) {
7030
+ setLatchedTiming(quote.timing);
7031
+ }
7032
+ const timingStateRef = useRef10("expected");
7033
+ const servedTimingLatchRef = useRef10(false);
7034
+ const updatePhaseTimings = useCallback10(
6624
7035
  (updater) => {
6625
7036
  setPhaseTimings((previous) => {
6626
7037
  const next = updater(previous);
@@ -6925,21 +7336,6 @@ function ProcessingStep({
6925
7336
  state.type,
6926
7337
  txHash
6927
7338
  ]);
6928
- useEffect14(() => {
6929
- if (directTransfer || state.type !== "processing") return;
6930
- const timeoutId = setTimeout(() => {
6931
- if (escalatedDelayRef.current) return;
6932
- escalatedDelayRef.current = true;
6933
- setHasEscalatedDelay(true);
6934
- const message = "Transfer is taking longer than expected. Your funds are safe and processing will continue automatically.";
6935
- debugLog(debug, "processing", "state:delay-escalated", {
6936
- txHash,
6937
- timeoutMs: ESCALATED_DELAY_MS
6938
- });
6939
- onErrorRef.current?.(message, "PROCESS_TIMEOUT");
6940
- }, ESCALATED_DELAY_MS);
6941
- return () => clearTimeout(timeoutId);
6942
- }, [debug, directTransfer, onErrorRef, state.type, txHash]);
6943
7339
  const isComplete = state.type === "complete";
6944
7340
  const isFailed = state.type === "failed";
6945
7341
  const isProcessing = state.type === "processing";
@@ -7039,9 +7435,43 @@ function ProcessingStep({
7039
7435
  const currentPhaseId = getCurrentPhaseId(state, phaseTimings);
7040
7436
  const activePhaseStartedAt = currentPhaseId ? getPhaseStartTime(currentPhaseId, phaseTimings) : void 0;
7041
7437
  const activePhaseElapsedMs = isProcessing && activePhaseStartedAt !== void 0 ? timelineNowMs - activePhaseStartedAt : 0;
7042
- const delayPhaseId = isProcessing && currentPhaseId && activePhaseElapsedMs >= SOFT_DELAY_MS[currentPhaseId] ? currentPhaseId : void 0;
7043
- void delayPhaseId;
7044
- void hasEscalatedDelay;
7438
+ if (latchedTiming !== void 0 && !servedTimingLatchRef.current) {
7439
+ servedTimingLatchRef.current = true;
7440
+ timingStateRef.current = "expected";
7441
+ }
7442
+ const timingState = isProcessing ? advancePendingTimingState(
7443
+ timingStateRef.current,
7444
+ resolvePendingTimingState({
7445
+ timing: latchedTiming,
7446
+ totalElapsedMs: elapsedSeconds * 1e3,
7447
+ phaseId: currentPhaseId,
7448
+ phaseElapsedMs: activePhaseElapsedMs
7449
+ })
7450
+ ) : timingStateRef.current;
7451
+ timingStateRef.current = timingState;
7452
+ const expectedDuration = latchedTiming && timingState === "expected" ? formatExpectedDuration(latchedTiming.expectedSeconds) : void 0;
7453
+ useEffect14(() => {
7454
+ if (directTransfer || !isProcessing || timingState !== "escalated") return;
7455
+ if (escalatedDelayRef.current) return;
7456
+ escalatedDelayRef.current = true;
7457
+ debugLog(debug, "processing", "state:delay-escalated", {
7458
+ txHash,
7459
+ timeoutMs: latchedTiming ? resolveServedThresholds(latchedTiming).escalatedMs : FALLBACK_ESCALATED_DELAY_MS,
7460
+ served: latchedTiming !== void 0
7461
+ });
7462
+ onErrorRef.current?.(
7463
+ "Transfer is taking longer than expected. Your funds are safe and processing will continue automatically.",
7464
+ "PROCESS_TIMEOUT"
7465
+ );
7466
+ }, [
7467
+ debug,
7468
+ directTransfer,
7469
+ isProcessing,
7470
+ latchedTiming,
7471
+ onErrorRef,
7472
+ timingState,
7473
+ txHash
7474
+ ]);
7045
7475
  const sourceChainIcon = getChainIcon(displaySourceChain);
7046
7476
  const targetChainIcon = getChainIcon(targetChainId);
7047
7477
  const sourceChainName = getChainName(displaySourceChain);
@@ -7270,6 +7700,11 @@ function ProcessingStep({
7270
7700
  })()
7271
7701
  ] }),
7272
7702
  isFailed && failureMessage && /* @__PURE__ */ jsx27(Callout, { variant: "error", children: failureMessage }),
7703
+ isProcessing && expectedDuration !== void 0 && /* @__PURE__ */ jsxs20("p", { className: "rs-pending-expectation", children: [
7704
+ "Usually arrives in ",
7705
+ expectedDuration
7706
+ ] }),
7707
+ isProcessing && latchedTiming !== void 0 && timingState !== "expected" && /* @__PURE__ */ jsx27(Callout, { variant: "warning", children: timingState === "soft" ? "Taking longer than usual. Your funds are safe and this will complete automatically." : "This is taking much longer than usual. Your funds are safe and processing continues automatically." }),
7273
7708
  isProcessing && /* @__PURE__ */ jsx27(
7274
7709
  Button,
7275
7710
  {
@@ -7529,8 +7964,11 @@ export {
7529
7964
  SwappedConnectFeeChangedError,
7530
7965
  SUPPORTED_CONNECT_EXCHANGE_COUNT,
7531
7966
  resolveAppFeeBps,
7967
+ isDepositPolicyRefusal,
7532
7968
  PermitDepositSubmitError,
7533
7969
  createDepositService,
7970
+ depositBoundsViolation,
7971
+ describePolicyRefusal,
7534
7972
  rpcUrlFor,
7535
7973
  RpcUrlsProvider,
7536
7974
  useRpcUrls,
@@ -7543,6 +7981,9 @@ export {
7543
7981
  useDepositChainIds,
7544
7982
  useDestinationChains,
7545
7983
  useTargetTokens,
7984
+ useDepositOffer,
7985
+ useDepositBounds,
7986
+ useRestrictedListExclusion,
7546
7987
  useIsDepositChain,
7547
7988
  useChainName,
7548
7989
  useReadClient,
@@ -7559,6 +8000,7 @@ export {
7559
8000
  isUnsupportedChainSwitchError,
7560
8001
  formatUserError,
7561
8002
  Tooltip,
8003
+ formatExpectedDuration,
7562
8004
  markSubmissionUncertain,
7563
8005
  isSubmissionUncertain,
7564
8006
  formatTokenAmount,