@oasisprotocol/privana-sdk 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  "use client";
2
- import { usePrivanaContext, createHostedAuthPendingStorageKey, clearHostedAuthPendingTransaction, HostedAuthRequiredError, HostedAuthError, createPkceVerifier, createPkceChallenge, createHostedAuthState, persistHostedAuthPendingTransaction, parseHostedAuthCallback, readHostedAuthPendingTransaction, HostedAuthStateMismatchError, buildHostedAuthSession, useSafePrivanaContext, usePrivateReadRequest, formatTokenAmount, submitPendingLock, PostDepositLockError, useDepositVerification, useEnsureCorrectChain, getTransactionReceipt, getBlockNumber, waitForTransactionReceipt, canUseBrowserStorage, clampLockAmount, requireDepositLockOwner, getWalletClient, createSignedLockRequest, requireServiceAddress, signWithdrawMessage, signLockMessage, signTransferMessage, signTransferLockedMessage, signModifyLockMessage, cn, Button, stripHostedAuthCallbackParams, AccountingApiError, getBrowserStorageItem, removeBrowserStorageItem, isSignedLockUsable, setBrowserStorageItem, shortenAddress, formatTimeRemaining, parseTokenAmount, getExplorerAddressUrl, getExplorerLabel, canUseSharedBrowserStorage, DEFAULT_LOCK_DURATION_SECONDS, Skeleton, formatCountdown, FiatOnRampForm, getSharedBrowserStorageItem, removeSharedBrowserStorageItem, setSharedBrowserStorageItem } from './chunk-DZURIIRE.js';
3
- export { AccountingApiError, Button, DEFAULT_LOCK_DURATION_SECONDS, DEFAULT_ONRAMP_LOCK_BUFFER, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, LOCK_TYPES, MODIFY_LOCK_TYPES, NETWORK_CONFIG, NetworkError, PostDepositLockError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, TRANSFER_LOCKED_TYPES, TRANSFER_TYPES, ValidationError, WITHDRAW_FROM_LOCK_TYPES, WITHDRAW_TYPES, applyLockBuffer, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clampLockAmount, clearHostedAuthPendingTransaction, clearPendingLock, createDomain, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createLockExpiry, createPkceChallenge, createPkceVerifier, createSignedLockRequest, getAccountingContract, getApiUrl, getChainById, getChainId, getExplorerAddressUrl, getExplorerLabel, isHostedAuthRefreshActive, isHostedAuthSessionActive, isSignedLockUsable, loadPendingLock, normalizeAddress, normalizeHex, parseHostedAuthCallback, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, requireDepositLockOwner, requireServiceAddress, savePendingLock, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, stripHostedAuthCallbackParams, submitPendingLock, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-DZURIIRE.js';
2
+ import { usePrivanaContext, createHostedAuthPendingStorageKey, clearHostedAuthPendingTransaction, HostedAuthRequiredError, HostedAuthError, createPkceVerifier, createPkceChallenge, createHostedAuthState, persistHostedAuthPendingTransaction, parseHostedAuthCallback, readHostedAuthPendingTransaction, HostedAuthStateMismatchError, buildHostedAuthSession, useSafePrivanaContext, usePrivateReadRequest, formatTokenAmount, submitPendingLock, PostDepositLockError, useDepositVerification, useEnsureCorrectChain, getTransactionReceipt, getBlockNumber, waitForTransactionReceipt, canUseBrowserStorage, clampLockAmount, requireDepositLockOwner, getWalletClient, createSignedLockRequest, requireServiceAddress, signWithdrawMessage, signLockMessage, signTransferMessage, signTransferLockedMessage, signModifyLockMessage, cn, stripHostedAuthCallbackParams, AccountingApiError, getBrowserStorageItem, removeBrowserStorageItem, isSignedLockUsable, setBrowserStorageItem, canUseSharedBrowserStorage, DEFAULT_LOCK_DURATION_SECONDS, parseTokenAmount, Skeleton, shortenAddress, formatCountdown, FiatOnRampForm, getExplorerAddressUrl, getExplorerLabel, formatTimeRemaining, getSharedBrowserStorageItem, removeSharedBrowserStorageItem, setSharedBrowserStorageItem } from './chunk-QQG4RPYZ.js';
3
+ export { AccountingApiError, Button, DEFAULT_LOCK_DURATION_SECONDS, DEFAULT_ONRAMP_LOCK_BUFFER, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, LOCK_TYPES, MODIFY_LOCK_TYPES, NETWORK_CONFIG, NetworkError, PostDepositLockError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, TRANSFER_LOCKED_TYPES, TRANSFER_TYPES, ValidationError, WITHDRAW_FROM_LOCK_TYPES, WITHDRAW_TYPES, applyLockBuffer, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clampLockAmount, clearHostedAuthPendingTransaction, clearPendingLock, createDomain, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createLockExpiry, createPkceChallenge, createPkceVerifier, createSignedLockRequest, getAccountingContract, getApiUrl, getChainById, getChainId, getExplorerAddressUrl, getExplorerLabel, isHostedAuthRefreshActive, isHostedAuthSessionActive, isSignedLockUsable, loadPendingLock, normalizeAddress, normalizeHex, parseHostedAuthCallback, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, requireDepositLockOwner, requireServiceAddress, savePendingLock, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, stripHostedAuthCallbackParams, submitPendingLock, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-QQG4RPYZ.js';
4
4
  import { useState, useRef, useMemo, useCallback, useContext, useEffect, useId } from 'react';
5
5
  import { QueryClientContext, useQuery, useQueryClient, useMutation, keepPreviousData } from '@tanstack/react-query';
6
6
  import { useAccount, useWalletClient, useConfig, useWriteContract, useSendTransaction, useBalance as useBalance$1, useReadContract } from 'wagmi';
7
7
  import { zeroAddress, erc20Abi, formatUnits } from 'viem';
8
+ import { MoonPayProvider, MoonPayContext } from '@moonpay/moonpay-react';
8
9
  import * as DialogPrimitive from '@radix-ui/react-dialog';
9
10
  import { XIcon } from 'lucide-react';
10
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
12
  import { toast } from 'sonner';
12
13
  import { QRCodeSVG } from 'qrcode.react';
13
- import { MoonPayProvider, MoonPayContext } from '@moonpay/moonpay-react';
14
14
 
15
15
  // src/sdk/hooks/use-privana-client.ts
16
16
  function usePrivanaClient() {
@@ -1488,16 +1488,6 @@ function DialogContent({
1488
1488
  )
1489
1489
  ] });
1490
1490
  }
1491
- function DialogHeader({ className, ...props }) {
1492
- return /* @__PURE__ */ jsx(
1493
- "div",
1494
- {
1495
- "data-slot": "dialog-header",
1496
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
1497
- ...props
1498
- }
1499
- );
1500
- }
1501
1491
  function DialogTitle({ className, ...props }) {
1502
1492
  return /* @__PURE__ */ jsx(
1503
1493
  DialogPrimitive.Title,
@@ -1521,2343 +1511,1319 @@ function DialogDescription({
1521
1511
  }
1522
1512
  );
1523
1513
  }
1524
- function USDCIcon({ className, size = 24 }) {
1525
- return /* @__PURE__ */ jsxs(
1526
- "svg",
1527
- {
1528
- width: size,
1529
- height: size,
1530
- viewBox: "0 0 2000 2000",
1531
- fill: "none",
1532
- xmlns: "http://www.w3.org/2000/svg",
1533
- className,
1534
- children: [
1535
- /* @__PURE__ */ jsx(
1536
- "path",
1537
- {
1538
- d: "M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z",
1539
- fill: "#2775ca"
1540
- }
1541
- ),
1542
- /* @__PURE__ */ jsx(
1543
- "path",
1544
- {
1545
- d: "M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z",
1546
- fill: "#fff"
1547
- }
1548
- ),
1549
- /* @__PURE__ */ jsx(
1550
- "path",
1551
- {
1552
- d: "M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z",
1553
- fill: "#fff"
1554
- }
1555
- )
1556
- ]
1557
- }
1558
- );
1514
+
1515
+ // src/sdk/hooks/external-deposit-lock.ts
1516
+ var ACTIVE_SESSION_PREFIX = "privana:external-deposit-lock:active:";
1517
+ var SESSION_PREFIX = "privana:external-deposit-lock:session:";
1518
+ var ExternalDepositLockSessionChangedError = class _ExternalDepositLockSessionChangedError extends Error {
1519
+ constructor() {
1520
+ super("External deposit lock session changed");
1521
+ this.name = "ExternalDepositLockSessionChangedError";
1522
+ Object.setPrototypeOf(this, _ExternalDepositLockSessionChangedError.prototype);
1523
+ }
1524
+ };
1525
+ function externalDepositSessionId(chainId, tokenId) {
1526
+ return `${chainId}:${tokenId.toLowerCase()}`;
1559
1527
  }
1560
- function BaseIcon({ className, size = 24 }) {
1561
- return /* @__PURE__ */ jsxs(
1562
- "svg",
1563
- {
1564
- width: size,
1565
- height: size,
1566
- viewBox: "0 0 2500 2500",
1567
- xmlns: "http://www.w3.org/2000/svg",
1568
- className,
1569
- children: [
1570
- /* @__PURE__ */ jsx("circle", { cx: "1250", cy: "1250", r: "1250", fill: "#0052FF" }),
1571
- /* @__PURE__ */ jsx(
1572
- "path",
1573
- {
1574
- d: "M1247.8,2500c691.6,0,1252.2-559.6,1252.2-1250C2500,559.6,1939.4,0,1247.8,0C591.7,0,53.5,503.8,0,1144.9h1655.1v210.2H0C53.5,1996.2,591.7,2500,1247.8,2500z",
1575
- fill: "white"
1576
- }
1577
- )
1578
- ]
1579
- }
1580
- );
1528
+ function isExternalDepositBlockInSession(session, blockNumber) {
1529
+ return Number.isSafeInteger(blockNumber) && blockNumber > session.startBlock;
1581
1530
  }
1582
- function WETHIcon({ className, size = 24 }) {
1583
- return /* @__PURE__ */ jsxs(
1584
- "svg",
1585
- {
1586
- width: size,
1587
- height: size,
1588
- viewBox: "0 0 250 250",
1589
- fill: "none",
1590
- xmlns: "http://www.w3.org/2000/svg",
1591
- className,
1592
- children: [
1593
- /* @__PURE__ */ jsx(
1594
- "path",
1595
- {
1596
- d: "M0 125C0 55.9644 55.9644 0 125 0C194.036 0 250 55.9644 250 125C250 194.036 194.036 250 125 250C55.9644 250 0 194.036 0 125Z",
1597
- fill: "#627EEA"
1598
- }
1599
- ),
1600
- /* @__PURE__ */ jsx(
1601
- "path",
1602
- {
1603
- d: "M125.047 30.5V100.351L184.086 126.732L125.047 30.5Z",
1604
- fill: "white",
1605
- fillOpacity: "0.602"
1606
- }
1607
- ),
1608
- /* @__PURE__ */ jsx("path", { d: "M125.047 30.5L66 126.732L125.047 100.351V30.5Z", fill: "white" }),
1609
- /* @__PURE__ */ jsx(
1610
- "path",
1611
- {
1612
- d: "M125.047 172V219.462L184.125 137.728L125.047 172Z",
1613
- fill: "white",
1614
- fillOpacity: "0.602"
1615
- }
1616
- ),
1617
- /* @__PURE__ */ jsx("path", { d: "M125.047 219.462V171.992L66 137.728L125.047 219.462Z", fill: "white" }),
1618
- /* @__PURE__ */ jsx(
1619
- "path",
1620
- {
1621
- d: "M125.047 161.013L184.086 126.733L125.047 100.368V161.013Z",
1622
- fill: "white",
1623
- fillOpacity: "0.2"
1624
- }
1625
- ),
1626
- /* @__PURE__ */ jsx("path", { d: "M66 126.733L125.047 161.013V100.368L66 126.733Z", fill: "white", fillOpacity: "0.602" })
1627
- ]
1628
- }
1629
- );
1531
+ function isCurrentExternalDepositVerification(session, txHash) {
1532
+ return session.creditedAmount === void 0 && session.verification?.hash.toLowerCase() === txHash.toLowerCase();
1630
1533
  }
1631
- function getTokenIcon(symbol, size) {
1632
- const iconSize = size ?? 24;
1633
- switch (symbol.toUpperCase()) {
1634
- case "USDC":
1635
- return /* @__PURE__ */ jsx(USDCIcon, { size: iconSize });
1636
- case "WETH":
1637
- return /* @__PURE__ */ jsx(WETHIcon, { size: iconSize });
1638
- default:
1639
- return /* @__PURE__ */ jsx(
1640
- "div",
1641
- {
1642
- className: "bg-muted flex items-center justify-center rounded-full text-xs font-bold",
1643
- style: { width: iconSize, height: iconSize },
1644
- children: symbol.slice(0, 2)
1645
- }
1646
- );
1534
+ function getExternalDepositMinimum(response, chainId) {
1535
+ const value = response?.min_deposit?.[String(chainId)]?.erc20;
1536
+ if (value === void 0) return void 0;
1537
+ try {
1538
+ const amount = BigInt(value);
1539
+ return amount >= 0n ? amount : void 0;
1540
+ } catch {
1541
+ return void 0;
1647
1542
  }
1648
1543
  }
1649
- function getChainIcon(chainId, size) {
1650
- const iconSize = size ?? 24;
1651
- switch (chainId) {
1652
- case 84532:
1653
- return /* @__PURE__ */ jsx(BaseIcon, { size: iconSize });
1654
- default:
1655
- return /* @__PURE__ */ jsx(
1656
- "div",
1657
- {
1658
- className: "bg-muted flex items-center justify-center rounded-full text-xs font-bold",
1659
- style: { width: iconSize, height: iconSize },
1660
- children: "?"
1661
- }
1662
- );
1544
+ function activeSessionKey(owner) {
1545
+ return `${ACTIVE_SESSION_PREFIX}${owner.toLowerCase()}`;
1546
+ }
1547
+ function sessionKey(owner, sessionId) {
1548
+ return `${SESSION_PREFIX}${owner.toLowerCase()}:${sessionId}`;
1549
+ }
1550
+ function activeSessionId(owner) {
1551
+ const value = getSharedBrowserStorageItem(activeSessionKey(owner));
1552
+ return value || void 0;
1553
+ }
1554
+ function isPositiveIntegerString(value) {
1555
+ if (typeof value !== "string") return false;
1556
+ try {
1557
+ return BigInt(value) > 0n;
1558
+ } catch {
1559
+ return false;
1663
1560
  }
1664
1561
  }
1665
- function CloseIcon() {
1666
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
1667
- "path",
1668
- {
1669
- d: "M11.99997 11.99997l-5.99995-5.99995-6.00002-6.00002m6.00002 6.00002l6.00001-6.00002m-12.00003 12.00003l6.00002-6.00001",
1670
- stroke: "currentColor",
1671
- strokeWidth: "1.5",
1672
- strokeLinecap: "round",
1673
- strokeLinejoin: "round"
1674
- }
1675
- ) });
1562
+ function isSessionRecord(value) {
1563
+ if (!value || typeof value !== "object") return false;
1564
+ const record = value;
1565
+ if (record.version !== 1 || typeof record.owner !== "string" || typeof record.serviceAddress !== "string" || !Number.isInteger(record.chainId) || (record.chainId ?? 0) <= 0 || typeof record.tokenId !== "string" || !Number.isSafeInteger(record.startBlock) || (record.startBlock ?? -1) < 0 || !isPositiveIntegerString(record.depositAmount) || !isPositiveIntegerString(record.maxLockAmount) || !Number.isInteger(record.lockDuration) || (record.lockDuration ?? 0) <= 0 || typeof record.generation !== "string" || !record.generation || record.submissionAmbiguous !== void 0 && typeof record.submissionAmbiguous !== "boolean" || record.creditedAmount !== void 0 && !isPositiveIntegerString(record.creditedAmount)) {
1566
+ return false;
1567
+ }
1568
+ if (record.verification && (typeof record.verification.hash !== "string" || record.verification.chainId !== record.chainId || !isPositiveIntegerString(record.verification.amount) || record.verification.logIndex !== void 0 && (!Number.isInteger(record.verification.logIndex) || record.verification.logIndex < 0))) {
1569
+ return false;
1570
+ }
1571
+ if (record.verification && record.creditedAmount !== void 0) return false;
1572
+ if (record.submissionAmbiguous && record.creditedAmount === void 0) return false;
1573
+ if (!record.payload) {
1574
+ return record.creditedAmount !== void 0 && !record.submissionAmbiguous;
1575
+ }
1576
+ if (typeof record.payload.signature !== "string" || typeof record.payload.token_id !== "string" || typeof record.payload.service_address !== "string" || record.payload.signature !== record.generation || record.payload.token_id.toLowerCase() !== record.tokenId.toLowerCase() || record.payload.service_address.toLowerCase() !== record.serviceAddress.toLowerCase() || !isPositiveIntegerString(record.payload.amount)) {
1577
+ return false;
1578
+ }
1579
+ return BigInt(record.payload.amount) <= BigInt(record.maxLockAmount);
1676
1580
  }
1677
- function ChevronRightIcon() {
1678
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "-rotate-90", children: /* @__PURE__ */ jsx(
1679
- "path",
1680
- {
1681
- d: "M0 0l6 6 6-6",
1682
- stroke: "currentColor",
1683
- strokeWidth: "1.5",
1684
- strokeLinecap: "round",
1685
- strokeLinejoin: "round",
1686
- fill: "none"
1687
- }
1688
- ) });
1581
+ function writeSessionRecord(session) {
1582
+ const id = externalDepositSessionId(session.chainId, session.tokenId);
1583
+ return setSharedBrowserStorageItem(sessionKey(session.owner, id), JSON.stringify(session));
1689
1584
  }
1690
- function ChevronLeftIcon() {
1691
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "rotate-90", children: /* @__PURE__ */ jsx(
1692
- "path",
1693
- {
1694
- d: "M0 0l6 6 6-6",
1695
- stroke: "currentColor",
1696
- strokeWidth: "1.5",
1697
- strokeLinecap: "round",
1698
- strokeLinejoin: "round",
1699
- fill: "none"
1585
+ function saveExternalDepositLockSession(session, expectedSession = null) {
1586
+ if (!isSessionRecord(session)) throw new Error("Invalid external deposit lock session");
1587
+ const current = loadExternalDepositLockSession(session.owner);
1588
+ if (expectedSession === null && current || expectedSession !== null && (!current || !isSameSessionSnapshot(current, expectedSession))) {
1589
+ throw new ExternalDepositLockSessionChangedError();
1590
+ }
1591
+ const nextId = externalDepositSessionId(session.chainId, session.tokenId);
1592
+ const previousId = activeSessionId(session.owner);
1593
+ if (!writeSessionRecord(session)) {
1594
+ throw new Error("Unable to persist external deposit lock session");
1595
+ }
1596
+ if (previousId !== nextId) {
1597
+ if (!setSharedBrowserStorageItem(activeSessionKey(session.owner), nextId)) {
1598
+ removeSharedBrowserStorageItem(sessionKey(session.owner, nextId));
1599
+ throw new Error("Unable to persist external deposit lock session");
1700
1600
  }
1701
- ) });
1702
- }
1703
- function ChevronDownIcon({
1704
- direction = "down",
1705
- className
1706
- }) {
1707
- return /* @__PURE__ */ jsx(
1708
- "svg",
1709
- {
1710
- width: "16",
1711
- height: "16",
1712
- viewBox: "0 0 16 16",
1713
- fill: "none",
1714
- className: cn(
1715
- "transition-transform",
1716
- direction === "right" && "-rotate-90",
1717
- direction === "up" && "rotate-180",
1718
- className
1719
- ),
1720
- children: /* @__PURE__ */ jsx(
1721
- "path",
1722
- {
1723
- d: "M4 6l4 4 4-4",
1724
- stroke: "currentColor",
1725
- strokeWidth: "1.5",
1726
- strokeLinecap: "round",
1727
- strokeLinejoin: "round"
1728
- }
1729
- )
1601
+ if (previousId) {
1602
+ removeSharedBrowserStorageItem(sessionKey(session.owner, previousId));
1730
1603
  }
1731
- );
1604
+ }
1732
1605
  }
1733
- function CopyIcon() {
1734
- return /* @__PURE__ */ jsxs(
1735
- "svg",
1736
- {
1737
- width: "16",
1738
- height: "16",
1739
- viewBox: "0 0 24 24",
1740
- fill: "none",
1741
- stroke: "currentColor",
1742
- strokeWidth: "2",
1743
- strokeLinecap: "round",
1744
- strokeLinejoin: "round",
1745
- children: [
1746
- /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
1747
- /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
1748
- ]
1606
+ function loadExternalDepositLockSession(owner) {
1607
+ const id = activeSessionId(owner);
1608
+ if (!id) return void 0;
1609
+ const key = sessionKey(owner, id);
1610
+ try {
1611
+ const raw = getSharedBrowserStorageItem(key);
1612
+ const parsed = raw ? JSON.parse(raw) : void 0;
1613
+ if (!isSessionRecord(parsed) || parsed.owner.toLowerCase() !== owner.toLowerCase() || externalDepositSessionId(parsed.chainId, parsed.tokenId) !== id) {
1614
+ removeSharedBrowserStorageItem(key);
1615
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
1616
+ return void 0;
1749
1617
  }
1750
- );
1618
+ return parsed;
1619
+ } catch {
1620
+ removeSharedBrowserStorageItem(key);
1621
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
1622
+ return void 0;
1623
+ }
1751
1624
  }
1752
- function CircleCheckIcon() {
1753
- return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
1754
- /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
1755
- /* @__PURE__ */ jsx(
1756
- "path",
1757
- {
1758
- d: "M6 10l2.5 2.5L14 7.5",
1759
- stroke: "currentColor",
1760
- strokeWidth: "1.5",
1761
- strokeLinecap: "round",
1762
- strokeLinejoin: "round"
1763
- }
1764
- )
1765
- ] });
1625
+ function subscribeExternalDepositLockSession(owner, onChange) {
1626
+ if (typeof window === "undefined") return () => void 0;
1627
+ let localStorage;
1628
+ try {
1629
+ localStorage = window.localStorage;
1630
+ } catch {
1631
+ return () => void 0;
1632
+ }
1633
+ const activeKey = activeSessionKey(owner);
1634
+ const ownerSessionPrefix = `${SESSION_PREFIX}${owner.toLowerCase()}:`;
1635
+ const handleStorage = (event) => {
1636
+ if (event.storageArea !== localStorage) return;
1637
+ if (event.key !== null && event.key !== activeKey && !event.key.startsWith(ownerSessionPrefix)) {
1638
+ return;
1639
+ }
1640
+ onChange(loadExternalDepositLockSession(owner));
1641
+ };
1642
+ window.addEventListener("storage", handleStorage);
1643
+ return () => window.removeEventListener("storage", handleStorage);
1766
1644
  }
1767
- function CircleXIcon() {
1768
- return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
1769
- /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
1770
- /* @__PURE__ */ jsx(
1771
- "path",
1772
- {
1773
- d: "M7 7l6 6M13 7l-6 6",
1774
- stroke: "currentColor",
1775
- strokeWidth: "1.5",
1776
- strokeLinecap: "round",
1777
- strokeLinejoin: "round"
1778
- }
1779
- )
1780
- ] });
1645
+ function clearExternalDepositLockSession(owner, expectedSessionId, expectedGeneration) {
1646
+ const id = activeSessionId(owner);
1647
+ if (!id || expectedSessionId && id !== expectedSessionId) return;
1648
+ if (expectedGeneration) {
1649
+ const current = loadExternalDepositLockSession(owner);
1650
+ if (!current || current.generation !== expectedGeneration) return;
1651
+ }
1652
+ removeSharedBrowserStorageItem(sessionKey(owner, id));
1653
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
1781
1654
  }
1782
- function ArrowRightIcon({ className }) {
1783
- return /* @__PURE__ */ jsx("svg", { width: "24", height: "8", viewBox: "0 0 24 8", fill: "none", className, children: /* @__PURE__ */ jsx(
1784
- "path",
1785
- {
1786
- d: "M0 4h22m0 0l-4-4m4 4l-4 4",
1787
- stroke: "currentColor",
1788
- strokeWidth: "1.5",
1789
- strokeLinecap: "round",
1790
- strokeLinejoin: "round"
1791
- }
1792
- ) });
1655
+ function isSameVerification(left, right) {
1656
+ if (!left || !right) return left === right;
1657
+ return left.hash.toLowerCase() === right.hash.toLowerCase() && left.chainId === right.chainId && left.amount === right.amount && (left.logIndex ?? 0) === (right.logIndex ?? 0);
1793
1658
  }
1794
- function CheckIcon() {
1795
- return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
1796
- "path",
1797
- {
1798
- d: "M2.5 6l2.5 2.5 4.5-4.5",
1799
- stroke: "currentColor",
1800
- strokeWidth: "1.5",
1801
- strokeLinecap: "round",
1802
- strokeLinejoin: "round"
1803
- }
1804
- ) });
1659
+ function isSamePayload(left, right) {
1660
+ if (!left || !right) return left === right;
1661
+ return left.service_address.toLowerCase() === right.service_address.toLowerCase() && left.token_id.toLowerCase() === right.token_id.toLowerCase() && left.amount === right.amount && left.expiry === right.expiry && left.nonce === right.nonce && left.signature === right.signature;
1805
1662
  }
1806
- function ExternalLinkIcon() {
1807
- return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
1808
- "path",
1663
+ function isSameSessionSnapshot(left, right) {
1664
+ return left.version === right.version && left.owner.toLowerCase() === right.owner.toLowerCase() && left.serviceAddress.toLowerCase() === right.serviceAddress.toLowerCase() && left.chainId === right.chainId && left.tokenId.toLowerCase() === right.tokenId.toLowerCase() && left.startBlock === right.startBlock && left.depositAmount === right.depositAmount && left.maxLockAmount === right.maxLockAmount && left.lockDuration === right.lockDuration && left.generation === right.generation && left.creditedAmount === right.creditedAmount && left.submissionAmbiguous === right.submissionAmbiguous && isSameVerification(left.verification, right.verification) && isSamePayload(left.payload, right.payload);
1665
+ }
1666
+ function isSameExternalDepositLockSession(left, right) {
1667
+ return isSameSessionSnapshot(left, right);
1668
+ }
1669
+ function clearExternalDepositVerification(session) {
1670
+ if (!session.verification || session.creditedAmount) return void 0;
1671
+ const current = loadExternalDepositLockSession(session.owner);
1672
+ if (!current || current.generation !== session.generation || current.creditedAmount || !isSameVerification(current.verification, session.verification)) {
1673
+ return void 0;
1674
+ }
1675
+ const next = { ...current, verification: void 0 };
1676
+ if (!writeSessionRecord(next)) {
1677
+ throw new Error("Unable to clear failed deposit verification");
1678
+ }
1679
+ return next;
1680
+ }
1681
+ function discardExternalDepositLockSession(session) {
1682
+ const current = loadExternalDepositLockSession(session.owner);
1683
+ if (!current) return true;
1684
+ if (!isSameSessionSnapshot(current, session)) return false;
1685
+ clearExternalDepositLockSession(
1686
+ current.owner,
1687
+ externalDepositSessionId(current.chainId, current.tokenId),
1688
+ current.generation
1689
+ );
1690
+ return loadExternalDepositLockSession(current.owner) === void 0;
1691
+ }
1692
+ function clearSubmittedPayload(session, signature, clearAmbiguous = false) {
1693
+ const current = loadExternalDepositLockSession(session.owner);
1694
+ if (!current?.payload || current.payload.signature !== signature || current.submissionAmbiguous && !clearAmbiguous) {
1695
+ return false;
1696
+ }
1697
+ return writeSessionRecord({ ...current, submissionAmbiguous: void 0, payload: void 0 });
1698
+ }
1699
+ function markSubmissionAmbiguous(session, signature) {
1700
+ const current = loadExternalDepositLockSession(session.owner);
1701
+ if (!current?.payload || current.payload.signature !== signature) return false;
1702
+ if (current.submissionAmbiguous) return true;
1703
+ return writeSessionRecord({ ...current, submissionAmbiguous: true });
1704
+ }
1705
+ function ambiguousSubmissionError(error) {
1706
+ return new PostDepositLockError(
1707
+ "Lock submission could not be confirmed. Retry only the saved signature, or check locked funds before stopping recovery.",
1708
+ error.reason,
1709
+ error.signedAmount,
1710
+ error.creditedAmount,
1809
1711
  {
1810
- d: "M3.5 1.5h7v7M10.5 1.5L1.5 10.5",
1811
- stroke: "currentColor",
1812
- strokeWidth: "1.5",
1813
- strokeLinecap: "round",
1814
- strokeLinejoin: "round"
1712
+ cause: error.cause,
1713
+ submissionMayHaveSucceeded: true
1815
1714
  }
1816
- ) });
1715
+ );
1817
1716
  }
1818
- function Spinner({ size = 20 }) {
1819
- return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none", className: "animate-spin", children: [
1820
- /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", opacity: "0.15" }),
1821
- /* @__PURE__ */ jsx("path", { d: "M10 2a8 8 0 016.93 4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
1822
- ] });
1717
+ function isDefinitiveSubmissionFailure(error) {
1718
+ const cause = error.cause;
1719
+ return cause instanceof AccountingApiError && cause.statusCode !== 408 && cause.statusCode < 500;
1823
1720
  }
1824
- function TransactionProgressView({ title, steps, onCancel }) {
1825
- const completed = steps.filter((s) => s.status === "completed").length;
1826
- const active = steps.find((s) => s.status === "active");
1827
- const total = steps.length;
1828
- const progress = (completed + (active ? 0.5 : 0)) / total * 100;
1829
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1830
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1831
- /* @__PURE__ */ jsx("div", { className: "text-primary", children: /* @__PURE__ */ jsx(Spinner, { size: 20 }) }),
1832
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: active?.label ?? title })
1833
- ] }),
1834
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
1835
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-1.5 w-full overflow-hidden rounded-full", children: /* @__PURE__ */ jsx(
1836
- "div",
1837
- {
1838
- className: "bg-primary h-full rounded-full transition-all duration-500",
1839
- style: { width: `${progress}%` }
1840
- }
1841
- ) }),
1842
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
1843
- "Step ",
1844
- Math.max(1, completed + (active ? 1 : 0)),
1845
- " of ",
1846
- total
1847
- ] })
1848
- ] }),
1849
- onCancel && /* @__PURE__ */ jsx(
1850
- "button",
1851
- {
1852
- onClick: onCancel,
1853
- className: "border-border text-foreground hover:bg-secondary flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors",
1854
- children: "Cancel"
1855
- }
1856
- )
1857
- ] });
1721
+ function submissionLockName(session) {
1722
+ return `privana:external-deposit-lock:submit:${session.owner.toLowerCase()}:${externalDepositSessionId(session.chainId, session.tokenId)}`;
1858
1723
  }
