@oasisprotocol/privana-sdk 0.5.1 → 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, 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-SAJ7K5WT.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-SAJ7K5WT.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);
@@ -479,8 +483,12 @@ function useDeposit(options = {}) {
479
483
  try {
480
484
  let confirmed = false;
481
485
  try {
482
- await getTransactionReceipt(config, { hash, chainId: persisted.chainId });
483
- confirmed = true;
486
+ const receipt = await getTransactionReceipt(config, {
487
+ hash,
488
+ chainId: persisted.chainId
489
+ });
490
+ const blockNumber = await getBlockNumber(config, { chainId: persisted.chainId });
491
+ confirmed = blockNumber - receipt.blockNumber + 1n >= BigInt(confirmations);
484
492
  } catch {
485
493
  }
486
494
  if (!confirmed) {
@@ -541,6 +549,7 @@ function useDeposit(options = {}) {
541
549
  const lockAmount = clampLockAmount(params.amount, params.postDepositLock?.maxAmount);
542
550
  let signedLock;
543
551
  if (params.postDepositLock) {
552
+ const lockOwner = requireDepositLockOwner(address, privateReadAddress);
544
553
  setIsSwitchingChain(true);
545
554
  try {
546
555
  await ensureCorrectChain(networkConfig.chainId);
@@ -554,7 +563,7 @@ function useDeposit(options = {}) {
554
563
  signedLock = await createSignedLockRequest({
555
564
  client,
556
565
  walletClient: signingWalletClient,
557
- userAddress: address,
566
+ userAddress: lockOwner,
558
567
  networkConfig,
559
568
  serviceAddress: requireServiceAddress(
560
569
  params.postDepositLock.serviceAddress ?? serviceAddress
@@ -563,6 +572,9 @@ function useDeposit(options = {}) {
563
572
  amount: lockAmount,
564
573
  lockDuration: params.postDepositLock.lockDuration
565
574
  });
575
+ if (privateReadAddressRef.current?.toLowerCase() !== lockOwner.toLowerCase()) {
576
+ throw new Error("Authenticated deposit account changed while signing");
577
+ }
566
578
  }
567
579
  if (isStale()) return;
568
580
  setIsSwitchingChain(true);
@@ -645,6 +657,7 @@ function useDeposit(options = {}) {
645
657
  enabledTokens,
646
658
  ensureCorrectChain,
647
659
  networkConfig,
660
+ privateReadAddress,
648
661
  queryClient,
649
662
  serviceAddress,
650
663
  writeContractAsync,
@@ -681,12 +694,6 @@ function useDeposit(options = {}) {
681
694
  reset
682
695
  };
683
696
  }
684
- function requireServiceAddress(serviceAddress) {
685
- if (!serviceAddress) {
686
- throw new Error("Service address not configured");
687
- }
688
- return serviceAddress;
689
- }
690
697
  function useDepositAddress(options = {}) {
691
698
  const queryClient = useContext(QueryClientContext);
692
699
  const accountingContext = useSafePrivanaContext();
@@ -699,11 +706,12 @@ function useDepositAddress(options = {}) {
699
706
  queryFn: async () => {
700
707
  if (!privateReadAddress) throw new Error("No authenticated account available");
701
708
  if (!client) throw new Error("No accounting client");
702
- return executePrivateRead(() => client.getDepositAddress());
709
+ return executePrivateRead((readClient) => readClient.getDepositAddress());
703
710
  },
704
711
  enabled: isReady && (options.enabled ?? true)
705
712
  });
706
713
  return {
714
+ response: query.data,
707
715
  depositAddress: query.data?.deposit_address ?? null,
708
716
  isReady,
709
717
  isLoading: query.isLoading,
@@ -1154,7 +1162,7 @@ function useLockedFunds(options = {}) {
1154
1162
  queryKey: ["accounting-locked-funds", ...privateReadQueryScope, serviceAddress ?? null],
1155
1163
  queryFn: async () => {
1156
1164
  if (!privateReadAddress) throw new Error("No authenticated account available");
1157
- return executePrivateRead(() => client.getLockedFunds(serviceAddress));
1165
+ return executePrivateRead((readClient) => readClient.getLockedFunds(serviceAddress));
1158
1166
  },
1159
1167
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1160
1168
  refetchInterval: pollingInterval,
@@ -1179,7 +1187,7 @@ function useTotalLockedBalance(options = {}) {
1179
1187
  queryFn: async () => {
1180
1188
  if (!privateReadAddress) throw new Error("No authenticated account available");
1181
1189
  if (!tokenId) throw new Error("No token ID provided");
1182
- return executePrivateRead(() => client.getTotalLockedBalance(tokenId));
1190
+ return executePrivateRead((readClient) => readClient.getTotalLockedBalance(tokenId));
1183
1191
  },
1184
1192
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!tokenId && !!client,
1185
1193
  refetchInterval: pollingInterval
@@ -1199,7 +1207,7 @@ function useExpiredLocks(options = {}) {
1199
1207
  queryKey: ["accounting-expired-locks", ...privateReadQueryScope],
1200
1208
  queryFn: async () => {
1201
1209
  if (!privateReadAddress) throw new Error("No authenticated account available");
1202
- return executePrivateRead(() => client.getExpiredLocks());
1210
+ return executePrivateRead((readClient) => readClient.getExpiredLocks());
1203
1211
  },
1204
1212
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1205
1213
  refetchInterval: pollingInterval
@@ -1232,7 +1240,7 @@ function usePendingDeposits(options = {}) {
1232
1240
  if (!client) throw new Error("No accounting client");
1233
1241
  try {
1234
1242
  return await executePrivateRead(
1235
- () => client.getPendingDeposits({
1243
+ (readClient) => readClient.getPendingDeposits({
1236
1244
  chain_id: chainId,
1237
1245
  token_address: tokenAddress,
1238
1246
  lookback_blocks: lookbackBlocks
@@ -1313,7 +1321,7 @@ function useHistory(options = {}) {
1313
1321
  queryKey: ["accounting-history", ...privateReadQueryScope, offset, limit],
1314
1322
  queryFn: async () => {
1315
1323
  if (!privateReadAddress) throw new Error("No authenticated account available");
1316
- return executePrivateRead(() => client.getHistory({ offset, limit }));
1324
+ return executePrivateRead((readClient) => readClient.getHistory({ offset, limit }));
1317
1325
  },
1318
1326
  enabled: (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!client,
1319
1327
  refetchInterval: pollingInterval,
@@ -1900,7 +1908,16 @@ function WarningIcon() {
1900
1908
  }
1901
1909
  ) });
1902
1910
  }
1903
- 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
+ }) {
1904
1921
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1905
1922
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1906
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, {}) }),
@@ -1909,14 +1926,26 @@ function TransactionWarningView({ title, message, onDone }) {
1909
1926
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: message })
1910
1927
  ] })
1911
1928
  ] }),
1912
- /* @__PURE__ */ jsx(
1913
- "button",
1914
- {
1915
- onClick: onDone,
1916
- 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",
1917
- children: "Done"
1918
- }
1919
- )
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
+ ] })
1920
1949
  ] });
