@oasisprotocol/privana-sdk 0.5.2 → 0.5.3

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,10 +1,10 @@
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, Button, stripHostedAuthCallbackParams, AccountingApiError, getBrowserStorageItem, removeBrowserStorageItem, isSignedLockUsable, setBrowserStorageItem, shortenAddress, formatTimeRemaining, parseTokenAmount, getExplorerAddressUrl, getExplorerLabel, canUseSharedBrowserStorage, DEFAULT_LOCK_DURATION_SECONDS, Skeleton, formatCountdown, FiatOnRampForm, getSharedBrowserStorageItem, removeSharedBrowserStorageItem, setSharedBrowserStorageItem } from './chunk-DZURIIRE.js';
3
+ export { AccountingApiError, Button, DEFAULT_LOCK_DURATION_SECONDS, DEFAULT_ONRAMP_LOCK_BUFFER, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, LOCK_TYPES, MODIFY_LOCK_TYPES, NETWORK_CONFIG, NetworkError, PostDepositLockError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, TRANSFER_LOCKED_TYPES, TRANSFER_TYPES, ValidationError, WITHDRAW_FROM_LOCK_TYPES, WITHDRAW_TYPES, applyLockBuffer, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clampLockAmount, clearHostedAuthPendingTransaction, clearPendingLock, createDomain, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createLockExpiry, createPkceChallenge, createPkceVerifier, createSignedLockRequest, getAccountingContract, getApiUrl, getChainById, getChainId, getExplorerAddressUrl, getExplorerLabel, isHostedAuthRefreshActive, isHostedAuthSessionActive, isSignedLockUsable, loadPendingLock, normalizeAddress, normalizeHex, parseHostedAuthCallback, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, requireDepositLockOwner, requireServiceAddress, savePendingLock, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, stripHostedAuthCallbackParams, submitPendingLock, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-DZURIIRE.js';
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
8
  import * as DialogPrimitive from '@radix-ui/react-dialog';
9
9
  import { XIcon } from 'lucide-react';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -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,
@@ -1904,7 +1908,16 @@ function WarningIcon() {
1904
1908
  }
1905
1909
  ) });
1906
1910
  }
1907
- function TransactionWarningView({ title, message, onDone }) {
1911
+ function TransactionWarningView({
1912
+ title,
1913
+ message,
1914
+ onDone,
1915
+ actionLabel = "Done",
1916
+ pendingLabel = actionLabel,
1917
+ isPending = false,
1918
+ onDismiss,
1919
+ dismissLabel = "Dismiss"
1920
+ }) {
1908
1921
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1909
1922
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1910
1923
  /* @__PURE__ */ jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-amber-400", children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
@@ -1913,14 +1926,26 @@ function TransactionWarningView({ title, message, onDone }) {
1913
1926
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1914
1927
  ] })
1915
1928
  ] }),
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
- )
1929
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1930
+ onDismiss && /* @__PURE__ */ jsx(
1931
+ "button",
1932
+ {
1933
+ onClick: onDismiss,
1934
+ disabled: isPending,
1935
+ className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1936
+ children: dismissLabel
1937
+ }
1938
+ ),
1939
+ /* @__PURE__ */ jsx(
1940
+ "button",
1941
+ {
1942
+ onClick: onDone,
1943
+ disabled: isPending,
1944
+ className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1945
+ children: isPending ? pendingLabel : actionLabel
1946
+ }
1947
+ )
1948
+ ] })
1924
1949
  ] });
1925
1950
  }