1859
- function TransactionSuccessView({
1860
- title,
1861
- message,
1862
- explorerUrl,
1863
- explorerLabel,
1864
- onDone
1865
- }) {
1866
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1867
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1868
- /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-400", children: /* @__PURE__ */ jsx(CheckIcon, {}) }),
1869
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
1870
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
1871
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1872
- ] })
1873
- ] }),
1874
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1875
- explorerUrl && /* @__PURE__ */ jsxs(
1876
- "a",
1877
- {
1878
- href: explorerUrl,
1879
- target: "_blank",
1880
- rel: "noopener noreferrer",
1881
- className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 items-center justify-center gap-1.5 rounded-[10px] border text-sm transition-colors",
1882
- children: [
1883
- explorerLabel ?? "View on Explorer",
1884
- /* @__PURE__ */ jsx(ExternalLinkIcon, {})
1885
- ]
1886
- }
1887
- ),
1888
- /* @__PURE__ */ jsx(
1889
- "button",
1890
- {
1891
- onClick: onDone,
1892
- className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
1893
- children: "Done"
1894
- }
1895
- )
1896
- ] })
1897
- ] });
1724
+ async function withSubmissionLock(session, task) {
1725
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
1726
+ if (!locks) return task(false);
1727
+ return locks.request(submissionLockName(session), () => task(true));
1898
1728
  }
1899
- function WarningIcon() {
1900
- return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: /* @__PURE__ */ jsx(
1901
- "path",
1902
- {
1903
- d: "M7 5v2.5M7 10h.005M6.13 2.5h1.74L13 11.5H1L6.13 2.5z",
1904
- stroke: "currentColor",
1905
- strokeWidth: "1.5",
1906
- strokeLinecap: "round",
1907
- strokeLinejoin: "round"
1729
+ function externalDepositRetryAmount(session) {
1730
+ if (!session.creditedAmount) throw new Error("No credited external deposit to recover");
1731
+ const creditedAmount = BigInt(session.creditedAmount);
1732
+ const maxLockAmount = BigInt(session.maxLockAmount);
1733
+ return creditedAmount < maxLockAmount ? creditedAmount : maxLockAmount;
1734
+ }
1735
+ async function submitExternalDepositLock(client, session) {
1736
+ return withSubmissionLock(session, async (crossTabExclusive) => {
1737
+ const current = loadExternalDepositLockSession(session.owner);
1738
+ if (!current || current.generation !== session.generation) {
1739
+ throw new PostDepositLockError(
1740
+ "External deposit lock session changed before submission",
1741
+ "not-found",
1742
+ BigInt(session.maxLockAmount),
1743
+ session.creditedAmount ? BigInt(session.creditedAmount) : void 0
1744
+ );
1908
1745
  }
1909
- ) });
1746
+ const result = await submitExternalDepositLockAttempt(client, current, crossTabExclusive);
1747
+ clearExternalDepositLockSession(
1748
+ current.owner,
1749
+ externalDepositSessionId(current.chainId, current.tokenId),
1750
+ current.generation
1751
+ );
1752
+ return result;
1753
+ });
1910
1754
  }
