@oasisprotocol/privana-sdk 0.5.2 → 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, getWalletClient, createSignedLockRequest, signWithdrawMessage, signLockMessage, signTransferMessage, signTransferLockedMessage, signModifyLockMessage, cn, Button, stripHostedAuthCallbackParams, AccountingApiError, getBrowserStorageItem, removeBrowserStorageItem, isSignedLockUsable, setBrowserStorageItem, shortenAddress, formatTimeRemaining, parseTokenAmount, getExplorerAddressUrl, getExplorerLabel, Skeleton, formatCountdown, FiatOnRampForm } from './chunk-ZVUMV4NR.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, savePendingLock, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, stripHostedAuthCallbackParams, submitPendingLock, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-ZVUMV4NR.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
- import { zeroAddress, erc20Abi } from 'viem';
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() {
@@ -234,7 +234,7 @@ function useBalance(options = {}) {
234
234
  if (!privateReadAddress) throw new Error("No authenticated account available");
235
235
  if (!tokenId) throw new Error("No token ID provided");
236
236
  if (!client) throw new Error("No accounting client");
237
- return executePrivateRead(() => client.getBalance(tokenId));
237
+ return executePrivateRead((readClient) => readClient.getBalance(tokenId));
238
238
  },
239
239
  enabled: hasProviders && (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!tokenId && !!client,
240
240
  refetchInterval: pollingInterval
@@ -259,7 +259,9 @@ function useBatchBalances(options) {
259
259
  queryKey: ["accounting-batch-balances", ...privateReadQueryScope, options.tokenIds],
260
260
  queryFn: async () => {
261
261
  if (!privateReadAddress) throw new Error("No authenticated account available");
262
- return executePrivateRead(() => client.getBatchBalances({ token_ids: options.tokenIds }));
262
+ return executePrivateRead(
263
+ (readClient) => readClient.getBatchBalances({ token_ids: options.tokenIds })
264
+ );
263
265
  },
264
266
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && options.tokenIds.length > 0 && !!client,
265
267
  refetchInterval: pollingInterval
@@ -313,7 +315,9 @@ function useDeposit(options = {}) {
313
315
  const { data: walletClient } = useWalletClient();
314
316
  const queryClient = useQueryClient();
315
317
  const config = useConfig();
316
- const { executePrivateRead } = usePrivateReadRequest();
318
+ const { executePrivateRead, privateReadAddress } = usePrivateReadRequest();
319
+ const privateReadAddressRef = useRef(privateReadAddress);
320
+ privateReadAddressRef.current = privateReadAddress;
317
321
  const confirmations = options.confirmations ?? 15;
318
322
  const [depositAddress, setDepositAddress] = useState(null);
319
323
  const [txHash, setTxHash] = useState();
@@ -349,12 +353,12 @@ function useDeposit(options = {}) {
349
353
  ]);
350
354
  const pendingLockRef = useRef(null);
351
355
  const submitPendingLockAfterCredit = useCallback(
352
- async (signedLock, response) => {
356
+ async (signedLock, creditedAmount) => {
353
357
  try {
354
358
  const result = await submitPendingLock({
355
359
  client,
356
360
  payload: signedLock,
357
- creditedAmount: response.amount != null ? BigInt(response.amount) : void 0
361
+ creditedAmount
358
362
  });
359
363
  queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
360
364
  queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
@@ -366,7 +370,7 @@ function useDeposit(options = {}) {
366
370
  err instanceof Error ? err.message : "Lock submission failed",
367
371
  "submission-failed",
368
372
  BigInt(signedLock.amount),
369
- void 0,
373
+ creditedAmount,
370
374
  { cause: err }
371
375
  );
372
376
  (onLockFailedRef.current ?? onErrorRef.current)?.(error2);
@@ -384,12 +388,12 @@ function useDeposit(options = {}) {
384
388
  } = useDepositVerification({
385
389
  pollInterval: options.pollInterval,
386
390
  pollTimeout: options.pollTimeout,
387
- onCredited: (hash, response) => {
391
+ onCredited: (hash, response, creditedAmount) => {
388
392
  verificationContextRef.current = null;
389
393
  const signedLock = pendingLockRef.current;
390
394
  pendingLockRef.current = null;
391
395
  if (signedLock) {
392
- void submitPendingLockAfterCredit(signedLock, response).finally(() => {
396
+ void submitPendingLockAfterCredit(signedLock, creditedAmount).finally(() => {
393
397
  if (address) clearPendingDeposit(address, hash);
394
398
  });
395
399
  } else if (address) {
@@ -407,7 +411,7 @@ function useDeposit(options = {}) {
407
411
  const addressMutation = useMutation({
408
412
  mutationFn: async () => {
409
413
  if (!address) throw new Error("No wallet connected");
410
- return executePrivateRead(() => client.getDepositAddress());
414
+ return executePrivateRead((readClient) => readClient.getDepositAddress());
411
415
  },
412
416
  onSuccess: (data) => {
413
417
  setDepositAddress(data);
@@ -545,6 +549,7 @@ function useDeposit(options = {}) {
545
549
  const lockAmount = clampLockAmount(params.amount, params.postDepositLock?.maxAmount);
546
550
  let signedLock;
547
551
  if (params.postDepositLock) {
552
+ const lockOwner = requireDepositLockOwner(address, privateReadAddress);
548
553
  setIsSwitchingChain(true);
549
554
  try {
550
555
  await ensureCorrectChain(networkConfig.chainId);
@@ -558,7 +563,7 @@ function useDeposit(options = {}) {
558
563
  signedLock = await createSignedLockRequest({
559
564
  client,
560
565
  walletClient: signingWalletClient,
561
- userAddress: address,
566
+ userAddress: lockOwner,
562
567
  networkConfig,
563
568
  serviceAddress: requireServiceAddress(
564
569
  params.postDepositLock.serviceAddress ?? serviceAddress
@@ -567,6 +572,9 @@ function useDeposit(options = {}) {
567
572
  amount: lockAmount,
568
573
  lockDuration: params.postDepositLock.lockDuration
569
574
  });
575
+ if (privateReadAddressRef.current?.toLowerCase() !== lockOwner.toLowerCase()) {
576
+ throw new Error("Authenticated deposit account changed while signing");
577
+ }
570
578
  }
571
579
  if (isStale()) return;
572
580
  setIsSwitchingChain(true);
@@ -649,6 +657,7 @@ function useDeposit(options = {}) {
649
657
  enabledTokens,
650
658
  ensureCorrectChain,
651
659
  networkConfig,
660
+ privateReadAddress,
652
661
  queryClient,
653
662
  serviceAddress,
654
663
  writeContractAsync,
@@ -685,12 +694,6 @@ function useDeposit(options = {}) {
685
694
  reset
686
695
  };
687
696
  }
688
- function requireServiceAddress(serviceAddress) {
689
- if (!serviceAddress) {
690
- throw new Error("Service address not configured");
691
- }
692
- return serviceAddress;
693
- }
694
697
  function useDepositAddress(options = {}) {
695
698
  const queryClient = useContext(QueryClientContext);
696
699
  const accountingContext = useSafePrivanaContext();
@@ -703,11 +706,12 @@ function useDepositAddress(options = {}) {
703
706
  queryFn: async () => {
704
707
  if (!privateReadAddress) throw new Error("No authenticated account available");
705
708
  if (!client) throw new Error("No accounting client");
706
- return executePrivateRead(() => client.getDepositAddress());
709
+ return executePrivateRead((readClient) => readClient.getDepositAddress());
707
710
  },
708
711
  enabled: isReady && (options.enabled ?? true)
709
712
  });
710
713
  return {
714
+ response: query.data,
711
715
  depositAddress: query.data?.deposit_address ?? null,
712
716
  isReady,
713
717
  isLoading: query.isLoading,
@@ -1158,7 +1162,7 @@ function useLockedFunds(options = {}) {
1158
1162
  queryKey: ["accounting-locked-funds", ...privateReadQueryScope, serviceAddress ?? null],
1159
1163
  queryFn: async () => {
1160
1164
  if (!privateReadAddress) throw new Error("No authenticated account available");
1161
- return executePrivateRead(() => client.getLockedFunds(serviceAddress));
1165
+ return executePrivateRead((readClient) => readClient.getLockedFunds(serviceAddress));
1162
1166
  },
1163
1167
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1164
1168
  refetchInterval: pollingInterval,
@@ -1183,7 +1187,7 @@ function useTotalLockedBalance(options = {}) {
1183
1187
  queryFn: async () => {
1184
1188
  if (!privateReadAddress) throw new Error("No authenticated account available");
1185
1189
  if (!tokenId) throw new Error("No token ID provided");
1186
- return executePrivateRead(() => client.getTotalLockedBalance(tokenId));
1190
+ return executePrivateRead((readClient) => readClient.getTotalLockedBalance(tokenId));
1187
1191
  },
1188
1192
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!tokenId && !!client,
1189
1193
  refetchInterval: pollingInterval
@@ -1203,7 +1207,7 @@ function useExpiredLocks(options = {}) {
1203
1207
  queryKey: ["accounting-expired-locks", ...privateReadQueryScope],
1204
1208
  queryFn: async () => {
1205
1209
  if (!privateReadAddress) throw new Error("No authenticated account available");
1206
- return executePrivateRead(() => client.getExpiredLocks());
1210
+ return executePrivateRead((readClient) => readClient.getExpiredLocks());
1207
1211
  },
1208
1212
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1209
1213
  refetchInterval: pollingInterval
@@ -1236,7 +1240,7 @@ function usePendingDeposits(options = {}) {
1236
1240
  if (!client) throw new Error("No accounting client");
1237
1241
  try {
1238
1242
  return await executePrivateRead(
1239
- () => client.getPendingDeposits({
1243
+ (readClient) => readClient.getPendingDeposits({
1240
1244
  chain_id: chainId,
1241
1245
  token_address: tokenAddress,
1242
1246
  lookback_blocks: lookbackBlocks
@@ -1317,7 +1321,7 @@ function useHistory(options = {}) {
1317
1321
  queryKey: ["accounting-history", ...privateReadQueryScope, offset, limit],
1318
1322
  queryFn: async () => {
1319
1323
  if (!privateReadAddress) throw new Error("No authenticated account available");
1320
- return executePrivateRead(() => client.getHistory({ offset, limit }));
1324
+ return executePrivateRead((readClient) => readClient.getHistory({ offset, limit }));
1321
1325
  },
1322
1326
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1323
1327
  refetchInterval: pollingInterval,
@@ -1484,16 +1488,6 @@ function DialogContent({
1484
1488
  )
1485
1489
  ] });
1486
1490
  }
1487
- function DialogHeader({ className, ...props }) {
1488
- return /* @__PURE__ */ jsx(
1489
- "div",
1490
- {
1491
- "data-slot": "dialog-header",
1492
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
1493
- ...props
1494
- }
1495
- );
1496
- }
1497
1491
  function DialogTitle({ className, ...props }) {
1498
1492
  return /* @__PURE__ */ jsx(
1499
1493
  DialogPrimitive.Title,
@@ -1517,6 +1511,745 @@ function DialogDescription({
1517
1511
  }
1518
1512
  );
1519
1513
  }
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()}`;
1527
+ }
1528
+ function isExternalDepositBlockInSession(session, blockNumber) {
1529
+ return Number.isSafeInteger(blockNumber) && blockNumber > session.startBlock;
1530
+ }
1531
+ function isCurrentExternalDepositVerification(session, txHash) {
1532
+ return session.creditedAmount === void 0 && session.verification?.hash.toLowerCase() === txHash.toLowerCase();
1533
+ }
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;
1542
+ }
1543
+ }
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;
1560
+ }
1561
+ }
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);
1580
+ }
1581
+ function writeSessionRecord(session) {
1582
+ const id = externalDepositSessionId(session.chainId, session.tokenId);
1583
+ return setSharedBrowserStorageItem(sessionKey(session.owner, id), JSON.stringify(session));
1584
+ }
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");
1600
+ }
1601
+ if (previousId) {
1602
+ removeSharedBrowserStorageItem(sessionKey(session.owner, previousId));
1603
+ }
1604
+ }
1605
+ }
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;
1617
+ }
1618
+ return parsed;
1619
+ } catch {
1620
+ removeSharedBrowserStorageItem(key);
1621
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
1622
+ return void 0;
1623
+ }
1624
+ }
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);
1644
+ }
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));
1654
+ }
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);
1658
+ }
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;
1662
+ }
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,
1711
+ {
1712
+ cause: error.cause,
1713
+ submissionMayHaveSucceeded: true
1714
+ }
1715
+ );
1716
+ }
1717
+ function isDefinitiveSubmissionFailure(error) {
1718
+ const cause = error.cause;
1719
+ return cause instanceof AccountingApiError && cause.statusCode !== 408 && cause.statusCode < 500;
1720
+ }
1721
+ function submissionLockName(session) {
1722
+ return `privana:external-deposit-lock:submit:${session.owner.toLowerCase()}:${externalDepositSessionId(session.chainId, session.tokenId)}`;
1723
+ }
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));
1728
+ }
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
+ );
1745
+ }
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
+ });
1754
+ }
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");
1781
+ }
1782
+ }
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);
1803
+ }
1804
+ }
1805
+ }
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;
1848
+ },
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);
1920
+ },
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;
1935
+ },
1936
+ [getTokenById, serviceAddress]
1937
+ );
1938
+ useEffect(() => {
1939
+ if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") {
1940
+ setSession(null);
1941
+ return;
1942
+ }
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
+ ]);
1976
+ useEffect(() => {
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");
1997
+ }
1998
+ if (!canUseSharedBrowserStorage()) {
1999
+ throw new Error("Browser storage is required for locked deposit recovery");
2000
+ }
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");
2004
+ }
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");
2009
+ }
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");
2032
+ }
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);
2054
+ }
2055
+ },
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);
2107
+ },
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);
2139
+ },
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");
2147
+ }
2148
+ if (privateReadAddress?.toLowerCase() !== active.owner.toLowerCase()) {
2149
+ throw new Error("Authenticated deposit account does not match the credited deposit");
2150
+ }
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");
2154
+ }
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");
2190
+ }
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
+ };
2252
+ }
1520
2253
  function USDCIcon({ className, size = 24 }) {
1521
2254
  return /* @__PURE__ */ jsxs(
1522
2255
  "svg",
@@ -1658,134 +2391,99 @@ function getChainIcon(chainId, size) {
1658
2391
  );
1659
2392
  }
1660
2393
  }
1661
- function CloseIcon() {
1662
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
1663
- "path",
1664
- {
1665
- 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",
1666
- stroke: "currentColor",
1667
- strokeWidth: "1.5",
1668
- strokeLinecap: "round",
1669
- strokeLinejoin: "round"
1670
- }
1671
- ) });
1672
- }
1673
- function ChevronRightIcon() {
1674
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "-rotate-90", children: /* @__PURE__ */ jsx(
1675
- "path",
1676
- {
1677
- d: "M0 0l6 6 6-6",
1678
- stroke: "currentColor",
1679
- strokeWidth: "1.5",
1680
- strokeLinecap: "round",
1681
- strokeLinejoin: "round",
1682
- fill: "none"
1683
- }
1684
- ) });
1685
- }
1686
- function ChevronLeftIcon() {
1687
- return /* @__PURE__ */ jsx("svg", { width: "10", height: "5", viewBox: "0 0 12 6", className: "rotate-90", children: /* @__PURE__ */ jsx(
1688
- "path",
1689
- {
1690
- d: "M0 0l6 6 6-6",
1691
- stroke: "currentColor",
1692
- strokeWidth: "1.5",
1693
- strokeLinecap: "round",
1694
- strokeLinejoin: "round",
1695
- fill: "none"
1696
- }
1697
- ) });
1698
- }
1699
- function ChevronDownIcon({
1700
- direction = "down",
1701
- className
1702
- }) {
1703
- return /* @__PURE__ */ jsx(
1704
- "svg",
1705
- {
1706
- width: "16",
1707
- height: "16",
1708
- viewBox: "0 0 16 16",
1709
- fill: "none",
1710
- className: cn(
1711
- "transition-transform",
1712
- direction === "right" && "-rotate-90",
1713
- direction === "up" && "rotate-180",
1714
- className
1715
- ),
1716
- children: /* @__PURE__ */ jsx(
1717
- "path",
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",
1718
2401
  {
1719
- d: "M4 6l4 4 4-4",
1720
- stroke: "currentColor",
1721
- strokeWidth: "1.5",
1722
- strokeLinecap: "round",
1723
- strokeLinejoin: "round"
1724
- }
1725
- )
1726
- }
1727
- );
1728
- }
1729
- function CopyIcon() {
1730
- return /* @__PURE__ */ jsxs(
1731
- "svg",
1732
- {
1733
- width: "16",
1734
- height: "16",
1735
- viewBox: "0 0 24 24",
1736
- fill: "none",
1737
- stroke: "currentColor",
1738
- strokeWidth: "2",
1739
- strokeLinecap: "round",
1740
- strokeLinejoin: "round",
1741
- children: [
1742
- /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
1743
- /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
1744
- ]
1745
- }
1746
- );
1747
- }
1748
- function CircleCheckIcon() {
1749
- return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
1750
- /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
1751
- /* @__PURE__ */ jsx(
1752
- "path",
1753
- {
1754
- d: "M6 10l2.5 2.5L14 7.5",
1755
- stroke: "currentColor",
1756
- strokeWidth: "1.5",
1757
- strokeLinecap: "round",
1758
- strokeLinejoin: "round"
1759
- }
1760
- )
1761
- ] });
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
+ ),
2408
+ children: [
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",
2416
+ {
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
+ )
2432
+ }
2433
+ ) })
2434
+ ]
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
+ ] }) });
1762
2441
  }
1763
- function CircleXIcon() {
1764
- return /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [
1765
- /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "9", stroke: "currentColor", strokeWidth: "1.5" }),
1766
- /* @__PURE__ */ jsx(
1767
- "path",
2442
+ function CreditCardWidgetView({
2443
+ token,
2444
+ amount,
2445
+ allowance,
2446
+ onCredited,
2447
+ onLockSubmitted,
2448
+ onLockFailed
2449
+ }) {
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,
1768
2460
  {
1769
- d: "M7 7l6 6M13 7l-6 6",
1770
- stroke: "currentColor",
1771
- strokeWidth: "1.5",
1772
- strokeLinecap: "round",
1773
- strokeLinejoin: "round"
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
1774
2475
  }
1775
- )
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
+ ] })
1776
2480
  ] });
1777
2481
  }
1778
- function ArrowRightIcon({ className }) {
1779
- return /* @__PURE__ */ jsx("svg", { width: "24", height: "8", viewBox: "0 0 24 8", fill: "none", className, children: /* @__PURE__ */ jsx(
1780
- "path",
1781
- {
1782
- d: "M0 4h22m0 0l-4-4m4 4l-4 4",
1783
- stroke: "currentColor",
1784
- strokeWidth: "1.5",
1785
- strokeLinecap: "round",
1786
- strokeLinejoin: "round"
1787
- }
1788
- ) });
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";
1789
2487
  }
1790
2488
  function CheckIcon() {
1791
2489
  return /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
@@ -1904,7 +2602,16 @@ function WarningIcon() {
1904
2602
  }
1905
2603
  ) });
1906
2604
  }
1907
- function TransactionWarningView({ title, message, onDone }) {
2605
+ function TransactionWarningView({
2606
+ title,
2607
+ message,
2608
+ onDone,
2609
+ actionLabel = "Done",
2610
+ pendingLabel = actionLabel,
2611
+ isPending = false,
2612
+ onDismiss,
2613
+ dismissLabel = "Dismiss"
2614
+ }) {
1908
2615
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1909
2616
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1910
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, {}) }),
@@ -1913,14 +2620,26 @@ function TransactionWarningView({ title, message, onDone }) {
1913
2620
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1914
2621
  ] })
1915
2622
  ] }),
1916
- /* @__PURE__ */ jsx(
1917
- "button",
1918
- {
1919
- onClick: onDone,
1920
- className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
1921
- children: "Done"
1922
- }
1923
- )
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
+ ] })
1924
2643
  ] });
1925
2644
  }
1926
2645
  function TransactionErrorView({
@@ -1931,7 +2650,8 @@ function TransactionErrorView({
1931
2650
  onRetry,
1932
2651
  onDismiss,
1933
2652
  isRetrying,
1934
- retryLabel = "Retry Verification"
2653
+ retryLabel = "Retry Verification",
2654
+ dismissLabel = "Close"
1935
2655
  }) {
1936
2656
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1937
2657
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -1955,13 +2675,13 @@ function TransactionErrorView({
1955
2675
  }
1956
2676
  ),
1957
2677
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1958
- /* @__PURE__ */ jsx(
2678
+ onDismiss && /* @__PURE__ */ jsx(
1959
2679
  "button",
1960
2680
  {
1961
2681
  onClick: onDismiss,
1962
2682
  disabled: isRetrying,
1963
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",
1964
- children: "Close"
2684
+ children: dismissLabel
1965
2685
  }
1966
2686
  ),
1967
2687
  /* @__PURE__ */ jsx(
@@ -1970,1130 +2690,141 @@ function TransactionErrorView({
1970
2690
  onClick: onRetry,
1971
2691
  disabled: isRetrying,
1972
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",
1973
- children: isRetrying ? "Retrying..." : retryLabel
2693
+ children: isRetrying ? "Retrying\u2026" : retryLabel
1974
2694
  }
1975
2695
  )
1976
2696
  ] })
1977
2697
  ] });
1978
2698
  }
1979
- function DepositForm({
1980
- selectedToken,
1981
- onTokenSelect,
1982
- onPendingChange,
1983
- onUnsafeToCloseChange,
1984
- onSuccess
1985
- }) {
1986
- const { isConnected, address } = useAccount();
1987
- const { chains, getChainById: getChainById2 } = usePrivanaContext();
1988
- const [amount, setAmount] = useState("");
1989
- const [showSuccess, setShowSuccess] = useState(false);
1990
- const [showTimeout, setShowTimeout] = useState(false);
1991
- const [cancelled, setCancelled] = useState(false);
1992
- const targetChain = getChainById2(selectedToken.chainId) ?? chains[0];
1993
- const isNative = selectedToken.contract === zeroAddress;
1994
- const { data: nativeBalanceData } = useBalance$1({
1995
- address,
1996
- chainId: targetChain?.id,
1997
- query: {
1998
- enabled: !!address && isNative
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"
1999
2708
  }
2000
- });
2001
- const { data: erc20Balance } = useReadContract({
2002
- address: selectedToken.contract,
2003
- abi: erc20Abi,
2004
- functionName: "balanceOf",
2005
- args: address ? [address] : void 0,
2006
- chainId: targetChain?.id,
2007
- query: {
2008
- enabled: !!address && !isNative
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"
2009
2721
  }
2010
- });
2011
- const walletBalance = isNative ? nativeBalanceData?.value : erc20Balance;
2012
- const formattedWalletBalance = walletBalance ? formatTokenAmount(walletBalance.toString(), selectedToken.decimals) : "0.00";
2013
- const handleMaxClick = () => {
2014
- if (formattedWalletBalance && parseFloat(formattedWalletBalance) > 0) {
2015
- setAmount(formattedWalletBalance.replace(/[\s\u2009]/g, ""));
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"
2016
2734
  }
2017
- };
2018
- const hasValidAmount = amount && parseFloat(amount) > 0;
2019
- const tooManyDecimals = hasValidAmount && amount.includes(".") && amount.split(".")[1].length > selectedToken.decimals;
2020
- const exceedsBalance = hasValidAmount && !tooManyDecimals && walletBalance != null && parseTokenAmount(amount, selectedToken.decimals) > walletBalance;
2021
- const {
2022
- txHash,
2023
- isGettingAddress,
2024
- isSwitchingChain,
2025
- isSendingTransaction,
2026
- isWaitingForConfirmation,
2027
- isWaitingForProcessing,
2028
- verificationFailed,
2029
- isPending,
2030
- error,
2031
- deposit,
2032
- retryVerification,
2033
- reset
2034
- } = useDeposit({
2035
- onCredited: () => {
2036
- setAmount("");
2037
- if (onSuccess) {
2038
- reset();
2039
- onSuccess();
2040
- } else {
2041
- setShowSuccess(true);
2042
- }
2043
- },
2044
- onCheckTimeout: () => {
2045
- setAmount("");
2046
- setShowTimeout(true);
2047
- }
2048
- });
2049
- const depositSteps = [
2050
- {
2051
- label: "Getting deposit address",
2052
- status: isGettingAddress ? "active" : isSwitchingChain || isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
2053
- },
2054
- {
2055
- label: `Switching to ${targetChain?.name ?? "deposit chain"}`,
2056
- status: isSwitchingChain ? "active" : isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
2057
- },
2058
- {
2059
- label: "Confirm in wallet",
2060
- status: isSendingTransaction ? "active" : isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
2061
- },
2062
- {
2063
- label: "Confirming transaction",
2064
- status: isWaitingForConfirmation ? "active" : isWaitingForProcessing ? "completed" : "pending"
2065
- },
2066
- {
2067
- label: "Verifying deposit \u2014 may take up to a few minutes",
2068
- status: isWaitingForProcessing ? "active" : "pending"
2069
- }
2070
- ];
2071
- useEffect(() => {
2072
- if (error && !verificationFailed) {
2073
- toast.error(error.message.length > 100 ? `${error.message.slice(0, 100)}...` : error.message);
2074
- }
2075
- }, [error, verificationFailed]);
2076
- useEffect(() => {
2077
- onPendingChange?.(isPending && !cancelled);
2078
- }, [isPending, cancelled, onPendingChange]);
2079
- useEffect(() => {
2080
- onUnsafeToCloseChange?.((isGettingAddress || isSendingTransaction) && !cancelled);
2081
- }, [isGettingAddress, isSendingTransaction, cancelled, onUnsafeToCloseChange]);
2082
- const handleCancel = () => {
2083
- setCancelled(true);
2084
- reset();
2085
- };
2086
- const handleDone = () => {
2087
- setShowSuccess(false);
2088
- setShowTimeout(false);
2089
- setCancelled(false);
2090
- reset();
2091
- };
2092
- const handleDismissVerificationError = () => {
2093
- setAmount("");
2094
- setCancelled(false);
2095
- reset();
2096
- };
2097
- const handleRetryVerification = () => {
2098
- retryVerification().catch(() => {
2099
- });
2100
- };
2101
- const explorerTxUrl = txHash && targetChain?.explorerUrl ? `${targetChain.explorerUrl}/tx/${txHash}` : void 0;
2102
- const handleSubmit = async () => {
2103
- if (!amount || !selectedToken || exceedsBalance) return;
2104
- setCancelled(false);
2105
- const amountInWei = parseTokenAmount(amount, selectedToken.decimals);
2106
- await deposit({
2107
- tokenId: selectedToken.id,
2108
- amount: amountInWei
2109
- });
2110
- };
2111
- const getButtonText = () => {
2112
- if (!isConnected) return "Connect Wallet";
2113
- if (isSwitchingChain) return "Switching...";
2114
- return "Deposit";
2115
- };
2116
- if (showSuccess) {
2117
- return /* @__PURE__ */ jsx(
2118
- TransactionSuccessView,
2119
- {
2120
- title: "Deposit Successful",
2121
- message: `Your ${selectedToken.symbol} deposit has been processed.`,
2122
- onDone: handleDone
2123
- }
2124
- );
2125
- }
2126
- if (showTimeout) {
2127
- return /* @__PURE__ */ jsx(
2128
- TransactionWarningView,
2129
- {
2130
- title: "Deposit Processing",
2131
- message: `Your transaction was confirmed but the deposit is still being processed. Please check your balance - it should update shortly.`,
2132
- onDone: handleDone
2133
- }
2134
- );
2135
- }
2136
- if (verificationFailed) {
2137
- const baseMessage = "Your transfer was sent on-chain but we could not verify the deposit. The funds are already at the deposit address \u2014 retry verification instead of starting a new deposit.";
2138
- const detail = error?.message;
2139
- const message = detail ? `${baseMessage} (${detail})` : baseMessage;
2140
- return /* @__PURE__ */ jsx(
2141
- TransactionErrorView,
2142
- {
2143
- title: "Verification failed",
2144
- message,
2145
- explorerUrl: explorerTxUrl,
2146
- explorerLabel: "View transaction",
2147
- onRetry: handleRetryVerification,
2148
- onDismiss: handleDismissVerificationError
2149
- }
2150
- );
2151
- }
2152
- if (isPending && !cancelled) {
2153
- const canCancel = isGettingAddress || isSendingTransaction;
2154
- return /* @__PURE__ */ jsx(
2155
- TransactionProgressView,
2156
- {
2157
- title: "Depositing...",
2158
- steps: depositSteps,
2159
- onCancel: canCancel ? handleCancel : void 0
2160
- }
2161
- );
2162
- }
2163
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-6", children: [
2164
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2165
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
2166
- /* @__PURE__ */ jsxs(
2167
- "button",
2168
- {
2169
- onClick: onTokenSelect,
2170
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3",
2171
- children: [
2172
- /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
2173
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-start gap-1", children: [
2174
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: selectedToken.symbol }),
2175
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2176
- "on ",
2177
- targetChain?.name ?? "Base Sepolia"
2178
- ] })
2179
- ] }),
2180
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2181
- ]
2182
- }
2183
- )
2184
- ] }),
2185
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2186
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2187
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Amount" }),
2188
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-sm", children: [
2189
- formattedWalletBalance,
2190
- " ",
2191
- selectedToken.symbol
2192
- ] })
2193
- ] }),
2194
- /* @__PURE__ */ jsxs("div", { className: "border-border bg-input flex items-center gap-2 rounded-[10px] border py-1 pr-1 pl-3", children: [
2195
- /* @__PURE__ */ jsx(
2196
- "input",
2197
- {
2198
- type: "text",
2199
- inputMode: "decimal",
2200
- placeholder: "Enter Amount",
2201
- value: amount,
2202
- onChange: (e) => {
2203
- const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
2204
- if (value.split(".").length <= 2) {
2205
- setAmount(value);
2206
- }
2207
- },
2208
- className: cn(
2209
- "text-foreground flex-1 bg-transparent text-sm outline-none",
2210
- "placeholder:text-muted-foreground/50"
2211
- )
2212
- }
2213
- ),
2214
- /* @__PURE__ */ jsx(
2215
- "button",
2216
- {
2217
- onClick: handleMaxClick,
2218
- className: "bg-secondary text-foreground hover:bg-secondary/80 cursor-pointer rounded px-3 py-2.5 text-xs font-semibold transition-colors",
2219
- children: "MAX"
2220
- }
2221
- )
2222
- ] }),
2223
- tooManyDecimals && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
2224
- "Too many decimal places (max: ",
2225
- selectedToken.decimals,
2226
- ")"
2227
- ] }),
2228
- exceedsBalance && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Insufficient balance" })
2229
- ] }),
2230
- /* @__PURE__ */ jsx(
2231
- "button",
2232
- {
2233
- onClick: handleSubmit,
2234
- disabled: !isConnected || !hasValidAmount || tooManyDecimals || !!exceedsBalance || isPending,
2235
- className: cn(
2236
- "flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
2237
- "bg-primary text-primary-foreground hover:bg-primary/90",
2238
- "disabled:cursor-not-allowed disabled:opacity-50"
2239
- ),
2240
- children: getButtonText()
2241
- }
2242
- )
2243
- ] });
2244
- }
2245
- function WithdrawForm({
2246
- selectedToken,
2247
- onTokenSelect,
2248
- onPendingChange,
2249
- onUnsafeToCloseChange
2250
- }) {
2251
- const { isConnected, address } = useAccount();
2252
- const { chains, getChainById: getChainById2 } = usePrivanaContext();
2253
- const [amount, setAmount] = useState("");
2254
- const [showSuccess, setShowSuccess] = useState(false);
2255
- const [showTimeout, setShowTimeout] = useState(false);
2256
- const [cancelled, setCancelled] = useState(false);
2257
- const targetChain = getChainById2(selectedToken.chainId) ?? chains[0];
2258
- const {
2259
- balanceWei,
2260
- isLoading: isBalanceLoading,
2261
- isError: isBalanceError
2262
- } = useBalance({
2263
- tokenId: selectedToken.id
2264
- });
2265
- const formattedBalance = formatTokenAmount(balanceWei, selectedToken.decimals);
2266
- const { withdraw, isPending, currentStep, error, reset } = useWithdraw({
2267
- onProcessingSuccess: () => {
2268
- setAmount("");
2269
- setShowSuccess(true);
2270
- },
2271
- onProcessingTimeout: () => {
2272
- setAmount("");
2273
- setShowTimeout(true);
2274
- }
2275
- });
2276
- const explorerUrl = address && targetChain ? getExplorerAddressUrl(targetChain.id, address) : void 0;
2277
- const getStepStatus = (step, after) => {
2278
- if (currentStep === step) return "active";
2279
- if (after.includes(currentStep)) return "completed";
2280
- return "pending";
2281
- };
2282
- const withdrawSteps = [
2283
- {
2284
- label: "Switching to signing chain",
2285
- status: getStepStatus("switching-chain", ["signing", "submitting", "processing"])
2286
- },
2287
- { label: "Sign in wallet", status: getStepStatus("signing", ["submitting", "processing"]) },
2288
- {
2289
- label: "Submitting withdrawal",
2290
- status: getStepStatus("submitting", ["processing"])
2291
- },
2292
- {
2293
- label: "Processing \u2014 may take a minute or two",
2294
- status: getStepStatus("processing", [])
2295
- }
2296
- ];
2297
- useEffect(() => {
2298
- if (error) {
2299
- toast.error(error.message.length > 100 ? `${error.message.slice(0, 100)}...` : error.message);
2300
- }
2301
- }, [error]);
2302
- useEffect(() => {
2303
- const pending = isPending && !cancelled;
2304
- onPendingChange?.(pending);
2305
- onUnsafeToCloseChange?.(pending);
2306
- }, [isPending, cancelled, onPendingChange, onUnsafeToCloseChange]);
2307
- const handleCancel = () => {
2308
- setCancelled(true);
2309
- reset();
2310
- };
2311
- const handleDone = () => {
2312
- setShowSuccess(false);
2313
- setShowTimeout(false);
2314
- setCancelled(false);
2315
- reset();
2316
- };
2317
- const handleWithdraw = async () => {
2318
- if (!amount || !selectedToken || exceedsBalance) return;
2319
- setCancelled(false);
2320
- const amountInWei = parseTokenAmount(amount, selectedToken.decimals);
2321
- await withdraw({
2322
- tokenId: selectedToken.id,
2323
- amount: amountInWei
2324
- });
2325
- };
2326
- const handleMaxClick = () => {
2327
- if (formattedBalance && parseFloat(formattedBalance) > 0) {
2328
- setAmount(formattedBalance.replace(/[\s\u2009]/g, ""));
2329
- }
2330
- };
2331
- const hasValidAmount = amount && parseFloat(amount) > 0;
2332
- const tooManyDecimals = hasValidAmount && amount.includes(".") && amount.split(".")[1].length > selectedToken.decimals;
2333
- const exceedsBalance = hasValidAmount && !tooManyDecimals && !isBalanceLoading && !isBalanceError && parseTokenAmount(amount, selectedToken.decimals) > BigInt(balanceWei);
2334
- const getButtonText = () => {
2335
- if (!isConnected) return "Connect Wallet";
2336
- return "Withdraw";
2337
- };
2338
- if (showSuccess) {
2339
- return /* @__PURE__ */ jsx(
2340
- TransactionSuccessView,
2341
- {
2342
- title: "Withdrawal Complete",
2343
- message: `Your ${selectedToken.symbol} withdrawal has been processed. Funds should appear in your wallet shortly.`,
2344
- explorerUrl,
2345
- explorerLabel: targetChain ? getExplorerLabel(targetChain.id) : void 0,
2346
- onDone: handleDone
2347
- }
2348
- );
2349
- }
2350
- if (showTimeout) {
2351
- return /* @__PURE__ */ jsx(
2352
- TransactionWarningView,
2353
- {
2354
- title: "Withdrawal Processing",
2355
- message: `Your withdrawal is still being processed. Please check your balance \u2014 it should update shortly.`,
2356
- onDone: handleDone
2357
- }
2358
- );
2359
- }
2360
- if (isPending && !cancelled) {
2361
- const canCancel = currentStep === "idle" || currentStep === "switching-chain" || currentStep === "signing";
2362
- return /* @__PURE__ */ jsx(
2363
- TransactionProgressView,
2364
- {
2365
- title: "Withdrawing...",
2366
- steps: withdrawSteps,
2367
- onCancel: canCancel ? handleCancel : void 0
2368
- }
2369
- );
2370
- }
2371
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-6", children: [
2372
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2373
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
2374
- /* @__PURE__ */ jsxs(
2375
- "button",
2376
- {
2377
- onClick: onTokenSelect,
2378
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3",
2379
- children: [
2380
- /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
2381
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-start gap-1", children: [
2382
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: selectedToken.symbol }),
2383
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2384
- "on ",
2385
- targetChain?.name ?? "Base Sepolia"
2386
- ] })
2387
- ] }),
2388
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2389
- ]
2390
- }
2391
- )
2392
- ] }),
2393
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [
2394
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2395
- /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Amount" }),
2396
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-sm", children: [
2397
- formattedBalance,
2398
- " ",
2399
- selectedToken.symbol
2400
- ] })
2401
- ] }),
2402
- /* @__PURE__ */ jsxs("div", { className: "border-border bg-input flex items-center gap-2 rounded-[10px] border py-1 pr-1 pl-3", children: [
2403
- /* @__PURE__ */ jsx(
2404
- "input",
2405
- {
2406
- type: "text",
2407
- inputMode: "decimal",
2408
- placeholder: "Enter Amount",
2409
- value: amount,
2410
- onChange: (e) => {
2411
- const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
2412
- if (value.split(".").length <= 2) {
2413
- setAmount(value);
2414
- }
2415
- },
2416
- className: cn(
2417
- "text-foreground flex-1 bg-transparent text-sm outline-none",
2418
- "placeholder:text-muted-foreground/50"
2419
- )
2420
- }
2421
- ),
2422
- /* @__PURE__ */ jsx(
2423
- "button",
2424
- {
2425
- onClick: handleMaxClick,
2426
- className: "bg-secondary text-foreground hover:bg-secondary/80 cursor-pointer rounded px-3 py-2.5 text-xs font-semibold transition-colors",
2427
- children: "MAX"
2428
- }
2429
- )
2430
- ] }),
2431
- tooManyDecimals && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
2432
- "Too many decimal places (max: ",
2433
- selectedToken.decimals,
2434
- ")"
2435
- ] }),
2436
- exceedsBalance && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Insufficient balance" })
2437
- ] }),
2438
- /* @__PURE__ */ jsx(
2439
- "button",
2440
- {
2441
- onClick: handleWithdraw,
2442
- disabled: !isConnected || !hasValidAmount || tooManyDecimals || !!exceedsBalance || isPending,
2443
- className: cn(
2444
- "flex h-10 w-full cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors",
2445
- "bg-primary text-primary-foreground hover:bg-primary/90",
2446
- "disabled:cursor-not-allowed disabled:opacity-50"
2447
- ),
2448
- children: getButtonText()
2449
- }
2450
- )
2451
- ] });
2452
- }
2453
- function BalanceCards({
2454
- selectedToken,
2455
- onLockedFundsClick,
2456
- onBalanceClick,
2457
- showLockedFunds = true,
2458
- disabled
2459
- }) {
2460
- const { balanceWei, isLoading: balanceLoading } = useBalance({
2461
- tokenId: selectedToken.id
2462
- });
2463
- const { totalLocked, isLoading: lockedLoading } = useLockedFunds({ enabled: showLockedFunds });
2464
- const formattedBalance = formatTokenAmount(balanceWei, selectedToken.decimals);
2465
- const formattedLocked = showLockedFunds ? formatTokenAmount(String(totalLocked), selectedToken.decimals) : "0.00";
2466
- return /* @__PURE__ */ jsxs("div", { className: cn("flex gap-2", disabled && "opacity-50"), children: [
2467
- /* @__PURE__ */ jsxs(
2468
- "button",
2469
- {
2470
- onClick: onBalanceClick,
2471
- disabled,
2472
- className: cn(
2473
- "bg-muted flex flex-1 flex-col gap-2 rounded-[10px] p-5 text-left transition-colors",
2474
- disabled ? "cursor-not-allowed" : "hover:bg-muted/80 cursor-pointer"
2475
- ),
2476
- children: [
2477
- /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between", children: [
2478
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2479
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Balance" }),
2480
- /* @__PURE__ */ jsx("span", { className: "bg-secondary text-muted-foreground rounded-full px-2 py-[5px] text-[10px] font-bold", children: selectedToken.symbol })
2481
- ] }),
2482
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-6 w-6 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2483
- ] }),
2484
- /* @__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 })
2485
- ]
2486
- }
2487
- ),
2488
- showLockedFunds && /* @__PURE__ */ jsxs(
2489
- "button",
2490
- {
2491
- onClick: onLockedFundsClick,
2492
- disabled,
2493
- className: cn(
2494
- "bg-muted flex flex-1 flex-col gap-2 rounded-[10px] p-5 text-left transition-colors",
2495
- disabled ? "cursor-not-allowed" : "hover:bg-muted/80 cursor-pointer"
2496
- ),
2497
- children: [
2498
- /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between", children: [
2499
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2500
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Locked Funds" }),
2501
- /* @__PURE__ */ jsx("span", { className: "bg-secondary text-muted-foreground rounded-full px-2 py-[5px] text-[10px] font-bold", children: selectedToken.symbol })
2502
- ] }),
2503
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-6 w-6 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
2504
- ] }),
2505
- /* @__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 })
2506
- ]
2507
- }
2508
- )
2509
- ] });
2510
- }
2511
- function Tabs({
2512
- activeTab,
2513
- onTabChange,
2514
- disabled
2515
- }) {
2516
- return /* @__PURE__ */ jsxs(
2517
- "div",
2518
- {
2519
- className: cn(
2520
- "bg-muted relative flex gap-2 overflow-hidden rounded-[10px] p-1",
2521
- disabled && "opacity-50"
2522
- ),
2523
- children: [
2524
- /* @__PURE__ */ jsx(
2525
- "div",
2526
- {
2527
- className: cn(
2528
- "bg-input absolute top-1 bottom-1 left-1 w-[calc(50%-8px)] rounded-md transition-transform duration-200",
2529
- activeTab === "withdraw" && "translate-x-[calc(100%+8px)]"
2530
- )
2531
- }
2532
- ),
2533
- /* @__PURE__ */ jsx(
2534
- "button",
2535
- {
2536
- onClick: () => !disabled && onTabChange("deposit"),
2537
- disabled,
2538
- className: cn(
2539
- "relative z-10 flex-1 rounded-md px-3 py-[9px] text-sm transition-colors",
2540
- activeTab === "deposit" ? "text-foreground" : "text-muted-foreground",
2541
- disabled ? "cursor-not-allowed" : "cursor-pointer"
2542
- ),
2543
- children: "Deposit"
2544
- }
2545
- ),
2546
- /* @__PURE__ */ jsx(
2547
- "button",
2548
- {
2549
- onClick: () => !disabled && onTabChange("withdraw"),
2550
- disabled,
2551
- className: cn(
2552
- "relative z-10 flex-1 rounded-md px-3 py-[9px] text-sm transition-colors",
2553
- activeTab === "withdraw" ? "text-foreground" : "text-muted-foreground",
2554
- disabled ? "cursor-not-allowed" : "cursor-pointer"
2555
- ),
2556
- children: "Withdraw"
2557
- }
2558
- )
2559
- ]
2560
- }
2561
- );
2562
- }
2563
- function LockedFundsView({ onBack }) {
2564
- const { getTokenById } = usePrivanaContext();
2565
- const { locks, isLoading } = useLockedFunds();
2566
- const { unlockFunds, unlockAllExpired, isPending } = useUnlockFunds();
2567
- const [collapsedSections, setCollapsedSections] = useState({});
2568
- const sections = useMemo(() => {
2569
- const sectionMap = {};
2570
- locks.forEach((lock) => {
2571
- const serviceName = shortenAddress(lock.service_address);
2572
- if (!sectionMap[lock.service_address]) {
2573
- sectionMap[lock.service_address] = {
2574
- title: `Service ${serviceName}`,
2575
- items: []
2576
- };
2577
- }
2578
- sectionMap[lock.service_address].items.push({
2579
- lockId: lock.lock_id,
2580
- amount: formatTokenAmount(String(lock.amount), getTokenById(lock.token_id)?.decimals ?? 18),
2581
- serviceAddress: lock.service_address,
2582
- time: lock.is_expired ? "Click to unlock" : formatTimeRemaining(lock.expiry),
2583
- isExpired: lock.is_expired
2584
- });
2585
- });
2586
- return Object.values(sectionMap);
2587
- }, [getTokenById, locks]);
2588
- const toggleSection = (title) => {
2589
- setCollapsedSections((prev) => ({
2590
- ...prev,
2591
- [title]: !prev[title]
2592
- }));
2593
- };
2594
- const expiredCount = locks.filter((l) => l.is_expired).length;
2595
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2596
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2597
- /* @__PURE__ */ jsx(
2598
- "button",
2599
- {
2600
- onClick: onBack,
2601
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2602
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2603
- }
2604
- ),
2605
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Locked Funds" })
2606
- ] }) }),
2607
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex min-h-0 flex-1 flex-col rounded-[10px] p-2", children: [
2608
- /* @__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: [
2609
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-10 w-10 rounded-full" }),
2610
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
2611
- /* @__PURE__ */ jsx("div", { className: "bg-secondary mb-2 h-3.5 w-24 rounded" }),
2612
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-3 w-32 rounded" })
2613
- ] })
2614
- ] }, 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: [
2615
- /* @__PURE__ */ jsxs(
2616
- "button",
2617
- {
2618
- onClick: () => toggleSection(section.title),
2619
- className: "text-muted-foreground hover:bg-secondary flex w-full cursor-pointer items-center justify-between rounded-lg px-4 py-4 transition-colors",
2620
- children: [
2621
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: section.title }),
2622
- /* @__PURE__ */ jsx(
2623
- ChevronDownIcon,
2624
- {
2625
- direction: collapsedSections[section.title] ? "right" : "down"
2626
- }
2627
- )
2628
- ]
2629
- }
2630
- ),
2631
- !collapsedSections[section.title] && section.items.map((item) => /* @__PURE__ */ jsxs(
2632
- "div",
2633
- {
2634
- className: cn(
2635
- "flex items-center justify-between gap-3 rounded-lg p-3",
2636
- item.isExpired && "bg-secondary"
2637
- ),
2638
- children: [
2639
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-3", children: [
2640
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-10 w-10 rounded-full" }),
2641
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2642
- /* @__PURE__ */ jsxs("span", { className: "text-foreground text-sm font-medium", children: [
2643
- item.amount,
2644
- " USDC"
2645
- ] }),
2646
- /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs", children: [
2647
- "Service: ",
2648
- shortenAddress(item.serviceAddress)
2649
- ] })
2650
- ] })
2651
- ] }),
2652
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-end gap-2", children: [
2653
- item.isExpired ? /* @__PURE__ */ jsx(
2654
- "button",
2655
- {
2656
- onClick: () => unlockFunds({ lockId: Number(item.lockId) }),
2657
- disabled: isPending,
2658
- className: "text-foreground hover:text-foreground/80 cursor-pointer text-sm transition-colors disabled:opacity-50",
2659
- children: "Click to unlock"
2660
- }
2661
- ) : /* @__PURE__ */ jsx("span", { className: "text-sm text-amber-500", children: item.time }),
2662
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: "on Base Sepolia" })
2663
- ] })
2664
- ]
2665
- },
2666
- `${item.serviceAddress}-${item.lockId}`
2667
- ))
2668
- ] }, section.title)) }),
2669
- expiredCount > 0 && /* @__PURE__ */ jsx("div", { className: "p-3", children: /* @__PURE__ */ jsxs(
2670
- "button",
2671
- {
2672
- onClick: () => unlockAllExpired(),
2673
- disabled: isPending,
2674
- 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",
2675
- children: [
2676
- "Unlock All (",
2677
- expiredCount,
2678
- ")"
2679
- ]
2680
- }
2681
- ) })
2682
- ] })
2683
- ] });
2684
- }
2685
- function BalanceTokenRow({ token }) {
2686
- const { balanceWei, isLoading } = useBalance({
2687
- tokenId: token.id
2688
- });
2689
- const formattedBalance = formatTokenAmount(balanceWei, token.decimals);
2690
- return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2 rounded-lg px-3 py-2.5", children: [
2691
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 18) }),
2692
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: token.symbol }),
2693
- 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 })
2694
- ] });
2695
- }
2696
- function BalanceDetailsView({ onBack }) {
2697
- const { enabledTokens, chains } = usePrivanaContext();
2698
- const [selectedChainId, setSelectedChainId] = useState(chains[0]?.id ?? 84532);
2699
- const chainTokens = useMemo(() => {
2700
- return enabledTokens.filter((t) => t.chainId === selectedChainId);
2701
- }, [enabledTokens, selectedChainId]);
2702
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2703
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2704
- /* @__PURE__ */ jsx(
2705
- "button",
2706
- {
2707
- onClick: onBack,
2708
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2709
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2710
- }
2711
- ),
2712
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Token Balances" })
2713
- ] }) }),
2714
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 gap-2", children: [
2715
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-1 flex-col overflow-hidden rounded-[10px] p-2", children: [
2716
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Network" }) }),
2717
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex-1 overflow-y-auto", children: chains.map((chain) => {
2718
- const isSelected = selectedChainId === chain.id;
2719
- return /* @__PURE__ */ jsxs(
2720
- "button",
2721
- {
2722
- onClick: () => setSelectedChainId(chain.id),
2723
- className: cn(
2724
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2725
- isSelected && "bg-secondary"
2726
- ),
2727
- children: [
2728
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getChainIcon(chain.id, 18) }),
2729
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: chain.name })
2730
- ]
2731
- },
2732
- chain.id
2733
- );
2734
- }) })
2735
- ] }),
2736
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-[2] flex-col overflow-hidden rounded-[10px] p-2", children: [
2737
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Token Balance" }) }),
2738
- /* @__PURE__ */ jsx("div", { className: "mt-2 flex-1 overflow-y-auto", children: chainTokens.map((token) => /* @__PURE__ */ jsx(BalanceTokenRow, { token }, token.id)) })
2739
- ] })
2740
- ] })
2741
- ] });
2742
- }
2743
- function TokenRow({
2744
- token,
2745
- isSelected,
2746
- onClick
2747
- }) {
2748
- const { address } = useAccount();
2749
- const isNative = token.contract === zeroAddress;
2750
- const { data: nativeBalanceData } = useBalance$1({
2751
- address,
2752
- chainId: token.chainId,
2753
- query: { enabled: !!address && isNative }
2754
- });
2755
- const { data: erc20Balance } = useReadContract({
2756
- address: token.contract,
2757
- abi: erc20Abi,
2758
- functionName: "balanceOf",
2759
- args: address ? [address] : void 0,
2760
- chainId: token.chainId,
2761
- query: { enabled: !!address && !isNative }
2762
- });
2763
- const walletBalance = isNative ? nativeBalanceData?.value : erc20Balance;
2764
- const formattedBalance = walletBalance ? formatTokenAmount(walletBalance.toString(), token.decimals) : "0.00";
2765
- return /* @__PURE__ */ jsxs(
2766
- "button",
2767
- {
2768
- onClick,
2769
- className: cn(
2770
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2771
- isSelected && "bg-secondary"
2772
- ),
2773
- children: [
2774
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 18) }),
2775
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: token.symbol }),
2776
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: formattedBalance })
2777
- ]
2778
- }
2779
- );
2780
- }
2781
- function TokenSelectorView({
2782
- onBack,
2783
- onSelect,
2784
- selectedTokenId
2785
- }) {
2786
- const { enabledTokens, chains } = usePrivanaContext();
2787
- const [selectedChainId, setSelectedChainId] = useState(chains[0]?.id ?? 84532);
2788
- const chainTokens = useMemo(() => {
2789
- return enabledTokens.filter((t) => t.chainId === selectedChainId);
2790
- }, [enabledTokens, selectedChainId]);
2791
- const handleTokenSelect = (token) => {
2792
- onSelect(token);
2793
- onBack();
2794
- };
2795
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2796
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2797
- /* @__PURE__ */ jsx(
2798
- "button",
2799
- {
2800
- onClick: onBack,
2801
- className: "text-muted-foreground hover:text-foreground -ml-2 flex h-6 w-6 cursor-pointer items-center justify-center transition-colors",
2802
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
2803
- }
2804
- ),
2805
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-6 font-medium", children: "Select Token" })
2806
- ] }) }),
2807
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 gap-2", children: [
2808
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-1 flex-col overflow-hidden rounded-[10px] p-2", children: [
2809
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Network" }) }),
2810
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex-1 overflow-y-auto", children: chains.map((chain) => {
2811
- const isSelected = selectedChainId === chain.id;
2812
- return /* @__PURE__ */ jsxs(
2813
- "button",
2814
- {
2815
- onClick: () => setSelectedChainId(chain.id),
2816
- className: cn(
2817
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors",
2818
- isSelected && "bg-secondary"
2819
- ),
2820
- children: [
2821
- /* @__PURE__ */ jsx("div", { className: "h-[18px] w-[18px] overflow-hidden rounded-full", children: getChainIcon(chain.id, 18) }),
2822
- /* @__PURE__ */ jsx("span", { className: "text-foreground flex-1 text-sm", children: chain.name })
2823
- ]
2824
- },
2825
- chain.id
2826
- );
2827
- }) })
2828
- ] }),
2829
- /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-[2] flex-col overflow-hidden rounded-[10px] p-2", children: [
2830
- /* @__PURE__ */ jsx("div", { className: "px-4 pt-4 pb-2", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Token" }) }),
2831
- /* @__PURE__ */ jsx("div", { className: "mt-2 flex-1 overflow-y-auto", children: chainTokens.map((token) => /* @__PURE__ */ jsx(
2832
- TokenRow,
2833
- {
2834
- token,
2835
- isSelected: selectedTokenId === token.id,
2836
- onClick: () => handleTokenSelect(token)
2837
- },
2838
- token.id
2839
- )) })
2840
- ] })
2841
- ] })
2842
- ] });
2843
- }
2844
- function ModalBody({
2845
- onViewChange,
2846
- onCloseBlockedChange,
2847
- showLockedFunds = true,
2848
- defaultTab = "deposit",
2849
- onDepositSuccess
2850
- }) {
2851
- const { defaultToken, tokensStatus } = usePrivanaContext();
2852
- const [selectedToken, setSelectedToken] = useState(defaultToken);
2853
- const [activeTab, setActiveTab] = useState(defaultTab);
2854
- const [currentView, setCurrentView] = useState("main");
2855
- const [isInteractionPending, setIsInteractionPending] = useState(false);
2856
- useEffect(() => {
2857
- if (!selectedToken && defaultToken) {
2858
- setSelectedToken(defaultToken);
2859
- }
2860
- }, [selectedToken, defaultToken]);
2861
- const handlePendingChange = (isPending) => {
2862
- setIsInteractionPending(isPending);
2863
- };
2864
- const handleCloseBlockedChange = (isBlocked) => {
2865
- onCloseBlockedChange?.(isBlocked);
2866
- };
2867
- const handleViewChange = (view) => {
2868
- const update = () => {
2869
- setCurrentView(view);
2870
- onViewChange?.(view);
2871
- };
2872
- if (typeof document !== "undefined" && typeof document.startViewTransition === "function") {
2873
- document.documentElement.dataset.privanaVtDir = view === "main" ? "back" : "forward";
2874
- const transition = document.startViewTransition(update);
2875
- transition.finished.then(() => {
2876
- delete document.documentElement.dataset.privanaVtDir;
2877
- });
2878
- } else {
2879
- update();
2880
- }
2881
- };
2882
- const handleTokenSelect = (token) => {
2883
- setSelectedToken(token);
2884
- };
2885
- if (tokensStatus === "loading" || !selectedToken) {
2886
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2887
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-25 animate-pulse rounded-[10px]" }),
2888
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-11 animate-pulse rounded-[10px]" }),
2889
- /* @__PURE__ */ jsx("div", { className: "bg-secondary h-50 animate-pulse rounded-[10px]" })
2890
- ] });
2891
- }
2892
- if (currentView === "locked-funds") {
2893
- return /* @__PURE__ */ jsx(LockedFundsView, { onBack: () => handleViewChange("main") });
2894
- }
2895
- if (currentView === "balance-details") {
2896
- return /* @__PURE__ */ jsx(BalanceDetailsView, { onBack: () => handleViewChange("main") });
2897
- }
2898
- if (currentView === "select-token") {
2899
- return /* @__PURE__ */ jsx(
2900
- TokenSelectorView,
2901
- {
2902
- onBack: () => handleViewChange("main"),
2903
- onSelect: handleTokenSelect,
2904
- selectedTokenId: selectedToken.id
2905
- }
2906
- );
2907
- }
2908
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 pb-4", children: [
2909
- /* @__PURE__ */ jsx("div", { className: cn(isInteractionPending && "pointer-events-none"), children: /* @__PURE__ */ jsx(
2910
- BalanceCards,
2911
- {
2912
- selectedToken,
2913
- onLockedFundsClick: () => handleViewChange("locked-funds"),
2914
- onBalanceClick: () => handleViewChange("balance-details"),
2915
- showLockedFunds,
2916
- disabled: isInteractionPending
2917
- }
2918
- ) }),
2919
- /* @__PURE__ */ jsx("div", { className: cn(isInteractionPending && "pointer-events-none"), children: /* @__PURE__ */ jsx(Tabs, { activeTab, onTabChange: setActiveTab, disabled: isInteractionPending }) }),
2920
- /* @__PURE__ */ jsx("div", { className: "bg-muted rounded-[10px] p-5", children: activeTab === "deposit" ? /* @__PURE__ */ jsx(
2921
- DepositForm,
2922
- {
2923
- selectedToken,
2924
- onTokenSelect: () => handleViewChange("select-token"),
2925
- onPendingChange: handlePendingChange,
2926
- onUnsafeToCloseChange: handleCloseBlockedChange,
2927
- onSuccess: onDepositSuccess
2928
- }
2929
- ) : /* @__PURE__ */ jsx(
2930
- WithdrawForm,
2931
- {
2932
- selectedToken,
2933
- onTokenSelect: () => handleViewChange("select-token"),
2934
- onPendingChange: handlePendingChange,
2935
- onUnsafeToCloseChange: handleCloseBlockedChange
2936
- }
2937
- ) })
2938
- ] }) });
2939
- }
2940
- function PrivanaModal({
2941
- open,
2942
- onClose,
2943
- showLockedFunds,
2944
- defaultTab,
2945
- onDepositSuccess
2946
- }) {
2947
- const [currentView, setCurrentView] = useState("main");
2948
- const titleId = useId();
2949
- const descId = useId();
2950
- const [isCloseBlocked, setIsCloseBlocked] = useState(false);
2951
- const handleClose = () => {
2952
- if (!isCloseBlocked) onClose();
2953
- };
2954
- return /* @__PURE__ */ jsx(
2955
- Dialog,
2956
- {
2957
- open,
2958
- onOpenChange: (isOpen) => {
2959
- if (!isOpen) handleClose();
2960
- },
2961
- children: /* @__PURE__ */ jsxs(
2962
- DialogContent,
2963
- {
2964
- "data-privana": true,
2965
- "data-view": currentView,
2966
- showCloseButton: false,
2967
- onInteractOutside: isCloseBlocked ? (e) => e.preventDefault() : void 0,
2968
- onEscapeKeyDown: isCloseBlocked ? (e) => e.preventDefault() : void 0,
2969
- 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]",
2970
- "aria-labelledby": titleId,
2971
- "aria-describedby": descId,
2972
- children: [
2973
- /* @__PURE__ */ jsx(
2974
- "button",
2975
- {
2976
- "data-privana-close": true,
2977
- onClick: handleClose,
2978
- disabled: isCloseBlocked,
2979
- "aria-label": "Close",
2980
- className: cn(
2981
- "absolute top-6 right-5 z-20 flex h-6 w-6 items-center justify-center transition-colors",
2982
- isCloseBlocked ? "text-muted-foreground/40 cursor-not-allowed" : "text-muted-foreground hover:text-foreground cursor-pointer"
2983
- ),
2984
- children: /* @__PURE__ */ jsx(CloseIcon, {})
2985
- }
2986
- ),
2987
- /* @__PURE__ */ jsxs(
2988
- "div",
2989
- {
2990
- "data-privana-content": true,
2991
- "data-view": currentView,
2992
- className: "flex min-h-0 flex-1 flex-col gap-2",
2993
- children: [
2994
- /* @__PURE__ */ jsx(DialogTitle, { id: titleId, className: "sr-only", children: "Privana" }),
2995
- /* @__PURE__ */ jsx(DialogDescription, { id: descId, className: "sr-only", children: "Deposit or withdraw tokens from your Privana" }),
2996
- 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" }) }) }),
2997
- /* @__PURE__ */ jsx(
2998
- ModalBody,
2999
- {
3000
- onCloseBlockedChange: setIsCloseBlocked,
3001
- onViewChange: setCurrentView,
3002
- showLockedFunds,
3003
- defaultTab,
3004
- onDepositSuccess
3005
- }
3006
- )
3007
- ]
3008
- }
3009
- ),
3010
- /* @__PURE__ */ jsx(
3011
- "a",
3012
- {
3013
- "data-privana-footer": true,
3014
- href: "https://privana.finance",
3015
- target: "_blank",
3016
- rel: "noopener noreferrer",
3017
- "aria-label": "Powered by Privana"
3018
- }
3019
- )
3020
- ]
3021
- }
3022
- )
3023
- }
3024
- );
2735
+ ) });
3025
2736
  }
3026
- function PrivanaInlineModal({
3027
- className,
3028
- showLockedFunds,
3029
- defaultTab,
3030
- onDepositSuccess
2737
+ function ChevronDownIcon({
2738
+ direction = "down",
2739
+ className
3031
2740
  }) {
3032
2741
  return /* @__PURE__ */ jsx(
3033
- "div",
2742
+ "svg",
3034
2743
  {
3035
- "data-privana": true,
2744
+ width: "16",
2745
+ height: "16",
2746
+ viewBox: "0 0 16 16",
2747
+ fill: "none",
3036
2748
  className: cn(
3037
- "bg-card flex w-[560px] max-w-full flex-col gap-2 overflow-hidden rounded-2xl p-2 shadow-lg",
2749
+ "transition-transform",
2750
+ direction === "right" && "-rotate-90",
2751
+ direction === "up" && "rotate-180",
3038
2752
  className
3039
2753
  ),
3040
2754
  children: /* @__PURE__ */ jsx(
3041
- ModalBody,
2755
+ "path",
3042
2756
  {
3043
- showLockedFunds,
3044
- defaultTab,
3045
- onDepositSuccess
2757
+ d: "M4 6l4 4 4-4",
2758
+ stroke: "currentColor",
2759
+ strokeWidth: "1.5",
2760
+ strokeLinecap: "round",
2761
+ strokeLinejoin: "round"
3046
2762
  }
3047
2763
  )
3048
2764
  }
3049
2765
  );
3050
2766
  }
3051
- function PrivanaButton({
3052
- children,
3053
- className,
3054
- variant = "outline",
3055
- size = "default",
3056
- asChild = false,
3057
- renderButton,
3058
- hideWhenDisconnected = true,
3059
- showLockedFunds = true,
3060
- defaultTab,
3061
- onDepositSuccess,
3062
- ...buttonProps
3063
- }) {
3064
- const [modalOpen, setModalOpen] = useState(false);
3065
- const { isConnected } = useAccount();
3066
- if (hideWhenDisconnected && !isConnected) {
3067
- return null;
3068
- }
3069
- const handleClick = () => setModalOpen(true);
3070
- const buttonElement = renderButton ? renderButton({ onClick: handleClick, isOpen: modalOpen }) : /* @__PURE__ */ jsx(
3071
- Button,
2767
+ function CopyIcon() {
2768
+ return /* @__PURE__ */ jsxs(
2769
+ "svg",
3072
2770
  {
3073
- variant,
3074
- size,
3075
- asChild,
3076
- className: cn(className),
3077
- onClick: handleClick,
3078
- disabled: !isConnected,
3079
- ...buttonProps,
3080
- children: children ?? "Privana"
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
+ ]
3081
2783
  }
3082
2784
  );
3083
- return /* @__PURE__ */ jsxs("span", { "data-privana": true, className: "contents", children: [
3084
- buttonElement,
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"
2797
+ }
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" }),
3085
2804
  /* @__PURE__ */ jsx(
3086
- PrivanaModal,
2805
+ "path",
3087
2806
  {
3088
- open: modalOpen,
3089
- onClose: () => setModalOpen(false),
3090
- showLockedFunds,
3091
- defaultTab,
3092
- onDepositSuccess
2807
+ d: "M7 7l6 6M13 7l-6 6",
2808
+ stroke: "currentColor",
2809
+ strokeWidth: "1.5",
2810
+ strokeLinecap: "round",
2811
+ strokeLinejoin: "round"
3093
2812
  }
3094
2813
  )
3095
2814
  ] });
3096
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
+ ) });
2827
+ }
3097
2828
  var DEFAULT_MOONPAY_API_URL = "https://api.moonpay.com/v3";
3098
2829
  function useMoonpayLimits({
3099
2830
  currencyCode,
@@ -3120,115 +2851,21 @@ function useMoonpayLimits({
3120
2851
  );
3121
2852
  if (!res.ok) {
3122
2853
  throw new Error(`MoonPay limits request failed: ${res.status}`);
3123
- }
3124
- const data = await res.json();
3125
- const asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
3126
- return {
3127
- minBuyAmount: asNumber(data.baseCurrency?.minBuyAmount),
3128
- maxBuyAmount: asNumber(data.baseCurrency?.maxBuyAmount)
3129
- };
3130
- }
3131
- });
3132
- return {
3133
- minBuyAmount: query.data?.minBuyAmount,
3134
- maxBuyAmount: query.data?.maxBuyAmount,
3135
- isLoading: query.isLoading,
3136
- error: query.error ?? null
3137
- };
3138
- }
3139
- function TokenSelectorView2({ selectedTokenId, onSelect }) {
3140
- const { enabledTokens } = usePrivanaContext();
3141
- 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: [
3142
- enabledTokens.map((token) => {
3143
- const isSelected = selectedTokenId === token.id;
3144
- return /* @__PURE__ */ jsxs(
3145
- "button",
3146
- {
3147
- type: "button",
3148
- onClick: () => onSelect(token.id),
3149
- className: cn(
3150
- "hover:bg-secondary flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors",
3151
- isSelected && "bg-secondary"
3152
- ),
3153
- children: [
3154
- /* @__PURE__ */ jsx("div", { className: "h-6 w-6 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(token.symbol, 24) }),
3155
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3156
- /* @__PURE__ */ jsx("div", { className: "text-foreground text-sm font-medium", children: token.symbol }),
3157
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-[11px]", children: token.name })
3158
- ] }),
3159
- isSelected && /* @__PURE__ */ jsx("div", { className: "bg-primary flex h-4 w-4 shrink-0 items-center justify-center rounded-full", children: /* @__PURE__ */ jsx(
3160
- "svg",
3161
- {
3162
- width: "10",
3163
- height: "10",
3164
- viewBox: "0 0 10 10",
3165
- fill: "none",
3166
- className: "text-primary-foreground",
3167
- children: /* @__PURE__ */ jsx(
3168
- "path",
3169
- {
3170
- d: "M2 5L4 7L8 3",
3171
- stroke: "currentColor",
3172
- strokeWidth: "1.5",
3173
- strokeLinecap: "round",
3174
- strokeLinejoin: "round"
3175
- }
3176
- )
3177
- }
3178
- ) })
3179
- ]
3180
- },
3181
- token.id
3182
- );
3183
- }),
3184
- enabledTokens.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-3 py-6 text-center text-sm", children: "No tokens available" })
3185
- ] }) });
3186
- }
3187
- function CreditCardWidgetView({
3188
- token,
3189
- amount,
3190
- allowance,
3191
- onCredited,
3192
- onLockSubmitted,
3193
- onLockFailed
3194
- }) {
3195
- const moonpayCurrencyCode = token?.moonpayCurrencyCode;
3196
- const containerRef = useRef(null);
3197
- const [widgetTheme, setWidgetTheme] = useState();
3198
- useEffect(() => {
3199
- if (containerRef.current) setWidgetTheme(resolveSdkTheme(containerRef.current));
3200
- }, []);
3201
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
3202
- /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Complete your purchase" }),
3203
- token && moonpayCurrencyCode ? /* @__PURE__ */ jsx(
3204
- FiatOnRampForm,
3205
- {
3206
- tokenId: token.id,
3207
- currencyCode: moonpayCurrencyCode,
3208
- quoteCurrencyAmount: amount || void 0,
3209
- lockAmount: true,
3210
- variant: "embedded",
3211
- autoStart: true,
3212
- theme: widgetTheme,
3213
- postDepositLock: allowance ? {
3214
- maxAmount: BigInt(allowance.value),
3215
- lockDuration: allowance.lockDuration
3216
- } : void 0,
3217
- onCredited,
3218
- onLockSubmitted,
3219
- onLockFailed
3220
- }
3221
- ) : /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
3222
- token?.symbol ?? "This token",
3223
- " isn\u2019t available for card purchases yet."
3224
- ] })
3225
- ] });
3226
- }
3227
- function resolveSdkTheme(el) {
3228
- const scheme = getComputedStyle(el).colorScheme;
3229
- if (scheme === "dark") return "dark";
3230
- if (scheme === "light") return "light";
3231
- return el.closest(".dark") ? "dark" : "light";
2854
+ }
2855
+ const data = await res.json();
2856
+ const asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
2857
+ return {
2858
+ minBuyAmount: asNumber(data.baseCurrency?.minBuyAmount),
2859
+ maxBuyAmount: asNumber(data.baseCurrency?.maxBuyAmount)
2860
+ };
2861
+ }
2862
+ });
2863
+ return {
2864
+ minBuyAmount: query.data?.minBuyAmount,
2865
+ maxBuyAmount: query.data?.maxBuyAmount,
2866
+ isLoading: query.isLoading,
2867
+ error: query.error ?? null
2868
+ };
3232
2869
  }
3233
2870
  function PrivanaIcon({ className, size = 32 }) {
3234
2871
  return /* @__PURE__ */ jsxs(
@@ -3322,91 +2959,26 @@ function AllowancePolicySection({
3322
2959
  ] })
3323
2960
  ] });
3324
2961
  }
3325
- function MoonPayGate({ enabled, children }) {
3326
- const { networkConfig } = usePrivanaContext();
3327
- const apiKey = networkConfig.moonpayApiKey;
3328
- if (!enabled || !apiKey) return /* @__PURE__ */ jsx(Fragment, { children });
3329
- return /* @__PURE__ */ jsx(MoonPayProvider, { apiKey, children });
3330
- }
3331
- function MethodTabs({
3332
- activeTab,
3333
- onTabChange
3334
- }) {
3335
- return /* @__PURE__ */ jsxs("div", { className: "bg-secondary relative flex gap-2 overflow-hidden rounded-[10px] p-1", children: [
3336
- /* @__PURE__ */ jsx(
3337
- "div",
3338
- {
3339
- className: cn(
3340
- "bg-input absolute top-1 bottom-1 left-1 w-[calc(50%-8px)] rounded-md transition-transform duration-200",
3341
- activeTab === "credit-card" && "translate-x-[calc(100%+8px)]"
3342
- )
3343
- }
3344
- ),
3345
- /* @__PURE__ */ jsx(
3346
- "button",
3347
- {
3348
- type: "button",
3349
- onClick: () => onTabChange("crypto"),
3350
- className: cn(
3351
- "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
3352
- activeTab === "crypto" ? "text-foreground" : "text-muted-foreground"
3353
- ),
3354
- children: "Crypto"
3355
- }
3356
- ),
3357
- /* @__PURE__ */ jsx(
3358
- "button",
3359
- {
3360
- type: "button",
3361
- onClick: () => onTabChange("credit-card"),
3362
- className: cn(
3363
- "relative z-10 flex-1 cursor-pointer rounded-md px-3 py-[9px] text-sm font-medium transition-colors",
3364
- activeTab === "credit-card" ? "text-foreground" : "text-muted-foreground"
3365
- ),
3366
- children: "Credit Card"
3367
- }
3368
- )
3369
- ] });
3370
- }
3371
- function MethodOption({
3372
- title,
3373
- description,
3374
- onClick
3375
- }) {
3376
- return /* @__PURE__ */ jsxs(
3377
- "button",
3378
- {
3379
- type: "button",
3380
- onClick,
3381
- 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",
3382
- children: [
3383
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
3384
- /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm leading-[14px] font-medium", children: title }),
3385
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs leading-3", children: description })
3386
- ] }),
3387
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex h-5 w-5 items-center justify-center", children: /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
3388
- ]
3389
- }
3390
- );
3391
- }
3392
2962
  function DepositView({
3393
2963
  source,
3394
2964
  selectedToken,
3395
2965
  amount,
3396
2966
  allowance,
2967
+ externalMinimum,
3397
2968
  onAmountChange,
3398
2969
  onSelectToken,
3399
2970
  onConnectWallet,
3400
2971
  onSubmit,
3401
2972
  isSubmitting = false
3402
2973
  }) {
3403
- const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig } = usePrivanaContext();
2974
+ const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig, hostedAuthConfig } = usePrivanaContext();
3404
2975
  const { address, isConnected } = useAccount();
3405
2976
  const appName = serviceName ?? "Privana";
3406
2977
  const chain = selectedToken ? getChainById2(selectedToken.chainId) : void 0;
3407
2978
  const targetChain = chain ?? chains[0];
3408
2979
  const sourceLabel = source === "connected" ? "Connected Wallet" : "External Wallet";
3409
2980
  const isConnectedSource = source === "connected";
2981
+ const isExternal = source === "external";
3410
2982
  const isCreditCard = source === "credit-card";
3411
2983
  const isNative = selectedToken?.contract === zeroAddress;
3412
2984
  const { data: nativeBalanceData } = useBalance$1({
@@ -3440,8 +3012,14 @@ function DepositView({
3440
3012
  const belowMoonpayMin = isCreditCard && hasValidAmount && moonpayMinBuy != null && parseFloat(amount) < moonpayMinBuy;
3441
3013
  const moonpayLimitsUnready = isCreditCard && !!selectedToken?.moonpayCurrencyCode && moonpayMinBuy == null;
3442
3014
  const creditCardUnavailable = isCreditCard && !!selectedToken && !selectedToken.moonpayCurrencyCode;
3443
- const needsConnect = isConnectedSource && !isConnected;
3444
- const canDeposit = hasValidAmount && !!selectedToken && !tooManyDecimals && !exceedsBalance && !belowMoonpayMin && !moonpayLimitsUnready && !creditCardUnavailable && !needsConnect;
3015
+ const externalTokenUnavailable = isExternal && !!selectedToken && isNative;
3016
+ const externalMinimumRequired = isExternal && !!allowance && !!selectedToken;
3017
+ const externalMinimumLoading = externalMinimumRequired && externalMinimum === void 0;
3018
+ const externalMinimumUnavailable = externalMinimumRequired && externalMinimum === null;
3019
+ const belowExternalMinimum = externalMinimumRequired && hasValidAmount && !tooManyDecimals && !!selectedToken && typeof externalMinimum === "bigint" && parseTokenAmount(amount, selectedToken.decimals) < externalMinimum;
3020
+ const externalNeedsWallet = isExternal && (!!allowance || !hostedAuthConfig);
3021
+ const needsConnect = (isConnectedSource || externalNeedsWallet) && !isConnected;
3022
+ const canDeposit = hasValidAmount && !!selectedToken && !tooManyDecimals && !exceedsBalance && !belowMoonpayMin && !moonpayLimitsUnready && !creditCardUnavailable && !externalTokenUnavailable && !externalMinimumLoading && !externalMinimumUnavailable && !belowExternalMinimum && !needsConnect;
3445
3023
  const handleMax = () => {
3446
3024
  const max = formattedWalletBalance.replace(/\s/g, "");
3447
3025
  if (parseFloat(max) > 0) onAmountChange(max);
@@ -3450,9 +3028,12 @@ function DepositView({
3450
3028
  isCreditCard ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3451
3029
  /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Buy with credit card and deposit" }),
3452
3030
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Enter your deposit amount and proceed to sign a policy." })
3453
- ] }) : /* @__PURE__ */ jsxs("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: [
3454
- "Deposit from ",
3455
- sourceLabel
3031
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3032
+ /* @__PURE__ */ jsxs("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: [
3033
+ "Deposit from ",
3034
+ sourceLabel
3035
+ ] }),
3036
+ allowance && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Enter your deposit amount and proceed to sign a policy." })
3456
3037
  ] }),
3457
3038
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3458
3039
  /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
@@ -3461,7 +3042,8 @@ function DepositView({
3461
3042
  {
3462
3043
  type: "button",
3463
3044
  onClick: onSelectToken,
3464
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3 text-left",
3045
+ disabled: isSubmitting,
3046
+ className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3 text-left disabled:cursor-not-allowed disabled:opacity-50",
3465
3047
  children: [
3466
3048
  selectedToken ? /* @__PURE__ */ jsxs(Fragment, { children: [
3467
3049
  /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
@@ -3502,6 +3084,7 @@ function DepositView({
3502
3084
  type: "text",
3503
3085
  inputMode: "decimal",
3504
3086
  placeholder: "Enter Amount",
3087
+ disabled: isSubmitting,
3505
3088
  value: amount,
3506
3089
  onChange: (e) => {
3507
3090
  const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
@@ -3533,11 +3116,24 @@ function DepositView({
3533
3116
  moonpayMinBuy,
3534
3117
  " USD."
3535
3118
  ] }),
3119
+ belowExternalMinimum && selectedToken && typeof externalMinimum === "bigint" && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
3120
+ "Minimum deposit is ",
3121
+ formatUnits(externalMinimum, selectedToken.decimals),
3122
+ " ",
3123
+ selectedToken.symbol,
3124
+ "."
3125
+ ] }),
3126
+ externalMinimumLoading && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Checking minimum deposit\u2026" }),
3127
+ externalMinimumUnavailable && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Couldn\u2019t load the minimum deposit. Please try again." }),
3536
3128
  isCreditCard && moonpayLimitsError && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Couldn\u2019t load purchase limits. Please try again." }),
3537
3129
  isCreditCard && moonpayLimitsLoading && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Checking purchase limits\u2026" }),
3538
3130
  creditCardUnavailable && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
3539
3131
  selectedToken?.symbol ?? "This token",
3540
3132
  " isn\u2019t available for card purchases yet."
3133
+ ] }),
3134
+ externalTokenUnavailable && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
3135
+ selectedToken?.symbol ?? "This token",
3136
+ " can\u2019t be used for external deposits. Choose an ERC20 token."
3541
3137
  ] })
3542
3138
  ] }),
3543
3139
  allowance && /* @__PURE__ */ jsx(
@@ -3612,19 +3208,33 @@ function AwaitingDepositStatus({
3612
3208
  function ExternalDepositView({
3613
3209
  token,
3614
3210
  amount,
3615
- onCredited
3211
+ depositAddressState,
3212
+ onCredited,
3213
+ onDiscardLock,
3214
+ lock
3616
3215
  }) {
3617
- const { getChainById: getChainById2 } = usePrivanaContext();
3618
- const { depositAddress, isReady, isLoading } = useDepositAddress();
3216
+ const { getChainById: getChainById2, getTokenById, serviceName } = usePrivanaContext();
3217
+ const appName = serviceName ?? "Privana";
3218
+ const { depositAddress, isReady, isLoading } = depositAddressState;
3619
3219
  const chain = token ? getChainById2(token.chainId) : void 0;
3220
+ const lockSession = lock?.session;
3221
+ const recordVerification = lock?.recordVerification;
3222
+ const settleAfterCredit = lock?.settleAfterCredit;
3223
+ const retryAfterCredit = lock?.retryAfterCredit;
3224
+ const discoveryToken = lockSession ? getTokenById(lockSession.tokenId) : token;
3620
3225
  const [copied, setCopied] = useState(false);
3621
3226
  const [deadline] = useState(() => Date.now() + LISTENING_WINDOW_MS);
3622
3227
  const secondsLeft = useCountdown(deadline);
3623
3228
  const listening = secondsLeft > 0;
3624
3229
  const [nothingFound, setNothingFound] = useState(false);
3625
3230
  const [credited, setCredited] = useState(null);
3231
+ const [showCancelWarning, setShowCancelWarning] = useState(false);
3626
3232
  const verification = useDepositVerification({
3627
- onCredited: (txHash) => {
3233
+ onCredited: (txHash, _response, creditedAmount) => {
3234
+ if (settleAfterCredit) {
3235
+ void settleAfterCredit(txHash, creditedAmount);
3236
+ return;
3237
+ }
3628
3238
  if (onCredited) {
3629
3239
  onCredited();
3630
3240
  return;
@@ -3632,8 +3242,9 @@ function ExternalDepositView({
3632
3242
  setCredited({ txHash });
3633
3243
  }
3634
3244
  });
3635
- const { isVerifying, verificationFailed, didTimeout, verify } = verification;
3636
- const scanPaused = isVerifying || verificationFailed || didTimeout || !!credited;
3245
+ const { isVerifying, verificationFailed, canCheckAnotherTransfer, didTimeout, verify } = verification;
3246
+ const scanPaused = isVerifying || verificationFailed || didTimeout || !!credited || !!lock?.isSigning || !!lock?.isSubmittingLock || !!lockSession?.verification || !!lockSession?.creditedAmount;
3247
+ const showDepositInstructions = !credited && !lockSession?.creditedAmount;
3637
3248
  const {
3638
3249
  pending,
3639
3250
  isFetching: isScanning,
@@ -3642,23 +3253,58 @@ function ExternalDepositView({
3642
3253
  isUnavailable,
3643
3254
  refetch: refetchPendingDeposits
3644
3255
  } = usePendingDeposits({
3645
- chainId: token?.chainId,
3646
- enabled: isReady && !!depositAddress && !!token,
3256
+ chainId: discoveryToken?.chainId,
3257
+ tokenAddress: discoveryToken?.contract === zeroAddress ? void 0 : discoveryToken?.contract,
3258
+ enabled: isReady && !!depositAddress && !!discoveryToken && discoveryToken.contract !== zeroAddress,
3647
3259
  refetchInterval: listening && !scanPaused ? 3e4 : false
3648
3260
  });
3649
3261
  const processedRef = useRef(/* @__PURE__ */ new Set());
3262
+ const verificationRunRef = useRef(null);
3263
+ useEffect(
3264
+ () => () => {
3265
+ verificationRunRef.current = null;
3266
+ },
3267
+ []
3268
+ );
3269
+ useEffect(() => {
3270
+ const stored = lockSession?.verification;
3271
+ if (!stored || lockSession.creditedAmount || isVerifying || verificationFailed || didTimeout) {
3272
+ return;
3273
+ }
3274
+ const key = `${lockSession.generation}:${stored.chainId}:${stored.hash}:${stored.logIndex ?? 0}`;
3275
+ if (verificationRunRef.current === key) return;
3276
+ verificationRunRef.current = key;
3277
+ processedRef.current.add(`${stored.hash}:${stored.logIndex ?? 0}`);
3278
+ void verify({
3279
+ hash: stored.hash,
3280
+ chainId: stored.chainId,
3281
+ amount: BigInt(stored.amount),
3282
+ logIndex: stored.logIndex
3283
+ });
3284
+ }, [didTimeout, isVerifying, lockSession, verificationFailed, verify]);
3650
3285
  useEffect(() => {
3651
3286
  if (scanPaused) return;
3652
- const next = [...pending].sort((a, b) => a.block_number - b.block_number).find((d) => !processedRef.current.has(`${d.tx_hash}:${d.log_index}`));
3287
+ const next = [...pending].sort((a, b) => a.block_number - b.block_number).find(
3288
+ (deposit) => (!lockSession || isExternalDepositBlockInSession(lockSession, deposit.block_number)) && !processedRef.current.has(`${deposit.tx_hash}:${deposit.log_index}`)
3289
+ );
3653
3290
  if (!next) return;
3654
- processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
3655
- void verify({
3291
+ const context = {
3656
3292
  hash: next.tx_hash,
3657
3293
  chainId: next.chain_id,
3658
3294
  amount: BigInt(next.amount),
3659
3295
  logIndex: next.log_index
3660
- });
3661
- }, [pending, scanPaused, verify]);
3296
+ };
3297
+ if (recordVerification) {
3298
+ try {
3299
+ recordVerification(context);
3300
+ } catch (err) {
3301
+ toast.error(err instanceof Error ? err.message : "Unable to save deposit recovery state");
3302
+ return;
3303
+ }
3304
+ }
3305
+ processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
3306
+ if (!recordVerification) void verify(context);
3307
+ }, [lockSession, pending, recordVerification, scanPaused, verify]);
3662
3308
  const nothingFoundTimerRef = useRef(void 0);
3663
3309
  useEffect(() => () => clearTimeout(nothingFoundTimerRef.current), []);
3664
3310
  const handleRefresh = () => {
@@ -3671,11 +3317,27 @@ function ExternalDepositView({
3671
3317
  }
3672
3318
  });
3673
3319
  };
3674
- const handleCopy = () => {
3320
+ const handleCopy = async () => {
3675
3321
  if (!depositAddress) return;
3676
- void navigator.clipboard.writeText(depositAddress);
3677
- setCopied(true);
3678
- setTimeout(() => setCopied(false), 2e3);
3322
+ try {
3323
+ await navigator.clipboard.writeText(depositAddress);
3324
+ setCopied(true);
3325
+ setTimeout(() => setCopied(false), 2e3);
3326
+ } catch {
3327
+ toast.error("Unable to copy the deposit address");
3328
+ }
3329
+ };
3330
+ const handleCheckAnotherTransfer = () => {
3331
+ try {
3332
+ if (!lock?.clearVerification()) {
3333
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
3334
+ return;
3335
+ }
3336
+ verificationRunRef.current = null;
3337
+ verification.reset();
3338
+ } catch (err) {
3339
+ toast.error(err instanceof Error ? err.message : "Unable to continue deposit discovery");
3340
+ }
3679
3341
  };
3680
3342
  return /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
3681
3343
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
@@ -3689,27 +3351,64 @@ function ExternalDepositView({
3689
3351
  /* @__PURE__ */ jsx("div", { className: "bg-border h-px w-full" }),
3690
3352
  /* @__PURE__ */ jsx(SummaryRow, { value: amount || "\u2014", label: "Value" })
3691
3353
  ] }),
3692
- /* @__PURE__ */ jsx("div", { className: "bg-secondary flex h-50 items-center justify-center rounded-[10px]", children: !isReady ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-6 text-center text-sm", children: "Connect your wallet to generate a deposit address." }) : depositAddress ? /* @__PURE__ */ jsx("div", { className: "rounded-[10px] bg-white p-3", children: /* @__PURE__ */ jsx(QRCodeSVG, { value: depositAddress, size: 160 }) }) : isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-[10px]" }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Deposit address unavailable" }) }),
3693
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3694
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Deposit Address" }),
3695
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3696
- /* @__PURE__ */ jsx("div", { className: "border-border flex h-10 min-w-0 flex-1 items-center rounded-[10px] border px-3", children: depositAddress ? /* @__PURE__ */ jsx("p", { className: "text-foreground min-w-0 flex-1 truncate text-sm", children: depositAddress }) : isReady && isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "\u2014" }) }),
3697
- /* @__PURE__ */ jsxs(
3698
- "button",
3699
- {
3700
- type: "button",
3701
- onClick: handleCopy,
3702
- disabled: !depositAddress,
3703
- className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 shrink-0 cursor-pointer items-center gap-2 rounded-[10px] px-3 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
3704
- children: [
3705
- /* @__PURE__ */ jsx(CopyIcon, {}),
3706
- copied ? "Copied" : "Copy"
3707
- ]
3708
- }
3709
- )
3354
+ showDepositInstructions && /* @__PURE__ */ jsxs(Fragment, { children: [
3355
+ !scanPaused && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Send the displayed amount in one transfer. Transfers below the minimum are not combined or automatically returned." }),
3356
+ /* @__PURE__ */ jsx("div", { className: "bg-secondary flex h-50 items-center justify-center rounded-[10px]", children: !isReady ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground px-6 text-center text-sm", children: "Connect your wallet to generate a deposit address." }) : depositAddress ? /* @__PURE__ */ jsx("div", { className: "rounded-[10px] bg-white p-3", children: /* @__PURE__ */ jsx(QRCodeSVG, { value: depositAddress, size: 160 }) }) : isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-[10px]" }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "Deposit address unavailable" }) }),
3357
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3358
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Deposit Address" }),
3359
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3360
+ /* @__PURE__ */ jsx("div", { className: "border-border flex h-10 min-w-0 flex-1 items-center rounded-[10px] border px-3", children: depositAddress ? /* @__PURE__ */ jsx("p", { className: "text-foreground min-w-0 flex-1 truncate text-sm", children: depositAddress }) : isReady && isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "\u2014" }) }),
3361
+ /* @__PURE__ */ jsxs(
3362
+ "button",
3363
+ {
3364
+ type: "button",
3365
+ onClick: handleCopy,
3366
+ disabled: !depositAddress,
3367
+ className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 shrink-0 cursor-pointer items-center gap-2 rounded-[10px] px-3 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
3368
+ children: [
3369
+ /* @__PURE__ */ jsx(CopyIcon, {}),
3370
+ copied ? "Copied" : "Copy"
3371
+ ]
3372
+ }
3373
+ )
3374
+ ] })
3710
3375
  ] })
3711
3376
  ] }),
3712
- credited ? /* @__PURE__ */ jsx(
3377
+ showCancelWarning && lockSession && !lockSession.verification && !lockSession.creditedAmount ? /* @__PURE__ */ jsx(
3378
+ TransactionWarningView,
3379
+ {
3380
+ title: "Cancel this deposit?",
3381
+ message: "Only cancel if you have not sent funds. Canceling after a transfer was sent stops recovery and can leave the transfer uncredited or unlocked.",
3382
+ onDone: () => onDiscardLock?.(),
3383
+ actionLabel: "I haven\u2019t sent funds",
3384
+ onDismiss: () => setShowCancelWarning(false),
3385
+ dismissLabel: "Keep waiting"
3386
+ }
3387
+ ) : lock?.isSigning ? /* @__PURE__ */ jsx(
3388
+ AwaitingDepositStatus,
3389
+ {
3390
+ title: "Confirm policy in your wallet",
3391
+ subtitle: "Sign a fresh policy for the funds already credited."
3392
+ }
3393
+ ) : lock?.isSubmittingLock ? /* @__PURE__ */ jsx(
3394
+ AwaitingDepositStatus,
3395
+ {
3396
+ title: `Locking funds for ${appName}`,
3397
+ subtitle: "Deposit credited. Applying your signed policy\u2026"
3398
+ }
3399
+ ) : lock?.session?.creditedAmount ? /* @__PURE__ */ jsx(
3400
+ TransactionWarningView,
3401
+ {
3402
+ title: lock.session.submissionAmbiguous ? "Lock status unknown" : "Deposit credited",
3403
+ message: lock.session.submissionAmbiguous ? "The lock may already have succeeded. Retry only the saved signature, or check locked funds before stopping recovery." : "The deposit is credited, but its signed lock still needs to be submitted.",
3404
+ onDone: () => {
3405
+ void retryAfterCredit?.().catch((err) => {
3406
+ toast.error(err instanceof Error ? err.message : "Policy recovery failed");
3407
+ });
3408
+ },
3409
+ actionLabel: lock.session.payload ? "Retry lock submission" : "Sign policy and lock funds"
3410
+ }
3411
+ ) : credited ? /* @__PURE__ */ jsx(
3713
3412
  TransactionSuccessView,
3714
3413
  {
3715
3414
  title: "Deposit Credited",
@@ -3726,14 +3425,15 @@ function ExternalDepositView({
3726
3425
  title: "Could not verify deposit",
3727
3426
  message: didTimeout ? "Verification timed out \u2014 your deposit may still be credited in the background." : verification.error?.message ?? "Your transfer was found but could not be verified.",
3728
3427
  onRetry: () => void verification.retryVerification(),
3729
- onDismiss: () => verification.reset(),
3428
+ onDismiss: lock ? canCheckAnotherTransfer ? handleCheckAnotherTransfer : void 0 : () => verification.reset(),
3429
+ dismissLabel: lock && canCheckAnotherTransfer ? "Check another transfer" : void 0,
3730
3430
  isRetrying: isVerifying
3731
3431
  }
3732
3432
  ) : isVerifying ? /* @__PURE__ */ jsx(
3733
3433
  AwaitingDepositStatus,
3734
3434
  {
3735
3435
  title: "Deposit detected",
3736
- subtitle: "Crediting the incoming transaction to your Privana balance..."
3436
+ subtitle: "Crediting the incoming transaction to your Privana balance\u2026"
3737
3437
  }
3738
3438
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
3739
3439
  /* @__PURE__ */ jsx(
@@ -3755,11 +3455,95 @@ function ExternalDepositView({
3755
3455
  children: "Refresh deposit status"
3756
3456
  }
3757
3457
  ),
3758
- isRateLimited ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Checked too recently \u2014 try again in a moment." }) : isUnavailable ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Deposit discovery is unavailable for this chain." }) : isScanError ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Chain temporarily unreachable \u2014 retrying automatically." }) : nothingFound ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "No incoming transaction found yet." }) : null
3458
+ lockSession && !lockSession.verification && !lockSession.creditedAmount && /* @__PURE__ */ jsx(
3459
+ "button",
3460
+ {
3461
+ type: "button",
3462
+ onClick: () => setShowCancelWarning(true),
3463
+ 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",
3464
+ children: "Cancel deposit"
3465
+ }
3466
+ ),
3467
+ isRateLimited ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Checked too recently. Try again in a moment." }) : isUnavailable ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Deposit discovery is unavailable for this chain." }) : isScanError ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Chain temporarily unreachable. Retrying automatically." }) : nothingFound ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "No incoming transaction found yet." }) : null
3759
3468
  ] })
3760
3469
  ] })
3761
3470
  ] });
3762
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
+ }
3763
3547
  function DepositModalContent({
3764
3548
  defaultTab = "crypto",
3765
3549
  allowance,
@@ -3774,7 +3558,7 @@ function DepositModalContent({
3774
3558
  onCloseBlockedChange,
3775
3559
  onExit
3776
3560
  }) {
3777
- const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2 } = usePrivanaContext();
3561
+ const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2, getTokenById } = usePrivanaContext();
3778
3562
  const { address } = useAccount();
3779
3563
  const appName = serviceName ?? "Privana";
3780
3564
  const [activeTab, setActiveTab] = useState(defaultTab);
@@ -3783,6 +3567,10 @@ function DepositModalContent({
3783
3567
  const [selectedTokenId, setSelectedTokenId] = useState(defaultToken?.id ?? "");
3784
3568
  const [amount, setAmount] = useState("");
3785
3569
  const selectedToken = enabledTokens.find((t) => t.id === selectedTokenId) ?? defaultToken;
3570
+ const externalDepositAddress = useDepositAddress({
3571
+ enabled: source === "external" && (view === "deposit" || view === "external-deposit")
3572
+ });
3573
+ const externalMinimum = source !== "external" || !selectedToken ? void 0 : externalDepositAddress.isError ? null : externalDepositAddress.response ? getExternalDepositMinimum(externalDepositAddress.response, selectedToken.chainId) ?? null : void 0;
3786
3574
  const prevAddressRef = useRef(address);
3787
3575
  useEffect(() => {
3788
3576
  const prev = prevAddressRef.current;
@@ -3802,9 +3590,10 @@ function DepositModalContent({
3802
3590
  const [showTimeout, setShowTimeout] = useState(false);
3803
3591
  const [cancelled, setCancelled] = useState(false);
3804
3592
  const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3805
- const [lockFailedMessage, setLockFailedMessage] = useState(null);
3593
+ const [lockFailure, setLockFailure] = useState(null);
3806
3594
  const finishDeposit = () => {
3807
3595
  setAmount("");
3596
+ if (view === "external-deposit") setView("deposit");
3808
3597
  if (onDepositSuccess) {
3809
3598
  resetDeposit();
3810
3599
  onDepositSuccess();
@@ -3839,6 +3628,7 @@ function DepositModalContent({
3839
3628
  },
3840
3629
  onLockSubmitted: () => {
3841
3630
  setIsSubmittingLock(false);
3631
+ setLockFailure(null);
3842
3632
  finishDeposit();
3843
3633
  },
3844
3634
  // The deposit credited; only the policy lock failed. Success must not
@@ -3846,7 +3636,7 @@ function DepositModalContent({
3846
3636
  // error view and let the host re-prompt for a fresh lock.
3847
3637
  onLockFailed: (err) => {
3848
3638
  setIsSubmittingLock(false);
3849
- setLockFailedMessage(err.message);
3639
+ setLockFailure(err);
3850
3640
  onLockFailed?.(err);
3851
3641
  },
3852
3642
  onCheckTimeout: () => {
@@ -3854,7 +3644,37 @@ function DepositModalContent({
3854
3644
  setShowTimeout(true);
3855
3645
  }
3856
3646
  });
3857
- const isUnsafeToClose = (isGettingAddress || isSendingTransaction) && !cancelled;
3647
+ const externalLock = useExternalDepositLock({
3648
+ allowance,
3649
+ onLockSubmitted: () => {
3650
+ setLockFailure(null);
3651
+ finishDeposit();
3652
+ },
3653
+ // Same contract as the connected flow: the deposit credited, only the
3654
+ // lock failed — never report success, show the lock-error view instead.
3655
+ onLockFailed: (err) => {
3656
+ setLockFailure(err);
3657
+ onLockFailed?.(err);
3658
+ }
3659
+ });
3660
+ useEffect(() => {
3661
+ const restored = externalLock.session;
3662
+ if (!restored) return;
3663
+ const canRestore = view === "method" || source === "external" && (view === "deposit" || view === "external-deposit");
3664
+ if (!canRestore) return;
3665
+ const token = getTokenById(restored.tokenId);
3666
+ if (!token) return;
3667
+ setSource("external");
3668
+ setSelectedTokenId(token.id);
3669
+ setAmount(formatUnits(BigInt(restored.depositAmount), token.decimals));
3670
+ setView("external-deposit");
3671
+ }, [externalLock.session, getTokenById, source, view]);
3672
+ useEffect(() => {
3673
+ if (view === "external-deposit" && allowance && !externalLock.session) {
3674
+ setView("deposit");
3675
+ }
3676
+ }, [allowance, externalLock.session, view]);
3677
+ const isUnsafeToClose = (isGettingAddress || isSendingTransaction) && !cancelled || externalLock.isSigning || externalLock.isSubmittingLock;
3858
3678
  useEffect(() => {
3859
3679
  onCloseBlockedChange?.(isUnsafeToClose);
3860
3680
  }, [isUnsafeToClose, onCloseBlockedChange]);
@@ -3867,7 +3687,33 @@ function DepositModalContent({
3867
3687
  }, [depositError, verificationFailed]);
3868
3688
  const handleSubmit = (args) => {
3869
3689
  if (args.source === "external") {
3870
- setView("external-deposit");
3690
+ const token2 = enabledTokens.find((candidate) => candidate.id === args.tokenId);
3691
+ if (!token2) return;
3692
+ if (token2.contract === zeroAddress) {
3693
+ toast.error("External deposits support ERC20 tokens only");
3694
+ return;
3695
+ }
3696
+ const depositAmount = parseTokenAmount(args.amount, token2.decimals);
3697
+ if (!allowance) {
3698
+ setView("external-deposit");
3699
+ return;
3700
+ }
3701
+ if (typeof externalMinimum !== "bigint") {
3702
+ toast.error("Minimum deposit is unavailable. Please try again.");
3703
+ return;
3704
+ }
3705
+ if (depositAmount < externalMinimum) {
3706
+ toast.error(
3707
+ `Minimum deposit is ${formatUnits(externalMinimum, token2.decimals)} ${token2.symbol}`
3708
+ );
3709
+ return;
3710
+ }
3711
+ externalLock.signAndPersist({
3712
+ tokenId: token2.id,
3713
+ amount: depositAmount
3714
+ }).then(() => setView("external-deposit")).catch((err) => {
3715
+ toast.error(err instanceof Error ? err.message : "Policy signing failed");
3716
+ });
3871
3717
  return;
3872
3718
  }
3873
3719
  if (args.source === "credit-card") {
@@ -3919,7 +3765,14 @@ function DepositModalContent({
3919
3765
  }
3920
3766
  ] : []
3921
3767
  ];
3922
- const flowView = showSuccess ? "deposit-success" : lockFailedMessage ? "lock-error" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled || isSubmittingLock ? "depositing" : null;
3768
+ const hasExternalLockRecovery = view === "external-deposit" && !!externalLock.session?.creditedAmount;
3769
+ const externalSubmissionAmbiguous = hasExternalLockRecovery && !!lockFailure?.submissionMayHaveSucceeded;
3770
+ const savedExternalPayloadUsable = !!externalLock.session?.payload && isSignedLockUsable(externalLock.session.payload);
3771
+ const ambiguousRecoveryMustBeAbandoned = externalSubmissionAmbiguous && !savedExternalPayloadUsable;
3772
+ const externalRecoveryExitLabel = externalSubmissionAmbiguous ? "Stop recovery" : "Keep funds unlocked";
3773
+ const lockFailureToken = hasExternalLockRecovery && externalLock.session ? getTokenById(externalLock.session.tokenId) ?? selectedToken : selectedToken;
3774
+ const lockFailureMessage = formatPostDepositLockFailure(lockFailure, lockFailureToken);
3775
+ const flowView = showSuccess ? "deposit-success" : lockFailure ? "lock-error" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled || isSubmittingLock ? "depositing" : null;
3923
3776
  const activeView = flowView ?? view;
3924
3777
  const handleDepositDone = () => {
3925
3778
  setShowSuccess(false);
@@ -3932,10 +3785,28 @@ function DepositModalContent({
3932
3785
  resetDeposit();
3933
3786
  };
3934
3787
  const handleLockFailedDone = () => {
3935
- setLockFailedMessage(null);
3788
+ if (hasExternalLockRecovery && !externalLock.discardSession()) {
3789
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
3790
+ return;
3791
+ }
3792
+ setLockFailure(null);
3936
3793
  setAmount("");
3937
3794
  setCancelled(false);
3938
3795
  resetDeposit();
3796
+ if (view === "external-deposit") setView("deposit");
3797
+ };
3798
+ const handleExternalLockRetry = () => {
3799
+ externalLock.retryAfterCredit().catch((err) => {
3800
+ toast.error(err instanceof Error ? err.message : "Policy signing failed");
3801
+ });
3802
+ };
3803
+ const handleExternalVerificationDiscard = () => {
3804
+ if (!externalLock.discardSession()) {
3805
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
3806
+ return;
3807
+ }
3808
+ setAmount("");
3809
+ setView("deposit");
3939
3810
  };
3940
3811
  const handleDismissVerificationError = () => {
3941
3812
  setAmount("");
@@ -3948,6 +3819,10 @@ function DepositModalContent({
3948
3819
  };
3949
3820
  const back = view === "external-deposit" ? { to: "deposit", label: "Deposit from External Wallet" } : view === "credit-card-widget" ? { to: "deposit", label: "Buy with credit card and deposit" } : view === "select-token" ? { to: "deposit", label: "Deposit" } : { to: "method", label: "Deposit Method" };
3950
3821
  const goBack = () => {
3822
+ if (view === "external-deposit" && externalLock.session) {
3823
+ toast.error("Cancel the active deposit from the address screen before going back");
3824
+ return;
3825
+ }
3951
3826
  if (back.to === "method") {
3952
3827
  setAmount("");
3953
3828
  setSelectedTokenId("");
@@ -3993,9 +3868,14 @@ function DepositModalContent({
3993
3868
  activeView === "lock-error" && /* @__PURE__ */ jsx(
3994
3869
  TransactionWarningView,
3995
3870
  {
3996
- title: "Deposit credited, lock failed",
3997
- message: `Your deposit was credited but locking the funds for ${appName} failed: ${lockFailedMessage}`,
3998
- onDone: handleLockFailedDone
3871
+ title: externalSubmissionAmbiguous ? "Deposit credited, lock status unknown" : "Deposit credited, lock failed",
3872
+ message: externalSubmissionAmbiguous ? `Your deposit was credited, but the lock for ${appName} may already have succeeded. Retry only the saved signature; if it has expired, check locked funds before stopping recovery.` : `Your deposit was credited but locking the funds for ${appName} failed: ${lockFailureMessage}`,
3873
+ onDone: hasExternalLockRecovery && !ambiguousRecoveryMustBeAbandoned ? handleExternalLockRetry : handleLockFailedDone,
3874
+ actionLabel: hasExternalLockRecovery ? ambiguousRecoveryMustBeAbandoned ? externalRecoveryExitLabel : externalLock.session?.payload ? "Retry lock submission" : "Sign policy and lock funds" : void 0,
3875
+ pendingLabel: externalLock.isSigning ? "Confirm policy in wallet\u2026" : "Locking funds\u2026",
3876
+ isPending: externalLock.isSigning || externalLock.isSubmittingLock,
3877
+ onDismiss: hasExternalLockRecovery && !ambiguousRecoveryMustBeAbandoned ? handleLockFailedDone : void 0,
3878
+ dismissLabel: externalRecoveryExitLabel
3999
3879
  }
4000
3880
  ),
4001
3881
  activeView === "deposit-timeout" && /* @__PURE__ */ jsx(
@@ -4010,7 +3890,7 @@ function DepositModalContent({
4010
3890
  TransactionErrorView,
4011
3891
  {
4012
3892
  title: "Verification failed",
4013
- message: depositError?.message ? `Your transfer was sent on-chain but we could not verify the deposit. The funds are already at the deposit address \u2014 retry verification instead of starting a new deposit. (${depositError.message})` : "Your transfer was sent on-chain but we could not verify the deposit. The funds are already at the deposit address \u2014 retry verification instead of starting a new deposit.",
3893
+ message: depositError?.message ? `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. (${depositError.message})` : "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.",
4014
3894
  explorerUrl: explorerTxUrl,
4015
3895
  explorerLabel: "View transaction",
4016
3896
  onRetry: handleRetryVerification,
@@ -4020,7 +3900,7 @@ function DepositModalContent({
4020
3900
  activeView === "depositing" && /* @__PURE__ */ jsx(
4021
3901
  TransactionProgressView,
4022
3902
  {
4023
- title: "Depositing...",
3903
+ title: "Depositing\u2026",
4024
3904
  steps: depositSteps,
4025
3905
  onCancel: isGettingAddress || isSendingTransaction ? handleDepositCancel : void 0
4026
3906
  }
@@ -4074,15 +3954,16 @@ function DepositModalContent({
4074
3954
  selectedToken,
4075
3955
  amount,
4076
3956
  allowance,
3957
+ externalMinimum,
4077
3958
  onAmountChange: setAmount,
4078
3959
  onSelectToken: () => setView("select-token"),
4079
3960
  onConnectWallet,
4080
3961
  onSubmit: handleSubmit,
4081
- isSubmitting: isPending
3962
+ isSubmitting: isPending || externalLock.isSigning || externalLock.isSubmittingLock
4082
3963
  }
4083
3964
  ) }),
4084
3965
  activeView === "select-token" && /* @__PURE__ */ jsx(
4085
- TokenSelectorView2,
3966
+ TokenSelectorView,
4086
3967
  {
4087
3968
  selectedTokenId: selectedToken?.id,
4088
3969
  onSelect: (id) => {
@@ -4091,12 +3972,15 @@ function DepositModalContent({
4091
3972
  }
4092
3973
  }
4093
3974
  ),
4094
- activeView === "external-deposit" && /* @__PURE__ */ jsx(
3975
+ activeView === "external-deposit" && (!allowance || externalLock.session) && /* @__PURE__ */ jsx(
4095
3976
  ExternalDepositView,
4096
3977
  {
4097
3978
  token: selectedToken,
4098
3979
  amount,
4099
- onCredited: allowance ? void 0 : onDepositSuccess
3980
+ depositAddressState: externalDepositAddress,
3981
+ onCredited: allowance ? void 0 : onDepositSuccess,
3982
+ onDiscardLock: handleExternalVerificationDiscard,
3983
+ lock: externalLock.session ? externalLock : void 0
4100
3984
  }
4101
3985
  ),
4102
3986
  activeView === "credit-card-widget" && /* @__PURE__ */ jsx(MoonPayGate, { enabled: true, children: /* @__PURE__ */ jsx(
@@ -4108,7 +3992,7 @@ function DepositModalContent({
4108
3992
  onCredited: allowance ? void 0 : finishCardPurchase,
4109
3993
  onLockSubmitted: finishCardPurchase,
4110
3994
  onLockFailed: (err) => {
4111
- setLockFailedMessage(err.message);
3995
+ setLockFailure(err);
4112
3996
  onLockFailed?.(err);
4113
3997
  }
4114
3998
  }
@@ -4437,7 +4321,7 @@ function WithdrawModalContent({
4437
4321
  }
4438
4322
  ),
4439
4323
  view === "select-token" && /* @__PURE__ */ jsx(
4440
- TokenSelectorView2,
4324
+ TokenSelectorView,
4441
4325
  {
4442
4326
  selectedTokenId: selectedToken?.id,
4443
4327
  onSelect: (id) => {
@@ -4868,7 +4752,7 @@ function WalletModalContent({
4868
4752
  onAmountChange: setAmount,
4869
4753
  onFundSession: onFundSession ? fundSession : void 0,
4870
4754
  onAddFunds: () => setView("deposit"),
4871
- onWithdraw: handleWithdraw
4755
+ onWithdraw: session && !onEndSession ? void 0 : handleWithdraw
4872
4756
  }
4873
4757
  ),
4874
4758
  view === "funding-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
@@ -4978,6 +4862,6 @@ function WalletInlineModal({ className, ...handlers }) {
4978
4862
  );
4979
4863
  }
4980
4864
 
4981
- 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 };
4982
4866
  //# sourceMappingURL=index.js.map
4983
4867
  //# sourceMappingURL=index.js.map