1926
1951
  function TransactionErrorView({
@@ -1931,7 +1956,8 @@ function TransactionErrorView({
1931
1956
  onRetry,
1932
1957
  onDismiss,
1933
1958
  isRetrying,
1934
- retryLabel = "Retry Verification"
1959
+ retryLabel = "Retry Verification",
1960
+ dismissLabel = "Close"
1935
1961
  }) {
1936
1962
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1937
1963
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -1955,13 +1981,13 @@ function TransactionErrorView({
1955
1981
  }
1956
1982
  ),
1957
1983
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1958
- /* @__PURE__ */ jsx(
1984
+ onDismiss && /* @__PURE__ */ jsx(
1959
1985
  "button",
1960
1986
  {
1961
1987
  onClick: onDismiss,
1962
1988
  disabled: isRetrying,
1963
1989
  className: "border-border text-foreground hover:bg-secondary flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1964
- children: "Close"
1990
+ children: dismissLabel
1965
1991
  }
1966
1992
  ),
1967
1993
  /* @__PURE__ */ jsx(
@@ -1970,7 +1996,7 @@ function TransactionErrorView({
1970
1996
  onClick: onRetry,
1971
1997
  disabled: isRetrying,
1972
1998
  className: "bg-primary text-primary-foreground hover:bg-primary/90 flex h-10 flex-1 cursor-pointer items-center justify-center rounded-[10px] px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50",
1973
- children: isRetrying ? "Retrying..." : retryLabel
1999
+ children: isRetrying ? "Retrying\u2026" : retryLabel
1974
2000
  }
1975
2001
  )
1976
2002
  ] })
@@ -2110,7 +2136,7 @@ function DepositForm({
2110
2136
  };
2111
2137
  const getButtonText = () => {
2112
2138
  if (!isConnected) return "Connect Wallet";
2113
- if (isSwitchingChain) return "Switching...";
2139
+ if (isSwitchingChain) return "Switching\u2026";
2114
2140
  return "Deposit";
2115
2141
  };
2116
2142
  if (showSuccess) {
@@ -2134,7 +2160,7 @@ function DepositForm({
2134
2160
  );
2135
2161
  }
2136
2162
  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.";
2163
+ const baseMessage = "Your transfer was sent on-chain but we could not verify the deposit. The funds are already at the deposit address. Retry verification instead of starting a new deposit.";
2138
2164
  const detail = error?.message;
2139
2165
  const message = detail ? `${baseMessage} (${detail})` : baseMessage;
2140
2166
  return /* @__PURE__ */ jsx(
@@ -2154,7 +2180,7 @@ function DepositForm({
2154
2180
  return /* @__PURE__ */ jsx(
2155
2181
  TransactionProgressView,
2156
2182
  {
2157
- title: "Depositing...",
2183
+ title: "Depositing\u2026",
2158
2184
  steps: depositSteps,
2159
2185
  onCancel: canCancel ? handleCancel : void 0
2160
2186
  }
@@ -3094,6 +3120,745 @@ function PrivanaButton({
3094
3120
  )
3095
3121
  ] });
3096
3122
  }
3123
+
3124
+ // src/sdk/hooks/external-deposit-lock.ts
3125
+ var ACTIVE_SESSION_PREFIX = "privana:external-deposit-lock:active:";
3126
+ var SESSION_PREFIX = "privana:external-deposit-lock:session:";
3127
+ var ExternalDepositLockSessionChangedError = class _ExternalDepositLockSessionChangedError extends Error {
3128
+ constructor() {
3129
+ super("External deposit lock session changed");
3130
+ this.name = "ExternalDepositLockSessionChangedError";
3131
+ Object.setPrototypeOf(this, _ExternalDepositLockSessionChangedError.prototype);
3132
+ }
3133
+ };
3134
+ function externalDepositSessionId(chainId, tokenId) {
3135
+ return `${chainId}:${tokenId.toLowerCase()}`;
3136
+ }
3137
+ function isExternalDepositBlockInSession(session, blockNumber) {
3138
+ return Number.isSafeInteger(blockNumber) && blockNumber > session.startBlock;
3139
+ }
3140
+ function isCurrentExternalDepositVerification(session, txHash) {
3141
+ return session.creditedAmount === void 0 && session.verification?.hash.toLowerCase() === txHash.toLowerCase();
3142
+ }
3143
+ function getExternalDepositMinimum(response, chainId) {
3144
+ const value = response?.min_deposit?.[String(chainId)]?.erc20;
3145
+ if (value === void 0) return void 0;
3146
+ try {
3147
+ const amount = BigInt(value);
3148
+ return amount >= 0n ? amount : void 0;
3149
+ } catch {
3150
+ return void 0;
3151
+ }
3152
+ }
3153
+ function activeSessionKey(owner) {
3154
+ return `${ACTIVE_SESSION_PREFIX}${owner.toLowerCase()}`;
3155
+ }
3156
+ function sessionKey(owner, sessionId) {
3157
+ return `${SESSION_PREFIX}${owner.toLowerCase()}:${sessionId}`;
3158
+ }
3159
+ function activeSessionId(owner) {
3160
+ const value = getSharedBrowserStorageItem(activeSessionKey(owner));
3161
+ return value || void 0;
3162
+ }
3163
+ function isPositiveIntegerString(value) {
3164
+ if (typeof value !== "string") return false;
3165
+ try {
3166
+ return BigInt(value) > 0n;
3167
+ } catch {
3168
+ return false;
3169
+ }
3170
+ }
3171
+ function isSessionRecord(value) {
3172
+ if (!value || typeof value !== "object") return false;
3173
+ const record = value;
3174
+ if (record.version !== 1 || typeof record.owner !== "string" || typeof record.serviceAddress !== "string" || !Number.isInteger(record.chainId) || (record.chainId ?? 0) <= 0 || typeof record.tokenId !== "string" || !Number.isSafeInteger(record.startBlock) || (record.startBlock ?? -1) < 0 || !isPositiveIntegerString(record.depositAmount) || !isPositiveIntegerString(record.maxLockAmount) || !Number.isInteger(record.lockDuration) || (record.lockDuration ?? 0) <= 0 || typeof record.generation !== "string" || !record.generation || record.submissionAmbiguous !== void 0 && typeof record.submissionAmbiguous !== "boolean" || record.creditedAmount !== void 0 && !isPositiveIntegerString(record.creditedAmount)) {
3175
+ return false;
3176
+ }
3177
+ if (record.verification && (typeof record.verification.hash !== "string" || record.verification.chainId !== record.chainId || !isPositiveIntegerString(record.verification.amount) || record.verification.logIndex !== void 0 && (!Number.isInteger(record.verification.logIndex) || record.verification.logIndex < 0))) {
3178
+ return false;
3179
+ }
3180
+ if (record.verification && record.creditedAmount !== void 0) return false;
3181
+ if (record.submissionAmbiguous && record.creditedAmount === void 0) return false;
3182
+ if (!record.payload) {
3183
+ return record.creditedAmount !== void 0 && !record.submissionAmbiguous;
3184
+ }
3185
+ if (typeof record.payload.signature !== "string" || typeof record.payload.token_id !== "string" || typeof record.payload.service_address !== "string" || record.payload.signature !== record.generation || record.payload.token_id.toLowerCase() !== record.tokenId.toLowerCase() || record.payload.service_address.toLowerCase() !== record.serviceAddress.toLowerCase() || !isPositiveIntegerString(record.payload.amount)) {
3186
+ return false;
3187
+ }
3188
+ return BigInt(record.payload.amount) <= BigInt(record.maxLockAmount);
3189
+ }
3190
+ function writeSessionRecord(session) {
3191
+ const id = externalDepositSessionId(session.chainId, session.tokenId);
3192
+ return setSharedBrowserStorageItem(sessionKey(session.owner, id), JSON.stringify(session));
3193
+ }
3194
+ function saveExternalDepositLockSession(session, expectedSession = null) {
3195
+ if (!isSessionRecord(session)) throw new Error("Invalid external deposit lock session");
3196
+ const current = loadExternalDepositLockSession(session.owner);
3197
+ if (expectedSession === null && current || expectedSession !== null && (!current || !isSameSessionSnapshot(current, expectedSession))) {
3198
+ throw new ExternalDepositLockSessionChangedError();
3199
+ }
3200
+ const nextId = externalDepositSessionId(session.chainId, session.tokenId);
3201
+ const previousId = activeSessionId(session.owner);
3202
+ if (!writeSessionRecord(session)) {
3203
+ throw new Error("Unable to persist external deposit lock session");
3204
+ }
3205
+ if (previousId !== nextId) {
3206
+ if (!setSharedBrowserStorageItem(activeSessionKey(session.owner), nextId)) {
3207
+ removeSharedBrowserStorageItem(sessionKey(session.owner, nextId));
3208
+ throw new Error("Unable to persist external deposit lock session");
3209
+ }
3210
+ if (previousId) {
3211
+ removeSharedBrowserStorageItem(sessionKey(session.owner, previousId));
3212
+ }
3213
+ }
3214
+ }
3215
+ function loadExternalDepositLockSession(owner) {
3216
+ const id = activeSessionId(owner);
3217
+ if (!id) return void 0;
3218
+ const key = sessionKey(owner, id);
3219
+ try {
3220
+ const raw = getSharedBrowserStorageItem(key);
3221
+ const parsed = raw ? JSON.parse(raw) : void 0;
3222
+ if (!isSessionRecord(parsed) || parsed.owner.toLowerCase() !== owner.toLowerCase() || externalDepositSessionId(parsed.chainId, parsed.tokenId) !== id) {
3223
+ removeSharedBrowserStorageItem(key);
3224
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
3225
+ return void 0;
3226
+ }
3227
+ return parsed;
3228
+ } catch {
3229
+ removeSharedBrowserStorageItem(key);
3230
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
3231
+ return void 0;
3232
+ }
3233
+ }
3234
+ function subscribeExternalDepositLockSession(owner, onChange) {
3235
+ if (typeof window === "undefined") return () => void 0;
3236
+ let localStorage;
3237
+ try {
3238
+ localStorage = window.localStorage;
3239
+ } catch {
3240
+ return () => void 0;
3241
+ }
3242
+ const activeKey = activeSessionKey(owner);
3243
+ const ownerSessionPrefix = `${SESSION_PREFIX}${owner.toLowerCase()}:`;
3244
+ const handleStorage = (event) => {
3245
+ if (event.storageArea !== localStorage) return;
3246
+ if (event.key !== null && event.key !== activeKey && !event.key.startsWith(ownerSessionPrefix)) {
3247
+ return;
3248
+ }
3249
+ onChange(loadExternalDepositLockSession(owner));
3250
+ };
3251
+ window.addEventListener("storage", handleStorage);
3252
+ return () => window.removeEventListener("storage", handleStorage);
3253
+ }
3254
+ function clearExternalDepositLockSession(owner, expectedSessionId, expectedGeneration) {
3255
+ const id = activeSessionId(owner);
3256
+ if (!id || expectedSessionId && id !== expectedSessionId) return;
3257
+ if (expectedGeneration) {
3258
+ const current = loadExternalDepositLockSession(owner);
3259
+ if (!current || current.generation !== expectedGeneration) return;
3260
+ }
3261
+ removeSharedBrowserStorageItem(sessionKey(owner, id));
3262
+ removeSharedBrowserStorageItem(activeSessionKey(owner));
3263
+ }
3264
+ function isSameVerification(left, right) {
3265
+ if (!left || !right) return left === right;
3266
+ return left.hash.toLowerCase() === right.hash.toLowerCase() && left.chainId === right.chainId && left.amount === right.amount && (left.logIndex ?? 0) === (right.logIndex ?? 0);
3267
+ }
3268
+ function isSamePayload(left, right) {
3269
+ if (!left || !right) return left === right;
3270
+ return left.service_address.toLowerCase() === right.service_address.toLowerCase() && left.token_id.toLowerCase() === right.token_id.toLowerCase() && left.amount === right.amount && left.expiry === right.expiry && left.nonce === right.nonce && left.signature === right.signature;
3271
+ }
3272
+ function isSameSessionSnapshot(left, right) {
3273
+ return left.version === right.version && left.owner.toLowerCase() === right.owner.toLowerCase() && left.serviceAddress.toLowerCase() === right.serviceAddress.toLowerCase() && left.chainId === right.chainId && left.tokenId.toLowerCase() === right.tokenId.toLowerCase() && left.startBlock === right.startBlock && left.depositAmount === right.depositAmount && left.maxLockAmount === right.maxLockAmount && left.lockDuration === right.lockDuration && left.generation === right.generation && left.creditedAmount === right.creditedAmount && left.submissionAmbiguous === right.submissionAmbiguous && isSameVerification(left.verification, right.verification) && isSamePayload(left.payload, right.payload);
3274
+ }
3275
+ function isSameExternalDepositLockSession(left, right) {
3276
+ return isSameSessionSnapshot(left, right);
3277
+ }
3278
+ function clearExternalDepositVerification(session) {
3279
+ if (!session.verification || session.creditedAmount) return void 0;
3280
+ const current = loadExternalDepositLockSession(session.owner);
3281
+ if (!current || current.generation !== session.generation || current.creditedAmount || !isSameVerification(current.verification, session.verification)) {
3282
+ return void 0;
3283
+ }
3284
+ const next = { ...current, verification: void 0 };
3285
+ if (!writeSessionRecord(next)) {
3286
+ throw new Error("Unable to clear failed deposit verification");
3287
+ }
3288
+ return next;
3289
+ }
3290
+ function discardExternalDepositLockSession(session) {
3291
+ const current = loadExternalDepositLockSession(session.owner);
3292
+ if (!current) return true;
3293
+ if (!isSameSessionSnapshot(current, session)) return false;
3294
+ clearExternalDepositLockSession(
3295
+ current.owner,
3296
+ externalDepositSessionId(current.chainId, current.tokenId),
3297
+ current.generation
3298
+ );
3299
+ return loadExternalDepositLockSession(current.owner) === void 0;
3300
+ }
3301
+ function clearSubmittedPayload(session, signature, clearAmbiguous = false) {
3302
+ const current = loadExternalDepositLockSession(session.owner);
3303
+ if (!current?.payload || current.payload.signature !== signature || current.submissionAmbiguous && !clearAmbiguous) {
3304
+ return false;
3305
+ }
3306
+ return writeSessionRecord({ ...current, submissionAmbiguous: void 0, payload: void 0 });
3307
+ }
3308
+ function markSubmissionAmbiguous(session, signature) {
3309
+ const current = loadExternalDepositLockSession(session.owner);
3310
+ if (!current?.payload || current.payload.signature !== signature) return false;
3311
+ if (current.submissionAmbiguous) return true;
3312
+ return writeSessionRecord({ ...current, submissionAmbiguous: true });
3313
+ }
3314
+ function ambiguousSubmissionError(error) {
3315
+ return new PostDepositLockError(
3316
+ "Lock submission could not be confirmed. Retry only the saved signature, or check locked funds before stopping recovery.",
3317
+ error.reason,
3318
+ error.signedAmount,
3319
+ error.creditedAmount,
3320
+ {
3321
+ cause: error.cause,
3322
+ submissionMayHaveSucceeded: true
3323
+ }
3324
+ );
3325
+ }
3326
+ function isDefinitiveSubmissionFailure(error) {
3327
+ const cause = error.cause;
3328
+ return cause instanceof AccountingApiError && cause.statusCode !== 408 && cause.statusCode < 500;
3329
+ }
3330
+ function submissionLockName(session) {
3331
+ return `privana:external-deposit-lock:submit:${session.owner.toLowerCase()}:${externalDepositSessionId(session.chainId, session.tokenId)}`;
3332
+ }
3333
+ async function withSubmissionLock(session, task) {
3334
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
3335
+ if (!locks) return task(false);
3336
+ return locks.request(submissionLockName(session), () => task(true));
3337
+ }
3338
+ function externalDepositRetryAmount(session) {
3339
+ if (!session.creditedAmount) throw new Error("No credited external deposit to recover");
3340
+ const creditedAmount = BigInt(session.creditedAmount);
3341
+ const maxLockAmount = BigInt(session.maxLockAmount);
3342
+ return creditedAmount < maxLockAmount ? creditedAmount : maxLockAmount;
3343
+ }
3344
+ async function submitExternalDepositLock(client, session) {
3345
+ return withSubmissionLock(session, async (crossTabExclusive) => {
3346
+ const current = loadExternalDepositLockSession(session.owner);
3347
+ if (!current || current.generation !== session.generation) {
3348
+ throw new PostDepositLockError(
3349
+ "External deposit lock session changed before submission",
3350
+ "not-found",
3351
+ BigInt(session.maxLockAmount),
3352
+ session.creditedAmount ? BigInt(session.creditedAmount) : void 0
3353
+ );
3354
+ }
3355
+ const result = await submitExternalDepositLockAttempt(client, current, crossTabExclusive);
3356
+ clearExternalDepositLockSession(
3357
+ current.owner,
3358
+ externalDepositSessionId(current.chainId, current.tokenId),
3359
+ current.generation
3360
+ );
3361
+ return result;
3362
+ });
3363
+ }
3364
+ async function submitExternalDepositLockAttempt(client, session, crossTabExclusive) {
3365
+ const creditedAmount = session.creditedAmount ? BigInt(session.creditedAmount) : void 0;
3366
+ if (!creditedAmount) {
3367
+ throw new PostDepositLockError(
3368
+ "External deposit has not been credited",
3369
+ "submission-failed",
3370
+ BigInt(session.maxLockAmount)
3371
+ );
3372
+ }
3373
+ const payload = session.payload;
3374
+ if (!payload) {
3375
+ throw new PostDepositLockError(
3376
+ "No persisted signed policy found for this deposit session",
3377
+ "not-found",
3378
+ BigInt(session.maxLockAmount),
3379
+ creditedAmount
3380
+ );
3381
+ }
3382
+ try {
3383
+ return await submitPendingLock({
3384
+ client,
3385
+ payload,
3386
+ creditedAmount,
3387
+ beforeSubmit: () => {
3388
+ if (!markSubmissionAmbiguous(session, payload.signature)) {
3389
+ throw new Error("Unable to persist lock submission recovery state");
3390
+ }
3391
+ }
3392
+ });
3393
+ } catch (err) {
3394
+ if (err instanceof PostDepositLockError && err.reason === "submission-failed" && crossTabExclusive && !session.submissionAmbiguous && isDefinitiveSubmissionFailure(err)) {
3395
+ if (!clearSubmittedPayload(session, payload.signature, true)) {
3396
+ const latest = loadExternalDepositLockSession(session.owner);
3397
+ if (latest?.submissionAmbiguous) throw ambiguousSubmissionError(err);
3398
+ }
3399
+ } else if (err instanceof PostDepositLockError && err.reason === "submission-failed") {
3400
+ const ambiguous = markSubmissionAmbiguous(session, payload.signature) || loadExternalDepositLockSession(session.owner)?.submissionAmbiguous === true;
3401
+ if (ambiguous) throw ambiguousSubmissionError(err);
3402
+ } else {
3403
+ const current = loadExternalDepositLockSession(session.owner);
3404
+ if (session.submissionAmbiguous || current?.submissionAmbiguous) {
3405
+ if (err instanceof PostDepositLockError) throw ambiguousSubmissionError(err);
3406
+ throw err;
3407
+ }
3408
+ if (!clearSubmittedPayload(session, payload.signature)) {
3409
+ const latest = loadExternalDepositLockSession(session.owner);
3410
+ if (latest?.submissionAmbiguous && err instanceof PostDepositLockError) {
3411
+ throw ambiguousSubmissionError(err);
3412
+ }
3413
+ }
3414
+ }
3415
+ throw err;
3416
+ }
3417
+ }
3418
+ function useExternalDepositLock({
3419
+ allowance,
3420
+ onLockSubmitted,
3421
+ onLockFailed
3422
+ }) {
3423
+ const { client, networkConfig, serviceAddress, getTokenById, tokensStatus } = usePrivanaContext();
3424
+ const config = useConfig();
3425
+ const queryClient = useQueryClient();
3426
+ const { address } = useAccount();
3427
+ const { privateReadAddress } = usePrivateReadRequest();
3428
+ const { ensureCorrectChain } = useEnsureCorrectChain();
3429
+ const [isSigning, setIsSigning] = useState(false);
3430
+ const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3431
+ const [session, setSession] = useState(null);
3432
+ const sessionRef = useRef(null);
3433
+ const signingRef = useRef(false);
3434
+ const settlingRef = useRef(false);
3435
+ const pendingCreditedTransitionRef = useRef(null);
3436
+ const hydratedScopeRef = useRef(null);
3437
+ const beneficiaryRef = useRef(privateReadAddress);
3438
+ beneficiaryRef.current = privateReadAddress;
3439
+ const onLockSubmittedRef = useRef(onLockSubmitted);
3440
+ const onLockFailedRef = useRef(onLockFailed);
3441
+ useEffect(() => {
3442
+ onLockSubmittedRef.current = onLockSubmitted;
3443
+ onLockFailedRef.current = onLockFailed;
3444
+ }, [onLockSubmitted, onLockFailed]);
3445
+ const installSession = useCallback((next) => {
3446
+ sessionRef.current = next;
3447
+ setSession(next);
3448
+ }, []);
3449
+ const preserveCreditedFallback = useCallback(
3450
+ (durable) => {
3451
+ const pending = pendingCreditedTransitionRef.current;
3452
+ if (pending && durable && isSameExternalDepositLockSession(durable, pending.predecessor)) {
3453
+ return pending.credited;
3454
+ }
3455
+ pendingCreditedTransitionRef.current = null;
3456
+ return durable;
3457
+ },
3458
+ []
3459
+ );
3460
+ const notifyFailure = useCallback((error) => {
3461
+ onLockFailedRef.current?.(error);
3462
+ }, []);
3463
+ const submitSession = useCallback(
3464
+ async (active) => {
3465
+ if (settlingRef.current) return;
3466
+ settlingRef.current = true;
3467
+ setIsSubmittingLock(true);
3468
+ let result;
3469
+ try {
3470
+ let submission = active;
3471
+ const storedBeforeSubmit = loadExternalDepositLockSession(active.owner);
3472
+ if (storedBeforeSubmit?.generation === active.generation) {
3473
+ if (active.creditedAmount && !storedBeforeSubmit.creditedAmount) {
3474
+ const pending = pendingCreditedTransitionRef.current;
3475
+ if (!pending || !isSameExternalDepositLockSession(active, pending.credited) || !isSameExternalDepositLockSession(storedBeforeSubmit, pending.predecessor)) {
3476
+ throw new ExternalDepositLockSessionChangedError();
3477
+ }
3478
+ saveExternalDepositLockSession(pending.credited, pending.predecessor);
3479
+ pendingCreditedTransitionRef.current = null;
3480
+ submission = pending.credited;
3481
+ } else {
3482
+ submission = storedBeforeSubmit;
3483
+ }
3484
+ }
3485
+ result = await submitExternalDepositLock(client, submission);
3486
+ } catch (err) {
3487
+ const stored = loadExternalDepositLockSession(active.owner);
3488
+ const error = err instanceof PostDepositLockError ? err : new PostDepositLockError(
3489
+ err instanceof Error ? err.message : "Lock submission failed",
3490
+ "submission-failed",
3491
+ BigInt(active.maxLockAmount),
3492
+ active.creditedAmount ? BigInt(active.creditedAmount) : void 0,
3493
+ { cause: err }
3494
+ );
3495
+ if (err instanceof ExternalDepositLockSessionChangedError) {
3496
+ pendingCreditedTransitionRef.current = null;
3497
+ installSession(stored ?? null);
3498
+ return;
3499
+ }
3500
+ if (!stored || stored.generation !== active.generation) {
3501
+ pendingCreditedTransitionRef.current = null;
3502
+ installSession(stored ?? null);
3503
+ return;
3504
+ }
3505
+ if (active.creditedAmount && !stored.creditedAmount) {
3506
+ const pending = pendingCreditedTransitionRef.current;
3507
+ if (pending && isSameExternalDepositLockSession(active, pending.credited) && isSameExternalDepositLockSession(stored, pending.predecessor)) {
3508
+ installSession(pending.credited);
3509
+ notifyFailure(error);
3510
+ return;
3511
+ }
3512
+ pendingCreditedTransitionRef.current = null;
3513
+ installSession(stored);
3514
+ return;
3515
+ }
3516
+ installSession(active.creditedAmount && !stored.creditedAmount ? active : stored);
3517
+ notifyFailure(error);
3518
+ return;
3519
+ } finally {
3520
+ settlingRef.current = false;
3521
+ setIsSubmittingLock(false);
3522
+ }
3523
+ installSession(loadExternalDepositLockSession(active.owner) ?? null);
3524
+ queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
3525
+ queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
3526
+ queryClient.invalidateQueries({ queryKey: ["accounting-total-locked-balance"] });
3527
+ queryClient.invalidateQueries({ queryKey: ["accounting-history"] });
3528
+ onLockSubmittedRef.current?.(result);
3529
+ },
3530
+ [client, installSession, notifyFailure, queryClient]
3531
+ );
3532
+ const validateRestoredSession = useCallback(
3533
+ (restored) => {
3534
+ if (!restored || !serviceAddress) return null;
3535
+ const token = getTokenById(restored.tokenId);
3536
+ const invalid = restored.serviceAddress.toLowerCase() !== serviceAddress.toLowerCase() || !token || token.chainId !== restored.chainId || token.contract === zeroAddress || !restored.creditedAmount && !restored.payload;
3537
+ if (!invalid) return restored;
3538
+ clearExternalDepositLockSession(
3539
+ restored.owner,
3540
+ externalDepositSessionId(restored.chainId, restored.tokenId),
3541
+ restored.generation
3542
+ );
3543
+ return null;
3544
+ },
3545
+ [getTokenById, serviceAddress]
3546
+ );
3547
+ useEffect(() => {
3548
+ if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") {
3549
+ setSession(null);
3550
+ return;
3551
+ }
3552
+ const hydrationKey = `${privateReadAddress.toLowerCase()}:${serviceAddress.toLowerCase()}`;
3553
+ const shouldResume = hydratedScopeRef.current !== hydrationKey;
3554
+ hydratedScopeRef.current = hydrationKey;
3555
+ const restored = preserveCreditedFallback(
3556
+ validateRestoredSession(loadExternalDepositLockSession(privateReadAddress))
3557
+ );
3558
+ installSession(restored);
3559
+ if (!restored) {
3560
+ return;
3561
+ }
3562
+ if (!shouldResume) return;
3563
+ if (restored.creditedAmount && restored.payload) {
3564
+ void submitSession(restored);
3565
+ } else if (restored.creditedAmount) {
3566
+ notifyFailure(
3567
+ new PostDepositLockError(
3568
+ "Deposit credited; sign a fresh policy to lock the funds",
3569
+ "not-found",
3570
+ BigInt(restored.maxLockAmount),
3571
+ BigInt(restored.creditedAmount)
3572
+ )
3573
+ );
3574
+ }
3575
+ }, [
3576
+ installSession,
3577
+ notifyFailure,
3578
+ privateReadAddress,
3579
+ preserveCreditedFallback,
3580
+ serviceAddress,
3581
+ submitSession,
3582
+ tokensStatus,
3583
+ validateRestoredSession
3584
+ ]);
3585
+ useEffect(() => {
3586
+ if (!privateReadAddress || !serviceAddress || tokensStatus !== "ready") return;
3587
+ const reconcile = (restored) => {
3588
+ installSession(preserveCreditedFallback(validateRestoredSession(restored)));
3589
+ };
3590
+ const unsubscribe = subscribeExternalDepositLockSession(privateReadAddress, reconcile);
3591
+ reconcile(loadExternalDepositLockSession(privateReadAddress));
3592
+ return unsubscribe;
3593
+ }, [
3594
+ installSession,
3595
+ preserveCreditedFallback,
3596
+ privateReadAddress,
3597
+ serviceAddress,
3598
+ tokensStatus,
3599
+ validateRestoredSession
3600
+ ]);
3601
+ const signAndPersist = useCallback(
3602
+ async ({ tokenId, amount }) => {
3603
+ if (!allowance) throw new Error("No allowance configured for this deposit");
3604
+ if (signingRef.current || settlingRef.current) {
3605
+ throw new Error("A policy operation is already in progress");
3606
+ }
3607
+ if (!canUseSharedBrowserStorage()) {
3608
+ throw new Error("Browser storage is required for locked deposit recovery");
3609
+ }
3610
+ const owner = requireDepositLockOwner(address, privateReadAddress);
3611
+ if (sessionRef.current || loadExternalDepositLockSession(owner)) {
3612
+ throw new Error("Finish or cancel the active external deposit before starting another");
3613
+ }
3614
+ const token = getTokenById(tokenId);
3615
+ if (!token) throw new Error(`Unknown token ID: ${tokenId}`);
3616
+ if (token.contract === zeroAddress) {
3617
+ throw new Error("External deposits support ERC20 tokens only");
3618
+ }
3619
+ const targetService = requireServiceAddress(serviceAddress);
3620
+ const lockDuration = allowance.lockDuration ?? DEFAULT_LOCK_DURATION_SECONDS;
3621
+ signingRef.current = true;
3622
+ setIsSigning(true);
3623
+ try {
3624
+ await ensureCorrectChain(networkConfig.chainId);
3625
+ const signingWalletClient = await getWalletClient(config, {
3626
+ chainId: networkConfig.chainId
3627
+ });
3628
+ const payload = await createSignedLockRequest({
3629
+ client,
3630
+ walletClient: signingWalletClient,
3631
+ userAddress: owner,
3632
+ networkConfig,
3633
+ serviceAddress: targetService,
3634
+ tokenId: token.id,
3635
+ amount: clampLockAmount(amount, BigInt(allowance.value)),
3636
+ lockDuration
3637
+ });
3638
+ const startBlock = await getBlockNumber(config, { chainId: token.chainId });
3639
+ if (startBlock > BigInt(Number.MAX_SAFE_INTEGER)) {
3640
+ throw new Error("Source-chain block number exceeds the supported range");
3641
+ }
3642
+ if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
3643
+ throw new Error("Authenticated deposit account changed while signing");
3644
+ }
3645
+ const next = {
3646
+ version: 1,
3647
+ owner,
3648
+ serviceAddress: targetService,
3649
+ chainId: token.chainId,
3650
+ tokenId: token.id,
3651
+ startBlock: Number(startBlock),
3652
+ depositAmount: amount.toString(),
3653
+ maxLockAmount: String(payload.amount),
3654
+ lockDuration,
3655
+ generation: payload.signature,
3656
+ payload
3657
+ };
3658
+ saveExternalDepositLockSession(next, null);
3659
+ installSession(next);
3660
+ } finally {
3661
+ signingRef.current = false;
3662
+ setIsSigning(false);
3663
+ }
3664
+ },
3665
+ [
3666
+ address,
3667
+ allowance,
3668
+ client,
3669
+ config,
3670
+ ensureCorrectChain,
3671
+ getTokenById,
3672
+ installSession,
3673
+ networkConfig,
3674
+ privateReadAddress,
3675
+ serviceAddress
3676
+ ]
3677
+ );
3678
+ const settleAfterCredit = useCallback(
3679
+ async (txHash, creditedAmount) => {
3680
+ const active = sessionRef.current;
3681
+ if (!active || settlingRef.current) return;
3682
+ if (!isCurrentExternalDepositVerification(active, txHash)) {
3683
+ return;
3684
+ }
3685
+ if (creditedAmount <= 0n) throw new Error("Credited amount must be positive");
3686
+ const credited = {
3687
+ ...active,
3688
+ verification: void 0,
3689
+ creditedAmount: creditedAmount.toString()
3690
+ };
3691
+ try {
3692
+ saveExternalDepositLockSession(credited, active);
3693
+ pendingCreditedTransitionRef.current = null;
3694
+ } catch (err) {
3695
+ const stored = loadExternalDepositLockSession(active.owner);
3696
+ if (err instanceof ExternalDepositLockSessionChangedError || !stored || !isSameExternalDepositLockSession(stored, active)) {
3697
+ pendingCreditedTransitionRef.current = null;
3698
+ installSession(stored ?? null);
3699
+ return;
3700
+ }
3701
+ pendingCreditedTransitionRef.current = { predecessor: active, credited };
3702
+ installSession(credited);
3703
+ notifyFailure(
3704
+ new PostDepositLockError(
3705
+ err instanceof Error ? err.message : "Unable to persist credited deposit state",
3706
+ "submission-failed",
3707
+ BigInt(active.maxLockAmount),
3708
+ creditedAmount,
3709
+ { cause: err }
3710
+ )
3711
+ );
3712
+ return;
3713
+ }
3714
+ installSession(credited);
3715
+ await submitSession(credited);
3716
+ },
3717
+ [installSession, notifyFailure, submitSession]
3718
+ );
3719
+ const recordVerification = useCallback(
3720
+ (context) => {
3721
+ const active = sessionRef.current;
3722
+ if (!active || active.creditedAmount) {
3723
+ throw new Error("No external deposit session is awaiting verification");
3724
+ }
3725
+ if (active.verification) {
3726
+ const sameTransfer = active.verification.hash.toLowerCase() === context.hash.toLowerCase() && active.verification.chainId === context.chainId && active.verification.amount === context.amount.toString() && (active.verification.logIndex ?? 0) === (context.logIndex ?? 0);
3727
+ if (sameTransfer) return;
3728
+ throw new Error("Another transfer is already being verified for this deposit session");
3729
+ }
3730
+ if (context.chainId !== active.chainId || context.amount <= 0n) {
3731
+ throw new Error("Discovered transfer does not match the signed deposit session");
3732
+ }
3733
+ const current = loadExternalDepositLockSession(active.owner);
3734
+ if (!current || current.generation !== active.generation) {
3735
+ throw new Error("The signed deposit session changed before verification");
3736
+ }
3737
+ const next = {
3738
+ ...active,
3739
+ verification: {
3740
+ hash: context.hash,
3741
+ chainId: context.chainId,
3742
+ amount: context.amount.toString(),
3743
+ logIndex: context.logIndex
3744
+ }
3745
+ };
3746
+ saveExternalDepositLockSession(next, active);
3747
+ installSession(next);
3748
+ },
3749
+ [installSession]
3750
+ );
3751
+ const retryAfterCredit = useCallback(async () => {
3752
+ const active = sessionRef.current;
3753
+ if (!active?.creditedAmount) throw new Error("No credited external deposit to recover");
3754
+ if (signingRef.current || settlingRef.current) {
3755
+ throw new Error("A policy operation is already in progress");
3756
+ }
3757
+ if (privateReadAddress?.toLowerCase() !== active.owner.toLowerCase()) {
3758
+ throw new Error("Authenticated deposit account does not match the credited deposit");
3759
+ }
3760
+ const token = getTokenById(active.tokenId);
3761
+ if (!token || token.chainId !== active.chainId || token.contract === zeroAddress) {
3762
+ throw new Error("The credited token is no longer available");
3763
+ }
3764
+ const targetService = requireServiceAddress(serviceAddress);
3765
+ if (targetService.toLowerCase() !== active.serviceAddress.toLowerCase()) {
3766
+ throw new Error("The service for this credited deposit has changed");
3767
+ }
3768
+ if (active.payload) {
3769
+ await submitSession(active);
3770
+ return;
3771
+ }
3772
+ if (active.submissionAmbiguous) {
3773
+ throw new Error(
3774
+ "The previous lock submission may have succeeded; a fresh signature is unsafe"
3775
+ );
3776
+ }
3777
+ const owner = requireDepositLockOwner(address, privateReadAddress);
3778
+ const amount = externalDepositRetryAmount(active);
3779
+ let retrySession;
3780
+ signingRef.current = true;
3781
+ setIsSigning(true);
3782
+ try {
3783
+ await ensureCorrectChain(networkConfig.chainId);
3784
+ const signingWalletClient = await getWalletClient(config, {
3785
+ chainId: networkConfig.chainId
3786
+ });
3787
+ const payload = await createSignedLockRequest({
3788
+ client,
3789
+ walletClient: signingWalletClient,
3790
+ userAddress: owner,
3791
+ networkConfig,
3792
+ serviceAddress: targetService,
3793
+ tokenId: active.tokenId,
3794
+ amount,
3795
+ lockDuration: active.lockDuration
3796
+ });
3797
+ if (beneficiaryRef.current?.toLowerCase() !== owner.toLowerCase()) {
3798
+ throw new Error("Authenticated deposit account changed while signing");
3799
+ }
3800
+ retrySession = {
3801
+ ...active,
3802
+ depositAmount: amount.toString(),
3803
+ generation: payload.signature,
3804
+ verification: void 0,
3805
+ submissionAmbiguous: void 0,
3806
+ payload
3807
+ };
3808
+ saveExternalDepositLockSession(retrySession, active);
3809
+ installSession(retrySession);
3810
+ } finally {
3811
+ signingRef.current = false;
3812
+ setIsSigning(false);
3813
+ }
3814
+ await submitSession(retrySession);
3815
+ }, [
3816
+ address,
3817
+ client,
3818
+ config,
3819
+ ensureCorrectChain,
3820
+ getTokenById,
3821
+ installSession,
3822
+ networkConfig,
3823
+ privateReadAddress,
3824
+ serviceAddress,
3825
+ submitSession
3826
+ ]);
3827
+ const clearVerification = useCallback(() => {
3828
+ const active = sessionRef.current;
3829
+ if (!active?.verification || active.creditedAmount) return false;
3830
+ const next = clearExternalDepositVerification(active);
3831
+ if (!next) {
3832
+ installSession(loadExternalDepositLockSession(active.owner) ?? null);
3833
+ return false;
3834
+ }
3835
+ installSession(next);
3836
+ return true;
3837
+ }, [installSession]);
3838
+ const discardSession = useCallback(() => {
3839
+ const active = sessionRef.current;
3840
+ if (!active) return true;
3841
+ if (signingRef.current || settlingRef.current) return false;
3842
+ const pending = pendingCreditedTransitionRef.current;
3843
+ const discardTarget = pending && isSameExternalDepositLockSession(active, pending.credited) ? pending.predecessor : active;
3844
+ const discarded = discardExternalDepositLockSession(discardTarget);
3845
+ const restored = loadExternalDepositLockSession(active.owner) ?? null;
3846
+ if (discarded) pendingCreditedTransitionRef.current = null;
3847
+ installSession(restored);
3848
+ return discarded && !restored;
3849
+ }, [installSession]);
3850
+ return {
3851
+ isSigning,
3852
+ isSubmittingLock,
3853
+ session,
3854
+ signAndPersist,
3855
+ recordVerification,
3856
+ settleAfterCredit,
3857
+ retryAfterCredit,
3858
+ clearVerification,
3859
+ discardSession
3860
+ };
3861
+ }
3097
3862
  var DEFAULT_MOONPAY_API_URL = "https://api.moonpay.com/v3";
3098
3863
  function useMoonpayLimits({
3099
3864
  currencyCode,
@@ -3328,6 +4093,14 @@ function MoonPayGate({ enabled, children }) {
3328
4093
  if (!enabled || !apiKey) return /* @__PURE__ */ jsx(Fragment, { children });
3329
4094
  return /* @__PURE__ */ jsx(MoonPayProvider, { apiKey, children });
3330
4095
  }
4096
+ function formatPostDepositLockFailure(error, token) {
4097
+ if (error?.reason === "credited-below-signed" && error.creditedAmount !== void 0 && error.signedAmount !== void 0 && token) {
4098
+ const creditedAmount = `${formatUnits(error.creditedAmount, token.decimals)} ${token.symbol}`;
4099
+ const signedAmount = `${formatUnits(error.signedAmount, token.decimals)} ${token.symbol}`;
4100
+ return `Credited amount (${creditedAmount}) is below the signed lock amount (${signedAmount})`;
4101
+ }
4102
+ return error?.message ?? "Lock submission failed";
4103
+ }
3331
4104
  function MethodTabs({
3332
4105
  activeTab,
3333
4106
  onTabChange
@@ -3394,19 +4167,21 @@ function DepositView({
3394
4167
  selectedToken,
3395
4168
  amount,
3396
4169
  allowance,
4170
+ externalMinimum,
3397
4171
  onAmountChange,
3398
4172
  onSelectToken,
3399
4173
  onConnectWallet,
3400
4174
  onSubmit,
3401
4175
  isSubmitting = false
3402
4176
  }) {
3403
- const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig } = usePrivanaContext();
4177
+ const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig, hostedAuthConfig } = usePrivanaContext();
3404
4178
  const { address, isConnected } = useAccount();
3405
4179
  const appName = serviceName ?? "Privana";
3406
4180
  const chain = selectedToken ? getChainById2(selectedToken.chainId) : void 0;
3407
4181
  const targetChain = chain ?? chains[0];
3408
4182
  const sourceLabel = source === "connected" ? "Connected Wallet" : "External Wallet";
3409
4183
  const isConnectedSource = source === "connected";
4184
+ const isExternal = source === "external";
3410
4185
  const isCreditCard = source === "credit-card";
3411
4186
  const isNative = selectedToken?.contract === zeroAddress;
3412
4187
  const { data: nativeBalanceData } = useBalance$1({
@@ -3440,8 +4215,14 @@ function DepositView({
3440
4215
  const belowMoonpayMin = isCreditCard && hasValidAmount && moonpayMinBuy != null && parseFloat(amount) < moonpayMinBuy;
3441
4216
  const moonpayLimitsUnready = isCreditCard && !!selectedToken?.moonpayCurrencyCode && moonpayMinBuy == null;
3442
4217
  const creditCardUnavailable = isCreditCard && !!selectedToken && !selectedToken.moonpayCurrencyCode;
3443
- const needsConnect = isConnectedSource && !isConnected;
3444
- const canDeposit = hasValidAmount && !!selectedToken && !tooManyDecimals && !exceedsBalance && !belowMoonpayMin && !moonpayLimitsUnready && !creditCardUnavailable && !needsConnect;
4218
+ const externalTokenUnavailable = isExternal && !!selectedToken && isNative;
4219
+ const externalMinimumRequired = isExternal && !!allowance && !!selectedToken;
4220
+ const externalMinimumLoading = externalMinimumRequired && externalMinimum === void 0;
4221
+ const externalMinimumUnavailable = externalMinimumRequired && externalMinimum === null;
4222
+ const belowExternalMinimum = externalMinimumRequired && hasValidAmount && !tooManyDecimals && !!selectedToken && typeof externalMinimum === "bigint" && parseTokenAmount(amount, selectedToken.decimals) < externalMinimum;
4223
+ const externalNeedsWallet = isExternal && (!!allowance || !hostedAuthConfig);
4224
+ const needsConnect = (isConnectedSource || externalNeedsWallet) && !isConnected;
4225
+ const canDeposit = hasValidAmount && !!selectedToken && !tooManyDecimals && !exceedsBalance && !belowMoonpayMin && !moonpayLimitsUnready && !creditCardUnavailable && !externalTokenUnavailable && !externalMinimumLoading && !externalMinimumUnavailable && !belowExternalMinimum && !needsConnect;
3445
4226
  const handleMax = () => {
3446
4227
  const max = formattedWalletBalance.replace(/\s/g, "");
3447
4228
  if (parseFloat(max) > 0) onAmountChange(max);
@@ -3450,9 +4231,12 @@ function DepositView({
3450
4231
  isCreditCard ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3451
4232
  /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Buy with credit card and deposit" }),
3452
4233
  /* @__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
4234
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
4235
+ /* @__PURE__ */ jsxs("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: [
4236
+ "Deposit from ",
4237
+ sourceLabel
4238
+ ] }),
4239
+ allowance && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Enter your deposit amount and proceed to sign a policy." })
3456
4240
  ] }),
3457
4241
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3458
4242
  /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
@@ -3461,7 +4245,8 @@ function DepositView({
3461
4245
  {
3462
4246
  type: "button",
3463
4247
  onClick: onSelectToken,
3464
- className: "border-border bg-input flex w-full cursor-pointer items-center gap-3 rounded-lg border p-3 text-left",
4248
+ disabled: isSubmitting,
4249
+ 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
4250
  children: [
3466
4251
  selectedToken ? /* @__PURE__ */ jsxs(Fragment, { children: [
3467
4252
  /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
@@ -3502,6 +4287,7 @@ function DepositView({
3502
4287
  type: "text",
3503
4288
  inputMode: "decimal",
3504
4289
  placeholder: "Enter Amount",
4290
+ disabled: isSubmitting,
3505
4291
  value: amount,
3506
4292
  onChange: (e) => {
3507
4293
  const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
@@ -3533,11 +4319,24 @@ function DepositView({
3533
4319
  moonpayMinBuy,
3534
4320
  " USD."
3535
4321
  ] }),
4322
+ belowExternalMinimum && selectedToken && typeof externalMinimum === "bigint" && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
4323
+ "Minimum deposit is ",
4324
+ formatUnits(externalMinimum, selectedToken.decimals),
4325
+ " ",
4326
+ selectedToken.symbol,
4327
+ "."
4328
+ ] }),
4329
+ externalMinimumLoading && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Checking minimum deposit\u2026" }),
4330
+ externalMinimumUnavailable && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Couldn\u2019t load the minimum deposit. Please try again." }),
3536
4331
  isCreditCard && moonpayLimitsError && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Couldn\u2019t load purchase limits. Please try again." }),
3537
4332
  isCreditCard && moonpayLimitsLoading && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Checking purchase limits\u2026" }),
3538
4333
  creditCardUnavailable && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
3539
4334
  selectedToken?.symbol ?? "This token",
3540
4335
  " isn\u2019t available for card purchases yet."
4336
+ ] }),
4337
+ externalTokenUnavailable && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
4338
+ selectedToken?.symbol ?? "This token",
4339
+ " can\u2019t be used for external deposits. Choose an ERC20 token."
3541
4340
  ] })
3542
4341
  ] }),
3543
4342
  allowance && /* @__PURE__ */ jsx(
@@ -3612,19 +4411,33 @@ function AwaitingDepositStatus({
3612
4411
  function ExternalDepositView({
3613
4412
  token,
3614
4413
  amount,
3615
- onCredited
4414
+ depositAddressState,
4415
+ onCredited,
4416
+ onDiscardLock,
4417
+ lock
3616
4418
  }) {
3617
- const { getChainById: getChainById2 } = usePrivanaContext();
3618
- const { depositAddress, isReady, isLoading } = useDepositAddress();
4419
+ const { getChainById: getChainById2, getTokenById, serviceName } = usePrivanaContext();
4420
+ const appName = serviceName ?? "Privana";
4421
+ const { depositAddress, isReady, isLoading } = depositAddressState;
3619
4422
  const chain = token ? getChainById2(token.chainId) : void 0;
4423
+ const lockSession = lock?.session;
4424
+ const recordVerification = lock?.recordVerification;
4425
+ const settleAfterCredit = lock?.settleAfterCredit;
4426
+ const retryAfterCredit = lock?.retryAfterCredit;
4427
+ const discoveryToken = lockSession ? getTokenById(lockSession.tokenId) : token;
3620
4428
  const [copied, setCopied] = useState(false);
3621
4429
  const [deadline] = useState(() => Date.now() + LISTENING_WINDOW_MS);
3622
4430
  const secondsLeft = useCountdown(deadline);
3623
4431
  const listening = secondsLeft > 0;
3624
4432
  const [nothingFound, setNothingFound] = useState(false);
3625
4433
  const [credited, setCredited] = useState(null);
4434
+ const [showCancelWarning, setShowCancelWarning] = useState(false);
3626
4435
  const verification = useDepositVerification({
3627
- onCredited: (txHash) => {
4436
+ onCredited: (txHash, _response, creditedAmount) => {
4437
+ if (settleAfterCredit) {
4438
+ void settleAfterCredit(txHash, creditedAmount);
4439
+ return;
4440
+ }
3628
4441
  if (onCredited) {
3629
4442
  onCredited();
3630
4443
  return;
@@ -3632,8 +4445,9 @@ function ExternalDepositView({
3632
4445
  setCredited({ txHash });
3633
4446
  }
3634
4447
  });
3635
- const { isVerifying, verificationFailed, didTimeout, verify } = verification;
3636
- const scanPaused = isVerifying || verificationFailed || didTimeout || !!credited;
4448
+ const { isVerifying, verificationFailed, canCheckAnotherTransfer, didTimeout, verify } = verification;
4449
+ const scanPaused = isVerifying || verificationFailed || didTimeout || !!credited || !!lock?.isSigning || !!lock?.isSubmittingLock || !!lockSession?.verification || !!lockSession?.creditedAmount;
4450
+ const showDepositInstructions = !credited && !lockSession?.creditedAmount;
3637
4451
  const {
3638
4452
  pending,
3639
4453
  isFetching: isScanning,
@@ -3642,23 +4456,58 @@ function ExternalDepositView({
3642
4456
  isUnavailable,
3643
4457
  refetch: refetchPendingDeposits
3644
4458
  } = usePendingDeposits({
3645
- chainId: token?.chainId,
3646
- enabled: isReady && !!depositAddress && !!token,
4459
+ chainId: discoveryToken?.chainId,
4460
+ tokenAddress: discoveryToken?.contract === zeroAddress ? void 0 : discoveryToken?.contract,
4461
+ enabled: isReady && !!depositAddress && !!discoveryToken && discoveryToken.contract !== zeroAddress,
3647
4462
  refetchInterval: listening && !scanPaused ? 3e4 : false
3648
4463
  });
3649
4464
  const processedRef = useRef(/* @__PURE__ */ new Set());
4465
+ const verificationRunRef = useRef(null);
4466
+ useEffect(
4467
+ () => () => {
4468
+ verificationRunRef.current = null;
4469
+ },
4470
+ []
4471
+ );
4472
+ useEffect(() => {
4473
+ const stored = lockSession?.verification;
4474
+ if (!stored || lockSession.creditedAmount || isVerifying || verificationFailed || didTimeout) {
4475
+ return;
4476
+ }
4477
+ const key = `${lockSession.generation}:${stored.chainId}:${stored.hash}:${stored.logIndex ?? 0}`;
4478
+ if (verificationRunRef.current === key) return;
4479
+ verificationRunRef.current = key;
4480
+ processedRef.current.add(`${stored.hash}:${stored.logIndex ?? 0}`);
4481
+ void verify({
4482
+ hash: stored.hash,
4483
+ chainId: stored.chainId,
4484
+ amount: BigInt(stored.amount),
4485
+ logIndex: stored.logIndex
4486
+ });
4487
+ }, [didTimeout, isVerifying, lockSession, verificationFailed, verify]);
3650
4488
  useEffect(() => {
3651
4489
  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}`));
4490
+ const next = [...pending].sort((a, b) => a.block_number - b.block_number).find(
4491
+ (deposit) => (!lockSession || isExternalDepositBlockInSession(lockSession, deposit.block_number)) && !processedRef.current.has(`${deposit.tx_hash}:${deposit.log_index}`)
4492
+ );
3653
4493
  if (!next) return;
3654
- processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
3655
- void verify({
4494
+ const context = {
3656
4495
  hash: next.tx_hash,
3657
4496
  chainId: next.chain_id,
3658
4497
  amount: BigInt(next.amount),
3659
4498
  logIndex: next.log_index
3660
- });
3661
- }, [pending, scanPaused, verify]);
4499
+ };
4500
+ if (recordVerification) {
4501
+ try {
4502
+ recordVerification(context);
4503
+ } catch (err) {
4504
+ toast.error(err instanceof Error ? err.message : "Unable to save deposit recovery state");
4505
+ return;
4506
+ }
4507
+ }
4508
+ processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
4509
+ if (!recordVerification) void verify(context);
4510
+ }, [lockSession, pending, recordVerification, scanPaused, verify]);
3662
4511
  const nothingFoundTimerRef = useRef(void 0);
3663
4512
  useEffect(() => () => clearTimeout(nothingFoundTimerRef.current), []);
3664
4513
  const handleRefresh = () => {
@@ -3671,11 +4520,27 @@ function ExternalDepositView({
3671
4520
  }
3672
4521
  });
3673
4522
  };
3674
- const handleCopy = () => {
4523
+ const handleCopy = async () => {
3675
4524
  if (!depositAddress) return;
3676
- void navigator.clipboard.writeText(depositAddress);
3677
- setCopied(true);
3678
- setTimeout(() => setCopied(false), 2e3);
4525
+ try {
4526
+ await navigator.clipboard.writeText(depositAddress);
4527
+ setCopied(true);
4528
+ setTimeout(() => setCopied(false), 2e3);
4529
+ } catch {
4530
+ toast.error("Unable to copy the deposit address");
4531
+ }
4532
+ };
4533
+ const handleCheckAnotherTransfer = () => {
4534
+ try {
4535
+ if (!lock?.clearVerification()) {
4536
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
4537
+ return;
4538
+ }
4539
+ verificationRunRef.current = null;
4540
+ verification.reset();
4541
+ } catch (err) {
4542
+ toast.error(err instanceof Error ? err.message : "Unable to continue deposit discovery");
4543
+ }
3679
4544
  };
3680
4545
  return /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
3681
4546
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
@@ -3689,27 +4554,64 @@ function ExternalDepositView({
3689
4554
  /* @__PURE__ */ jsx("div", { className: "bg-border h-px w-full" }),
3690
4555
  /* @__PURE__ */ jsx(SummaryRow, { value: amount || "\u2014", label: "Value" })
3691
4556
  ] }),
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
- )
4557
+ showDepositInstructions && /* @__PURE__ */ jsxs(Fragment, { children: [
4558
+ !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." }),
4559
+ /* @__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" }) }),
4560
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
4561
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Deposit Address" }),
4562
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4563
+ /* @__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" }) }),
4564
+ /* @__PURE__ */ jsxs(
4565
+ "button",
4566
+ {
4567
+ type: "button",
4568
+ onClick: handleCopy,
4569
+ disabled: !depositAddress,
4570
+ 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",
4571
+ children: [
4572
+ /* @__PURE__ */ jsx(CopyIcon, {}),
4573
+ copied ? "Copied" : "Copy"
4574
+ ]
4575
+ }
4576
+ )
4577
+ ] })
3710
4578
  ] })
3711
4579
  ] }),
3712
- credited ? /* @__PURE__ */ jsx(
4580
+ showCancelWarning && lockSession && !lockSession.verification && !lockSession.creditedAmount ? /* @__PURE__ */ jsx(
4581
+ TransactionWarningView,
4582
+ {
4583
+ title: "Cancel this deposit?",
4584
+ 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.",
4585
+ onDone: () => onDiscardLock?.(),
4586
+ actionLabel: "I haven\u2019t sent funds",
4587
+ onDismiss: () => setShowCancelWarning(false),
4588
+ dismissLabel: "Keep waiting"
4589
+ }
4590
+ ) : lock?.isSigning ? /* @__PURE__ */ jsx(
4591
+ AwaitingDepositStatus,
4592
+ {
4593
+ title: "Confirm policy in your wallet",
4594
+ subtitle: "Sign a fresh policy for the funds already credited."
4595
+ }
4596
+ ) : lock?.isSubmittingLock ? /* @__PURE__ */ jsx(
4597
+ AwaitingDepositStatus,
4598
+ {
4599
+ title: `Locking funds for ${appName}`,
4600
+ subtitle: "Deposit credited. Applying your signed policy\u2026"
4601
+ }
4602
+ ) : lock?.session?.creditedAmount ? /* @__PURE__ */ jsx(
4603
+ TransactionWarningView,
4604
+ {
4605
+ title: lock.session.submissionAmbiguous ? "Lock status unknown" : "Deposit credited",
4606
+ 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.",
4607
+ onDone: () => {
4608
+ void retryAfterCredit?.().catch((err) => {
4609
+ toast.error(err instanceof Error ? err.message : "Policy recovery failed");
4610
+ });
4611
+ },
4612
+ actionLabel: lock.session.payload ? "Retry lock submission" : "Sign policy and lock funds"
4613
+ }
4614
+ ) : credited ? /* @__PURE__ */ jsx(
3713
4615
  TransactionSuccessView,
3714
4616
  {
3715
4617
  title: "Deposit Credited",
@@ -3726,14 +4628,15 @@ function ExternalDepositView({
3726
4628
  title: "Could not verify deposit",
3727
4629
  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
4630
  onRetry: () => void verification.retryVerification(),
3729
- onDismiss: () => verification.reset(),
4631
+ onDismiss: lock ? canCheckAnotherTransfer ? handleCheckAnotherTransfer : void 0 : () => verification.reset(),
4632
+ dismissLabel: lock && canCheckAnotherTransfer ? "Check another transfer" : void 0,
3730
4633
  isRetrying: isVerifying
3731
4634
  }
3732
4635
  ) : isVerifying ? /* @__PURE__ */ jsx(
3733
4636
  AwaitingDepositStatus,
3734
4637
  {
3735
4638
  title: "Deposit detected",
3736
- subtitle: "Crediting the incoming transaction to your Privana balance..."
4639
+ subtitle: "Crediting the incoming transaction to your Privana balance\u2026"
3737
4640
  }
3738
4641
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
3739
4642
  /* @__PURE__ */ jsx(
@@ -3755,7 +4658,16 @@ function ExternalDepositView({
3755
4658
  children: "Refresh deposit status"
3756
4659
  }
3757
4660
  ),
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
4661
+ lockSession && !lockSession.verification && !lockSession.creditedAmount && /* @__PURE__ */ jsx(
4662
+ "button",
4663
+ {
4664
+ type: "button",
4665
+ onClick: () => setShowCancelWarning(true),
4666
+ 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",
4667
+ children: "Cancel deposit"
4668
+ }
4669
+ ),
4670
+ 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
4671
  ] })
3760
4672
  ] })
3761
4673
  ] });
@@ -3774,7 +4686,7 @@ function DepositModalContent({
3774
4686
  onCloseBlockedChange,
3775
4687
  onExit
3776
4688
  }) {
3777
- const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2 } = usePrivanaContext();
4689
+ const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2, getTokenById } = usePrivanaContext();
3778
4690
  const { address } = useAccount();
3779
4691
  const appName = serviceName ?? "Privana";
3780
4692
  const [activeTab, setActiveTab] = useState(defaultTab);
@@ -3783,6 +4695,10 @@ function DepositModalContent({
3783
4695
  const [selectedTokenId, setSelectedTokenId] = useState(defaultToken?.id ?? "");
3784
4696
  const [amount, setAmount] = useState("");
3785
4697
  const selectedToken = enabledTokens.find((t) => t.id === selectedTokenId) ?? defaultToken;
4698
+ const externalDepositAddress = useDepositAddress({
4699
+ enabled: source === "external" && (view === "deposit" || view === "external-deposit")
4700
+ });
4701
+ const externalMinimum = source !== "external" || !selectedToken ? void 0 : externalDepositAddress.isError ? null : externalDepositAddress.response ? getExternalDepositMinimum(externalDepositAddress.response, selectedToken.chainId) ?? null : void 0;
3786
4702
  const prevAddressRef = useRef(address);
3787
4703
  useEffect(() => {
3788
4704
  const prev = prevAddressRef.current;
@@ -3802,9 +4718,10 @@ function DepositModalContent({
3802
4718
  const [showTimeout, setShowTimeout] = useState(false);
3803
4719
  const [cancelled, setCancelled] = useState(false);
3804
4720
  const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3805
- const [lockFailedMessage, setLockFailedMessage] = useState(null);
4721
+ const [lockFailure, setLockFailure] = useState(null);
3806
4722
  const finishDeposit = () => {
3807
4723
  setAmount("");
4724
+ if (view === "external-deposit") setView("deposit");
3808
4725
  if (onDepositSuccess) {
3809
4726
  resetDeposit();
3810
4727
  onDepositSuccess();
@@ -3839,6 +4756,7 @@ function DepositModalContent({
3839
4756
  },
3840
4757
  onLockSubmitted: () => {
3841
4758
  setIsSubmittingLock(false);
4759
+ setLockFailure(null);
3842
4760
  finishDeposit();
3843
4761
  },
3844
4762
  // The deposit credited; only the policy lock failed. Success must not
@@ -3846,7 +4764,7 @@ function DepositModalContent({
3846
4764
  // error view and let the host re-prompt for a fresh lock.
3847
4765
  onLockFailed: (err) => {
3848
4766
  setIsSubmittingLock(false);
3849
- setLockFailedMessage(err.message);
4767
+ setLockFailure(err);
3850
4768
  onLockFailed?.(err);
3851
4769
  },
3852
4770
  onCheckTimeout: () => {
@@ -3854,7 +4772,37 @@ function DepositModalContent({
3854
4772
  setShowTimeout(true);
3855
4773
  }
3856
4774
  });
3857
- const isUnsafeToClose = (isGettingAddress || isSendingTransaction) && !cancelled;
4775
+ const externalLock = useExternalDepositLock({
4776
+ allowance,
4777
+ onLockSubmitted: () => {
4778
+ setLockFailure(null);
4779
+ finishDeposit();
4780
+ },
4781
+ // Same contract as the connected flow: the deposit credited, only the
4782
+ // lock failed — never report success, show the lock-error view instead.
4783
+ onLockFailed: (err) => {
4784
+ setLockFailure(err);
4785
+ onLockFailed?.(err);
4786
+ }
4787
+ });
4788
+ useEffect(() => {
4789
+ const restored = externalLock.session;
4790
+ if (!restored) return;
4791
+ const canRestore = view === "method" || source === "external" && (view === "deposit" || view === "external-deposit");
4792
+ if (!canRestore) return;
4793
+ const token = getTokenById(restored.tokenId);
4794
+ if (!token) return;
4795
+ setSource("external");
4796
+ setSelectedTokenId(token.id);
4797
+ setAmount(formatUnits(BigInt(restored.depositAmount), token.decimals));
4798
+ setView("external-deposit");
4799
+ }, [externalLock.session, getTokenById, source, view]);
4800
+ useEffect(() => {
4801
+ if (view === "external-deposit" && allowance && !externalLock.session) {
4802
+ setView("deposit");
4803
+ }
4804
+ }, [allowance, externalLock.session, view]);
4805
+ const isUnsafeToClose = (isGettingAddress || isSendingTransaction) && !cancelled || externalLock.isSigning || externalLock.isSubmittingLock;
3858
4806
  useEffect(() => {
3859
4807
  onCloseBlockedChange?.(isUnsafeToClose);
3860
4808
  }, [isUnsafeToClose, onCloseBlockedChange]);
@@ -3867,7 +4815,33 @@ function DepositModalContent({
3867
4815
  }, [depositError, verificationFailed]);
3868
4816
  const handleSubmit = (args) => {
3869
4817
  if (args.source === "external") {
3870
- setView("external-deposit");
4818
+ const token2 = enabledTokens.find((candidate) => candidate.id === args.tokenId);
4819
+ if (!token2) return;
4820
+ if (token2.contract === zeroAddress) {
4821
+ toast.error("External deposits support ERC20 tokens only");
4822
+ return;
4823
+ }
4824
+ const depositAmount = parseTokenAmount(args.amount, token2.decimals);
4825
+ if (!allowance) {
4826
+ setView("external-deposit");
4827
+ return;
4828
+ }
4829
+ if (typeof externalMinimum !== "bigint") {
4830
+ toast.error("Minimum deposit is unavailable. Please try again.");
4831
+ return;
4832
+ }
4833
+ if (depositAmount < externalMinimum) {
4834
+ toast.error(
4835
+ `Minimum deposit is ${formatUnits(externalMinimum, token2.decimals)} ${token2.symbol}`
4836
+ );
4837
+ return;
4838
+ }
4839
+ externalLock.signAndPersist({
4840
+ tokenId: token2.id,
4841
+ amount: depositAmount
4842
+ }).then(() => setView("external-deposit")).catch((err) => {
4843
+ toast.error(err instanceof Error ? err.message : "Policy signing failed");
4844
+ });
3871
4845
  return;
3872
4846
  }
3873
4847
  if (args.source === "credit-card") {
@@ -3919,7 +4893,14 @@ function DepositModalContent({
3919
4893
  }
3920
4894
  ] : []
3921
4895
  ];
3922
- const flowView = showSuccess ? "deposit-success" : lockFailedMessage ? "lock-error" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled || isSubmittingLock ? "depositing" : null;
4896
+ const hasExternalLockRecovery = view === "external-deposit" && !!externalLock.session?.creditedAmount;
4897
+ const externalSubmissionAmbiguous = hasExternalLockRecovery && !!lockFailure?.submissionMayHaveSucceeded;
4898
+ const savedExternalPayloadUsable = !!externalLock.session?.payload && isSignedLockUsable(externalLock.session.payload);
4899
+ const ambiguousRecoveryMustBeAbandoned = externalSubmissionAmbiguous && !savedExternalPayloadUsable;
4900
+ const externalRecoveryExitLabel = externalSubmissionAmbiguous ? "Stop recovery" : "Keep funds unlocked";
4901
+ const lockFailureToken = hasExternalLockRecovery && externalLock.session ? getTokenById(externalLock.session.tokenId) ?? selectedToken : selectedToken;
4902
+ const lockFailureMessage = formatPostDepositLockFailure(lockFailure, lockFailureToken);
4903
+ const flowView = showSuccess ? "deposit-success" : lockFailure ? "lock-error" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled || isSubmittingLock ? "depositing" : null;
3923
4904
  const activeView = flowView ?? view;
3924
4905
  const handleDepositDone = () => {
3925
4906
  setShowSuccess(false);
@@ -3932,10 +4913,28 @@ function DepositModalContent({
3932
4913
  resetDeposit();
3933
4914
  };
3934
4915
  const handleLockFailedDone = () => {
3935
- setLockFailedMessage(null);
4916
+ if (hasExternalLockRecovery && !externalLock.discardSession()) {
4917
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
4918
+ return;
4919
+ }
4920
+ setLockFailure(null);
3936
4921
  setAmount("");
3937
4922
  setCancelled(false);
3938
4923
  resetDeposit();
4924
+ if (view === "external-deposit") setView("deposit");
4925
+ };
4926
+ const handleExternalLockRetry = () => {
4927
+ externalLock.retryAfterCredit().catch((err) => {
4928
+ toast.error(err instanceof Error ? err.message : "Policy signing failed");
4929
+ });
4930
+ };
4931
+ const handleExternalVerificationDiscard = () => {
4932
+ if (!externalLock.discardSession()) {
4933
+ toast.error("Deposit recovery changed in another tab. Please review the current state.");
4934
+ return;
4935
+ }
4936
+ setAmount("");
4937
+ setView("deposit");
3939
4938
  };
3940
4939
  const handleDismissVerificationError = () => {
3941
4940
  setAmount("");
@@ -3948,6 +4947,10 @@ function DepositModalContent({
3948
4947
  };
3949
4948
  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
4949
  const goBack = () => {
4950
+ if (view === "external-deposit" && externalLock.session) {
4951
+ toast.error("Cancel the active deposit from the address screen before going back");
4952
+ return;
4953
+ }
3951
4954
  if (back.to === "method") {
3952
4955
  setAmount("");
3953
4956
  setSelectedTokenId("");
@@ -3993,9 +4996,14 @@ function DepositModalContent({
3993
4996
  activeView === "lock-error" && /* @__PURE__ */ jsx(
3994
4997
  TransactionWarningView,
3995
4998
  {
3996
- title: "Deposit credited, lock failed",
3997
- message: `Your deposit was credited but locking the funds for ${appName} failed: ${lockFailedMessage}`,
3998
- onDone: handleLockFailedDone
4999
+ title: externalSubmissionAmbiguous ? "Deposit credited, lock status unknown" : "Deposit credited, lock failed",
5000
+ 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}`,
5001
+ onDone: hasExternalLockRecovery && !ambiguousRecoveryMustBeAbandoned ? handleExternalLockRetry : handleLockFailedDone,
5002
+ actionLabel: hasExternalLockRecovery ? ambiguousRecoveryMustBeAbandoned ? externalRecoveryExitLabel : externalLock.session?.payload ? "Retry lock submission" : "Sign policy and lock funds" : void 0,
5003
+ pendingLabel: externalLock.isSigning ? "Confirm policy in wallet\u2026" : "Locking funds\u2026",
5004
+ isPending: externalLock.isSigning || externalLock.isSubmittingLock,
5005
+ onDismiss: hasExternalLockRecovery && !ambiguousRecoveryMustBeAbandoned ? handleLockFailedDone : void 0,
5006
+ dismissLabel: externalRecoveryExitLabel
3999
5007
  }
4000
5008
  ),
4001
5009
  activeView === "deposit-timeout" && /* @__PURE__ */ jsx(
@@ -4010,7 +5018,7 @@ function DepositModalContent({
4010
5018
  TransactionErrorView,
4011
5019
  {
4012
5020
  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.",
5021
+ 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
5022
  explorerUrl: explorerTxUrl,
4015
5023
  explorerLabel: "View transaction",
4016
5024
  onRetry: handleRetryVerification,
@@ -4020,7 +5028,7 @@ function DepositModalContent({
4020
5028
  activeView === "depositing" && /* @__PURE__ */ jsx(
4021
5029
  TransactionProgressView,
4022
5030
  {
4023
- title: "Depositing...",
5031
+ title: "Depositing\u2026",
4024
5032
  steps: depositSteps,
4025
5033
  onCancel: isGettingAddress || isSendingTransaction ? handleDepositCancel : void 0
4026
5034
  }
@@ -4074,11 +5082,12 @@ function DepositModalContent({
4074
5082
  selectedToken,
4075
5083
  amount,
4076
5084
  allowance,
5085
+ externalMinimum,
4077
5086
  onAmountChange: setAmount,
4078
5087
  onSelectToken: () => setView("select-token"),
4079
5088
  onConnectWallet,
4080
5089
  onSubmit: handleSubmit,
4081
- isSubmitting: isPending
5090
+ isSubmitting: isPending || externalLock.isSigning || externalLock.isSubmittingLock
4082
5091
  }
4083
5092
  ) }),
4084
5093
  activeView === "select-token" && /* @__PURE__ */ jsx(
@@ -4091,12 +5100,15 @@ function DepositModalContent({
4091
5100
  }
4092
5101
  }
4093
5102
  ),
4094
- activeView === "external-deposit" && /* @__PURE__ */ jsx(
5103
+ activeView === "external-deposit" && (!allowance || externalLock.session) && /* @__PURE__ */ jsx(
4095
5104
  ExternalDepositView,
4096
5105
  {
4097
5106
  token: selectedToken,
4098
5107
  amount,
4099
- onCredited: allowance ? void 0 : onDepositSuccess
5108
+ depositAddressState: externalDepositAddress,
5109
+ onCredited: allowance ? void 0 : onDepositSuccess,
5110
+ onDiscardLock: handleExternalVerificationDiscard,
5111
+ lock: externalLock.session ? externalLock : void 0
4100
5112
  }
4101
5113
  ),
4102
5114
  activeView === "credit-card-widget" && /* @__PURE__ */ jsx(MoonPayGate, { enabled: true, children: /* @__PURE__ */ jsx(
@@ -4108,7 +5120,7 @@ function DepositModalContent({
4108
5120
  onCredited: allowance ? void 0 : finishCardPurchase,
4109
5121
  onLockSubmitted: finishCardPurchase,
4110
5122
  onLockFailed: (err) => {
4111
- setLockFailedMessage(err.message);
5123
+ setLockFailure(err);
4112
5124
  onLockFailed?.(err);
4113
5125
  }
4114
5126
  }
@@ -4868,7 +5880,7 @@ function WalletModalContent({
4868
5880
  onAmountChange: setAmount,
4869
5881
  onFundSession: onFundSession ? fundSession : void 0,
4870
5882
  onAddFunds: () => setView("deposit"),
4871
- onWithdraw: handleWithdraw
5883
+ onWithdraw: session && !onEndSession ? void 0 : handleWithdraw
4872
5884
  }
4873
5885
  ),
4874
5886
  view === "funding-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(