1911
- function TransactionWarningView({
1912
- title,
1913
- message,
1914
- onDone,
1915
- actionLabel = "Done",
1916
- pendingLabel = actionLabel,
1917
- isPending = false,
1918
- onDismiss,
1919
- dismissLabel = "Dismiss"
1920
- }) {
1921
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1922
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1923
- /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-amber-400", children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
1924
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
1925
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
1926
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1927
- ] })
1928
- ] }),
1929
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1930
- onDismiss && /* @__PURE__ */ jsx(
1931
- "button",
1932
- {
1933
- onClick: onDismiss,
1934
- disabled: isPending,
1935
- className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1936
- children: dismissLabel
1937
- }
1938
- ),
1939
- /* @__PURE__ */ jsx(
1940
- "button",
1941
- {
1942
- onClick: onDone,
1943
- disabled: isPending,
1944
- className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1945
- children: isPending ? pendingLabel : actionLabel
1755
+ async function submitExternalDepositLockAttempt(client, session, crossTabExclusive) {
1756
+ const creditedAmount = session.creditedAmount ? BigInt(session.creditedAmount) : void 0;
1757
+ if (!creditedAmount) {
1758
+ throw new PostDepositLockError(
1759
+ "External deposit has not been credited",
1760
+ "submission-failed",
1761
+ BigInt(session.maxLockAmount)
1762
+ );
1763
+ }
1764
+ const payload = session.payload;
1765
+ if (!payload) {
1766
+ throw new PostDepositLockError(
1767
+ "No persisted signed policy found for this deposit session",
1768
+ "not-found",
1769
+ BigInt(session.maxLockAmount),
1770
+ creditedAmount
1771
+ );
1772
+ }
1773
+ try {
1774
+ return await submitPendingLock({
1775
+ client,
1776
+ payload,
1777
+ creditedAmount,
1778
+ beforeSubmit: () => {
1779
+ if (!markSubmissionAmbiguous(session, payload.signature)) {
1780
+ throw new Error("Unable to persist lock submission recovery state");
1946
1781
  }
1947
- )
1948
- ] })
1949
- ] });
1950
- }
1951
- function TransactionErrorView({
1952
- title,
1953
- message,
1954
- explorerUrl,
1955
- explorerLabel,
1956
- onRetry,
1957
- onDismiss,
1958
- isRetrying,
1959
- retryLabel = "Retry Verification",
1960
- dismissLabel = "Close"
1961
- }) {
1962
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1963
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1964
- /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-amber-400", children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
1965
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
1966
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
1967
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1968
- ] })
1969
- ] }),
1970
- explorerUrl && /* @__PURE__ */ jsxs(
1971
- "a",
1972
- {
1973
- href: explorerUrl,
1974
- target: "_blank",
1975
- rel: "noopener noreferrer",
1976
- className: "border-border text-foreground hover:bg-secondary flex h-10 w-full items-center justify-center gap-1.5 rounded-[10px] border text-sm transition-colors",
1977
- children: [
1978
- explorerLabel ?? "View on Explorer",
1979
- /* @__PURE__ */ jsx(ExternalLinkIcon, {})
1980
- ]
1981
1782
  }
1982
- ),
1983
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1984
- onDismiss && /* @__PURE__ */ jsx(
1985
- "button",
1986
- {
1987
- onClick: onDismiss,
1988
- disabled: isRetrying,
1989
- className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1990
- children: dismissLabel
1991
- }
1992
- ),
1993
- /* @__PURE__ */ jsx(
1994
- "button",
1995
- {
1996
- onClick: onRetry,
1997
- disabled: isRetrying,
1998
- className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1999
- children: isRetrying ? "Retrying\u2026" : retryLabel
1783
+ });
1784
+ } catch (err) {
1785
+ if (err instanceof PostDepositLockError && err.reason === "submission-failed" && crossTabExclusive && !session.submissionAmbiguous && isDefinitiveSubmissionFailure(err)) {
1786
+ if (!clearSubmittedPayload(session, payload.signature, true)) {
1787
+ const latest = loadExternalDepositLockSession(session.owner);
1788
+ if (latest?.submissionAmbiguous) throw ambiguousSubmissionError(err);
1789
+ }
1790
+ } else if (err instanceof PostDepositLockError && err.reason === "submission-failed") {
1791
+ const ambiguous = markSubmissionAmbiguous(session, payload.signature) || loadExternalDepositLockSession(session.owner)?.submissionAmbiguous === true;
1792
+ if (ambiguous) throw ambiguousSubmissionError(err);
1793
+ } else {
1794
+ const current = loadExternalDepositLockSession(session.owner);
1795
+ if (session.submissionAmbiguous || current?.submissionAmbiguous) {
1796
+ if (err instanceof PostDepositLockError) throw ambiguousSubmissionError(err);
1797
+ throw err;
1798
+ }
1799
+ if (!clearSubmittedPayload(session, payload.signature)) {
1800
+ const latest = loadExternalDepositLockSession(session.owner);
1801
+ if (latest?.submissionAmbiguous && err instanceof PostDepositLockError) {
1802
+ throw ambiguousSubmissionError(err);
2000
1803
  }
2001
- )
2002
- ] })
2003
- ] });
2004
- }
2005
- function DepositForm({
2006
- selectedToken,
2007
- onTokenSelect,
2008
- onPendingChange,
2009
- onUnsafeToCloseChange,
2010
- onSuccess
2011
- }) {
2012
- const { isConnected, address } = useAccount();
2013
- const { chains, getChainById: getChainById2 } = usePrivanaContext();
2014
- const [amount, setAmount] = useState("");
2015
- const [showSuccess, setShowSuccess] = useState(false);
2016
- const [showTimeout, setShowTimeout] = useState(false);
2017
- const [cancelled, setCancelled] = useState(false);
2018
- const targetChain = getChainById2(selectedToken.chainId) ?? chains[0];
2019
- const isNative = selectedToken.contract === zeroAddress;
2020
- const { data: nativeBalanceData } = useBalance$1({
2021
- address,
2022
- chainId: targetChain?.id,
2023
- query: {
2024
- enabled: !!address && isNative
2025
- }
2026
- });
2027
- const { data: erc20Balance } = useReadContract({
2028
- address: selectedToken.contract,
2029
- abi: erc20Abi,
2030
- functionName: "balanceOf",
2031
- args: address ? [address] : void 0,
2032
- chainId: targetChain?.id,
2033
- query: {
2034
- enabled: !!address && !isNative
2035
- }
2036
- });
2037
- const walletBalance = isNative ? nativeBalanceData?.value : erc20Balance;
2038
- const formattedWalletBalance = walletBalance ? formatTokenAmount(walletBalance.toString(), selectedToken.decimals) : "0.00";
2039
- const handleMaxClick = () => {
2040
- if (formattedWalletBalance && parseFloat(formattedWalletBalance) > 0) {
2041
- setAmount(formattedWalletBalance.replace(/[\s\u2009]/g, ""));
2042
- }
2043
- };
2044
- const hasValidAmount = amount && parseFloat(amount) > 0;
2045
- const tooManyDecimals = hasValidAmount && amount.includes(".") && amount.split(".")[1].length > selectedToken.decimals;
2046
- const exceedsBalance = hasValidAmount && !tooManyDecimals && walletBalance != null && parseTokenAmount(amount, selectedToken.decimals) > walletBalance;
2047
- const {
2048
- txHash,
2049
- isGettingAddress,
2050
- isSwitchingChain,
2051
- isSendingTransaction,
2052
- isWaitingForConfirmation,
2053
- isWaitingForProcessing,
2054
- verificationFailed,
2055
- isPending,
2056
- error,
2057
- deposit,
2058
- retryVerification,
2059
- reset
2060
- } = useDeposit({
2061
- onCredited: () => {
2062
- setAmount("");
2063
- if (onSuccess) {
2064
- reset();
2065
- onSuccess();
2066
- } else {
2067
- setShowSuccess(true);
2068
1804
  }
2069
- },
2070
- onCheckTimeout: () => {
2071
- setAmount("");
2072
- setShowTimeout(true);
2073
1805
  }
2074
- });
2075
- const depositSteps = [
2076
- {
2077
- label: "Getting deposit address",
2078
- status: isGettingAddress ? "active" : isSwitchingChain || isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
2079
- },
2080
- {
2081
- label: `Switching to ${targetChain?.name ?? "deposit chain"}`,
2082
- status: isSwitchingChain ? "active" : isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
1806
+ throw err;
1807
+ }
1808
+ }
1809
+ function useExternalDepositLock({
1810
+ allowance,
1811
+ onLockSubmitted,
1812
+ onLockFailed
1813
+ }) {
1814
+ const { client, networkConfig, serviceAddress, getTokenById, tokensStatus } = usePrivanaContext();
1815
+ const config = useConfig();
1816
+ const queryClient = useQueryClient();
1817
+ const { address } = useAccount();
1818
+ const { privateReadAddress } = usePrivateReadRequest();
1819
+ const { ensureCorrectChain } = useEnsureCorrectChain();
1820
+ const [isSigning, setIsSigning] = useState(false);
1821
+ const [isSubmittingLock, setIsSubmittingLock] = useState(false);
1822
+ const [session, setSession] = useState(null);
1823
+ const sessionRef = useRef(null);
1824
+ const signingRef = useRef(false);
1825
+ const settlingRef = useRef(false);
1826
+ const pendingCreditedTransitionRef = useRef(null);
1827
+ const hydratedScopeRef = useRef(null);
1828
+ const beneficiaryRef = useRef(privateReadAddress);
1829
+ beneficiaryRef.current = privateReadAddress;
1830
+ const onLockSubmittedRef = useRef(onLockSubmitted);
1831
+ const onLockFailedRef = useRef(onLockFailed);
1832
+ useEffect(() => {
1833
+ onLockSubmittedRef.current = onLockSubmitted;
1834
+ onLockFailedRef.current = onLockFailed;
1835
+ }, [onLockSubmitted, onLockFailed]);
1836
+ const installSession = useCallback((next) => {
1837
+ sessionRef.current = next;
1838
+ setSession(next);
1839
+ }, []);
1840
+ const preserveCreditedFallback = useCallback(
1841
+ (durable) => {
1842
+ const pending = pendingCreditedTransitionRef.current;
1843
+ if (pending && durable && isSameExternalDepositLockSession(durable, pending.predecessor)) {
1844
+ return pending.credited;
1845
+ }
1846
+ pendingCreditedTransitionRef.current = null;
1847
+ return durable;
2083
1848
  },
2084
- {
2085
- label: "Confirm in wallet",
2086
- status: isSendingTransaction ? "active" : isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
1849
+ []
1850
+ );
1851
+ const notifyFailure = useCallback((error) => {
1852
+ onLockFailedRef.current?.(error);
1853
+ }, []);
1854
+ const submitSession = useCallback(
1855
+ async (active) => {
1856
+ if (settlingRef.current) return;
1857
+ settlingRef.current = true;
1858
+ setIsSubmittingLock(true);
1859
+ let result;
1860
+ try {
1861
+ let submission = active;
1862
+ const storedBeforeSubmit = loadExternalDepositLockSession(active.owner);
1863
+ if (storedBeforeSubmit?.generation === active.generation) {
1864
+ if (active.creditedAmount && !storedBeforeSubmit.creditedAmount) {
1865
+ const pending = pendingCreditedTransitionRef.current;
1866
+ if (!pending || !isSameExternalDepositLockSession(active, pending.credited) || !isSameExternalDepositLockSession(storedBeforeSubmit, pending.predecessor)) {
1867
+ throw new ExternalDepositLockSessionChangedError();
1868
+ }
1869
+ saveExternalDepositLockSession(pending.credited, pending.predecessor);
1870
+ pendingCreditedTransitionRef.current = null;
1871
+ submission = pending.credited;
1872
+ } else {
1873
+ submission = storedBeforeSubmit;
1874
+ }
1875
+ }
1876
+ result = await submitExternalDepositLock(client, submission);
1877
+ } catch (err) {
1878
+ const stored = loadExternalDepositLockSession(active.owner);
1879
+ const error = err instanceof PostDepositLockError ? err : new PostDepositLockError(
1880
+ err instanceof Error ? err.message : "Lock submission failed",
1881
+ "submission-failed",
1882
+ BigInt(active.maxLockAmount),
1883
+ active.creditedAmount ? BigInt(active.creditedAmount) : void 0,
1884
+ { cause: err }
1885
+ );
1886
+ if (err instanceof ExternalDepositLockSessionChangedError) {
1887
+ pendingCreditedTransitionRef.current = null;
1888
+ installSession(stored ?? null);
1889
+ return;
1890
+ }
1891
+ if (!stored || stored.generation !== active.generation) {
1892
+ pendingCreditedTransitionRef.current = null;
1893
+ installSession(stored ?? null);
1894
+ return;
1895
+ }
1896
+ if (active.creditedAmount && !stored.creditedAmount) {
1897
+ const pending = pendingCreditedTransitionRef.current;
1898
+ if (pending && isSameExternalDepositLockSession(active, pending.credited) && isSameExternalDepositLockSession(stored, pending.predecessor)) {
1899
+ installSession(pending.credited);
1900
+ notifyFailure(error);
1901
+ return;
1902
+ }
1903
+ pendingCreditedTransitionRef.current = null;
1904
+ installSession(stored);
1905
+ return;
1906
+ }
1907
+ installSession(active.creditedAmount && !stored.creditedAmount ? active : stored);
1908
+ notifyFailure(error);
1909
+ return;
1910
+ } finally {
1911
+ settlingRef.current = false;
1912
+ setIsSubmittingLock(false);
1913
+ }
1914
+ installSession(loadExternalDepositLockSession(active.owner) ?? null);
1915
+ queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
1916
+ queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
1917
+ queryClient.invalidateQueries({ queryKey: ["accounting-total-locked-balance"] });
1918
+ queryClient.invalidateQueries({ queryKey: ["accounting-history"] });
1919
+ onLockSubmittedRef.current?.(result);
2087
1920
  },
2088
- {
2089
- label: "Confirming transaction",
2090
- status: isWaitingForConfirmation ? "active" : isWaitingForProcessing ? "completed" : "pending"
1921
+ [client, installSession, notifyFailure, queryClient]
1922
+ );
1923
+ const validateRestoredSession = useCallback(
1924
+ (restored) => {
1925
+ if (!restored || !serviceAddress) return null;
1926
+ const token = getTokenById(restored.tokenId);
1927
+ const invalid = restored.serviceAddress.toLowerCase() !== serviceAddress.toLowerCase() || !token || token.chainId !== restored.chainId || token.contract === zeroAddress || !restored.creditedAmount && !restored.payload;
1928
+ if (!invalid) return restored;
1929
+ clearExternalDepositLockSession(
1930
+ restored.owner,
1931
+ externalDepositSessionId(restored.chainId, restored.tokenId),
1932
+ restored.generation
1933
+ );
1934
+ return null;
2091
1935
  },
2092
- {
2093
- label: "Verifying deposit \u2014 may take up to a few minutes",
2094
- status: isWaitingForProcessing ? "active" : "pending"
2095
- }
2096
- ];
1936
+ [getTokenById, serviceAddress]
1937
+ );
2097
1938
  useEffect(() => {
2098
- if (error && !verificationFailed) {
2099
- toast.error(error.message.length > 100 ? `${error.message.slice(0, 100)}...` : error.message);
1939
+ if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") {
1940
+ setSession(null);
1941
+ return;
2100
1942
  }
2101
- }, [error, verificationFailed]);
2102
- useEffect(() => {
2103
- onPendingChange?.(isPending && !cancelled);
2104
- }, [isPending, cancelled, onPendingChange]);
1943
+ const hydrationKey = `${privateReadAddress.toLowerCase()}:${serviceAddress.toLowerCase()}`;
1944
+ const shouldResume = hydratedScopeRef.current !== hydrationKey;
1945
+ hydratedScopeRef.current = hydrationKey;
1946
+ const restored = preserveCreditedFallback(
1947
+ validateRestoredSession(loadExternalDepositLockSession(privateReadAddress))
1948
+ );
1949
+ installSession(restored);
1950
+ if (!restored) {
1951
+ return;
1952
+ }
1953
+ if (!shouldResume) return;
1954
+ if (restored.creditedAmount && restored.payload) {
1955
+ void submitSession(restored);
1956
+ } else if (restored.creditedAmount) {
1957
+ notifyFailure(
1958
+ new PostDepositLockError(
1959
+ "Deposit credited; sign a fresh policy to lock the funds",
1960
+ "not-found",
1961
+ BigInt(restored.maxLockAmount),
1962
+ BigInt(restored.creditedAmount)
1963
+ )
1964
+ );
1965
+ }
1966
+ }, [
1967
+ installSession,
1968
+ notifyFailure,
1969
+ privateReadAddress,
1970
+ preserveCreditedFallback,
1971
+ serviceAddress,
1972
+ submitSession,
1973
+ tokensStatus,
1974
+ validateRestoredSession
1975
+ ]);
2105
1976
  useEffect(() => {
2106
- onUnsafeToCloseChange?.((isGettingAddress || isSendingTransaction) && !cancelled);
2107
- }, [isGettingAddress, isSendingTransaction, cancelled, onUnsafeToCloseChange]);
2108
- const handleCancel = () => {
2109
- setCancelled(true);
2110
- reset();
2111
- };
2112
- const handleDone = () => {
2113
- setShowSuccess(false);
2114
- setShowTimeout(false);
2115
- setCancelled(false);
2116
- reset();
2117
- };
2118
- const handleDismissVerificationError = () => {
2119
- setAmount("");
2120
- setCancelled(false);
2121
- reset();
2122
- };
2123
- const handleRetryVerification = () => {
2124
- retryVerification().catch(() => {
2125
- });
2126
- };
2127
- const explorerTxUrl = txHash && targetChain?.explorerUrl ? `${targetChain.explorerUrl}/tx/${txHash}` : void 0;
2128
- const handleSubmit = async () => {
2129
- if (!amount || !selectedToken || exceedsBalance) return;
2130
- setCancelled(false);
2131
- const amountInWei = parseTokenAmount(amount, selectedToken.decimals);
2132
- await deposit({
2133
- tokenId: selectedToken.id,
2134
- amount: amountInWei
2135
- });
2136
- };
2137
- const getButtonText = () => {
2138
- if (!isConnected) return "Connect Wallet";
2139
- if (isSwitchingChain) return "Switching\u2026";
2140
- return "Deposit";
2141
- };
2142
- if (showSuccess) {
2143
- return /* @__PURE__ */ jsx(
2144
- TransactionSuccessView,
2145
- {
2146
- title: "Deposit Successful",
2147
- message: `Your ${selectedToken.symbol} deposit has been processed.`,
2148
- onDone: handleDone
1977
+ if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") return;
1978
+ const reconcile = (restored) => {
1979
+ installSession(preserveCreditedFallback(validateRestoredSession(restored)));
1980
+ };
1981
+ const unsubscribe = subscribeExternalDepositLockSession(privateReadAddress, reconcile);
1982
+ reconcile(loadExternalDepositLockSession(privateReadAddress));
1983
+ return unsubscribe;
1984
+ }, [
1985
+ installSession,
1986
+ preserveCreditedFallback,
1987
+ privateReadAddress,
1988
+ serviceAddress,
1989
+ tokensStatus,
1990
+ validateRestoredSession
1991
+ ]);
1992
+ const signAndPersist = useCallback(
1993
+ async ({ tokenId, amount }) => {
1994
+ if (!allowance) throw new Error("No allowance configured for this deposit");
1995
+ if (signingRef.current || settlingRef.current) {
1996
+ throw new Error("A policy operation is already in progress");
2149
1997
  }
2150
- );
2151
- }
2152
- if (showTimeout) {
2153
- return /* @__PURE__ */ jsx(
2154
- TransactionWarningView,
2155
- {
2156
- title: "Deposit Processing",
2157
- message: `Your transaction was confirmed but the deposit is still being processed. Please check your balance - it should update shortly.`,
2158
- onDone: handleDone
1998
+ if (!canUseSharedBrowserStorage()) {
1999
+ throw new Error("Browser storage is required for locked deposit recovery");
2159
2000
  }
2160
- );
2161
- }
2162
- if (verificationFailed) {
2163
- const baseMessage = "Your transfer was sent on-chain but we could not verify the deposit. The funds are already at the deposit address. Retry verification instead of starting a new deposit.";
2164
- const detail = error?.message;
2165
- const message = detail ? `${baseMessage} (${detail})` : baseMessage;
2166
- return /* @__PURE__ */ jsx(
2167
- TransactionErrorView,
2168
- {
2169
- title: "Verification failed",
2170
- message,
2171
- explorerUrl: explorerTxUrl,
2172
- explorerLabel: "View transaction",
2173
- onRetry: handleRetryVerification,
2174
- onDismiss: handleDismissVerificationError
2001
+ const owner = requireDepositLockOwner(address, privateReadAddress);
2002
+ if (sessionRef.current || loadExternalDepositLockSession(owner)) {
2003
+ throw new Error("Finish or cancel the active external deposit before starting another");
2175
2004
  }
2176
- );
2177
- }
2178
- if (isPending && !cancelled) {
2179
- const canCancel = isGettingAddress || isSendingTransaction;
2180
- return /* @__PURE__ */ jsx(
2181
- TransactionProgressView,
2182
- {
2183
- title: "Depositing\u2026",
2184
- steps: depositSteps,
2185
- onCancel: canCancel ? handleCancel : void 0
2005
+ const token = getTokenById(tokenId);
2006
+ if (!token) throw new Error(`Unknown token ID: ${tokenId}`);
2007
+ if (token.contract === zeroAddress) {
2008
+ throw new Error("External deposits support ERC20 tokens only");
2186
2009
  }
2187
- );
2188
- }
2189
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-6", children: [
2190
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2191
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
2192
- /* @__PURE__ */ jsxs(
2193
- "button",
2194
- {
2195
- onClick: onTokenSelect,
2196
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3",
2197
- children: [
2198
- /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
2199
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-start gap-1", children: [
2200
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: selectedToken.symbol }),
2201
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2202
- "on ",
2203
- targetChain?.name ?? "Base Sepolia"
2204
- ] })
2205
- ] }),
2206
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2207
- ]
2010
+ const targetService = requireServiceAddress(serviceAddress);
2011
+ const lockDuration = allowance.lockDuration ?? DEFAULT_LOCK_DURATION_SECONDS;
2012
+ signingRef.current = true;
2013
+ setIsSigning(true);
2014
+ try {
2015
+ await ensureCorrectChain(networkConfig.chainId);
2016
+ const signingWalletClient = await getWalletClient(config, {
2017
+ chainId: networkConfig.chainId
2018
+ });
2019
+ const payload = await createSignedLockRequest({
2020
+ client,
2021
+ walletClient: signingWalletClient,
2022
+ userAddress: owner,
2023
+ networkConfig,
2024
+ serviceAddress: targetService,
2025
+ tokenId: token.id,
2026
+ amount: clampLockAmount(amount, BigInt(allowance.value)),
2027
+ lockDuration
2028
+ });
2029
+ const startBlock = await getBlockNumber(config, { chainId: token.chainId });
2030
+ if (startBlock > BigInt(Number.MAX_SAFE_INTEGER)) {
2031
+ throw new Error("Source-chain block number exceeds the supported range");
2208
2032
  }
2209
- )
2210
- ] }),
2211
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2212
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2213
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Amount" }),
2214
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-sm", children: [
2215
- formattedWalletBalance,
2216
- " ",
2217
- selectedToken.symbol
2218
- ] })
2219
- ] }),
2220
- /* @__PURE__ */ jsxs("div", { className: "border-border bg-input flex items-center gap-2 rounded-[10px] border py-1 pr-1 pl-3", children: [
2221
- /* @__PURE__ */ jsx(
2222
- "input",
2223
- {
2224
- type: "text",
2225
- inputMode: "decimal",
2226
- placeholder: "Enter Amount",
2227
- value: amount,
2228
- onChange: (e) => {
2229
- const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
2230
- if (value.split(".").length <= 2) {
2231
- setAmount(value);
2232
- }
2233
- },
2234
- className: cn(
2235
- "text-foreground flex-1 bg-transparent text-sm outline-none",
2236
- "placeholder:text-muted-foreground/50"
2237
- )
2238
- }
2239
- ),
2240
- /* @__PURE__ */ jsx(
2241
- "button",
2242
- {
2243
- onClick: handleMaxClick,
2244
- className: "bg-secondary text-foreground hover:bg-secondary/80 cursor-pointer rounded px-3 py-2.5 text-xs font-semibold transition-colors",
2245
- children: "MAX"
2246
- }
2247
- )
2248
- ] }),
2249
- tooManyDecimals && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
2250
- "Too many decimal places (max: ",
2251
- selectedToken.decimals,
2252
- ")"
2253
- ] }),
2254
- exceedsBalance && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Insufficient balance" })
2255
- ] }),
2256
- /* @__PURE__ */ jsx(
2257
- "button",
2258
- {
2259
- onClick: handleSubmit,
2260
- disabled: !isConnected || !hasValidAmount || tooManyDecimals || !!exceedsBalance || isPending,
2261
- className: cn(
2262
- "flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
2263
- "bg-primary text-primary-foreground hover:bg-primary/90",
2264
- "disabled:cursor-not-allowed disabled:opacity-50"
2265
- ),
2266
- children: getButtonText()
2033
+ if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
2034
+ throw new Error("Authenticated deposit account changed while signing");
2035
+ }
2036
+ const next = {
2037
+ version: 1,
2038
+ owner,
2039
+ serviceAddress: targetService,
2040
+ chainId: token.chainId,
2041
+ tokenId: token.id,
2042
+ startBlock: Number(startBlock),
2043
+ depositAmount: amount.toString(),
2044
+ maxLockAmount: String(payload.amount),
2045
+ lockDuration,
2046
+ generation: payload.signature,
2047
+ payload
2048
+ };
2049
+ saveExternalDepositLockSession(next, null);
2050
+ installSession(next);
2051
+ } finally {
2052
+ signingRef.current = false;
2053
+ setIsSigning(false);
2267
2054
  }