1921
1950
  }
1922
1951
  function TransactionErrorView({
@@ -1926,7 +1955,9 @@ function TransactionErrorView({
1926
1955
  explorerLabel,
1927
1956
  onRetry,
1928
1957
  onDismiss,
1929
- isRetrying
1958
+ isRetrying,
1959
+ retryLabel = "Retry Verification",
1960
+ dismissLabel = "Close"
1930
1961
  }) {
1931
1962
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
1932
1963
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -1950,13 +1981,13 @@ function TransactionErrorView({
1950
1981
  }
1951
1982
  ),
1952
1983
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1953
- /* @__PURE__ */ jsx(
1984
+ onDismiss && /* @__PURE__ */ jsx(
1954
1985
  "button",
1955
1986
  {
1956
1987
  onClick: onDismiss,
1957
1988
  disabled: isRetrying,
1958
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",
1959
- children: "Close"
1990
+ children: dismissLabel
1960
1991
  }
1961
1992
  ),
1962
1993
  /* @__PURE__ */ jsx(
@@ -1965,7 +1996,7 @@ function TransactionErrorView({
1965
1996
  onClick: onRetry,
1966
1997
  disabled: isRetrying,
1967
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",
1968
- children: isRetrying ? "Retrying..." : "Retry Verification"
1999
+ children: isRetrying ? "Retrying\u2026" : retryLabel
1969
2000
  }
1970
2001
  )
1971
2002
  ] })
@@ -2105,7 +2136,7 @@ function DepositForm({
2105
2136
  };
2106
2137
  const getButtonText = () => {
2107
2138
  if (!isConnected) return "Connect Wallet";
2108
- if (isSwitchingChain) return "Switching...";
2139
+ if (isSwitchingChain) return "Switching\u2026";
2109
2140
  return "Deposit";
2110
2141
  };
2111
2142
  if (showSuccess) {
@@ -2129,7 +2160,7 @@ function DepositForm({
2129
2160
  );
2130
2161
  }
2131
2162
  if (verificationFailed) {
2132
- 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.";
2133
2164
  const detail = error?.message;
2134
2165
  const message = detail ? `${baseMessage} (${detail})` : baseMessage;
2135
2166
  return /* @__PURE__ */ jsx(
@@ -2149,7 +2180,7 @@ function DepositForm({
2149
2180
  return /* @__PURE__ */ jsx(
2150
2181
  TransactionProgressView,
2151
2182
  {
2152
- title: "Depositing...",
2183
+ title: "Depositing\u2026",
2153
2184
  steps: depositSteps,
2154
2185
  onCancel: canCancel ? handleCancel : void 0
2155
2186
  }
@@ -3089,6 +3120,745 @@ function PrivanaButton({
3089
3120
  )
3090
3121
  ] });
3091
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
+ }
3092
3862
  var DEFAULT_MOONPAY_API_URL = "https://api.moonpay.com/v3";
3093
3863
  function useMoonpayLimits({
3094
3864
  currencyCode,
@@ -3323,6 +4093,14 @@ function MoonPayGate({ enabled, children }) {
3323
4093
  if (!enabled || !apiKey) return /* @__PURE__ */ jsx(Fragment, { children });
3324
4094
  return /* @__PURE__ */ jsx(MoonPayProvider, { apiKey, children });
3325
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
+ }
3326
4104
  function MethodTabs({
3327
4105
  activeTab,
3328
4106
  onTabChange
@@ -3389,19 +4167,21 @@ function DepositView({
3389
4167
  selectedToken,
3390
4168
  amount,
3391
4169
  allowance,
4170
+ externalMinimum,
3392
4171
  onAmountChange,
3393
4172
  onSelectToken,
3394
4173
  onConnectWallet,
3395
4174
  onSubmit,
3396
4175
  isSubmitting = false
3397
4176
  }) {
3398
- const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig } = usePrivanaContext();
4177
+ const { getChainById: getChainById2, chains, serviceName, serviceIcon, networkConfig, hostedAuthConfig } = usePrivanaContext();
3399
4178
  const { address, isConnected } = useAccount();
3400
4179
  const appName = serviceName ?? "Privana";
3401
4180
  const chain = selectedToken ? getChainById2(selectedToken.chainId) : void 0;
3402
4181
  const targetChain = chain ?? chains[0];
3403
4182
  const sourceLabel = source === "connected" ? "Connected Wallet" : "External Wallet";
3404
4183
  const isConnectedSource = source === "connected";
4184
+ const isExternal = source === "external";
3405
4185
  const isCreditCard = source === "credit-card";
3406
4186
  const isNative = selectedToken?.contract === zeroAddress;
3407
4187
  const { data: nativeBalanceData } = useBalance$1({
@@ -3435,8 +4215,14 @@ function DepositView({
3435
4215
  const belowMoonpayMin = isCreditCard && hasValidAmount && moonpayMinBuy != null && parseFloat(amount) < moonpayMinBuy;
3436
4216
  const moonpayLimitsUnready = isCreditCard && !!selectedToken?.moonpayCurrencyCode && moonpayMinBuy == null;
3437
4217
  const creditCardUnavailable = isCreditCard && !!selectedToken && !selectedToken.moonpayCurrencyCode;
3438
- const needsConnect = isConnectedSource && !isConnected;
3439
- 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;
3440
4226
  const handleMax = () => {
3441
4227
  const max = formattedWalletBalance.replace(/\s/g, "");
3442
4228
  if (parseFloat(max) > 0) onAmountChange(max);
@@ -3445,9 +4231,12 @@ function DepositView({
3445
4231
  isCreditCard ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3446
4232
  /* @__PURE__ */ jsx("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: "Buy with credit card and deposit" }),
3447
4233
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Enter your deposit amount and proceed to sign a policy." })
3448
- ] }) : /* @__PURE__ */ jsxs("h2", { className: "text-foreground text-[28px] leading-8 font-medium", children: [
3449
- "Deposit from ",
3450
- 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." })
3451
4240
  ] }),
3452
4241
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3453
4242
  /* @__PURE__ */ jsx("label", { className: "text-muted-foreground text-sm", children: "Token" }),
@@ -3456,7 +4245,8 @@ function DepositView({
3456
4245
  {
3457
4246
  type: "button",
3458
4247
  onClick: onSelectToken,
3459
- 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",
3460
4250
  children: [
3461
4251
  selectedToken ? /* @__PURE__ */ jsxs(Fragment, { children: [
3462
4252
  /* @__PURE__ */ jsx("div", { className: "h-8 w-8 shrink-0 overflow-hidden rounded-full", children: getTokenIcon(selectedToken.symbol, 32) }),
@@ -3497,6 +4287,7 @@ function DepositView({
3497
4287
  type: "text",
3498
4288
  inputMode: "decimal",
3499
4289
  placeholder: "Enter Amount",
4290
+ disabled: isSubmitting,
3500
4291
  value: amount,
3501
4292
  onChange: (e) => {
3502
4293
  const value = e.target.value.replace(/[^0-9.,]/g, "").replace(/,/g, ".");
@@ -3528,11 +4319,24 @@ function DepositView({
3528
4319
  moonpayMinBuy,
3529
4320
  " USD."
3530
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." }),
3531
4331
  isCreditCard && moonpayLimitsError && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", children: "Couldn\u2019t load purchase limits. Please try again." }),
3532
4332
  isCreditCard && moonpayLimitsLoading && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Checking purchase limits\u2026" }),
3533
4333
  creditCardUnavailable && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", children: [
3534
4334
  selectedToken?.symbol ?? "This token",
3535
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."
3536
4340
  ] })
3537
4341
  ] }),
3538
4342
  allowance && /* @__PURE__ */ jsx(
@@ -3607,19 +4411,33 @@ function AwaitingDepositStatus({
3607
4411
  function ExternalDepositView({
3608
4412
  token,
3609
4413
  amount,
3610
- onCredited
4414
+ depositAddressState,
4415
+ onCredited,
4416
+ onDiscardLock,
4417
+ lock
3611
4418
  }) {
3612
- const { getChainById: getChainById2 } = usePrivanaContext();
3613
- const { depositAddress, isReady, isLoading } = useDepositAddress();
4419
+ const { getChainById: getChainById2, getTokenById, serviceName } = usePrivanaContext();
4420
+ const appName = serviceName ?? "Privana";
4421
+ const { depositAddress, isReady, isLoading } = depositAddressState;
3614
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;
3615
4428
  const [copied, setCopied] = useState(false);
3616
4429
  const [deadline] = useState(() => Date.now() + LISTENING_WINDOW_MS);
3617
4430
  const secondsLeft = useCountdown(deadline);
3618
4431
  const listening = secondsLeft > 0;
3619
4432
  const [nothingFound, setNothingFound] = useState(false);
3620
4433
  const [credited, setCredited] = useState(null);
4434
+ const [showCancelWarning, setShowCancelWarning] = useState(false);
3621
4435
  const verification = useDepositVerification({
3622
- onCredited: (txHash) => {
4436
+ onCredited: (txHash, _response, creditedAmount) => {
4437
+ if (settleAfterCredit) {
4438
+ void settleAfterCredit(txHash, creditedAmount);
4439
+ return;
4440
+ }
3623
4441
  if (onCredited) {
3624
4442
  onCredited();
3625
4443
  return;
@@ -3627,8 +4445,9 @@ function ExternalDepositView({
3627
4445
  setCredited({ txHash });
3628
4446
  }
3629
4447
  });
3630
- const { isVerifying, verificationFailed, didTimeout, verify } = verification;
3631
- 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;
3632
4451
  const {
3633
4452
  pending,
3634
4453
  isFetching: isScanning,
@@ -3637,23 +4456,58 @@ function ExternalDepositView({
3637
4456
  isUnavailable,
3638
4457
  refetch: refetchPendingDeposits
3639
4458
  } = usePendingDeposits({
3640
- chainId: token?.chainId,
3641
- 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,
3642
4462
  refetchInterval: listening && !scanPaused ? 3e4 : false
3643
4463
  });
3644
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]);
3645
4488
  useEffect(() => {
3646
4489
  if (scanPaused) return;
3647
- 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
+ );
3648
4493
  if (!next) return;
3649
- processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
3650
- void verify({
4494
+ const context = {
3651
4495
  hash: next.tx_hash,
3652
4496
  chainId: next.chain_id,
3653
4497
  amount: BigInt(next.amount),
3654
4498
  logIndex: next.log_index
3655
- });
3656
- }, [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]);
3657
4511
  const nothingFoundTimerRef = useRef(void 0);
3658
4512
  useEffect(() => () => clearTimeout(nothingFoundTimerRef.current), []);
3659
4513
  const handleRefresh = () => {
@@ -3666,11 +4520,27 @@ function ExternalDepositView({
3666
4520
  }
3667
4521
  });
3668
4522
  };
3669
- const handleCopy = () => {
4523
+ const handleCopy = async () => {
3670
4524
  if (!depositAddress) return;
3671
- void navigator.clipboard.writeText(depositAddress);
3672
- setCopied(true);
3673
- 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
+ }
3674
4544
  };
3675
4545
  return /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
3676
4546
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
@@ -3684,27 +4554,64 @@ function ExternalDepositView({
3684
4554
  /* @__PURE__ */ jsx("div", { className: "bg-border h-px w-full" }),
3685
4555
  /* @__PURE__ */ jsx(SummaryRow, { value: amount || "\u2014", label: "Value" })
3686
4556
  ] }),
3687
- /* @__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" }) }),
3688
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3689
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Deposit Address" }),
3690
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3691
- /* @__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" }) }),
3692
- /* @__PURE__ */ jsxs(
3693
- "button",
3694
- {
3695
- type: "button",
3696
- onClick: handleCopy,
3697
- disabled: !depositAddress,
3698
- 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",
3699
- children: [
3700
- /* @__PURE__ */ jsx(CopyIcon, {}),
3701
- copied ? "Copied" : "Copy"
3702
- ]
3703
- }
3704
- )
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
+ ] })
3705
4578
  ] })
3706
4579
  ] }),
3707
- 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(
3708
4615
  TransactionSuccessView,
3709
4616
  {
3710
4617
  title: "Deposit Credited",
@@ -3721,14 +4628,15 @@ function ExternalDepositView({
3721
4628
  title: "Could not verify deposit",
3722
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.",
3723
4630
  onRetry: () => void verification.retryVerification(),
3724
- onDismiss: () => verification.reset(),
4631
+ onDismiss: lock ? canCheckAnotherTransfer ? handleCheckAnotherTransfer : void 0 : () => verification.reset(),
4632
+ dismissLabel: lock && canCheckAnotherTransfer ? "Check another transfer" : void 0,
3725
4633
  isRetrying: isVerifying
3726
4634
  }
3727
4635
  ) : isVerifying ? /* @__PURE__ */ jsx(
3728
4636
  AwaitingDepositStatus,
3729
4637
  {
3730
4638
  title: "Deposit detected",
3731
- subtitle: "Crediting the incoming transaction to your Privana balance..."
4639
+ subtitle: "Crediting the incoming transaction to your Privana balance\u2026"
3732
4640
  }
3733
4641
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
3734
4642
  /* @__PURE__ */ jsx(
@@ -3750,7 +4658,16 @@ function ExternalDepositView({
3750
4658
  children: "Refresh deposit status"
3751
4659
  }
3752
4660
  ),
3753
- 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
3754
4671
  ] })
3755
4672
  ] })
3756
4673
  ] });
@@ -3769,7 +4686,7 @@ function DepositModalContent({
3769
4686
  onCloseBlockedChange,
3770
4687
  onExit
3771
4688
  }) {
3772
- const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2 } = usePrivanaContext();
4689
+ const { serviceName, enabledTokens, defaultToken, hostedAuthConfig, getChainById: getChainById2, getTokenById } = usePrivanaContext();
3773
4690
  const { address } = useAccount();
3774
4691
  const appName = serviceName ?? "Privana";
3775
4692
  const [activeTab, setActiveTab] = useState(defaultTab);
@@ -3778,6 +4695,10 @@ function DepositModalContent({
3778
4695
  const [selectedTokenId, setSelectedTokenId] = useState(defaultToken?.id ?? "");
3779
4696
  const [amount, setAmount] = useState("");
3780
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;
3781
4702
  const prevAddressRef = useRef(address);
3782
4703
  useEffect(() => {
3783
4704
  const prev = prevAddressRef.current;
@@ -3797,9 +4718,10 @@ function DepositModalContent({
3797
4718
  const [showTimeout, setShowTimeout] = useState(false);
3798
4719
  const [cancelled, setCancelled] = useState(false);
3799
4720
  const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3800
- const [lockFailedMessage, setLockFailedMessage] = useState(null);
4721
+ const [lockFailure, setLockFailure] = useState(null);
3801
4722
  const finishDeposit = () => {
3802
4723
  setAmount("");
4724
+ if (view === "external-deposit") setView("deposit");
3803
4725
  if (onDepositSuccess) {
3804
4726
  resetDeposit();
3805
4727
  onDepositSuccess();
@@ -3834,6 +4756,7 @@ function DepositModalContent({
3834
4756
  },
3835
4757
  onLockSubmitted: () => {
3836
4758
  setIsSubmittingLock(false);
4759
+ setLockFailure(null);
3837
4760
  finishDeposit();
3838
4761
  },
3839
4762
  // The deposit credited; only the policy lock failed. Success must not
@@ -3841,7 +4764,7 @@ function DepositModalContent({
3841
4764
  // error view and let the host re-prompt for a fresh lock.
3842
4765
  onLockFailed: (err) => {
3843
4766
  setIsSubmittingLock(false);
3844
- setLockFailedMessage(err.message);
4767
+ setLockFailure(err);
3845
4768
  onLockFailed?.(err);
3846
4769
  },
3847
4770
  onCheckTimeout: () => {
@@ -3849,7 +4772,37 @@ function DepositModalContent({
3849
4772
  setShowTimeout(true);
3850
4773
  }
3851
4774
  });
3852
- 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;
3853
4806
  useEffect(() => {
3854
4807
  onCloseBlockedChange?.(isUnsafeToClose);
3855
4808
  }, [isUnsafeToClose, onCloseBlockedChange]);
@@ -3862,7 +4815,33 @@ function DepositModalContent({
3862
4815
  }, [depositError, verificationFailed]);
3863
4816
  const handleSubmit = (args) => {
3864
4817
  if (args.source === "external") {
3865
- 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
+ });
3866
4845
  return;
3867
4846
  }
3868
4847
  if (args.source === "credit-card") {
@@ -3914,7 +4893,14 @@ function DepositModalContent({
3914
4893
  }
3915
4894
  ] : []
3916
4895
  ];
3917
- 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;
3918
4904
  const activeView = flowView ?? view;
3919
4905
  const handleDepositDone = () => {
3920
4906
  setShowSuccess(false);
@@ -3927,10 +4913,28 @@ function DepositModalContent({
3927
4913
  resetDeposit();
3928
4914
  };
3929
4915
  const handleLockFailedDone = () => {
3930
- 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);
3931
4921
  setAmount("");
3932
4922
  setCancelled(false);
3933
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");
3934
4938
  };
3935
4939
  const handleDismissVerificationError = () => {
3936
4940
  setAmount("");
@@ -3943,6 +4947,10 @@ function DepositModalContent({
3943
4947
  };
3944
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" };
3945
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
+ }
3946
4954
  if (back.to === "method") {
3947
4955
  setAmount("");
3948
4956
  setSelectedTokenId("");
@@ -3988,9 +4996,14 @@ function DepositModalContent({
3988
4996
  activeView === "lock-error" && /* @__PURE__ */ jsx(
3989
4997
  TransactionWarningView,
3990
4998
  {
3991
- title: "Deposit credited, lock failed",
3992
- message: `Your deposit was credited but locking the funds for ${appName} failed: ${lockFailedMessage}`,
3993
- 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
3994
5007
  }
3995
5008
  ),
3996
5009
  activeView === "deposit-timeout" && /* @__PURE__ */ jsx(
@@ -4005,7 +5018,7 @@ function DepositModalContent({
4005
5018
  TransactionErrorView,
4006
5019
  {
4007
5020
  title: "Verification failed",
4008
- 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.",
4009
5022
  explorerUrl: explorerTxUrl,
4010
5023
  explorerLabel: "View transaction",
4011
5024
  onRetry: handleRetryVerification,
@@ -4015,7 +5028,7 @@ function DepositModalContent({
4015
5028
  activeView === "depositing" && /* @__PURE__ */ jsx(
4016
5029
  TransactionProgressView,
4017
5030
  {
4018
- title: "Depositing...",
5031
+ title: "Depositing\u2026",
4019
5032
  steps: depositSteps,
4020
5033
  onCancel: isGettingAddress || isSendingTransaction ? handleDepositCancel : void 0
4021
5034
  }
@@ -4069,11 +5082,12 @@ function DepositModalContent({
4069
5082
  selectedToken,
4070
5083
  amount,
4071
5084
  allowance,
5085
+ externalMinimum,
4072
5086
  onAmountChange: setAmount,
4073
5087
  onSelectToken: () => setView("select-token"),
4074
5088
  onConnectWallet,
4075
5089
  onSubmit: handleSubmit,
4076
- isSubmitting: isPending
5090
+ isSubmitting: isPending || externalLock.isSigning || externalLock.isSubmittingLock
4077
5091
  }
4078
5092
  ) }),
4079
5093
  activeView === "select-token" && /* @__PURE__ */ jsx(
@@ -4086,12 +5100,15 @@ function DepositModalContent({
4086
5100
  }
4087
5101
  }
4088
5102
  ),
4089
- activeView === "external-deposit" && /* @__PURE__ */ jsx(
5103
+ activeView === "external-deposit" && (!allowance || externalLock.session) && /* @__PURE__ */ jsx(
4090
5104
  ExternalDepositView,
4091
5105
  {
4092
5106
  token: selectedToken,
4093
5107
  amount,
4094
- onCredited: allowance ? void 0 : onDepositSuccess
5108
+ depositAddressState: externalDepositAddress,
5109
+ onCredited: allowance ? void 0 : onDepositSuccess,
5110
+ onDiscardLock: handleExternalVerificationDiscard,
5111
+ lock: externalLock.session ? externalLock : void 0
4095
5112
  }
4096
5113
  ),
4097
5114
  activeView === "credit-card-widget" && /* @__PURE__ */ jsx(MoonPayGate, { enabled: true, children: /* @__PURE__ */ jsx(
@@ -4103,7 +5120,7 @@ function DepositModalContent({
4103
5120
  onCredited: allowance ? void 0 : finishCardPurchase,
4104
5121
  onLockSubmitted: finishCardPurchase,
4105
5122
  onLockFailed: (err) => {
4106
- setLockFailedMessage(err.message);
5123
+ setLockFailure(err);
4107
5124
  onLockFailed?.(err);
4108
5125
  }
4109
5126
  }
@@ -4550,7 +5567,7 @@ function WalletBalanceView({
4550
5567
  allowance,
4551
5568
  amount,
4552
5569
  onAmountChange,
4553
- onPlay,
5570
+ onFundSession,
4554
5571
  onAddFunds,
4555
5572
  onWithdraw
4556
5573
  }) {
@@ -4575,18 +5592,17 @@ function WalletBalanceView({
4575
5592
  const hasValidAmount = !!amount && parseFloat(amount) > 0;
4576
5593
  const tooManyDecimals = hasValidAmount && !!token && amount.includes(".") && amount.split(".")[1].length > token.decimals;
4577
5594
  const exceedsBalance = hasValidAmount && !tooManyDecimals && !!token && !isBalanceLoading && !isBalanceError && parseTokenAmount(amount, token.decimals) > availableWei;
4578
- const canPlay = hasValidAmount && !!token && !tooManyDecimals && !exceedsBalance && !isBalanceLoading && !isBalanceError;
5595
+ const canFundSession = hasValidAmount && !!token && !tooManyDecimals && !exceedsBalance && !isBalanceLoading && !isBalanceError;
4579
5596
  const handleMax = () => {
4580
5597
  const max = availableFormatted.replace(/\s/g, "");
4581
5598
  if (parseFloat(max) > 0) onAmountChange(max);
4582
5599
  };
4583
- const handlePlay = () => {
4584
- if (!token || !canPlay || !onPlay) return;
4585
- onPlay({ tokenId: token.id, amount });
4586
- onAmountChange("");
5600
+ const handleFundSession = () => {
5601
+ if (!token || !canFundSession || !onFundSession) return;
5602
+ onFundSession({ tokenId: token.id, amount });
4587
5603
  };
4588
5604
  const showInput = variant !== "fully-in-use";
4589
- const showInlinePlay = variant === "session-zero" || variant === "mixed";
5605
+ const showInlineFund = variant === "session-zero" || variant === "mixed";
4590
5606
  return /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
4591
5607
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
4592
5608
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -4638,14 +5654,14 @@ function WalletBalanceView({
4638
5654
  }
4639
5655
  )
4640
5656
  ] }),
4641
- showInlinePlay && /* @__PURE__ */ jsx(
5657
+ showInlineFund && /* @__PURE__ */ jsx(
4642
5658
  "button",
4643
5659
  {
4644
5660
  type: "button",
4645
- disabled: !onPlay || !canPlay,
4646
- onClick: handlePlay,
5661
+ disabled: !onFundSession || !canFundSession,
5662
+ onClick: handleFundSession,
4647
5663
  className: "border-border flex h-10 min-w-20 cursor-pointer items-center justify-center rounded-[10px] border px-3 text-sm font-medium text-[#4fc77f] transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50",
4648
- children: "Play"
5664
+ children: "Fund session"
4649
5665
  }
4650
5666
  )
4651
5667
  ] }),
@@ -4669,10 +5685,10 @@ function WalletBalanceView({
4669
5685
  "button",
4670
5686
  {
4671
5687
  type: "button",
4672
- disabled: !onPlay || !canPlay,
4673
- onClick: handlePlay,
5688
+ disabled: !onFundSession || !canFundSession,
5689
+ onClick: handleFundSession,
4674
5690
  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",
4675
- children: "Play"
5691
+ children: "Fund session"
4676
5692
  }
4677
5693
  ),
4678
5694
  /* @__PURE__ */ jsx(
@@ -4709,10 +5725,34 @@ function WalletBalanceView({
4709
5725
  ] }) })
4710
5726
  ] });
4711
5727
  }
5728
+ function invokeGuarded(runRef, invoke, handlers) {
5729
+ const run = ++runRef.current;
5730
+ const isCurrent = () => runRef.current === run;
5731
+ let result;
5732
+ try {
5733
+ result = invoke();
5734
+ } catch (err) {
5735
+ if (isCurrent()) handlers.onError(err);
5736
+ return;
5737
+ }
5738
+ if (result == null || typeof result.then !== "function") {
5739
+ handlers.onSyncResult();
5740
+ return;
5741
+ }
5742
+ handlers.onAsyncStart();
5743
+ result.then(
5744
+ () => {
5745
+ if (isCurrent()) handlers.onSuccess();
5746
+ },
5747
+ (err) => {
5748
+ if (isCurrent()) handlers.onError(err);
5749
+ }
5750
+ );
5751
+ }
4712
5752
  function WalletModalContent({
4713
5753
  session,
4714
5754
  allowance,
4715
- onPlay,
5755
+ onFundSession,
4716
5756
  onEndSession,
4717
5757
  onDepositSuccess,
4718
5758
  onClose,
@@ -4727,7 +5767,10 @@ function WalletModalContent({
4727
5767
  const [depositBlocked, setDepositBlocked] = useState(false);
4728
5768
  const [withdrawPending, setWithdrawPending] = useState(false);
4729
5769
  const [endSessionError, setEndSessionError] = useState(null);
5770
+ const [fundSessionError, setFundSessionError] = useState(null);
4730
5771
  const endSessionRunRef = useRef(0);
5772
+ const fundSessionRunRef = useRef(0);
5773
+ const [pendingFundSession, setPendingFundSession] = useState(null);
4731
5774
  const prevAddressRef = useRef(address);
4732
5775
  useEffect(() => {
4733
5776
  const prev = prevAddressRef.current;
@@ -4739,10 +5782,13 @@ function WalletModalContent({
4739
5782
  setDepositBlocked(false);
4740
5783
  setWithdrawPending(false);
4741
5784
  setEndSessionError(null);
5785
+ setFundSessionError(null);
5786
+ setPendingFundSession(null);
4742
5787
  endSessionRunRef.current++;
5788
+ fundSessionRunRef.current++;
4743
5789
  }
4744
5790
  }, [address, hostedAuthConfig]);
4745
- const closeBlocked = depositBlocked || withdrawPending || view === "ending-session";
5791
+ const closeBlocked = depositBlocked || withdrawPending || view === "funding-session" || view === "ending-session";
4746
5792
  useEffect(() => {
4747
5793
  onCloseBlockedChange?.(closeBlocked);
4748
5794
  }, [closeBlocked, onCloseBlockedChange]);
@@ -4756,20 +5802,16 @@ function WalletModalContent({
4756
5802
  };
4757
5803
  const startEndSession = () => {
4758
5804
  if (!onEndSession) return;
4759
- const run = ++endSessionRunRef.current;
4760
5805
  setEndSessionError(null);
4761
- setView("ending-session");
4762
- Promise.resolve().then(() => onEndSession()).then(
4763
- () => {
4764
- if (endSessionRunRef.current !== run) return;
4765
- setView("withdraw");
4766
- },
4767
- (err) => {
4768
- if (endSessionRunRef.current !== run) return;
5806
+ invokeGuarded(endSessionRunRef, () => onEndSession(), {
5807
+ onSyncResult: () => setView("withdraw"),
5808
+ onAsyncStart: () => setView("ending-session"),
5809
+ onSuccess: () => setView("withdraw"),
5810
+ onError: (err) => {
4769
5811
  setEndSessionError(err instanceof Error ? err.message : null);
4770
5812
  setView("end-session-error");
4771
5813
  }
4772
- );
5814
+ });
4773
5815
  };
4774
5816
  const handleWithdraw = () => {
4775
5817
  if (session && onEndSession) {
@@ -4783,6 +5825,36 @@ function WalletModalContent({
4783
5825
  setEndSessionError(null);
4784
5826
  setView("balance");
4785
5827
  };
5828
+ const fundSession = (args) => {
5829
+ if (!onFundSession) return;
5830
+ setFundSessionError(null);
5831
+ setPendingFundSession(args);
5832
+ setAmount("");
5833
+ invokeGuarded(fundSessionRunRef, () => onFundSession(args), {
5834
+ onSyncResult: () => {
5835
+ setPendingFundSession(null);
5836
+ setView("balance");
5837
+ },
5838
+ onAsyncStart: () => setView("funding-session"),
5839
+ onSuccess: () => {
5840
+ setPendingFundSession(null);
5841
+ setView("balance");
5842
+ },
5843
+ onError: (err) => {
5844
+ setFundSessionError(err instanceof Error ? err.message : null);
5845
+ setView("fund-session-error");
5846
+ }
5847
+ });
5848
+ };
5849
+ const dismissFundSessionError = () => {
5850
+ fundSessionRunRef.current++;
5851
+ setFundSessionError(null);
5852
+ if (pendingFundSession) {
5853
+ setAmount(pendingFundSession.amount);
5854
+ setPendingFundSession(null);
5855
+ }
5856
+ setView("balance");
5857
+ };
4786
5858
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4787
5859
  onClose && /* @__PURE__ */ jsx(
4788
5860
  "button",
@@ -4798,7 +5870,7 @@ function WalletModalContent({
4798
5870
  children: /* @__PURE__ */ jsx(CloseIcon, {})
4799
5871
  }
4800
5872
  ),
4801
- (view === "balance" || view === "ending-session" || view === "end-session-error") && /* @__PURE__ */ jsx("div", { className: "flex items-center px-5 py-4", children: /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-5 font-medium", children: appName }) }),
5873
+ (view === "balance" || view === "funding-session" || view === "fund-session-error" || view === "ending-session" || view === "end-session-error") && /* @__PURE__ */ jsx("div", { className: "flex items-center px-5 py-4", children: /* @__PURE__ */ jsx("span", { className: "text-foreground text-xl leading-5 font-medium", children: appName }) }),
4802
5874
  view === "balance" && /* @__PURE__ */ jsx(
4803
5875
  WalletBalanceView,
4804
5876
  {
@@ -4806,11 +5878,30 @@ function WalletModalContent({
4806
5878
  allowance,
4807
5879
  amount,
4808
5880
  onAmountChange: setAmount,
4809
- onPlay,
5881
+ onFundSession: onFundSession ? fundSession : void 0,
4810
5882
  onAddFunds: () => setView("deposit"),
4811
- onWithdraw: handleWithdraw
5883
+ onWithdraw: session && !onEndSession ? void 0 : handleWithdraw
4812
5884
  }
4813
5885
  ),
5886
+ view === "funding-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
5887
+ TransactionProgressView,
5888
+ {
5889
+ title: "Funding session...",
5890
+ steps: [{ label: "Waiting for the session to be funded", status: "active" }]
5891
+ }
5892
+ ) }),
5893
+ view === "fund-session-error" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
5894
+ TransactionErrorView,
5895
+ {
5896
+ title: "Could not fund session",
5897
+ message: fundSessionError ? `Your session could not be funded. (${fundSessionError})` : "Your session could not be funded.",
5898
+ onRetry: () => {
5899
+ if (pendingFundSession) fundSession(pendingFundSession);
5900
+ },
5901
+ onDismiss: dismissFundSessionError,
5902
+ retryLabel: "Try again"
5903
+ }
5904
+ ) }),
4814
5905
  view === "ending-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
4815
5906
  TransactionProgressView,
4816
5907
  {
@@ -4824,7 +5915,8 @@ function WalletModalContent({
4824
5915
  title: "Could not end session",
4825
5916
  message: endSessionError ? `Your game session could not be ended, so the funds committed to it are not available to withdraw yet. (${endSessionError})` : "Your game session could not be ended, so the funds committed to it are not available to withdraw yet.",
4826
5917
  onRetry: startEndSession,
4827
- onDismiss: dismissEndSessionError
5918
+ onDismiss: dismissEndSessionError,
5919
+ retryLabel: "Try again"
4828
5920
  }
4829
5921
  ) }),
4830
5922
  view === "withdraw" && /* @__PURE__ */ jsx(WithdrawModalContent, { onBack: exitWithdraw, onPendingChange: setWithdrawPending }),