2268
- )
2269
- ] });
2270
- }
2271
- function WithdrawForm({
2272
- selectedToken,
2273
- onTokenSelect,
2274
- onPendingChange,
2275
- onUnsafeToCloseChange
2276
- }) {
2277
- const { isConnected, address } = useAccount();
2278
- const { chains, getChainById: getChainById2 } = usePrivanaContext();
2279
- const [amount, setAmount] = useState("");
2280
- const [showSuccess, setShowSuccess] = useState(false);
2281
- const [showTimeout, setShowTimeout] = useState(false);
2282
- const [cancelled, setCancelled] = useState(false);
2283
- const targetChain = getChainById2(selectedToken.chainId) ?? chains[0];
2284
- const {
2285
- balanceWei,
2286
- isLoading: isBalanceLoading,
2287
- isError: isBalanceError
2288
- } = useBalance({
2289
- tokenId: selectedToken.id
2290
- });
2291
- const formattedBalance = formatTokenAmount(balanceWei, selectedToken.decimals);
2292
- const { withdraw, isPending, currentStep, error, reset } = useWithdraw({
2293
- onProcessingSuccess: () => {
2294
- setAmount("");
2295
- setShowSuccess(true);
2296
2055
  },
2297
- onProcessingTimeout: () => {
2298
- setAmount("");
2299
- setShowTimeout(true);
2300
- }
2301
- });
2302
- const explorerUrl = address && targetChain ? getExplorerAddressUrl(targetChain.id, address) : void 0;
2303
- const getStepStatus = (step, after) => {
2304
- if (currentStep === step) return "active";
2305
- if (after.includes(currentStep)) return "completed";
2306
- return "pending";
2307
- };
2308
- const withdrawSteps = [
2309
- {
2310
- label: "Switching to signing chain",
2311
- status: getStepStatus("switching-chain", ["signing", "submitting", "processing"])
2056
+ [
2057
+ address,
2058
+ allowance,
2059
+ client,
2060
+ config,
2061
+ ensureCorrectChain,
2062
+ getTokenById,
2063
+ installSession,
2064
+ networkConfig,
2065
+ privateReadAddress,
2066
+ serviceAddress
2067
+ ]
2068
+ );
2069
+ const settleAfterCredit = useCallback(
2070
+ async (txHash, creditedAmount) => {
2071
+ const active = sessionRef.current;
2072
+ if (!active || settlingRef.current) return;
2073
+ if (!isCurrentExternalDepositVerification(active, txHash)) {
2074
+ return;
2075
+ }
2076
+ if (creditedAmount <= 0n) throw new Error("Credited amount must be positive");
2077
+ const credited = {
2078
+ ...active,
2079
+ verification: void 0,
2080
+ creditedAmount: creditedAmount.toString()
2081
+ };
2082
+ try {
2083
+ saveExternalDepositLockSession(credited, active);
2084
+ pendingCreditedTransitionRef.current = null;
2085
+ } catch (err) {
2086
+ const stored = loadExternalDepositLockSession(active.owner);
2087
+ if (err instanceof ExternalDepositLockSessionChangedError || !stored || !isSameExternalDepositLockSession(stored, active)) {
2088
+ pendingCreditedTransitionRef.current = null;
2089
+ installSession(stored ?? null);
2090
+ return;
2091
+ }
2092
+ pendingCreditedTransitionRef.current = { predecessor: active, credited };
2093
+ installSession(credited);
2094
+ notifyFailure(
2095
+ new PostDepositLockError(
2096
+ err instanceof Error ? err.message : "Unable to persist credited deposit state",
2097
+ "submission-failed",
2098
+ BigInt(active.maxLockAmount),
2099
+ creditedAmount,
2100
+ { cause: err }
2101
+ )
2102
+ );
2103
+ return;
2104
+ }
2105
+ installSession(credited);
2106
+ await submitSession(credited);
2312
2107
  },
2313
- { label: "Sign in wallet", status: getStepStatus("signing", ["submitting", "processing"]) },
2314
- {
2315
- label: "Submitting withdrawal",
2316
- status: getStepStatus("submitting", ["processing"])
2108
+ [installSession, notifyFailure, submitSession]
2109
+ );
2110
+ const recordVerification = useCallback(
2111
+ (context) => {
2112
+ const active = sessionRef.current;
2113
+ if (!active || active.creditedAmount) {
2114
+ throw new Error("No external deposit session is awaiting verification");
2115
+ }
2116
+ if (active.verification) {
2117
+ const sameTransfer = active.verification.hash.toLowerCase() === context.hash.toLowerCase() && active.verification.chainId === context.chainId && active.verification.amount === context.amount.toString() && (active.verification.logIndex ?? 0) === (context.logIndex ?? 0);
2118
+ if (sameTransfer) return;
2119
+ throw new Error("Another transfer is already being verified for this deposit session");
2120
+ }
2121
+ if (context.chainId !== active.chainId || context.amount <= 0n) {
2122
+ throw new Error("Discovered transfer does not match the signed deposit session");
2123
+ }
2124
+ const current = loadExternalDepositLockSession(active.owner);
2125
+ if (!current || current.generation !== active.generation) {
2126
+ throw new Error("The signed deposit session changed before verification");
2127
+ }
2128
+ const next = {
2129
+ ...active,
2130
+ verification: {
2131
+ hash: context.hash,
2132
+ chainId: context.chainId,
2133
+ amount: context.amount.toString(),
2134
+ logIndex: context.logIndex
2135
+ }
2136
+ };
2137
+ saveExternalDepositLockSession(next, active);
2138
+ installSession(next);
2317
2139
  },
2318
- {
2319
- label: "Processing \u2014 may take a minute or two",
2320
- status: getStepStatus("processing", [])
2140
+ [installSession]
2141
+ );
2142
+ const retryAfterCredit = useCallback(async () => {
2143
+ const active = sessionRef.current;
2144
+ if (!active?.creditedAmount) throw new Error("No credited external deposit to recover");
2145
+ if (signingRef.current || settlingRef.current) {
2146
+ throw new Error("A policy operation is already in progress");
2321
2147
  }
2322
- ];
2323
- useEffect(() => {
2324
- if (error) {
2325
- toast.error(error.message.length > 100 ? `${error.message.slice(0, 100)}...` : error.message);
2148
+ if (privateReadAddress?.toLowerCase() !== active.owner.toLowerCase()) {
2149
+ throw new Error("Authenticated deposit account does not match the credited deposit");
2326
2150
  }
2327
- }, [error]);
2328
- useEffect(() => {
2329
- const pending = isPending && !cancelled;
2330
- onPendingChange?.(pending);
2331
- onUnsafeToCloseChange?.(pending);
2332
- }, [isPending, cancelled, onPendingChange, onUnsafeToCloseChange]);
2333
- const handleCancel = () => {
2334
- setCancelled(true);
2335
- reset();
2336
- };
2337
- const handleDone = () => {
2338
- setShowSuccess(false);
2339
- setShowTimeout(false);
2340
- setCancelled(false);
2341
- reset();
2342
- };
2343
- const handleWithdraw = async () => {
2344
- if (!amount || !selectedToken || exceedsBalance) return;
2345
- setCancelled(false);
2346
- const amountInWei = parseTokenAmount(amount, selectedToken.decimals);
2347
- await withdraw({
2348
- tokenId: selectedToken.id,
2349
- amount: amountInWei
2350
- });
2351
- };
2352
- const handleMaxClick = () => {
2353
- if (formattedBalance && parseFloat(formattedBalance) > 0) {
2354
- setAmount(formattedBalance.replace(/[\s\u2009]/g, ""));
2151
+ const token = getTokenById(active.tokenId);
2152
+ if (!token || token.chainId !== active.chainId || token.contract === zeroAddress) {
2153
+ throw new Error("The credited token is no longer available");
2355
2154
  }
2356
- };
2357
- const hasValidAmount = amount && parseFloat(amount) > 0;
2358
- const tooManyDecimals = hasValidAmount && amount.includes(".") && amount.split(".")[1].length > selectedToken.decimals;
2359
- const exceedsBalance = hasValidAmount && !tooManyDecimals && !isBalanceLoading && !isBalanceError && parseTokenAmount(amount, selectedToken.decimals) > BigInt(balanceWei);
2360
- const getButtonText = () => {
2361
- if (!isConnected) return "Connect Wallet";
2362
- return "Withdraw";
2363
- };
2364
- if (showSuccess) {
2365
- return /* @__PURE__ */ jsx(
2366
- TransactionSuccessView,
2367
- {
2368
- title: "Withdrawal Complete",
2369
- message: `Your ${selectedToken.symbol} withdrawal has been processed. Funds should appear in your wallet shortly.`,
2370
- explorerUrl,
2371
- explorerLabel: targetChain ? getExplorerLabel(targetChain.id) : void 0,
2372
- onDone: handleDone
2373
- }
2374
- );
2375
- }
2376
- if (showTimeout) {
2377
- return /* @__PURE__ */ jsx(
2378
- TransactionWarningView,
2379
- {
2380
- title: "Withdrawal Processing",
2381
- message: `Your withdrawal is still being processed. Please check your balance \u2014 it should update shortly.`,
2382
- onDone: handleDone
2383
- }
2384
- );
2385
- }
2386
- if (isPending && !cancelled) {
2387
- const canCancel = currentStep === "idle" || currentStep === "switching-chain" || currentStep === "signing";
2388
- return /* @__PURE__ */ jsx(
2389
- TransactionProgressView,
2390
- {
2391
- title: "Withdrawing...",
2392
- steps: withdrawSteps,
2393
- onCancel: canCancel ? handleCancel : void 0
2394
- }
2395
- );
2396
- }
2397
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-6", children: [
2398
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2399
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
2400
- /* @__PURE__ */ jsxs(
2401
- "button",
2402
- {
2403
- onClick: onTokenSelect,
2404
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3",
2405
- children: [
2406
- /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
2407
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-start gap-1", children: [
2408
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: selectedToken.symbol }),
2409
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2410
- "on ",
2411
- targetChain?.name ?? "Base Sepolia"
2412
- ] })
2413
- ] }),
2414
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2415
- ]
2416
- }
2417
- )
2418
- ] }),
2419
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2420
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2421
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Amount" }),
2422
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-sm", children: [
2423
- formattedBalance,
2424
- " ",
2425
- selectedToken.symbol
2426
- ] })
2427
- ] }),
2428
- /* @__PURE__ */ jsxs("div", { className: "border-border bg-input flex items-center gap-2 rounded-[10px] border py-1 pr-1 pl-3", children: [
2429
- /* @__PURE__ */ jsx(
2430
- "input",
2431
- {
2432
- type: "text",
2433
- inputMode: "decimal",
2434
- placeholder: "Enter Amount",
2435
- value: amount,
2436
- onChange: (e) => {
2437
- const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
2438
- if (value.split(".").length <= 2) {
2439
- setAmount(value);
2440
- }
2441
- },
2442
- className: cn(
2443
- "text-foreground flex-1 bg-transparent text-sm outline-none",
2444
- "placeholder:text-muted-foreground/50"
2445
- )
2446
- }
2447
- ),
2448
- /* @__PURE__ */ jsx(
2449
- "button",
2450
- {
2451
- onClick: handleMaxClick,
2452
- className: "bg-secondary text-foreground hover:bg-secondary/80 cursor-pointer rounded px-3 py-2.5 text-xs font-semibold transition-colors",
2453
- children: "MAX"
2454
- }
2455
- )
2456
- ] }),
2457
- tooManyDecimals && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
2458
- "Too many decimal places (max: ",
2459
- selectedToken.decimals,
2460
- ")"
2461
- ] }),
2462
- exceedsBalance && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Insufficient balance" })
2463
- ] }),
2464
- /* @__PURE__ */ jsx(
2465
- "button",
2466
- {
2467
- onClick: handleWithdraw,
2468
- disabled: !isConnected || !hasValidAmount || tooManyDecimals || !!exceedsBalance || isPending,
2469
- className: cn(
2470
- "flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
2471
- "bg-primary text-primary-foreground hover:bg-primary/90",
2472
- "disabled:cursor-not-allowed disabled:opacity-50"
2473
- ),
2474
- children: getButtonText()
2475
- }
2476
- )
2477
- ] });
2478
- }
2479
- function BalanceCards({
2480
- selectedToken,
2481
- onLockedFundsClick,
2482
- onBalanceClick,
2483
- showLockedFunds = true,
2484
- disabled
2485
- }) {
2486
- const { balanceWei, isLoading: balanceLoading } = useBalance({
2487
- tokenId: selectedToken.id
2488
- });
2489
- const { totalLocked, isLoading: lockedLoading } = useLockedFunds({ enabled: showLockedFunds });
2490
- const formattedBalance = formatTokenAmount(balanceWei, selectedToken.decimals);
2491
- const formattedLocked = showLockedFunds ? formatTokenAmount(String(totalLocked), selectedToken.decimals) : "0.00";
2492
- return /* @__PURE__ */ jsxs("div", { className: cn("flex gap-2", disabled && "opacity-50"), children: [
2493
- /* @__PURE__ */ jsxs(
2494
- "button",
2495
- {
2496
- onClick: onBalanceClick,
2497
- disabled,
2498
- className: cn(
2499
- "bg-muted flex flex-1 flex-col gap-2 rounded-[10px] p-5 text-left transition-colors",
2500
- disabled ? "cursor-not-allowed" : "hover:bg-muted/80 cursor-pointer"
2501
- ),
2502
- children: [
2503
- /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between", children: [
2504
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2505
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Balance" }),
2506
- /* @__PURE__ */ jsx("span", { className: "bg-secondary text-muted-foreground rounded-full px-2 py-[5px] text-[10px] font-bold", children: selectedToken.symbol })
2507
- ] }),
2508
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-6 w-6 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2509
- ] }),
2510
- /* @__PURE__ */ jsx("div", { className: "text-foreground text-xl font-medium", children: balanceLoading ? /* @__PURE__ */ jsx("span", { className: "bg-secondary inline-block h-6 w-24 animate-pulse rounded" }) : formattedBalance })
2511
- ]
2512
- }
2513
- ),
2514
- showLockedFunds && /* @__PURE__ */ jsxs(
2515
- "button",
2516
- {
2517
- onClick: onLockedFundsClick,
2518
- disabled,
2519
- className: cn(
2520
- "bg-muted flex flex-1 flex-col gap-2 rounded-[10px] p-5 text-left transition-colors",
2521
- disabled ? "cursor-not-allowed" : "hover:bg-muted/80 cursor-pointer"
2522
- ),
2523
- children: [
2524
- /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between", children: [
2525
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2526
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Locked Funds" }),
2527
- /* @__PURE__ */ jsx("span", { className: "bg-secondary text-muted-foreground rounded-full px-2 py-[5px] text-[10px] font-bold", children: selectedToken.symbol })
2528
- ] }),
2529
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-6 w-6 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2530
- ] }),
2531
- /* @__PURE__ */ jsx("div", { className: "text-foreground text-xl font-medium", children: lockedLoading ? /* @__PURE__ */ jsx("span", { className: "bg-secondary inline-block h-6 w-20 animate-pulse rounded" }) : formattedLocked })
2532
- ]
2155
+ const targetService = requireServiceAddress(serviceAddress);
2156
+ if (targetService.toLowerCase() !== active.serviceAddress.toLowerCase()) {
2157
+ throw new Error("The service for this credited deposit has changed");
2158
+ }
2159
+ if (active.payload) {
2160
+ await submitSession(active);
2161
+ return;
2162
+ }
2163
+ if (active.submissionAmbiguous) {
2164
+ throw new Error(
2165
+ "The previous lock submission may have succeeded; a fresh signature is unsafe"
2166
+ );
2167
+ }
2168
+ const owner = requireDepositLockOwner(address, privateReadAddress);
2169
+ const amount = externalDepositRetryAmount(active);
2170
+ let retrySession;
2171
+ signingRef.current = true;
2172
+ setIsSigning(true);
2173
+ try {
2174
+ await ensureCorrectChain(networkConfig.chainId);
2175
+ const signingWalletClient = await getWalletClient(config, {
2176
+ chainId: networkConfig.chainId
2177
+ });
2178
+ const payload = await createSignedLockRequest({
2179
+ client,
2180
+ walletClient: signingWalletClient,
2181
+ userAddress: owner,
2182
+ networkConfig,
2183
+ serviceAddress: targetService,
2184
+ tokenId: active.tokenId,
2185
+ amount,
2186
+ lockDuration: active.lockDuration
2187
+ });
2188
+ if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
2189
+ throw new Error("Authenticated deposit account changed while signing");
2533
2190
  }
2534
- )
2535
- ] });
2191
+ retrySession = {
2192
+ ...active,
2193
+ depositAmount: amount.toString(),
2194
+ generation: payload.signature,
2195
+ verification: void 0,
2196
+ submissionAmbiguous: void 0,
2197
+ payload
2198
+ };
2199
+ saveExternalDepositLockSession(retrySession, active);
2200
+ installSession(retrySession);
2201
+ } finally {
2202
+ signingRef.current = false;
2203
+ setIsSigning(false);
2204
+ }
2205
+ await submitSession(retrySession);
2206
+ }, [
2207
+ address,
2208
+ client,
2209
+ config,
2210
+ ensureCorrectChain,
2211
+ getTokenById,
2212
+ installSession,
2213
+ networkConfig,
2214
+ privateReadAddress,
2215
+ serviceAddress,
2216
+ submitSession
2217
+ ]);
2218
+ const clearVerification = useCallback(() => {
2219
+ const active = sessionRef.current;
2220
+ if (!active?.verification || active.creditedAmount) return false;
2221
+ const next = clearExternalDepositVerification(active);
2222
+ if (!next) {
2223
+ installSession(loadExternalDepositLockSession(active.owner) ?? null);
2224
+ return false;
2225
+ }
2226
+ installSession(next);
2227
+ return true;
2228
+ }, [installSession]);
2229
+ const discardSession = useCallback(() => {
2230
+ const active = sessionRef.current;
2231
+ if (!active) return true;
2232
+ if (signingRef.current || settlingRef.current) return false;
2233
+ const pending = pendingCreditedTransitionRef.current;
2234
+ const discardTarget = pending && isSameExternalDepositLockSession(active, pending.credited) ? pending.predecessor : active;
2235
+ const discarded = discardExternalDepositLockSession(discardTarget);
2236
+ const restored = loadExternalDepositLockSession(active.owner) ?? null;
2237
+ if (discarded) pendingCreditedTransitionRef.current = null;
2238
+ installSession(restored);
2239
+ return discarded && !restored;
2240
+ }, [installSession]);
2241
+ return {
2242
+ isSigning,
2243
+ isSubmittingLock,
2244
+ session,
2245
+ signAndPersist,
2246
+ recordVerification,
2247
+ settleAfterCredit,
2248
+ retryAfterCredit,
2249
+ clearVerification,
2250
+ discardSession
2251
+ };
2536
2252
  }
2537
- function Tabs({
2538
- activeTab,
2539
- onTabChange,
2540
- disabled
2541
- }) {
2253
+ function USDCIcon({ className, size = 24 }) {
2542
2254
  return /* @__PURE__ */ jsxs(
2543
- "div",
2255
+ "svg",
2544
2256
  {
2545
- className: cn(
2546
- "bg-muted relative flex gap-2 overflow-hidden rounded-[10px] p-1",
2547
- disabled && "opacity-50"
2548
- ),
2257
+ width: size,
2258
+ height: size,
2259
+ viewBox: "0 0 2000 2000",
2260
+ fill: "none",
2261
+ xmlns: "http://www.w3.org/2000/svg",
2262
+ className,
2549
2263
  children: [
2550
2264
  /* @__PURE__ */ jsx(
2551
- "div",
2265
+ "path",
2552
2266
  {
2553
- className: cn(
2554
- "bg-input absolute top-1 bottom-1 left-1 w-[calc(50%-8px)] rounded-md transition-transform duration-200",
2555
- activeTab === "withdraw" && "translate-x-[calc(100%+8px)]"
2556
- )
2267
+ d: "M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z",
2268
+ fill: "#2775ca"
2557
2269
  }
2558
2270
  ),
2559
2271
  /* @__PURE__ */ jsx(
2560
- "button",
2272
+ "path",
2561
2273
  {
2562
- onClick: () => !disabled && onTabChange("deposit"),
2563
- disabled,
2564
- className: cn(
2565
- "relative z-10 flex-1 rounded-md px-3 py-[9px] text-sm transition-colors",
2566
- activeTab === "deposit" ? "text-foreground" : "text-muted-foreground",
2567
- disabled ? "cursor-not-allowed" : "cursor-pointer"
2568
- ),
2569
- children: "Deposit"
2274
+ d: "M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z",
2275
+ fill: "#fff"
2570
2276
  }
2571
2277
  ),
2572
2278
  /* @__PURE__ */ jsx(
2573
- "button",
2279
+ "path",
2574
2280
  {
2575
- onClick: () => !disabled && onTabChange("withdraw"),
2576
- disabled,
2577
- className: cn(
2578
- "relative z-10 flex-1 rounded-md px-3 py-[9px] text-sm transition-colors",
2579
- activeTab === "withdraw" ? "text-foreground" : "text-muted-foreground",
2580
- disabled ? "cursor-not-allowed" : "cursor-pointer"
2581
- ),
2582
- children: "Withdraw"
2281
+ d: "M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z",
2282
+ fill: "#fff"
2583
2283
  }
2584
2284
  )
2585
2285
  ]
2586
2286
  }
2587
2287
  );
2588
2288
  }
2589
- function LockedFundsView({ onBack }) {
2590
- const { getTokenById } = usePrivanaContext();
2591
- const { locks, isLoading } = useLockedFunds();
2592
- const { unlockFunds, unlockAllExpired, isPending } = useUnlockFunds();
2593
- const [collapsedSections, setCollapsedSections] = useState({});
2594
- const sections = useMemo(() => {
2595
- const sectionMap = {};
2596
- locks.forEach((lock) => {
2597
- const serviceName = shortenAddress(lock.service_address);
2598
- if (!sectionMap[lock.service_address]) {
2599
- sectionMap[lock.service_address] = {
2600
- title: `Service ${serviceName}`,
2601
- items: []
2602
- };
2603
- }
2604
- sectionMap[lock.service_address].items.push({
2605
- lockId: lock.lock_id,
2606
- amount: formatTokenAmount(String(lock.amount), getTokenById(lock.token_id)?.decimals ?? 18),
2607
- serviceAddress: lock.service_address,
2608
- time: lock.is_expired ? "Click to unlock" : formatTimeRemaining(lock.expiry),
2609
- isExpired: lock.is_expired
2610
- });
2611
- });
2612
- return Object.values(sectionMap);
2613
- }, [getTokenById, locks]);
2614
- const toggleSection = (title) => {
2615
- setCollapsedSections((prev) => ({
2616
- ...prev,
2617
- [title]: !prev[title]
2618
- }));
2619
- };
2620
- const expiredCount = locks.filter((l) => l.is_expired).length;
2621
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2622
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2623
- /* @__PURE__ */ jsx(
2624
- "button",
2625
- {
2626
- onClick: onBack,
2627
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2628
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2629
- }
2630
- ),
2631
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Locked Funds" })
2632
- ] }) }),
2633
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex min-h-0 flex-1 flex-col rounded-[10px] p-2", children: [
2634
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 p-3", children: [1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "flex animate-pulse items-center gap-3 rounded-lg p-3", children: [
2635
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-10 w-10 rounded-full" }),
2636
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
2637
- /* @__PURE__ */ jsx("div", { className: "bg-secondary mb-2 h-3.5 w-24 rounded" }),
2638
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-3 w-32 rounded" })
2639
- ] })
2640
- ] }, i)) }) : sections.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-8", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "No locked funds" }) }) : sections.map((section) => /* @__PURE__ */ jsxs("div", { children: [
2641
- /* @__PURE__ */ jsxs(
2642
- "button",
2643
- {
2644
- onClick: () => toggleSection(section.title),
2645
- className: "text-muted-foreground hover:bg-secondary flex w-full cursor-pointer items-center justify-between rounded-lg px-4 py-4 transition-colors",
2646
- children: [
2647
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: section.title }),
2648
- /* @__PURE__ */ jsx(
2649
- ChevronDownIcon,
2650
- {
2651
- direction: collapsedSections[section.title] ? "right" : "down"
2652
- }
2653
- )
2654
- ]
2655
- }
2656
- ),
2657
- !collapsedSections[section.title] && section.items.map((item) => /* @__PURE__ */ jsxs(
2658
- "div",
2659
- {
2660
- className: cn(
2661
- "flex items-center justify-between gap-3 rounded-lg p-3",
2662
- item.isExpired && "bg-secondary"
2663
- ),
2664
- children: [
2665
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-3", children: [
2666
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-10 w-10 rounded-full" }),
2667
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2668
- /* @__PURE__ */ jsxs("span", { className: "text-foreground text-sm font-medium", children: [
2669
- item.amount,
2670
- " USDC"
2671
- ] }),
2672
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2673
- "Service: ",
2674
- shortenAddress(item.serviceAddress)
2675
- ] })
2676
- ] })
2677
- ] }),
2678
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-end gap-2", children: [
2679
- item.isExpired ? /* @__PURE__ */ jsx(
2680
- "button",
2681
- {
2682
- onClick: () => unlockFunds({ lockId: Number(item.lockId) }),
2683
- disabled: isPending,
2684
- className: "text-foreground hover:text-foreground/80 cursor-pointer text-sm transition-colors disabled:opacity-50",
2685
- children: "Click to unlock"
2686
- }
2687
- ) : /* @__PURE__ */ jsx("span", { className: "text-sm text-amber-500", children: item.time }),
2688
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: "on Base Sepolia" })
2689
- ] })
2690
- ]
2691
- },
2692
- `${item.serviceAddress}-${item.lockId}`
2693
- ))
2694
- ] }, section.title)) }),
2695
- expiredCount > 0 && /* @__PURE__ */ jsx("div", { className: "p-3", children: /* @__PURE__ */ jsxs(
2696
- "button",
2697
- {
2698
- onClick: () => unlockAllExpired(),
2699
- disabled: isPending,
2700
- className: "border-border text-foreground hover:bg-secondary flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50",
2701
- children: [
2702
- "Unlock All (",
2703
- expiredCount,
2704
- ")"
2705
- ]
2706
- }
2707
- ) })
2708
- ] })
2709
- ] });
2710
- }
2711
- function BalanceTokenRow({ token }) {
2712
- const { balanceWei, isLoading } = useBalance({
2713
- tokenId: token.id
2714
- });
2715
- const formattedBalance = formatTokenAmount(balanceWei, token.decimals);
2716
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2 rounded-lg px-3 py-2.5", children: [
2717
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 18) }),
2718
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: token.symbol }),
2719
- isLoading ? /* @__PURE__ */ jsx("span", { className: "bg-secondary h-4 w-16 animate-pulse rounded" }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: formattedBalance })
2720
- ] });
2721
- }
2722
- function BalanceDetailsView({ onBack }) {
2723
- const { enabledTokens, chains } = usePrivanaContext();
2724
- const [selectedChainId, setSelectedChainId] = useState(chains[0]?.id ?? 84532);
2725
- const chainTokens = useMemo(() => {
2726
- return enabledTokens.filter((t) => t.chainId === selectedChainId);
2727
- }, [enabledTokens, selectedChainId]);
2728
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2729
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2730
- /* @__PURE__ */ jsx(
2731
- "button",
2732
- {
2733
- onClick: onBack,
2734
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2735
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2736
- }
2737
- ),
2738
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Token Balances" })
2739
- ] }) }),
2740
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 gap-2", children: [
2741
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-1 flex-col overflow-hidden rounded-[10px] p-2", children: [
2742
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Network" }) }),
2743
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex-1 overflow-y-auto", children: chains.map((chain) => {
2744
- const isSelected = selectedChainId === chain.id;
2745
- return /* @__PURE__ */ jsxs(
2746
- "button",
2747
- {
2748
- onClick: () => setSelectedChainId(chain.id),
2749
- className: cn(
2750
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2751
- isSelected && "bg-secondary"
2752
- ),
2753
- children: [
2754
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getChainIcon(chain.id, 18) }),
2755
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: chain.name })
2756
- ]
2757
- },
2758
- chain.id
2759
- );
2760
- }) })
2761
- ] }),
2762
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-[2] flex-col overflow-hidden rounded-[10px] p-2", children: [
2763
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Token Balance" }) }),
2764
- /* @__PURE__ */ jsx("div", { className: "mt-2 flex-1 overflow-y-auto", children: chainTokens.map((token) => /* @__PURE__ */ jsx(BalanceTokenRow, { token }, token.id)) })
2765
- ] })
2766
- ] })
2767
- ] });
2289
+ function BaseIcon({ className, size = 24 }) {
2290
+ return /* @__PURE__ */ jsxs(
2291
+ "svg",
2292
+ {
2293
+ width: size,
2294
+ height: size,
2295
+ viewBox: "0 0 2500 2500",
2296
+ xmlns: "http://www.w3.org/2000/svg",
2297
+ className,
2298
+ children: [
2299
+ /* @__PURE__ */ jsx("circle", { cx: "1250", cy: "1250", r: "1250", fill: "#0052FF" }),
2300
+ /* @__PURE__ */ jsx(
2301
+ "path",
2302
+ {
2303
+ d: "M1247.8,2500c691.6,0,1252.2-559.6,1252.2-1250C2500,559.6,1939.4,0,1247.8,0C591.7,0,53.5,503.8,0,1144.9h1655.1v210.2H0C53.5,1996.2,591.7,2500,1247.8,2500z",
2304
+ fill: "white"
2305
+ }
2306
+ )
2307
+ ]
2308
+ }
2309
+ );
2768
2310
  }
2769
- function TokenRow({
2770
- token,
2771
- isSelected,
2772
- onClick
2773
- }) {
2774
- const { address } = useAccount();
2775
- const isNative = token.contract === zeroAddress;
2776
- const { data: nativeBalanceData } = useBalance$1({
2777
- address,
2778
- chainId: token.chainId,
2779
- query: { enabled: !!address && isNative }
2780
- });
2781
- const { data: erc20Balance } = useReadContract({
2782
- address: token.contract,
2783
- abi: erc20Abi,
2784
- functionName: "balanceOf",
2785
- args: address ? [address] : void 0,
2786
- chainId: token.chainId,
2787
- query: { enabled: !!address && !isNative }
2788
- });
2789
- const walletBalance = isNative ? nativeBalanceData?.value : erc20Balance;
2790
- const formattedBalance = walletBalance ? formatTokenAmount(walletBalance.toString(), token.decimals) : "0.00";
2311
+ function WETHIcon({ className, size = 24 }) {
2791
2312
  return /* @__PURE__ */ jsxs(
2792
- "button",
2313
+ "svg",
2793
2314
  {
2794
- onClick,
2795
- className: cn(
2796
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2797
- isSelected && "bg-secondary"
2798
- ),
2315
+ width: size,
2316
+ height: size,
2317
+ viewBox: "0 0 250 250",
2318
+ fill: "none",
2319
+ xmlns: "http://www.w3.org/2000/svg",
2320
+ className,
2799
2321
  children: [
2800
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 18) }),
2801
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: token.symbol }),
2802
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: formattedBalance })
2322
+ /* @__PURE__ */ jsx(
2323
+ "path",
2324
+ {
2325
+ d: "M0 125C0 55.9644 55.9644 0 125 0C194.036 0 250 55.9644 250 125C250 194.036 194.036 250 125 250C55.9644 250 0 194.036 0 125Z",
2326
+ fill: "#627EEA"
2327
+ }
2328
+ ),
2329
+ /* @__PURE__ */ jsx(
2330
+ "path",
2331
+ {
2332
+ d: "M125.047 30.5V100.351L184.086 126.732L125.047 30.5Z",
2333
+ fill: "white",
2334
+ fillOpacity: "0.602"
2335
+ }
2336
+ ),
2337
+ /* @__PURE__ */ jsx("path", { d: "M125.047 30.5L66 126.732L125.047 100.351V30.5Z", fill: "white" }),
2338
+ /* @__PURE__ */ jsx(
2339
+ "path",
2340
+ {
2341
+ d: "M125.047 172V219.462L184.125 137.728L125.047 172Z",
2342
+ fill: "white",
2343
+ fillOpacity: "0.602"
2344
+ }
2345
+ ),
2346
+ /* @__PURE__ */ jsx("path", { d: "M125.047 219.462V171.992L66 137.728L125.047 219.462Z", fill: "white" }),
2347
+ /* @__PURE__ */ jsx(
2348
+ "path",
2349
+ {
2350
+ d: "M125.047 161.013L184.086 126.733L125.047 100.368V161.013Z",
2351
+ fill: "white",
2352
+ fillOpacity: "0.2"
2353
+ }
2354
+ ),
2355
+ /* @__PURE__ */ jsx("path", { d: "M66 126.733L125.047 161.013V100.368L66 126.733Z", fill: "white", fillOpacity: "0.602" })
2803
2356
  ]
2804
2357
  }
2805
2358
  );
2806
2359
  }
2807
- function TokenSelectorView({
2808
- onBack,
2809
- onSelect,
2810
- selectedTokenId
2811
- }) {
2812
- const { enabledTokens, chains } = usePrivanaContext();
2813
- const [selectedChainId, setSelectedChainId] = useState(chains[0]?.id ?? 84532);
2814
- const chainTokens = useMemo(() => {
2815
- return enabledTokens.filter((t) => t.chainId === selectedChainId);
2816
- }, [enabledTokens, selectedChainId]);
2817
- const handleTokenSelect = (token) => {
2818
- onSelect(token);
2819
- onBack();
2820
- };
2821
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2822
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2823
- /* @__PURE__ */ jsx(
2824
- "button",
2360
+ function getTokenIcon(symbol, size) {
2361
+ const iconSize = size ?? 24;
2362
+ switch (symbol.toUpperCase()) {
2363
+ case "USDC":
2364
+ return /* @__PURE__ */ jsx(USDCIcon, { size: iconSize });
2365
+ case "WETH":
2366
+ return /* @__PURE__ */ jsx(WETHIcon, { size: iconSize });
2367
+ default:
2368
+ return /* @__PURE__ */ jsx(
2369
+ "div",
2825
2370
  {
2826
- onClick: onBack,
2827
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2828
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2371
+ className: "bg-muted flex items-center justify-center rounded-full text-xs font-bold",
2372
+ style: { width: iconSize, height: iconSize },
2373
+ children: symbol.slice(0, 2)
2829
2374
  }
2830
- ),
2831
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Select Token" })
2832
- ] }) }),
2833
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 gap-2", children: [
2834
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-1 flex-col overflow-hidden rounded-[10px] p-2", children: [
2835
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Network" }) }),
2836
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex-1 overflow-y-auto", children: chains.map((chain) => {
2837
- const isSelected = selectedChainId === chain.id;
2838
- return /* @__PURE__ */ jsxs(
2839
- "button",
2840
- {
2841
- onClick: () => setSelectedChainId(chain.id),
2842
- className: cn(
2843
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2844
- isSelected && "bg-secondary"
2845
- ),
2846
- children: [
2847
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getChainIcon(chain.id, 18) }),
2848
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: chain.name })
2849
- ]
2850
- },
2851
- chain.id
2852
- );
2853
- }) })
2854
- ] }),
2855
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-[2] flex-col overflow-hidden rounded-[10px] p-2", children: [
2856
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Token" }) }),
2857
- /* @__PURE__ */ jsx("div", { className: "mt-2 flex-1 overflow-y-auto", children: chainTokens.map((token) => /* @__PURE__ */ jsx(
2858
- TokenRow,
2859
- {
2860
- token,
2861
- isSelected: selectedTokenId === token.id,
2862
- onClick: () => handleTokenSelect(token)
2863
- },
2864
- token.id
2865
- )) })
2866
- ] })
2867
- ] })
2868
- ] });
2869
- }
2870
- function ModalBody({
2871
- onViewChange,
2872
- onCloseBlockedChange,
2873
- showLockedFunds = true,
2874
- defaultTab = "deposit",
2875
- onDepositSuccess
2876
- }) {
2877
- const { defaultToken, tokensStatus } = usePrivanaContext();
2878
- const [selectedToken, setSelectedToken] = useState(defaultToken);
2879
- const [activeTab, setActiveTab] = useState(defaultTab);
2880
- const [currentView, setCurrentView] = useState("main");
2881
- const [isInteractionPending, setIsInteractionPending] = useState(false);
2882
- useEffect(() => {
2883
- if (!selectedToken && defaultToken) {
2884
- setSelectedToken(defaultToken);
2885
- }
2886
- }, [selectedToken, defaultToken]);
2887
- const handlePendingChange = (isPending) => {
2888
- setIsInteractionPending(isPending);
2889
- };
2890
- const handleCloseBlockedChange = (isBlocked) => {
2891
- onCloseBlockedChange?.(isBlocked);
2892
- };
2893
- const handleViewChange = (view) => {
2894
- const update = () => {
2895
- setCurrentView(view);
2896
- onViewChange?.(view);
2897
- };
2898
- if (typeof document !== "undefined" && typeof document.startViewTransition === "function") {
2899
- document.documentElement.dataset.privanaVtDir = view === "main" ? "back" : "forward";
2900
- const transition = document.startViewTransition(update);
2901
- transition.finished.then(() => {
2902
- delete document.documentElement.dataset.privanaVtDir;
2903
- });
2904
- } else {
2905
- update();
2906
- }
2907
- };
2908
- const handleTokenSelect = (token) => {
2909
- setSelectedToken(token);
2910
- };
2911
- if (tokensStatus === "loading" || !selectedToken) {
2912
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2913
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-25 animate-pulse rounded-[10px]" }),
2914
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-11 animate-pulse rounded-[10px]" }),
2915
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-50 animate-pulse rounded-[10px]" })
2916
- ] });
2917
- }
2918
- if (currentView === "locked-funds") {
2919
- return /* @__PURE__ */ jsx(LockedFundsView, { onBack: () => handleViewChange("main") });
2920
- }
2921
- if (currentView === "balance-details") {
2922
- return /* @__PURE__ */ jsx(BalanceDetailsView, { onBack: () => handleViewChange("main") });
2375
+ );
2923
2376
  }
2924
- if (currentView === "select-token") {
2925
- return /* @__PURE__ */ jsx(
2926
- TokenSelectorView,
2927
- {
2928
- onBack: () => handleViewChange("main"),
2929
- onSelect: handleTokenSelect,
2930
- selectedTokenId: selectedToken.id
2931
- }
2932
- );
2377
+ }
2378
+ function getChainIcon(chainId, size) {
2379
+ const iconSize = size ?? 24;
2380
+ switch (chainId) {
2381
+ case 84532:
2382
+ return /* @__PURE__ */ jsx(BaseIcon, { size: iconSize });
2383
+ default:
2384
+ return /* @__PURE__ */ jsx(
2385
+ "div",
2386
+ {
2387
+ className: "bg-muted flex items-center justify-center rounded-full text-xs font-bold",
2388
+ style: { width: iconSize, height: iconSize },
2389
+ children: "?"
2390
+ }
2391
+ );
2933
2392
  }
2934
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pb-4", children: [
2935
- /* @__PURE__ */ jsx("div", { className: cn(isInteractionPending && "pointer-events-none"), children: /* @__PURE__ */ jsx(
2936
- BalanceCards,
2937
- {
2938
- selectedToken,
2939
- onLockedFundsClick: () => handleViewChange("locked-funds"),
2940
- onBalanceClick: () => handleViewChange("balance-details"),
2941
- showLockedFunds,
2942
- disabled: isInteractionPending
2943
- }
2944
- ) }),
2945
- /* @__PURE__ */ jsx("div", { className: cn(isInteractionPending && "pointer-events-none"), children: /* @__PURE__ */ jsx(Tabs, { activeTab, onTabChange: setActiveTab, disabled: isInteractionPending }) }),
2946
- /* @__PURE__ */ jsx("div", { className: "bg-muted rounded-[10px] p-5", children: activeTab === "deposit" ? /* @__PURE__ */ jsx(
2947
- DepositForm,
2948
- {
2949
- selectedToken,
2950
- onTokenSelect: () => handleViewChange("select-token"),
2951
- onPendingChange: handlePendingChange,
2952
- onUnsafeToCloseChange: handleCloseBlockedChange,
2953
- onSuccess: onDepositSuccess
2954
- }
2955
- ) : /* @__PURE__ */ jsx(
2956
- WithdrawForm,
2957
- {
2958
- selectedToken,
2959
- onTokenSelect: () => handleViewChange("select-token"),
2960
- onPendingChange: handlePendingChange,
2961
- onUnsafeToCloseChange: handleCloseBlockedChange
2962
- }
2963
- ) })
2964
- ] }) });
2965
2393
  }
2966
- function PrivanaModal({
2967
- open,
2968
- onClose,
2969
- showLockedFunds,
2970
- defaultTab,
2971
- onDepositSuccess
2972
- }) {
2973
- const [currentView, setCurrentView] = useState("main");
2974
- const titleId = useId();
2975
- const descId = useId();
2976
- const [isCloseBlocked, setIsCloseBlocked] = useState(false);
2977
- const handleClose = () => {
2978
- if (!isCloseBlocked) onClose();
2979
- };
2980
- return /* @__PURE__ */ jsx(
2981
- Dialog,
2982
- {
2983
- open,
2984
- onOpenChange: (isOpen) => {
2985
- if (!isOpen) handleClose();
2986
- },
2987
- children: /* @__PURE__ */ jsxs(
2988
- DialogContent,
2394
+ function TokenSelectorView({ selectedTokenId, onSelect }) {
2395
+ const { enabledTokens } = usePrivanaContext();
2396
+ return /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col rounded-[10px] p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex max-h-72 flex-col gap-1 overflow-y-auto", children: [
2397
+ enabledTokens.map((token) => {
2398
+ const isSelected = selectedTokenId === token.id;
2399
+ return /* @__PURE__ */ jsxs(
2400
+ "button",
2989
2401
  {
2990
- "data-privana": true,
2991
- "data-view": currentView,
2992
- showCloseButton: false,
2993
- onInteractOutside: isCloseBlocked ? (e) => e.preventDefault() : void 0,
2994
- onEscapeKeyDown: isCloseBlocked ? (e) => e.preventDefault() : void 0,
2995
- className: "bg-card flex h-[596px] max-h-[95dvh] w-[560px] max-w-[95vw] flex-col gap-2 overflow-hidden rounded-2xl border-0 p-2 pb-[2.75rem]",
2996
- "aria-labelledby": titleId,
2997
- "aria-describedby": descId,
2402
+ type: "button",
2403
+ onClick: () => onSelect(token.id),
2404
+ className: cn(
2405
+ "hover:bg-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors",
2406
+ isSelected && "bg-secondary"
2407
+ ),
2998
2408
  children: [
2999
- /* @__PURE__ */ jsx(
3000
- "button",
3001
- {
3002
- "data-privana-close": true,
3003
- onClick: handleClose,
3004
- disabled: isCloseBlocked,
3005
- "aria-label": "Close",
3006
- className: cn(
3007
- "absolute top-6 right-5 z-20 flex h-6 w-6 items-center justify-center transition-colors",
3008
- isCloseBlocked ? "text-muted-foreground/40 cursor-not-allowed" : "text-muted-foreground hover:text-foreground cursor-pointer"
3009
- ),
3010
- children: /* @__PURE__ */ jsx(CloseIcon, {})
3011
- }
3012
- ),
3013
- /* @__PURE__ */ jsxs(
3014
- "div",
3015
- {
3016
- "data-privana-content": true,
3017
- "data-view": currentView,
3018
- className: "flex min-h-0 flex-1 flex-col gap-2",
3019
- children: [
3020
- /* @__PURE__ */ jsx(DialogTitle, { id: titleId, className: "sr-only", children: "Privana" }),
3021
- /* @__PURE__ */ jsx(DialogDescription, { id: descId, className: "sr-only", children: "Deposit or withdraw tokens from your Privana" }),
3022
- currentView === "main" && /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center px-5 py-4", children: /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Privana" }) }) }),
3023
- /* @__PURE__ */ jsx(
3024
- ModalBody,
3025
- {
3026
- onCloseBlockedChange: setIsCloseBlocked,
3027
- onViewChange: setCurrentView,
3028
- showLockedFunds,
3029
- defaultTab,
3030
- onDepositSuccess
3031
- }
3032
- )
3033
- ]
3034
- }
3035
- ),
3036
- /* @__PURE__ */ jsx(
3037
- "a",
2409
+ /* @__PURE__ */ jsx("div", { className: "h-6 w-6 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 24) }),
2410
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2411
+ /* @__PURE__ */ jsx("div", { className: "text-foreground text-sm font-medium", children: token.symbol }),
2412
+ /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-[11px]", children: token.name })
2413
+ ] }),
2414
+ isSelected && /* @__PURE__ */ jsx("div", { className: "bg-primary flex h-4 w-4 shrink-0 items-center justify-center rounded-full", children: /* @__PURE__ */ jsx(
2415
+ "svg",
3038
2416
  {
3039
- "data-privana-footer": true,
3040
- href: "https://privana.finance",
3041
- target: "_blank",
3042
- rel: "noopener noreferrer",
3043
- "aria-label": "Powered by Privana"
2417
+ width: "10",
2418
+ height: "10",
2419
+ viewBox: "0 0 10 10",
2420
+ fill: "none",
2421
+ className: "text-primary-foreground",
2422
+ children: /* @__PURE__ */ jsx(
2423
+ "path",
2424
+ {
2425
+ d: "M2 5L4 7L8 3",
2426
+ stroke: "currentColor",
2427
+ strokeWidth: "1.5",
2428
+ strokeLinecap: "round",
2429
+ strokeLinejoin: "round"
2430
+ }
2431
+ )
3044
2432
  }
3045
- )
2433
+ ) })
3046
2434
  ]
3047
- }
3048
- )
3049
- }
3050
- );
2435
+ },
2436
+ token.id
2437
+ );
2438
+ }),
2439
+ enabledTokens.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-sm", children: "No tokens available" })
2440
+ ] }) });
3051
2441
  }
3052
- function PrivanaInlineModal({
3053
- className,
3054
- showLockedFunds,
3055
- defaultTab,
3056
- onDepositSuccess
2442
+ function CreditCardWidgetView({
2443
+ token,
2444
+ amount,
2445
+ allowance,
2446
+ onCredited,
2447
+ onLockSubmitted,
2448
+ onLockFailed
3057
2449
  }) {
3058
- return /* @__PURE__ */ jsx(
3059
- "div",
2450
+ const moonpayCurrencyCode = token?.moonpayCurrencyCode;
2451
+ const containerRef = useRef(null);
2452
+ const [widgetTheme, setWidgetTheme] = useState();
2453
+ useEffect(() => {
2454
+ if (containerRef.current) setWidgetTheme(resolveSdkTheme(containerRef.current));
2455
+ }, []);
2456
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
2457
+ /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Complete your purchase" }),
2458
+ token && moonpayCurrencyCode ? /* @__PURE__ */ jsx(
2459
+ FiatOnRampForm,
2460
+ {
2461
+ tokenId: token.id,
2462
+ currencyCode: moonpayCurrencyCode,
2463
+ quoteCurrencyAmount: amount || void 0,
2464
+ lockAmount: true,
2465
+ variant: "embedded",
2466
+ autoStart: true,
2467
+ theme: widgetTheme,
2468
+ postDepositLock: allowance ? {
2469
+ maxAmount: BigInt(allowance.value),
2470
+ lockDuration: allowance.lockDuration
2471
+ } : void 0,
2472
+ onCredited,
2473
+ onLockSubmitted,
2474
+ onLockFailed
2475
+ }
2476
+ ) : /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
2477
+ token?.symbol ?? "This token",
2478
+ " isn\u2019t available for card purchases yet."
2479
+ ] })
2480
+ ] });
2481
+ }
2482
+ function resolveSdkTheme(el) {
2483
+ const scheme = getComputedStyle(el).colorScheme;
2484
+ if (scheme === "dark") return "dark";
2485
+ if (scheme === "light") return "light";
2486
+ return el.closest(".dark") ? "dark" : "light";
2487
+ }
2488
+ function CheckIcon() {
2489
+ return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
2490
+ "path",
3060
2491
  {
3061
- "data-privana": true,
3062
- className: cn(
3063
- "bg-card flex w-[560px] max-w-full flex-col gap-2 overflow-hidden rounded-2xl p-2 shadow-lg",
3064
- className
3065
- ),
3066
- children: /* @__PURE__ */ jsx(
3067
- ModalBody,
3068
- {
3069
- showLockedFunds,
3070
- defaultTab,
3071
- onDepositSuccess
3072
- }
3073
- )
2492
+ d: "M2.5 6l2.5 2.5 4.5-4.5",
2493
+ stroke: "currentColor",
2494
+ strokeWidth: "1.5",
2495
+ strokeLinecap: "round",
2496
+ strokeLinejoin: "round"
3074
2497
  }
3075
- );
2498
+ ) });
3076
2499
  }
3077
- function PrivanaButton({
3078
- children,
3079
- className,
3080
- variant = "outline",
3081
- size = "default",
3082
- asChild = false,
3083
- renderButton,
3084
- hideWhenDisconnected = true,
3085
- showLockedFunds = true,
3086
- defaultTab,
3087
- onDepositSuccess,
3088
- ...buttonProps
3089
- }) {
3090
- const [modalOpen, setModalOpen] = useState(false);
3091
- const { isConnected } = useAccount();
3092
- if (hideWhenDisconnected && !isConnected) {
3093
- return null;
3094
- }
3095
- const handleClick = () => setModalOpen(true);
3096
- const buttonElement = renderButton ? renderButton({ onClick: handleClick, isOpen: modalOpen }) : /* @__PURE__ */ jsx(
3097
- Button,
2500
+ function ExternalLinkIcon() {
2501
+ return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
2502
+ "path",
3098
2503
  {
3099
- variant,
3100
- size,
3101
- asChild,
3102
- className: cn(className),
3103
- onClick: handleClick,
3104
- disabled: !isConnected,
3105
- ...buttonProps,
3106
- children: children ?? "Privana"
2504
+ d: "M3.5 1.5h7v7M10.5 1.5L1.5 10.5",
2505
+ stroke: "currentColor",
2506
+ strokeWidth: "1.5",
2507
+ strokeLinecap: "round",
2508
+ strokeLinejoin: "round"
3107
2509
  }
3108
- );
3109
- return /* @__PURE__ */ jsxs("span", { "data-privana": true, className: "contents", children: [
3110
- buttonElement,
3111
- /* @__PURE__ */ jsx(
3112
- PrivanaModal,
2510
+ ) });
2511
+ }
2512
+ function Spinner({ size = 20 }) {
2513
+ return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none", className: "animate-spin", children: [
2514
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", opacity: "0.15" }),
2515
+ /* @__PURE__ */ jsx("path", { d: "M10 2a8 8 0 016.93 4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
2516
+ ] });
2517
+ }
2518
+ function TransactionProgressView({ title, steps, onCancel }) {
2519
+ const completed = steps.filter((s) => s.status === "completed").length;
2520
+ const active = steps.find((s) => s.status === "active");
2521
+ const total = steps.length;
2522
+ const progress = (completed + (active ? 0.5 : 0)) / total * 100;
2523
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
2524
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2525
+ /* @__PURE__ */ jsx("div", { className: "text-primary", children: /* @__PURE__ */ jsx(Spinner, { size: 20 }) }),
2526
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: active?.label ?? title })
2527
+ ] }),
2528
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
2529
+ /* @__PURE__ */ jsx("div", { className: "bg-secondary h-1.5 w-full overflow-hidden rounded-full", children: /* @__PURE__ */ jsx(
2530
+ "div",
2531
+ {
2532
+ className: "bg-primary h-full rounded-full transition-all duration-500",
2533
+ style: { width: `${progress}%` }
2534
+ }
2535
+ ) }),
2536
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2537
+ "Step ",
2538
+ Math.max(1, completed + (active ? 1 : 0)),
2539
+ " of ",
2540
+ total
2541
+ ] })
2542
+ ] }),
2543
+ onCancel && /* @__PURE__ */ jsx(
2544
+ "button",
3113
2545
  {
3114
- open: modalOpen,
3115
- onClose: () => setModalOpen(false),
3116
- showLockedFunds,
3117
- defaultTab,
3118
- onDepositSuccess
2546
+ onClick: onCancel,
2547
+ className: "border-border text-foreground hover:bg-secondary flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors",
2548
+ children: "Cancel"
3119
2549
  }
3120
2550
  )
3121
2551
  ] });
3122
2552
  }
3123
-
3124
- // src/sdk/hooks/external-deposit-lock.ts
3125
- var ACTIVE_SESSION_PREFIX = "privana:external-deposit-lock:active:";
3126
- var SESSION_PREFIX = "privana:external-deposit-lock:session:";
3127
- var ExternalDepositLockSessionChangedError = class _ExternalDepositLockSessionChangedError extends Error {
3128
- constructor() {
3129
- super("External deposit lock session changed");
3130
- this.name = "ExternalDepositLockSessionChangedError";
3131
- Object.setPrototypeOf(this, _ExternalDepositLockSessionChangedError.prototype);
3132
- }
3133
- };
3134
- function externalDepositSessionId(chainId, tokenId) {
3135
- return `${chainId}:${tokenId.toLowerCase()}`;
3136
- }
3137
- function isExternalDepositBlockInSession(session, blockNumber) {
3138
- return Number.isSafeInteger(blockNumber) && blockNumber > session.startBlock;
3139
- }
3140
- function isCurrentExternalDepositVerification(session, txHash) {
3141
- return session.creditedAmount === void 0 && session.verification?.hash.toLowerCase() === txHash.toLowerCase();
3142
- }
3143
- function getExternalDepositMinimum(response, chainId) {
3144
- const value = response?.min_deposit?.[String(chainId)]?.erc20;
3145
- if (value === void 0) return void 0;
3146
- try {
3147
- const amount = BigInt(value);
3148
- return amount >= 0n ? amount : void 0;
3149
- } catch {
3150
- return void 0;
3151
- }
3152
- }
3153
- function activeSessionKey(owner) {
3154
- return `${ACTIVE_SESSION_PREFIX}${owner.toLowerCase()}`;
2553
+ function TransactionSuccessView({
2554
+ title,
2555
+ message,
2556
+ explorerUrl,
2557
+ explorerLabel,
2558
+ onDone
2559
+ }) {
2560
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
2561
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2562
+ /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-400", children: /* @__PURE__ */ jsx(CheckIcon, {}) }),
2563
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
2564
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
2565
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
2566
+ ] })
2567
+ ] }),
2568
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2569
+ explorerUrl && /* @__PURE__ */ jsxs(
2570
+ "a",
2571
+ {
2572
+ href: explorerUrl,
2573
+ target: "_blank",
2574
+ rel: "noopener noreferrer",
2575
+ className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 items-center justify-center gap-1.5 rounded-[10px] border text-sm transition-colors",
2576
+ children: [
2577
+ explorerLabel ?? "View on Explorer",
2578
+ /* @__PURE__ */ jsx(ExternalLinkIcon, {})
2579
+ ]
2580
+ }
2581
+ ),
2582
+ /* @__PURE__ */ jsx(
2583
+ "button",
2584
+ {
2585
+ onClick: onDone,
2586
+ className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
2587
+ children: "Done"
2588
+ }
2589
+ )
2590
+ ] })
2591
+ ] });
3155
2592
  }
3156
- function sessionKey(owner, sessionId) {
3157
- return `${SESSION_PREFIX}${owner.toLowerCase()}:${sessionId}`;
2593
+ function WarningIcon() {
2594
+ return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: /* @__PURE__ */ jsx(
2595
+ "path",
2596
+ {
2597
+ d: "M7 5v2.5M7 10h.005M6.13 2.5h1.74L13 11.5H1L6.13 2.5z",
2598
+ stroke: "currentColor",
2599
+ strokeWidth: "1.5",
2600
+ strokeLinecap: "round",
2601
+ strokeLinejoin: "round"
2602
+ }
2603
+ ) });
3158
2604
  }
3159
- function activeSessionId(owner) {
3160
- const value = getSharedBrowserStorageItem(activeSessionKey(owner));
3161
- return value || void 0;
2605
+ function TransactionWarningView({
2606
+ title,
2607
+ message,
2608
+ onDone,
2609
+ actionLabel = "Done",
2610
+ pendingLabel = actionLabel,
2611
+ isPending = false,
2612
+ onDismiss,
2613
+ dismissLabel = "Dismiss"
2614
+ }) {
2615
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
2616
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2617
+ /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-amber-400", children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
2618
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
2619
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
2620
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
2621
+ ] })
2622
+ ] }),
2623
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2624
+ onDismiss && /* @__PURE__ */ jsx(
2625
+ "button",
2626
+ {
2627
+ onClick: onDismiss,
2628
+ disabled: isPending,
2629
+ className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
2630
+ children: dismissLabel
2631
+ }
2632
+ ),
2633
+ /* @__PURE__ */ jsx(
2634
+ "button",
2635
+ {
2636
+ onClick: onDone,
2637
+ disabled: isPending,
2638
+ className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
2639
+ children: isPending ? pendingLabel : actionLabel
2640
+ }
2641
+ )
2642
+ ] })
2643
+ ] });
3162
2644
  }
3163
- function isPositiveIntegerString(value) {
3164
- if (typeof value !== "string") return false;
3165
- try {
3166
- return BigInt(value) > 0n;
3167
- } catch {
3168
- return false;
3169
- }
3170
- }
3171
- function isSessionRecord(value) {
3172
- if (!value || typeof value !== "object") return false;
3173
- const record = value;
3174
- if (record.version !== 1 || typeof record.owner !== "string" || typeof record.serviceAddress !== "string" || !Number.isInteger(record.chainId) || (record.chainId ?? 0) <= 0 || typeof record.tokenId !== "string" || !Number.isSafeInteger(record.startBlock) || (record.startBlock ?? -1) < 0 || !isPositiveIntegerString(record.depositAmount) || !isPositiveIntegerString(record.maxLockAmount) || !Number.isInteger(record.lockDuration) || (record.lockDuration ?? 0) <= 0 || typeof record.generation !== "string" || !record.generation || record.submissionAmbiguous !== void 0 && typeof record.submissionAmbiguous !== "boolean" || record.creditedAmount !== void 0 && !isPositiveIntegerString(record.creditedAmount)) {
3175
- return false;
3176
- }
3177
- if (record.verification && (typeof record.verification.hash !== "string" || record.verification.chainId !== record.chainId || !isPositiveIntegerString(record.verification.amount) || record.verification.logIndex !== void 0 && (!Number.isInteger(record.verification.logIndex) || record.verification.logIndex < 0))) {
3178
- return false;
3179
- }
3180
- if (record.verification && record.creditedAmount !== void 0) return false;
3181
- if (record.submissionAmbiguous && record.creditedAmount === void 0) return false;
3182
- if (!record.payload) {
3183
- return record.creditedAmount !== void 0 && !record.submissionAmbiguous;
3184
- }
3185
- if (typeof record.payload.signature !== "string" || typeof record.payload.token_id !== "string" || typeof record.payload.service_address !== "string" || record.payload.signature !== record.generation || record.payload.token_id.toLowerCase() !== record.tokenId.toLowerCase() || record.payload.service_address.toLowerCase() !== record.serviceAddress.toLowerCase() || !isPositiveIntegerString(record.payload.amount)) {
3186
- return false;
3187
- }
3188
- return BigInt(record.payload.amount) <= BigInt(record.maxLockAmount);
3189
- }
3190
- function writeSessionRecord(session) {
3191
- const id = externalDepositSessionId(session.chainId, session.tokenId);
3192
- return setSharedBrowserStorageItem(sessionKey(session.owner, id), JSON.stringify(session));
3193
- }
3194
- function saveExternalDepositLockSession(session, expectedSession = null) {
3195
- if (!isSessionRecord(session)) throw new Error("Invalid external deposit lock session");
3196
- const current = loadExternalDepositLockSession(session.owner);
3197
- if (expectedSession === null && current || expectedSession !== null && (!current || !isSameSessionSnapshot(current, expectedSession))) {
3198
- throw new ExternalDepositLockSessionChangedError();
3199
- }
3200
- const nextId = externalDepositSessionId(session.chainId, session.tokenId);
3201
- const previousId = activeSessionId(session.owner);
3202
- if (!writeSessionRecord(session)) {
3203
- throw new Error("Unable to persist external deposit lock session");
3204
- }
3205
- if (previousId !== nextId) {
3206
- if (!setSharedBrowserStorageItem(activeSessionKey(session.owner), nextId)) {
3207
- removeSharedBrowserStorageItem(sessionKey(session.owner, nextId));
3208
- throw new Error("Unable to persist external deposit lock session");
3209
- }
3210
- if (previousId) {
3211
- removeSharedBrowserStorageItem(sessionKey(session.owner, previousId));
3212
- }
3213
- }
3214
- }
3215
- function loadExternalDepositLockSession(owner) {
3216
- const id = activeSessionId(owner);
3217
- if (!id) return void 0;
3218
- const key = sessionKey(owner, id);
3219
- try {
3220
- const raw = getSharedBrowserStorageItem(key);
3221
- const parsed = raw ? JSON.parse(raw) : void 0;
3222
- if (!isSessionRecord(parsed) || parsed.owner.toLowerCase() !== owner.toLowerCase() || externalDepositSessionId(parsed.chainId, parsed.tokenId) !== id) {
3223
- removeSharedBrowserStorageItem(key);
3224
- removeSharedBrowserStorageItem(activeSessionKey(owner));
3225
- return void 0;
3226
- }
3227
- return parsed;
3228
- } catch {
3229
- removeSharedBrowserStorageItem(key);
3230
- removeSharedBrowserStorageItem(activeSessionKey(owner));
3231
- return void 0;
3232
- }
3233
- }
3234
- function subscribeExternalDepositLockSession(owner, onChange) {
3235
- if (typeof window === "undefined") return () => void 0;
3236
- let localStorage;
3237
- try {
3238
- localStorage = window.localStorage;
3239
- } catch {
3240
- return () => void 0;
3241
- }
3242
- const activeKey = activeSessionKey(owner);
3243
- const ownerSessionPrefix = `${SESSION_PREFIX}${owner.toLowerCase()}:`;
3244
- const handleStorage = (event) => {
3245
- if (event.storageArea !== localStorage) return;
3246
- if (event.key !== null && event.key !== activeKey && !event.key.startsWith(ownerSessionPrefix)) {
3247
- return;
3248
- }
3249
- onChange(loadExternalDepositLockSession(owner));
3250
- };
3251
- window.addEventListener("storage", handleStorage);
3252
- return () => window.removeEventListener("storage", handleStorage);
3253
- }
3254
- function clearExternalDepositLockSession(owner, expectedSessionId, expectedGeneration) {
3255
- const id = activeSessionId(owner);
3256
- if (!id || expectedSessionId && id !== expectedSessionId) return;
3257
- if (expectedGeneration) {
3258
- const current = loadExternalDepositLockSession(owner);
3259
- if (!current || current.generation !== expectedGeneration) return;
3260
- }
3261
- removeSharedBrowserStorageItem(sessionKey(owner, id));
3262
- removeSharedBrowserStorageItem(activeSessionKey(owner));
3263
- }
3264
- function isSameVerification(left, right) {
3265
- if (!left || !right) return left === right;
3266
- return left.hash.toLowerCase() === right.hash.toLowerCase() && left.chainId === right.chainId && left.amount === right.amount && (left.logIndex ?? 0) === (right.logIndex ?? 0);
3267
- }
3268
- function isSamePayload(left, right) {
3269
- if (!left || !right) return left === right;
3270
- return left.service_address.toLowerCase() === right.service_address.toLowerCase() && left.token_id.toLowerCase() === right.token_id.toLowerCase() && left.amount === right.amount && left.expiry === right.expiry && left.nonce === right.nonce && left.signature === right.signature;
3271
- }
3272
- function isSameSessionSnapshot(left, right) {
3273
- return left.version === right.version && left.owner.toLowerCase() === right.owner.toLowerCase() && left.serviceAddress.toLowerCase() === right.serviceAddress.toLowerCase() && left.chainId === right.chainId && left.tokenId.toLowerCase() === right.tokenId.toLowerCase() && left.startBlock === right.startBlock && left.depositAmount === right.depositAmount && left.maxLockAmount === right.maxLockAmount && left.lockDuration === right.lockDuration && left.generation === right.generation && left.creditedAmount === right.creditedAmount && left.submissionAmbiguous === right.submissionAmbiguous && isSameVerification(left.verification, right.verification) && isSamePayload(left.payload, right.payload);
3274
- }
3275
- function isSameExternalDepositLockSession(left, right) {
3276
- return isSameSessionSnapshot(left, right);
3277
- }
3278
- function clearExternalDepositVerification(session) {
3279
- if (!session.verification || session.creditedAmount) return void 0;
3280
- const current = loadExternalDepositLockSession(session.owner);
3281
- if (!current || current.generation !== session.generation || current.creditedAmount || !isSameVerification(current.verification, session.verification)) {
3282
- return void 0;
3283
- }
3284
- const next = { ...current, verification: void 0 };
3285
- if (!writeSessionRecord(next)) {
3286
- throw new Error("Unable to clear failed deposit verification");
3287
- }
3288
- return next;
3289
- }
3290
- function discardExternalDepositLockSession(session) {
3291
- const current = loadExternalDepositLockSession(session.owner);
3292
- if (!current) return true;
3293
- if (!isSameSessionSnapshot(current, session)) return false;
3294
- clearExternalDepositLockSession(
3295
- current.owner,
3296
- externalDepositSessionId(current.chainId, current.tokenId),
3297
- current.generation
3298
- );
3299
- return loadExternalDepositLockSession(current.owner) === void 0;
3300
- }
3301
- function clearSubmittedPayload(session, signature, clearAmbiguous = false) {
3302
- const current = loadExternalDepositLockSession(session.owner);
3303
- if (!current?.payload || current.payload.signature !== signature || current.submissionAmbiguous && !clearAmbiguous) {
3304
- return false;
3305
- }
3306
- return writeSessionRecord({ ...current, submissionAmbiguous: void 0, payload: void 0 });
3307
- }
3308
- function markSubmissionAmbiguous(session, signature) {
3309
- const current = loadExternalDepositLockSession(session.owner);
3310
- if (!current?.payload || current.payload.signature !== signature) return false;
3311
- if (current.submissionAmbiguous) return true;
3312
- return writeSessionRecord({ ...current, submissionAmbiguous: true });
3313
- }
3314
- function ambiguousSubmissionError(error) {
3315
- return new PostDepositLockError(
3316
- "Lock submission could not be confirmed. Retry only the saved signature, or check locked funds before stopping recovery.",
3317
- error.reason,
3318
- error.signedAmount,
3319
- error.creditedAmount,
3320
- {
3321
- cause: error.cause,
3322
- submissionMayHaveSucceeded: true
3323
- }
3324
- );
3325
- }
3326
- function isDefinitiveSubmissionFailure(error) {
3327
- const cause = error.cause;
3328
- return cause instanceof AccountingApiError && cause.statusCode !== 408 && cause.statusCode < 500;
3329
- }
3330
- function submissionLockName(session) {
3331
- return `privana:external-deposit-lock:submit:${session.owner.toLowerCase()}:${externalDepositSessionId(session.chainId, session.tokenId)}`;
3332
- }
3333
- async function withSubmissionLock(session, task) {
3334
- const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
3335
- if (!locks) return task(false);
3336
- return locks.request(submissionLockName(session), () => task(true));
3337
- }
3338
- function externalDepositRetryAmount(session) {
3339
- if (!session.creditedAmount) throw new Error("No credited external deposit to recover");
3340
- const creditedAmount = BigInt(session.creditedAmount);
3341
- const maxLockAmount = BigInt(session.maxLockAmount);
3342
- return creditedAmount < maxLockAmount ? creditedAmount : maxLockAmount;
3343
- }
3344
- async function submitExternalDepositLock(client, session) {
3345
- return withSubmissionLock(session, async (crossTabExclusive) => {
3346
- const current = loadExternalDepositLockSession(session.owner);
3347
- if (!current || current.generation !== session.generation) {
3348
- throw new PostDepositLockError(
3349
- "External deposit lock session changed before submission",
3350
- "not-found",
3351
- BigInt(session.maxLockAmount),
3352
- session.creditedAmount ? BigInt(session.creditedAmount) : void 0
3353
- );
3354
- }
3355
- const result = await submitExternalDepositLockAttempt(client, current, crossTabExclusive);
3356
- clearExternalDepositLockSession(
3357
- current.owner,
3358
- externalDepositSessionId(current.chainId, current.tokenId),
3359
- current.generation
3360
- );
3361
- return result;
3362
- });
3363
- }
3364
- async function submitExternalDepositLockAttempt(client, session, crossTabExclusive) {
3365
- const creditedAmount = session.creditedAmount ? BigInt(session.creditedAmount) : void 0;
3366
- if (!creditedAmount) {
3367
- throw new PostDepositLockError(
3368
- "External deposit has not been credited",
3369
- "submission-failed",
3370
- BigInt(session.maxLockAmount)
3371
- );
3372
- }
3373
- const payload = session.payload;
3374
- if (!payload) {
3375
- throw new PostDepositLockError(
3376
- "No persisted signed policy found for this deposit session",
3377
- "not-found",
3378
- BigInt(session.maxLockAmount),
3379
- creditedAmount
3380
- );
3381
- }
3382
- try {
3383
- return await submitPendingLock({
3384
- client,
3385
- payload,
3386
- creditedAmount,
3387
- beforeSubmit: () => {
3388
- if (!markSubmissionAmbiguous(session, payload.signature)) {
3389
- throw new Error("Unable to persist lock submission recovery state");
3390
- }
3391
- }
3392
- });
3393
- } catch (err) {
3394
- if (err instanceof PostDepositLockError && err.reason === "submission-failed" && crossTabExclusive && !session.submissionAmbiguous && isDefinitiveSubmissionFailure(err)) {
3395
- if (!clearSubmittedPayload(session, payload.signature, true)) {
3396
- const latest = loadExternalDepositLockSession(session.owner);
3397
- if (latest?.submissionAmbiguous) throw ambiguousSubmissionError(err);
3398
- }
3399
- } else if (err instanceof PostDepositLockError && err.reason === "submission-failed") {
3400
- const ambiguous = markSubmissionAmbiguous(session, payload.signature) || loadExternalDepositLockSession(session.owner)?.submissionAmbiguous === true;
3401
- if (ambiguous) throw ambiguousSubmissionError(err);
3402
- } else {
3403
- const current = loadExternalDepositLockSession(session.owner);
3404
- if (session.submissionAmbiguous || current?.submissionAmbiguous) {
3405
- if (err instanceof PostDepositLockError) throw ambiguousSubmissionError(err);
3406
- throw err;
3407
- }
3408
- if (!clearSubmittedPayload(session, payload.signature)) {
3409
- const latest = loadExternalDepositLockSession(session.owner);
3410
- if (latest?.submissionAmbiguous && err instanceof PostDepositLockError) {
3411
- throw ambiguousSubmissionError(err);
3412
- }
3413
- }
3414
- }
3415
- throw err;
3416
- }
3417
- }
3418
- function useExternalDepositLock({
3419
- allowance,
3420
- onLockSubmitted,
3421
- onLockFailed
3422
- }) {
3423
- const { client, networkConfig, serviceAddress, getTokenById, tokensStatus } = usePrivanaContext();
3424
- const config = useConfig();
3425
- const queryClient = useQueryClient();
3426
- const { address } = useAccount();
3427
- const { privateReadAddress } = usePrivateReadRequest();
3428
- const { ensureCorrectChain } = useEnsureCorrectChain();
3429
- const [isSigning, setIsSigning] = useState(false);
3430
- const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3431
- const [session, setSession] = useState(null);
3432
- const sessionRef = useRef(null);
3433
- const signingRef = useRef(false);
3434
- const settlingRef = useRef(false);
3435
- const pendingCreditedTransitionRef = useRef(null);
3436
- const hydratedScopeRef = useRef(null);
3437
- const beneficiaryRef = useRef(privateReadAddress);
3438
- beneficiaryRef.current = privateReadAddress;
3439
- const onLockSubmittedRef = useRef(onLockSubmitted);
3440
- const onLockFailedRef = useRef(onLockFailed);
3441
- useEffect(() => {
3442
- onLockSubmittedRef.current = onLockSubmitted;
3443
- onLockFailedRef.current = onLockFailed;
3444
- }, [onLockSubmitted, onLockFailed]);
3445
- const installSession = useCallback((next) => {
3446
- sessionRef.current = next;
3447
- setSession(next);
3448
- }, []);
3449
- const preserveCreditedFallback = useCallback(
3450
- (durable) => {
3451
- const pending = pendingCreditedTransitionRef.current;
3452
- if (pending && durable && isSameExternalDepositLockSession(durable, pending.predecessor)) {
3453
- return pending.credited;
3454
- }
3455
- pendingCreditedTransitionRef.current = null;
3456
- return durable;
3457
- },
3458
- []
3459
- );
3460
- const notifyFailure = useCallback((error) => {
3461
- onLockFailedRef.current?.(error);
3462
- }, []);
3463
- const submitSession = useCallback(
3464
- async (active) => {
3465
- if (settlingRef.current) return;
3466
- settlingRef.current = true;
3467
- setIsSubmittingLock(true);
3468
- let result;
3469
- try {
3470
- let submission = active;
3471
- const storedBeforeSubmit = loadExternalDepositLockSession(active.owner);
3472
- if (storedBeforeSubmit?.generation === active.generation) {
3473
- if (active.creditedAmount && !storedBeforeSubmit.creditedAmount) {
3474
- const pending = pendingCreditedTransitionRef.current;
3475
- if (!pending || !isSameExternalDepositLockSession(active, pending.credited) || !isSameExternalDepositLockSession(storedBeforeSubmit, pending.predecessor)) {
3476
- throw new ExternalDepositLockSessionChangedError();
3477
- }
3478
- saveExternalDepositLockSession(pending.credited, pending.predecessor);
3479
- pendingCreditedTransitionRef.current = null;
3480
- submission = pending.credited;
3481
- } else {
3482
- submission = storedBeforeSubmit;
3483
- }
3484
- }
3485
- result = await submitExternalDepositLock(client, submission);
3486
- } catch (err) {
3487
- const stored = loadExternalDepositLockSession(active.owner);
3488
- const error = err instanceof PostDepositLockError ? err : new PostDepositLockError(
3489
- err instanceof Error ? err.message : "Lock submission failed",
3490
- "submission-failed",
3491
- BigInt(active.maxLockAmount),
3492
- active.creditedAmount ? BigInt(active.creditedAmount) : void 0,
3493
- { cause: err }
3494
- );
3495
- if (err instanceof ExternalDepositLockSessionChangedError) {
3496
- pendingCreditedTransitionRef.current = null;
3497
- installSession(stored ?? null);
3498
- return;
3499
- }
3500
- if (!stored || stored.generation !== active.generation) {
3501
- pendingCreditedTransitionRef.current = null;
3502
- installSession(stored ?? null);
3503
- return;
3504
- }
3505
- if (active.creditedAmount && !stored.creditedAmount) {
3506
- const pending = pendingCreditedTransitionRef.current;
3507
- if (pending && isSameExternalDepositLockSession(active, pending.credited) && isSameExternalDepositLockSession(stored, pending.predecessor)) {
3508
- installSession(pending.credited);
3509
- notifyFailure(error);
3510
- return;
3511
- }
3512
- pendingCreditedTransitionRef.current = null;
3513
- installSession(stored);
3514
- return;
3515
- }
3516
- installSession(active.creditedAmount && !stored.creditedAmount ? active : stored);
3517
- notifyFailure(error);
3518
- return;
3519
- } finally {
3520
- settlingRef.current = false;
3521
- setIsSubmittingLock(false);
3522
- }
3523
- installSession(loadExternalDepositLockSession(active.owner) ?? null);
3524
- queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
3525
- queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
3526
- queryClient.invalidateQueries({ queryKey: ["accounting-total-locked-balance"] });
3527
- queryClient.invalidateQueries({ queryKey: ["accounting-history"] });
3528
- onLockSubmittedRef.current?.(result);
3529
- },
3530
- [client, installSession, notifyFailure, queryClient]
3531
- );
3532
- const validateRestoredSession = useCallback(
3533
- (restored) => {
3534
- if (!restored || !serviceAddress) return null;
3535
- const token = getTokenById(restored.tokenId);
3536
- const invalid = restored.serviceAddress.toLowerCase() !== serviceAddress.toLowerCase() || !token || token.chainId !== restored.chainId || token.contract === zeroAddress || !restored.creditedAmount && !restored.payload;
3537
- if (!invalid) return restored;
3538
- clearExternalDepositLockSession(
3539
- restored.owner,
3540
- externalDepositSessionId(restored.chainId, restored.tokenId),
3541
- restored.generation
3542
- );
3543
- return null;
3544
- },
3545
- [getTokenById, serviceAddress]
3546
- );
3547
- useEffect(() => {
3548
- if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") {
3549
- setSession(null);
3550
- return;
3551
- }
3552
- const hydrationKey = `${privateReadAddress.toLowerCase()}:${serviceAddress.toLowerCase()}`;
3553
- const shouldResume = hydratedScopeRef.current !== hydrationKey;
3554
- hydratedScopeRef.current = hydrationKey;
3555
- const restored = preserveCreditedFallback(
3556
- validateRestoredSession(loadExternalDepositLockSession(privateReadAddress))
3557
- );
3558
- installSession(restored);
3559
- if (!restored) {
3560
- return;
3561
- }
3562
- if (!shouldResume) return;
3563
- if (restored.creditedAmount && restored.payload) {
3564
- void submitSession(restored);
3565
- } else if (restored.creditedAmount) {
3566
- notifyFailure(
3567
- new PostDepositLockError(
3568
- "Deposit credited; sign a fresh policy to lock the funds",
3569
- "not-found",
3570
- BigInt(restored.maxLockAmount),
3571
- BigInt(restored.creditedAmount)
3572
- )
3573
- );
3574
- }
3575
- }, [
3576
- installSession,
3577
- notifyFailure,
3578
- privateReadAddress,
3579
- preserveCreditedFallback,
3580
- serviceAddress,
3581
- submitSession,
3582
- tokensStatus,
3583
- validateRestoredSession
3584
- ]);
3585
- useEffect(() => {
3586
- if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") return;
3587
- const reconcile = (restored) => {
3588
- installSession(preserveCreditedFallback(validateRestoredSession(restored)));
3589
- };
3590
- const unsubscribe = subscribeExternalDepositLockSession(privateReadAddress, reconcile);
3591
- reconcile(loadExternalDepositLockSession(privateReadAddress));
3592
- return unsubscribe;
3593
- }, [
3594
- installSession,
3595
- preserveCreditedFallback,
3596
- privateReadAddress,
3597
- serviceAddress,
3598
- tokensStatus,
3599
- validateRestoredSession
3600
- ]);
3601
- const signAndPersist = useCallback(
3602
- async ({ tokenId, amount }) => {
3603
- if (!allowance) throw new Error("No allowance configured for this deposit");
3604
- if (signingRef.current || settlingRef.current) {
3605
- throw new Error("A policy operation is already in progress");
3606
- }
3607
- if (!canUseSharedBrowserStorage()) {
3608
- throw new Error("Browser storage is required for locked deposit recovery");
3609
- }
3610
- const owner = requireDepositLockOwner(address, privateReadAddress);
3611
- if (sessionRef.current || loadExternalDepositLockSession(owner)) {
3612
- throw new Error("Finish or cancel the active external deposit before starting another");
3613
- }
3614
- const token = getTokenById(tokenId);
3615
- if (!token) throw new Error(`Unknown token ID: ${tokenId}`);
3616
- if (token.contract === zeroAddress) {
3617
- throw new Error("External deposits support ERC20 tokens only");
3618
- }
3619
- const targetService = requireServiceAddress(serviceAddress);
3620
- const lockDuration = allowance.lockDuration ?? DEFAULT_LOCK_DURATION_SECONDS;
3621
- signingRef.current = true;
3622
- setIsSigning(true);
3623
- try {
3624
- await ensureCorrectChain(networkConfig.chainId);
3625
- const signingWalletClient = await getWalletClient(config, {
3626
- chainId: networkConfig.chainId
3627
- });
3628
- const payload = await createSignedLockRequest({
3629
- client,
3630
- walletClient: signingWalletClient,
3631
- userAddress: owner,
3632
- networkConfig,
3633
- serviceAddress: targetService,
3634
- tokenId: token.id,
3635
- amount: clampLockAmount(amount, BigInt(allowance.value)),
3636
- lockDuration
3637
- });
3638
- const startBlock = await getBlockNumber(config, { chainId: token.chainId });
3639
- if (startBlock > BigInt(Number.MAX_SAFE_INTEGER)) {
3640
- throw new Error("Source-chain block number exceeds the supported range");
3641
- }
3642
- if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
3643
- throw new Error("Authenticated deposit account changed while signing");
3644
- }
3645
- const next = {
3646
- version: 1,
3647
- owner,
3648
- serviceAddress: targetService,
3649
- chainId: token.chainId,
3650
- tokenId: token.id,
3651
- startBlock: Number(startBlock),
3652
- depositAmount: amount.toString(),
3653
- maxLockAmount: String(payload.amount),
3654
- lockDuration,
3655
- generation: payload.signature,
3656
- payload
3657
- };
3658
- saveExternalDepositLockSession(next, null);
3659
- installSession(next);
3660
- } finally {
3661
- signingRef.current = false;
3662
- setIsSigning(false);
3663
- }
3664
- },
3665
- [
3666
- address,
3667
- allowance,
3668
- client,
3669
- config,
3670
- ensureCorrectChain,
3671
- getTokenById,
3672
- installSession,
3673
- networkConfig,
3674
- privateReadAddress,
3675
- serviceAddress
3676
- ]
3677
- );
3678
- const settleAfterCredit = useCallback(
3679
- async (txHash, creditedAmount) => {
3680
- const active = sessionRef.current;
3681
- if (!active || settlingRef.current) return;
3682
- if (!isCurrentExternalDepositVerification(active, txHash)) {
3683
- return;
3684
- }
3685
- if (creditedAmount <= 0n) throw new Error("Credited amount must be positive");
3686
- const credited = {
3687
- ...active,
3688
- verification: void 0,
3689
- creditedAmount: creditedAmount.toString()
3690
- };
3691
- try {
3692
- saveExternalDepositLockSession(credited, active);
3693
- pendingCreditedTransitionRef.current = null;
3694
- } catch (err) {
3695
- const stored = loadExternalDepositLockSession(active.owner);
3696
- if (err instanceof ExternalDepositLockSessionChangedError || !stored || !isSameExternalDepositLockSession(stored, active)) {
3697
- pendingCreditedTransitionRef.current = null;
3698
- installSession(stored ?? null);
3699
- return;
3700
- }
3701
- pendingCreditedTransitionRef.current = { predecessor: active, credited };
3702
- installSession(credited);
3703
- notifyFailure(
3704
- new PostDepositLockError(
3705
- err instanceof Error ? err.message : "Unable to persist credited deposit state",
3706
- "submission-failed",
3707
- BigInt(active.maxLockAmount),
3708
- creditedAmount,
3709
- { cause: err }
3710
- )
3711
- );
3712
- return;
3713
- }
3714
- installSession(credited);
3715
- await submitSession(credited);
3716
- },
3717
- [installSession, notifyFailure, submitSession]
3718
- );
3719
- const recordVerification = useCallback(
3720
- (context) => {
3721
- const active = sessionRef.current;
3722
- if (!active || active.creditedAmount) {
3723
- throw new Error("No external deposit session is awaiting verification");
3724
- }
3725
- if (active.verification) {
3726
- const sameTransfer = active.verification.hash.toLowerCase() === context.hash.toLowerCase() && active.verification.chainId === context.chainId && active.verification.amount === context.amount.toString() && (active.verification.logIndex ?? 0) === (context.logIndex ?? 0);
3727
- if (sameTransfer) return;
3728
- throw new Error("Another transfer is already being verified for this deposit session");
3729
- }
3730
- if (context.chainId !== active.chainId || context.amount <= 0n) {
3731
- throw new Error("Discovered transfer does not match the signed deposit session");
3732
- }
3733
- const current = loadExternalDepositLockSession(active.owner);
3734
- if (!current || current.generation !== active.generation) {
3735
- throw new Error("The signed deposit session changed before verification");
2645
+ function TransactionErrorView({
2646
+ title,
2647
+ message,
2648
+ explorerUrl,
2649
+ explorerLabel,
2650
+ onRetry,
2651
+ onDismiss,
2652
+ isRetrying,
2653
+ retryLabel = "Retry Verification",
2654
+ dismissLabel = "Close"
2655
+ }) {
2656
+ return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
2657
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2658
+ /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-amber-400", children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
2659
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
2660
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: title }),
2661
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
2662
+ ] })
2663
+ ] }),
2664
+ explorerUrl && /* @__PURE__ */ jsxs(
2665
+ "a",
2666
+ {
2667
+ href: explorerUrl,
2668
+ target: "_blank",
2669
+ rel: "noopener noreferrer",
2670
+ className: "border-border text-foreground hover:bg-secondary flex h-10 w-full items-center justify-center gap-1.5 rounded-[10px] border text-sm transition-colors",
2671
+ children: [
2672
+ explorerLabel ?? "View on Explorer",
2673
+ /* @__PURE__ */ jsx(ExternalLinkIcon, {})
2674
+ ]
3736
2675
  }
3737
- const next = {
3738
- ...active,
3739
- verification: {
3740
- hash: context.hash,
3741
- chainId: context.chainId,
3742
- amount: context.amount.toString(),
3743
- logIndex: context.logIndex
2676
+ ),
2677
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2678
+ onDismiss && /* @__PURE__ */ jsx(
2679
+ "button",
2680
+ {
2681
+ onClick: onDismiss,
2682
+ disabled: isRetrying,
2683
+ className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
2684
+ children: dismissLabel
3744
2685
  }
3745
- };
3746
- saveExternalDepositLockSession(next, active);
3747
- installSession(next);
3748
- },
3749
- [installSession]
3750
- );
3751
- const retryAfterCredit = useCallback(async () => {
3752
- const active = sessionRef.current;
3753
- if (!active?.creditedAmount) throw new Error("No credited external deposit to recover");
3754
- if (signingRef.current || settlingRef.current) {
3755
- throw new Error("A policy operation is already in progress");
3756
- }
3757
- if (privateReadAddress?.toLowerCase() !== active.owner.toLowerCase()) {
3758
- throw new Error("Authenticated deposit account does not match the credited deposit");
2686
+ ),
2687
+ /* @__PURE__ */ jsx(
2688
+ "button",
2689
+ {
2690
+ onClick: onRetry,
2691
+ disabled: isRetrying,
2692
+ className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
2693
+ children: isRetrying ? "Retrying\u2026" : retryLabel
2694
+ }
2695
+ )
2696
+ ] })
2697
+ ] });
2698
+ }
2699
+ function CloseIcon() {
2700
+ return /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
2701
+ "path",
2702
+ {
2703
+ d: "M11.99997 11.99997l-5.99995-5.99995-6.00002-6.00002m6.00002 6.00002l6.00001-6.00002m-12.00003 12.00003l6.00002-6.00001",
2704
+ stroke: "currentColor",
2705
+ strokeWidth: "1.5",
2706
+ strokeLinecap: "round",
2707
+ strokeLinejoin: "round"
3759
2708
  }
3760
- const token = getTokenById(active.tokenId);
3761
- if (!token || token.chainId !== active.chainId || token.contract === zeroAddress) {
3762
- throw new Error("The credited token is no longer available");
2709
+ ) });
2710
+ }
2711
+ function ChevronRightIcon() {
2712
+ return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "-rotate-90", children: /* @__PURE__ */ jsx(
2713
+ "path",
2714
+ {
2715
+ d: "M0 0l6 6 6-6",
2716
+ stroke: "currentColor",
2717
+ strokeWidth: "1.5",
2718
+ strokeLinecap: "round",
2719
+ strokeLinejoin: "round",
2720
+ fill: "none"
3763
2721
  }
3764
- const targetService = requireServiceAddress(serviceAddress);
3765
- if (targetService.toLowerCase() !== active.serviceAddress.toLowerCase()) {
3766
- throw new Error("The service for this credited deposit has changed");
2722
+ ) });
2723
+ }
2724
+ function ChevronLeftIcon() {
2725
+ return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "rotate-90", children: /* @__PURE__ */ jsx(
2726
+ "path",
2727
+ {
2728
+ d: "M0 0l6 6 6-6",
2729
+ stroke: "currentColor",
2730
+ strokeWidth: "1.5",
2731
+ strokeLinecap: "round",
2732
+ strokeLinejoin: "round",
2733
+ fill: "none"
3767
2734
  }
3768
- if (active.payload) {
3769
- await submitSession(active);
3770
- return;
2735
+ ) });
2736
+ }
2737
+ function ChevronDownIcon({
2738
+ direction = "down",
2739
+ className
2740
+ }) {
2741
+ return /* @__PURE__ */ jsx(
2742
+ "svg",
2743
+ {
2744
+ width: "16",
2745
+ height: "16",
2746
+ viewBox: "0 0 16 16",
2747
+ fill: "none",
2748
+ className: cn(
2749
+ "transition-transform",
2750
+ direction === "right" && "-rotate-90",
2751
+ direction === "up" && "rotate-180",
2752
+ className
2753
+ ),
2754
+ children: /* @__PURE__ */ jsx(
2755
+ "path",
2756
+ {
2757
+ d: "M4 6l4 4 4-4",
2758
+ stroke: "currentColor",
2759
+ strokeWidth: "1.5",
2760
+ strokeLinecap: "round",
2761
+ strokeLinejoin: "round"
2762
+ }
2763
+ )
3771
2764
  }
3772
- if (active.submissionAmbiguous) {
3773
- throw new Error(
3774
- "The previous lock submission may have succeeded; a fresh signature is unsafe"
3775
- );
2765
+ );
2766
+ }
2767
+ function CopyIcon() {
2768
+ return /* @__PURE__ */ jsxs(
2769
+ "svg",
2770
+ {
2771
+ width: "16",
2772
+ height: "16",
2773
+ viewBox: "0 0 24 24",
2774
+ fill: "none",
2775
+ stroke: "currentColor",
2776
+ strokeWidth: "2",
2777
+ strokeLinecap: "round",
2778
+ strokeLinejoin: "round",
2779
+ children: [
2780
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
2781
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
2782
+ ]
3776
2783
  }
3777
- const owner = requireDepositLockOwner(address, privateReadAddress);
3778
- const amount = externalDepositRetryAmount(active);
3779
- let retrySession;
3780
- signingRef.current = true;
3781
- setIsSigning(true);
3782
- try {
3783
- await ensureCorrectChain(networkConfig.chainId);
3784
- const signingWalletClient = await getWalletClient(config, {
3785
- chainId: networkConfig.chainId
3786
- });
3787
- const payload = await createSignedLockRequest({
3788
- client,
3789
- walletClient: signingWalletClient,
3790
- userAddress: owner,
3791
- networkConfig,
3792
- serviceAddress: targetService,
3793
- tokenId: active.tokenId,
3794
- amount,
3795
- lockDuration: active.lockDuration
3796
- });
3797
- if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
3798
- throw new Error("Authenticated deposit account changed while signing");
2784
+ );
2785
+ }
2786
+ function CircleCheckIcon() {
2787
+ return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
2788
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
2789
+ /* @__PURE__ */ jsx(
2790
+ "path",
2791
+ {
2792
+ d: "M6 10l2.5 2.5L14 7.5",
2793
+ stroke: "currentColor",
2794
+ strokeWidth: "1.5",
2795
+ strokeLinecap: "round",
2796
+ strokeLinejoin: "round"
3799
2797
  }
3800
- retrySession = {
3801
- ...active,
3802
- depositAmount: amount.toString(),
3803
- generation: payload.signature,
3804
- verification: void 0,
3805
- submissionAmbiguous: void 0,
3806
- payload
3807
- };
3808
- saveExternalDepositLockSession(retrySession, active);
3809
- installSession(retrySession);
3810
- } finally {
3811
- signingRef.current = false;
3812
- setIsSigning(false);
3813
- }
3814
- await submitSession(retrySession);
3815
- }, [
3816
- address,
3817
- client,
3818
- config,
3819
- ensureCorrectChain,
3820
- getTokenById,
3821
- installSession,
3822
- networkConfig,
3823
- privateReadAddress,
3824
- serviceAddress,
3825
- submitSession
3826
- ]);
3827
- const clearVerification = useCallback(() => {
3828
- const active = sessionRef.current;
3829
- if (!active?.verification || active.creditedAmount) return false;
3830
- const next = clearExternalDepositVerification(active);
3831
- if (!next) {
3832
- installSession(loadExternalDepositLockSession(active.owner) ?? null);
3833
- return false;
3834
- }
3835
- installSession(next);
3836
- return true;
3837
- }, [installSession]);
3838
- const discardSession = useCallback(() => {
3839
- const active = sessionRef.current;
3840
- if (!active) return true;
3841
- if (signingRef.current || settlingRef.current) return false;
3842
- const pending = pendingCreditedTransitionRef.current;
3843
- const discardTarget = pending && isSameExternalDepositLockSession(active, pending.credited) ? pending.predecessor : active;
3844
- const discarded = discardExternalDepositLockSession(discardTarget);
3845
- const restored = loadExternalDepositLockSession(active.owner) ?? null;
3846
- if (discarded) pendingCreditedTransitionRef.current = null;
3847
- installSession(restored);
3848
- return discarded && !restored;
3849
- }, [installSession]);
3850
- return {
3851
- isSigning,
3852
- isSubmittingLock,
3853
- session,
3854
- signAndPersist,
3855
- recordVerification,
3856
- settleAfterCredit,
3857
- retryAfterCredit,
3858
- clearVerification,
3859
- discardSession
3860
- };
2798
+ )
2799
+ ] });
2800
+ }
2801
+ function CircleXIcon() {
2802
+ return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
2803
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
2804
+ /* @__PURE__ */ jsx(
2805
+ "path",
2806
+ {
2807
+ d: "M7 7l6 6M13 7l-6 6",
2808
+ stroke: "currentColor",
2809
+ strokeWidth: "1.5",
2810
+ strokeLinecap: "round",
2811
+ strokeLinejoin: "round"
2812
+ }
2813
+ )
2814
+ ] });
2815
+ }
2816
+ function ArrowRightIcon({ className }) {
2817
+ return /* @__PURE__ */ jsx("svg", { width: "24", height: "8", viewBox: "0 0 24 8", fill: "none", className, children: /* @__PURE__ */ jsx(
2818
+ "path",
2819
+ {
2820
+ d: "M0 4h22m0 0l-4-4m4 4l-4 4",
2821
+ stroke: "currentColor",
2822
+ strokeWidth: "1.5",
2823
+ strokeLinecap: "round",
2824
+ strokeLinejoin: "round"
2825
+ }
2826
+ ) });
3861
2827
  }
3862
2828
  var DEFAULT_MOONPAY_API_URL = "https://api.moonpay.com/v3";
3863
2829
  function useMoonpayLimits({
@@ -3901,100 +2867,6 @@ function useMoonpayLimits({
3901
2867
  error: query.error ?? null
3902
2868
  };
3903
2869
  }
3904
- function TokenSelectorView2({ selectedTokenId, onSelect }) {
3905
- const { enabledTokens } = usePrivanaContext();
3906
- return /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col rounded-[10px] p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex max-h-72 flex-col gap-1 overflow-y-auto", children: [
3907
- enabledTokens.map((token) => {
3908
- const isSelected = selectedTokenId === token.id;
3909
- return /* @__PURE__ */ jsxs(
3910
- "button",
3911
- {
3912
- type: "button",
3913
- onClick: () => onSelect(token.id),
3914
- className: cn(
3915
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors",
3916
- isSelected && "bg-secondary"
3917
- ),
3918
- children: [
3919
- /* @__PURE__ */ jsx("div", { className: "h-6 w-6 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 24) }),
3920
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3921
- /* @__PURE__ */ jsx("div", { className: "text-foreground text-sm font-medium", children: token.symbol }),
3922
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-[11px]", children: token.name })
3923
- ] }),
3924
- isSelected && /* @__PURE__ */ jsx("div", { className: "bg-primary flex h-4 w-4 shrink-0 items-center justify-center rounded-full", children: /* @__PURE__ */ jsx(
3925
- "svg",
3926
- {
3927
- width: "10",
3928
- height: "10",
3929
- viewBox: "0 0 10 10",
3930
- fill: "none",
3931
- className: "text-primary-foreground",
3932
- children: /* @__PURE__ */ jsx(
3933
- "path",
3934
- {
3935
- d: "M2 5L4 7L8 3",
3936
- stroke: "currentColor",
3937
- strokeWidth: "1.5",
3938
- strokeLinecap: "round",
3939
- strokeLinejoin: "round"
3940
- }
3941
- )
3942
- }
3943
- ) })
3944
- ]
3945
- },
3946
- token.id
3947
- );
3948
- }),
3949
- enabledTokens.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-sm", children: "No tokens available" })
3950
- ] }) });
3951
- }
3952
- function CreditCardWidgetView({
3953
- token,
3954
- amount,
3955
- allowance,
3956
- onCredited,
3957
- onLockSubmitted,
3958
- onLockFailed
3959
- }) {
3960
- const moonpayCurrencyCode = token?.moonpayCurrencyCode;
3961
- const containerRef = useRef(null);
3962
- const [widgetTheme, setWidgetTheme] = useState();
3963
- useEffect(() => {
3964
- if (containerRef.current) setWidgetTheme(resolveSdkTheme(containerRef.current));
3965
- }, []);
3966
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
3967
- /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Complete your purchase" }),
3968
- token && moonpayCurrencyCode ? /* @__PURE__ */ jsx(
3969
- FiatOnRampForm,
3970
- {
3971
- tokenId: token.id,
3972
- currencyCode: moonpayCurrencyCode,
3973
- quoteCurrencyAmount: amount || void 0,
3974
- lockAmount: true,
3975
- variant: "embedded",
3976
- autoStart: true,
3977
- theme: widgetTheme,
3978
- postDepositLock: allowance ? {
3979
- maxAmount: BigInt(allowance.value),
3980
- lockDuration: allowance.lockDuration
3981
- } : void 0,
3982
- onCredited,
3983
- onLockSubmitted,
3984
- onLockFailed
3985
- }
3986
- ) : /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
3987
- token?.symbol ?? "This token",
3988
- " isn\u2019t available for card purchases yet."
3989
- ] })
3990
- ] });
3991
- }
3992
- function resolveSdkTheme(el) {
3993
- const scheme = getComputedStyle(el).colorScheme;
3994
- if (scheme === "dark") return "dark";
3995
- if (scheme === "light") return "light";
3996
- return el.closest(".dark") ? "dark" : "light";
3997
- }
3998
2870
  function PrivanaIcon({ className, size = 32 }) {
3999
2871
  return /* @__PURE__ */ jsxs(
4000
2872
  "svg",
@@ -4087,81 +2959,6 @@ function AllowancePolicySection({
4087
2959
  ] })
4088
2960
  ] });
4089
2961
  }
4090
- function MoonPayGate({ enabled, children }) {
4091
- const { networkConfig } = usePrivanaContext();
4092
- const apiKey = networkConfig.moonpayApiKey;
4093
- if (!enabled || !apiKey) return /* @__PURE__ */ jsx(Fragment, { children });
4094
- return /* @__PURE__ */ jsx(MoonPayProvider, { apiKey, children });
4095
- }
4096
- function formatPostDepositLockFailure(error, token) {
4097
- if (error?.reason === "credited-below-signed" && error.creditedAmount !== void 0 && error.signedAmount !== void 0 && token) {
4098
- const creditedAmount = `${formatUnits(error.creditedAmount, token.decimals)} ${token.symbol}`;
4099
- const signedAmount = `${formatUnits(error.signedAmount, token.decimals)} ${token.symbol}`;
4100
- return `Credited amount (${creditedAmount}) is below the signed lock amount (${signedAmount})`;
4101
- }
4102
- return error?.message ?? "Lock submission failed";
4103
- }
4104
- function MethodTabs({
4105
- activeTab,
4106
- onTabChange
4107
- }) {
4108
- return /* @__PURE__ */ jsxs("div", { className: "bg-secondary relative flex gap-2 overflow-hidden rounded-[10px] p-1", children: [
4109
- /* @__PURE__ */ jsx(
4110
- "div",
4111
- {
4112
- className: cn(
4113
- "bg-input absolute top-1 bottom-1 left-1 w-[calc(50%-8px)] rounded-md transition-transform duration-200",
4114
- activeTab === "credit-card" && "translate-x-[calc(100%+8px)]"
4115
- )
4116
- }
4117
- ),
4118
- /* @__PURE__ */ jsx(
4119
- "button",
4120
- {
4121
- type: "button",
4122
- onClick: () => onTabChange("crypto"),
4123
- className: cn(
4124
- "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
4125
- activeTab === "crypto" ? "text-foreground" : "text-muted-foreground"
4126
- ),
4127
- children: "Crypto"
4128
- }
4129
- ),
4130
- /* @__PURE__ */ jsx(
4131
- "button",
4132
- {
4133
- type: "button",
4134
- onClick: () => onTabChange("credit-card"),
4135
- className: cn(
4136
- "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
4137
- activeTab === "credit-card" ? "text-foreground" : "text-muted-foreground"
4138
- ),
4139
- children: "Credit Card"
4140
- }
4141
- )
4142
- ] });
4143
- }
4144
- function MethodOption({
4145
- title,
4146
- description,
4147
- onClick
4148
- }) {
4149
- return /* @__PURE__ */ jsxs(
4150
- "button",
4151
- {
4152
- type: "button",
4153
- onClick,
4154
- className: "bg-input hover:bg-input/70 border-border flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3 text-left transition-colors",
4155
- children: [
4156
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
4157
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm leading-[14px] font-medium", children: title }),
4158
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs leading-3", children: description })
4159
- ] }),
4160
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
4161
- ]
4162
- }
4163
- );
4164
- }
4165
2962
  function DepositView({
4166
2963
  source,
4167
2964
  selectedToken,
@@ -4672,6 +3469,81 @@ function ExternalDepositView({
4672
3469
  ] })
4673
3470
  ] });
4674
3471
  }
3472
+ function MoonPayGate({ enabled, children }) {
3473
+ const { networkConfig } = usePrivanaContext();
3474
+ const apiKey = networkConfig.moonpayApiKey;
3475
+ if (!enabled || !apiKey) return /* @__PURE__ */ jsx(Fragment, { children });
3476
+ return /* @__PURE__ */ jsx(MoonPayProvider, { apiKey, children });
3477
+ }
3478
+ function formatPostDepositLockFailure(error, token) {
3479
+ if (error?.reason === "credited-below-signed" && error.creditedAmount !== void 0 && error.signedAmount !== void 0 && token) {
3480
+ const creditedAmount = `${formatUnits(error.creditedAmount, token.decimals)} ${token.symbol}`;
3481
+ const signedAmount = `${formatUnits(error.signedAmount, token.decimals)} ${token.symbol}`;
3482
+ return `Credited amount (${creditedAmount}) is below the signed lock amount (${signedAmount})`;
3483
+ }
3484
+ return error?.message ?? "Lock submission failed";
3485
+ }
3486
+ function MethodTabs({
3487
+ activeTab,
3488
+ onTabChange
3489
+ }) {
3490
+ return /* @__PURE__ */ jsxs("div", { className: "bg-secondary relative flex gap-2 overflow-hidden rounded-[10px] p-1", children: [
3491
+ /* @__PURE__ */ jsx(
3492
+ "div",
3493
+ {
3494
+ className: cn(
3495
+ "bg-input absolute top-1 bottom-1 left-1 w-[calc(50%-8px)] rounded-md transition-transform duration-200",
3496
+ activeTab === "credit-card" && "translate-x-[calc(100%+8px)]"
3497
+ )
3498
+ }
3499
+ ),
3500
+ /* @__PURE__ */ jsx(
3501
+ "button",
3502
+ {
3503
+ type: "button",
3504
+ onClick: () => onTabChange("crypto"),
3505
+ className: cn(
3506
+ "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
3507
+ activeTab === "crypto" ? "text-foreground" : "text-muted-foreground"
3508
+ ),
3509
+ children: "Crypto"
3510
+ }
3511
+ ),
3512
+ /* @__PURE__ */ jsx(
3513
+ "button",
3514
+ {
3515
+ type: "button",
3516
+ onClick: () => onTabChange("credit-card"),
3517
+ className: cn(
3518
+ "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
3519
+ activeTab === "credit-card" ? "text-foreground" : "text-muted-foreground"
3520
+ ),
3521
+ children: "Credit Card"
3522
+ }
3523
+ )
3524
+ ] });
3525
+ }
3526
+ function MethodOption({
3527
+ title,
3528
+ description,
3529
+ onClick
3530
+ }) {
3531
+ return /* @__PURE__ */ jsxs(
3532
+ "button",
3533
+ {
3534
+ type: "button",
3535
+ onClick,
3536
+ className: "bg-input hover:bg-input/70 border-border flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3 text-left transition-colors",
3537
+ children: [
3538
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
3539
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm leading-[14px] font-medium", children: title }),
3540
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs leading-3", children: description })
3541
+ ] }),
3542
+ /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
3543
+ ]
3544
+ }
3545
+ );
3546
+ }
4675
3547
  function DepositModalContent({
4676
3548
  defaultTab = "crypto",
4677
3549
  allowance,
@@ -5091,7 +3963,7 @@ function DepositModalContent({
5091
3963
  }
5092
3964
  ) }),
5093
3965
  activeView === "select-token" && /* @__PURE__ */ jsx(
5094
- TokenSelectorView2,
3966
+ TokenSelectorView,
5095
3967
  {
5096
3968
  selectedTokenId: selectedToken?.id,
5097
3969
  onSelect: (id) => {
@@ -5449,7 +4321,7 @@ function WithdrawModalContent({
5449
4321
  }
5450
4322
  ),
5451
4323
  view === "select-token" && /* @__PURE__ */ jsx(
5452
- TokenSelectorView2,
4324
+ TokenSelectorView,
5453
4325
  {
5454
4326
  selectedTokenId: selectedToken?.id,
5455
4327
  onSelect: (id) => {
@@ -5990,6 +4862,6 @@ function WalletInlineModal({ className, ...handlers }) {
5990
4862
  );
5991
4863
  }
5992
4864
 
5993
- export { DepositInlineModal, DepositModal, PrivanaButton, PrivanaIcon, PrivanaInlineModal, PrivanaModal, WalletInlineModal, WalletModal, WithdrawInlineModal, WithdrawModal, getChainIcon, getTokenIcon, useBalance, useBatchBalances, useDeposit, useDepositAddress, useExpiredLocks, useHistory, useHostedRedirectAuth, useLockFunds, useLockedFunds, useModifyLock, usePendingDeposits, usePendingWithdrawals, usePrivanaClient, useTokenInfo, useTokenList, useTotalLockedBalance, useTransfer, useUnlockFunds, useWithdraw };
4865
+ export { DepositInlineModal, DepositModal, PrivanaIcon, WalletInlineModal, WalletModal, WithdrawInlineModal, WithdrawModal, getChainIcon, getTokenIcon, useBalance, useBatchBalances, useDeposit, useDepositAddress, useExpiredLocks, useHistory, useHostedRedirectAuth, useLockFunds, useLockedFunds, useModifyLock, usePendingDeposits, usePendingWithdrawals, usePrivanaClient, useTokenInfo, useTokenList, useTotalLockedBalance, useTransfer, useUnlockFunds, useWithdraw };
5994
4866
  //# sourceMappingURL=index.js.map
5995
4867
  //# sourceMappingURL=index.js.map