@oasisprotocol/privana-sdk 0.5.0 → 0.5.2

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,9 +1,9 @@
1
1
  "use client";
2
- import { usePrivanaContext, createHostedAuthPendingStorageKey, clearHostedAuthPendingTransaction, HostedAuthRequiredError, HostedAuthError, createPkceVerifier, createPkceChallenge, createHostedAuthState, persistHostedAuthPendingTransaction, parseHostedAuthCallback, readHostedAuthPendingTransaction, HostedAuthStateMismatchError, buildHostedAuthSession, useSafePrivanaContext, usePrivateReadRequest, formatTokenAmount, useDepositVerification, getTransactionReceipt, waitForTransactionReceipt, cn, Button, stripHostedAuthCallbackParams, AccountingApiError, getChainId2, shortenAddress, formatTimeRemaining, parseTokenAmount, getExplorerAddressUrl, getExplorerLabel, Skeleton, FiatOnRampForm } from './chunk-54FC6TO3.js';
3
- export { AccountingApiError, Button, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, NETWORK_CONFIG, NetworkError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, ValidationError, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clearHostedAuthPendingTransaction, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createPkceChallenge, createPkceVerifier, getAccountingContract, getApiUrl, getChainById, getChainId, getExplorerAddressUrl, getExplorerLabel, isHostedAuthRefreshActive, isHostedAuthSessionActive, normalizeAddress, normalizeHex, parseHostedAuthCallback, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, stripHostedAuthCallbackParams, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-54FC6TO3.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, getWalletClient, createSignedLockRequest, signWithdrawMessage, signLockMessage, signTransferMessage, signTransferLockedMessage, signModifyLockMessage, cn, Button, stripHostedAuthCallbackParams, AccountingApiError, getBrowserStorageItem, removeBrowserStorageItem, isSignedLockUsable, setBrowserStorageItem, shortenAddress, formatTimeRemaining, parseTokenAmount, getExplorerAddressUrl, getExplorerLabel, Skeleton, formatCountdown, FiatOnRampForm } from './chunk-ZVUMV4NR.js';
3
+ export { AccountingApiError, Button, DEFAULT_LOCK_DURATION_SECONDS, DEFAULT_ONRAMP_LOCK_BUFFER, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, LOCK_TYPES, MODIFY_LOCK_TYPES, NETWORK_CONFIG, NetworkError, PostDepositLockError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, TRANSFER_LOCKED_TYPES, TRANSFER_TYPES, ValidationError, WITHDRAW_FROM_LOCK_TYPES, WITHDRAW_TYPES, applyLockBuffer, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clampLockAmount, clearHostedAuthPendingTransaction, clearPendingLock, createDomain, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createLockExpiry, createPkceChallenge, createPkceVerifier, createSignedLockRequest, getAccountingContract, getApiUrl, getChainById, getChainId, getExplorerAddressUrl, getExplorerLabel, isHostedAuthRefreshActive, isHostedAuthSessionActive, isSignedLockUsable, loadPendingLock, normalizeAddress, normalizeHex, parseHostedAuthCallback, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, savePendingLock, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, stripHostedAuthCallbackParams, submitPendingLock, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, useSafeAccount, useSafePrivanaContext, useSiweAuth } from './chunk-ZVUMV4NR.js';
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
- import { useAccount, useWalletClient, useConfig, useWriteContract, useSendTransaction, useChainId, useSwitchChain, useBalance as useBalance$1, useReadContract } from 'wagmi';
6
+ import { useAccount, useWalletClient, useConfig, useWriteContract, useSendTransaction, useBalance as useBalance$1, useReadContract } from 'wagmi';
7
7
  import { zeroAddress, erc20Abi } from 'viem';
8
8
  import * as DialogPrimitive from '@radix-ui/react-dialog';
9
9
  import { XIcon } from 'lucide-react';
@@ -12,202 +12,6 @@ import { toast } from 'sonner';
12
12
  import { QRCodeSVG } from 'qrcode.react';
13
13
  import { MoonPayProvider, MoonPayContext } from '@moonpay/moonpay-react';
14
14
 
15
- // src/sdk/signatures/eip712-types.ts
16
- function createDomain(chainId, verifyingContract) {
17
- return {
18
- name: "AccountingModule",
19
- version: "1",
20
- chainId,
21
- verifyingContract
22
- };
23
- }
24
- var LOCK_TYPES = {
25
- Lock: [
26
- { name: "serviceAddress", type: "address" },
27
- { name: "tokenId", type: "bytes32" },
28
- { name: "amount", type: "uint256" },
29
- { name: "expiry", type: "uint256" },
30
- { name: "nonce", type: "uint256" }
31
- ]
32
- };
33
- var TRANSFER_TYPES = {
34
- Transfer: [
35
- { name: "toAddress", type: "address" },
36
- { name: "tokenId", type: "bytes32" },
37
- { name: "amount", type: "uint256" },
38
- { name: "nonce", type: "uint256" }
39
- ]
40
- };
41
- var TRANSFER_LOCKED_TYPES = {
42
- TransferLocked: [
43
- { name: "userAddress", type: "address" },
44
- { name: "toAddress", type: "address" },
45
- { name: "lockId", type: "uint256" },
46
- { name: "amount", type: "uint256" },
47
- { name: "nonce", type: "uint256" },
48
- { name: "serviceAddress", type: "address" }
49
- ]
50
- };
51
- var WITHDRAW_TYPES = {
52
- Withdraw: [
53
- { name: "tokenId", type: "bytes32" },
54
- { name: "amount", type: "uint256" },
55
- { name: "nonce", type: "uint256" }
56
- ]
57
- };
58
- var MODIFY_LOCK_TYPES = {
59
- ModifyLock: [
60
- { name: "lockId", type: "uint256" },
61
- { name: "amount", type: "uint256" },
62
- { name: "newExpiry", type: "uint256" },
63
- { name: "nonce", type: "uint256" }
64
- ]
65
- };
66
- var WITHDRAW_FROM_LOCK_TYPES = {
67
- WithdrawFromLock: [
68
- { name: "userAddress", type: "address" },
69
- { name: "toAddress", type: "address" },
70
- { name: "lockId", type: "uint256" },
71
- { name: "amount", type: "uint256" },
72
- { name: "nonce", type: "uint256" }
73
- ]
74
- };
75
-
76
- // src/sdk/signatures/sign-lock.ts
77
- async function signLockMessage({
78
- walletClient,
79
- chainId,
80
- verifyingContract,
81
- message
82
- }) {
83
- const account = walletClient.account;
84
- if (!account) {
85
- throw new Error("No account connected to wallet client");
86
- }
87
- const domain = createDomain(chainId, verifyingContract);
88
- const signature = await walletClient.signTypedData({
89
- account,
90
- domain,
91
- types: LOCK_TYPES,
92
- primaryType: "Lock",
93
- message
94
- });
95
- return signature;
96
- }
97
- function createLockExpiry(minutesFromNow = 60) {
98
- return BigInt(Math.floor(Date.now() / 1e3) + minutesFromNow * 60);
99
- }
100
-
101
- // src/sdk/signatures/sign-modify-lock.ts
102
- async function signModifyLockMessage({
103
- walletClient,
104
- chainId,
105
- verifyingContract,
106
- message
107
- }) {
108
- const account = walletClient.account;
109
- if (!account) {
110
- throw new Error("No account connected to wallet client");
111
- }
112
- const domain = createDomain(chainId, verifyingContract);
113
- const signature = await walletClient.signTypedData({
114
- account,
115
- domain,
116
- types: MODIFY_LOCK_TYPES,
117
- primaryType: "ModifyLock",
118
- message
119
- });
120
- return signature;
121
- }
122
-
123
- // src/sdk/signatures/sign-transfer.ts
124
- async function signTransferMessage({
125
- walletClient,
126
- chainId,
127
- verifyingContract,
128
- message
129
- }) {
130
- const account = walletClient.account;
131
- if (!account) {
132
- throw new Error("No account connected to wallet client");
133
- }
134
- const domain = createDomain(chainId, verifyingContract);
135
- const signature = await walletClient.signTypedData({
136
- account,
137
- domain,
138
- types: TRANSFER_TYPES,
139
- primaryType: "Transfer",
140
- message
141
- });
142
- return signature;
143
- }
144
-
145
- // src/sdk/signatures/sign-transfer-locked.ts
146
- async function signTransferLockedMessage({
147
- walletClient,
148
- chainId,
149
- verifyingContract,
150
- message
151
- }) {
152
- const account = walletClient.account;
153
- if (!account) {
154
- throw new Error("No account connected to wallet client");
155
- }
156
- const domain = createDomain(chainId, verifyingContract);
157
- const signature = await walletClient.signTypedData({
158
- account,
159
- domain,
160
- types: TRANSFER_LOCKED_TYPES,
161
- primaryType: "TransferLocked",
162
- message
163
- });
164
- return signature;
165
- }
166
-
167
- // src/sdk/signatures/sign-withdraw.ts
168
- async function signWithdrawMessage({
169
- walletClient,
170
- chainId,
171
- verifyingContract,
172
- message
173
- }) {
174
- const account = walletClient.account;
175
- if (!account) {
176
- throw new Error("No account connected to wallet client");
177
- }
178
- const domain = createDomain(chainId, verifyingContract);
179
- const signature = await walletClient.signTypedData({
180
- account,
181
- domain,
182
- types: WITHDRAW_TYPES,
183
- primaryType: "Withdraw",
184
- message
185
- });
186
- return signature;
187
- }
188
-
189
- // src/sdk/signatures/sign-withdraw-from-lock.ts
190
- async function signWithdrawFromLockMessage({
191
- walletClient,
192
- chainId,
193
- verifyingContract,
194
- message
195
- }) {
196
- const account = walletClient.account;
197
- if (!account) {
198
- throw new Error("No account connected to wallet client");
199
- }
200
- const domain = createDomain(chainId, verifyingContract);
201
- const signature = await walletClient.signTypedData({
202
- account,
203
- domain,
204
- types: WITHDRAW_FROM_LOCK_TYPES,
205
- primaryType: "WithdrawFromLock",
206
- message
207
- });
208
- return signature;
209
- }
210
-
211
15
  // src/sdk/hooks/use-privana-client.ts
212
16
  function usePrivanaClient() {
213
17
  const { client } = usePrivanaContext();
@@ -468,75 +272,22 @@ function useBatchBalances(options) {
468
272
  refetch: query.refetch
469
273
  };
470
274
  }
471
- function useEnsureCorrectChain() {
472
- const config = useConfig();
473
- const chainId = useChainId();
474
- const { switchChainAsync } = useSwitchChain();
475
- const waitUntilOnChain = useCallback(
476
- async (expectedChainId, timeoutMs, pollIntervalMs = 250) => {
477
- const startedAt = Date.now();
478
- while (Date.now() - startedAt < timeoutMs) {
479
- const currentChainId = getChainId2(config);
480
- if (currentChainId === expectedChainId) return true;
481
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
482
- }
483
- return false;
484
- },
485
- [config]
486
- );
487
- const ensureCorrectChain = useCallback(
488
- async (targetChainId) => {
489
- const currentChainId = getChainId2(config);
490
- if (currentChainId === targetChainId) return false;
491
- let switchErrorMessage;
492
- try {
493
- const timeoutPromise = new Promise((_, reject) => {
494
- setTimeout(() => reject(new Error("Chain switch timeout")), 3e3);
495
- });
496
- await Promise.race([switchChainAsync({ chainId: targetChainId }), timeoutPromise]);
497
- } catch (error) {
498
- if (error instanceof Error && error.message.includes("Unsupported Chain")) {
499
- console.warn("Got 'Unsupported Chain' error, chain may have switched anyway.");
500
- } else {
501
- switchErrorMessage = error instanceof Error ? error.message : "Unknown chain switch error";
502
- }
503
- }
504
- const settled = await waitUntilOnChain(targetChainId, 2e4);
505
- if (settled) return true;
506
- if (switchErrorMessage) {
507
- throw new Error(
508
- `Failed to switch to chain (Chain ID: ${targetChainId}): ${switchErrorMessage}`
509
- );
510
- }
511
- throw new Error(`Chain switch did not settle in time (expected ${targetChainId}).`);
512
- },
513
- [config, switchChainAsync, waitUntilOnChain]
514
- );
515
- const isOnChain = useCallback((targetChainId) => chainId === targetChainId, [chainId]);
516
- return {
517
- chainId,
518
- ensureCorrectChain,
519
- isOnChain
520
- };
521
- }
522
-
523
- // src/sdk/hooks/use-deposit.ts
524
275
  var STALE_MS = 30 * 60 * 1e3;
525
276
  function storageKey(address) {
526
277
  return `privana:pending-deposit:${address.toLowerCase()}`;
527
278
  }
528
279
  function savePendingDeposit(address, data) {
529
- try {
530
- sessionStorage.setItem(storageKey(address), JSON.stringify(data));
531
- } catch {
280
+ const stored = setBrowserStorageItem(storageKey(address), JSON.stringify(data));
281
+ if (!stored && data.signedLock) {
282
+ throw new Error("Unable to persist pending locked deposit for recovery");
532
283
  }
533
284
  }
534
285
  function loadPendingDeposit(address) {
535
286
  try {
536
- const raw = sessionStorage.getItem(storageKey(address));
287
+ const raw = getBrowserStorageItem(storageKey(address));
537
288
  if (!raw) return null;
538
289
  const data = JSON.parse(raw);
539
- if (Date.now() - data.savedAt > STALE_MS) {
290
+ if (Date.now() - data.savedAt > STALE_MS && !(data.signedLock && isSignedLockUsable(data.signedLock))) {
540
291
  clearPendingDeposit(address);
541
292
  return null;
542
293
  }
@@ -545,15 +296,20 @@ function loadPendingDeposit(address) {
545
296
  return null;
546
297
  }
547
298
  }
548
- function clearPendingDeposit(address) {
549
- try {
550
- sessionStorage.removeItem(storageKey(address));
551
- } catch {
299
+ function clearPendingDeposit(address, onlyForTxHash) {
300
+ if (onlyForTxHash) {
301
+ try {
302
+ const raw = getBrowserStorageItem(storageKey(address));
303
+ const recordTxHash = raw ? JSON.parse(raw).txHash : void 0;
304
+ if (recordTxHash && recordTxHash !== onlyForTxHash) return;
305
+ } catch {
306
+ }
552
307
  }
308
+ removeBrowserStorageItem(storageKey(address));
553
309
  }
554
310
  function useDeposit(options = {}) {
555
311
  const { address } = useAccount();
556
- const { client, enabledTokens, getChainById: getChainById2 } = usePrivanaContext();
312
+ const { client, enabledTokens, getChainById: getChainById2, networkConfig, serviceAddress } = usePrivanaContext();
557
313
  const { data: walletClient } = useWalletClient();
558
314
  const queryClient = useQueryClient();
559
315
  const config = useConfig();
@@ -570,21 +326,54 @@ function useDeposit(options = {}) {
570
326
  const onDepositAddressReceivedRef = useRef(options.onDepositAddressReceived);
571
327
  const onDepositSuccessRef = useRef(options.onDepositSuccess);
572
328
  const onCreditedRef = useRef(options.onCredited);
329
+ const onLockSubmittedRef = useRef(options.onLockSubmitted);
330
+ const onLockFailedRef = useRef(options.onLockFailed);
573
331
  const onCheckTimeoutRef = useRef(options.onCheckTimeout);
574
332
  const onErrorRef = useRef(options.onError);
575
333
  useEffect(() => {
576
334
  onDepositAddressReceivedRef.current = options.onDepositAddressReceived;
577
335
  onDepositSuccessRef.current = options.onDepositSuccess;
578
336
  onCreditedRef.current = options.onCredited;
337
+ onLockSubmittedRef.current = options.onLockSubmitted;
338
+ onLockFailedRef.current = options.onLockFailed;
579
339
  onCheckTimeoutRef.current = options.onCheckTimeout;
580
340
  onErrorRef.current = options.onError;
581
341
  }, [
582
342
  options.onDepositAddressReceived,
583
343
  options.onDepositSuccess,
584
344
  options.onCredited,
345
+ options.onLockSubmitted,
346
+ options.onLockFailed,
585
347
  options.onCheckTimeout,
586
348
  options.onError
587
349
  ]);
350
+ const pendingLockRef = useRef(null);
351
+ const submitPendingLockAfterCredit = useCallback(
352
+ async (signedLock, response) => {
353
+ try {
354
+ const result = await submitPendingLock({
355
+ client,
356
+ payload: signedLock,
357
+ creditedAmount: response.amount != null ? BigInt(response.amount) : void 0
358
+ });
359
+ queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
360
+ queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
361
+ queryClient.invalidateQueries({ queryKey: ["accounting-total-locked-balance"] });
362
+ queryClient.invalidateQueries({ queryKey: ["accounting-history"] });
363
+ onLockSubmittedRef.current?.(result);
364
+ } catch (err) {
365
+ const error2 = err instanceof PostDepositLockError ? err : new PostDepositLockError(
366
+ err instanceof Error ? err.message : "Lock submission failed",
367
+ "submission-failed",
368
+ BigInt(signedLock.amount),
369
+ void 0,
370
+ { cause: err }
371
+ );
372
+ (onLockFailedRef.current ?? onErrorRef.current)?.(error2);
373
+ }
374
+ },
375
+ [client, queryClient]
376
+ );
588
377
  const {
589
378
  isVerifying,
590
379
  didTimeout,
@@ -597,8 +386,16 @@ function useDeposit(options = {}) {
597
386
  pollTimeout: options.pollTimeout,
598
387
  onCredited: (hash, response) => {
599
388
  verificationContextRef.current = null;
600
- if (address) clearPendingDeposit(address);
601
- onCreditedRef.current?.(hash, response);
389
+ const signedLock = pendingLockRef.current;
390
+ pendingLockRef.current = null;
391
+ if (signedLock) {
392
+ void submitPendingLockAfterCredit(signedLock, response).finally(() => {
393
+ if (address) clearPendingDeposit(address, hash);
394
+ });
395
+ } else if (address) {
396
+ clearPendingDeposit(address);
397
+ }
398
+ onCreditedRef.current?.(hash, response, signedLock !== null);
602
399
  },
603
400
  onCheckTimeout: (hash) => {
604
401
  onCheckTimeoutRef.current?.(hash);
@@ -649,6 +446,7 @@ function useDeposit(options = {}) {
649
446
  resetVerification();
650
447
  if (address) clearPendingDeposit(address);
651
448
  verificationContextRef.current = null;
449
+ pendingLockRef.current = null;
652
450
  setDepositAddress(null);
653
451
  setTxHash(void 0);
654
452
  setIsSwitchingChain(false);
@@ -668,6 +466,7 @@ function useDeposit(options = {}) {
668
466
  setTxHash(hash);
669
467
  setDepositAddress(persisted.depositAddress);
670
468
  setIsWaitingForConfirmation(true);
469
+ pendingLockRef.current = persisted.signedLock ?? null;
671
470
  const ctx = {
672
471
  hash,
673
472
  chainId: persisted.chainId,
@@ -680,8 +479,12 @@ function useDeposit(options = {}) {
680
479
  try {
681
480
  let confirmed = false;
682
481
  try {
683
- await getTransactionReceipt(config, { hash, chainId: persisted.chainId });
684
- confirmed = true;
482
+ const receipt = await getTransactionReceipt(config, {
483
+ hash,
484
+ chainId: persisted.chainId
485
+ });
486
+ const blockNumber = await getBlockNumber(config, { chainId: persisted.chainId });
487
+ confirmed = blockNumber - receipt.blockNumber + 1n >= BigInt(confirmations);
685
488
  } catch {
686
489
  }
687
490
  if (!confirmed) {
@@ -724,13 +527,6 @@ function useDeposit(options = {}) {
724
527
  if (!sourceChain) throw new Error(`Chain ${token.chainId} not configured`);
725
528
  const addrResponse = await addressMutation.mutateAsync();
726
529
  if (isStale()) return;
727
- setIsSwitchingChain(true);
728
- try {
729
- await ensureCorrectChain(sourceChain.id);
730
- } finally {
731
- if (!isStale()) setIsSwitchingChain(false);
732
- }
733
- if (isStale()) return;
734
530
  const depositAddr = addrResponse.deposit_address;
735
531
  if (!depositAddr || depositAddr === zeroAddress) {
736
532
  throw new Error("Invalid deposit address received from API");
@@ -743,6 +539,43 @@ function useDeposit(options = {}) {
743
539
  `Amount is below the minimum deposit (${minAmountStr}) for ${isNative ? "native" : "ERC-20"} on chain ${sourceChain.id}`
744
540
  );
745
541
  }
542
+ if (params.postDepositLock && !canUseBrowserStorage()) {
543
+ throw new Error("Browser storage is required for locked deposit recovery");
544
+ }
545
+ const lockAmount = clampLockAmount(params.amount, params.postDepositLock?.maxAmount);
546
+ let signedLock;
547
+ if (params.postDepositLock) {
548
+ setIsSwitchingChain(true);
549
+ try {
550
+ await ensureCorrectChain(networkConfig.chainId);
551
+ } finally {
552
+ if (!isStale()) setIsSwitchingChain(false);
553
+ }
554
+ if (isStale()) return;
555
+ const signingWalletClient = await getWalletClient(config, {
556
+ chainId: networkConfig.chainId
557
+ });
558
+ signedLock = await createSignedLockRequest({
559
+ client,
560
+ walletClient: signingWalletClient,
561
+ userAddress: address,
562
+ networkConfig,
563
+ serviceAddress: requireServiceAddress(
564
+ params.postDepositLock.serviceAddress ?? serviceAddress
565
+ ),
566
+ tokenId: params.tokenId,
567
+ amount: lockAmount,
568
+ lockDuration: params.postDepositLock.lockDuration
569
+ });
570
+ }
571
+ if (isStale()) return;
572
+ setIsSwitchingChain(true);
573
+ try {
574
+ await ensureCorrectChain(sourceChain.id);
575
+ } finally {
576
+ if (!isStale()) setIsSwitchingChain(false);
577
+ }
578
+ if (isStale()) return;
746
579
  const hash = token.contract === zeroAddress ? await sendTransactionAsync({
747
580
  to: depositAddr,
748
581
  value: params.amount,
@@ -762,13 +595,19 @@ function useDeposit(options = {}) {
762
595
  amount: params.amount
763
596
  };
764
597
  verificationContextRef.current = ctx;
765
- savePendingDeposit(address, {
766
- txHash: hash,
767
- chainId: sourceChain.id,
768
- amount: params.amount.toString(),
769
- depositAddress: addrResponse,
770
- savedAt: Date.now()
771
- });
598
+ pendingLockRef.current = signedLock ?? null;
599
+ try {
600
+ savePendingDeposit(address, {
601
+ txHash: hash,
602
+ chainId: sourceChain.id,
603
+ amount: params.amount.toString(),
604
+ depositAddress: addrResponse,
605
+ signedLock,
606
+ savedAt: Date.now()
607
+ });
608
+ } catch (err) {
609
+ console.warn("Failed to persist pending deposit after transfer broadcast:", err);
610
+ }
772
611
  try {
773
612
  setIsWaitingForConfirmation(true);
774
613
  try {
@@ -803,12 +642,15 @@ function useDeposit(options = {}) {
803
642
  address,
804
643
  walletClient,
805
644
  addressMutation,
645
+ client,
806
646
  getChainById2,
807
647
  config,
808
648
  confirmations,
809
649
  enabledTokens,
810
650
  ensureCorrectChain,
651
+ networkConfig,
811
652
  queryClient,
653
+ serviceAddress,
812
654
  writeContractAsync,
813
655
  sendTransactionAsync,
814
656
  reset,
@@ -843,6 +685,12 @@ function useDeposit(options = {}) {
843
685
  reset
844
686
  };
845
687
  }
688
+ function requireServiceAddress(serviceAddress) {
689
+ if (!serviceAddress) {
690
+ throw new Error("Service address not configured");
691
+ }
692
+ return serviceAddress;
693
+ }
846
694
  function useDepositAddress(options = {}) {
847
695
  const queryClient = useContext(QueryClientContext);
848
696
  const accountingContext = useSafePrivanaContext();
@@ -1370,6 +1218,59 @@ function useExpiredLocks(options = {}) {
1370
1218
  refetch: query.refetch
1371
1219
  };
1372
1220
  }
1221
+ function statusCodeOf(error) {
1222
+ return error instanceof AccountingApiError ? error.statusCode : void 0;
1223
+ }
1224
+ function usePendingDeposits(options = {}) {
1225
+ const queryClient = useContext(QueryClientContext);
1226
+ const accountingContext = useSafePrivanaContext();
1227
+ const hasProviders = !!queryClient && !!accountingContext;
1228
+ const client = accountingContext?.client;
1229
+ const { executePrivateRead, privateReadAddress, privateReadQueryScope, privateReadReady } = usePrivateReadRequest();
1230
+ const { chainId, tokenAddress, lookbackBlocks } = options;
1231
+ const query = useQuery({
1232
+ queryKey: ["accounting-pending-deposits", ...privateReadQueryScope, chainId, tokenAddress],
1233
+ queryFn: async () => {
1234
+ if (!privateReadAddress) throw new Error("No authenticated account available");
1235
+ if (!chainId) throw new Error("No chain ID provided");
1236
+ if (!client) throw new Error("No accounting client");
1237
+ try {
1238
+ return await executePrivateRead(
1239
+ () => client.getPendingDeposits({
1240
+ chain_id: chainId,
1241
+ token_address: tokenAddress,
1242
+ lookback_blocks: lookbackBlocks
1243
+ })
1244
+ );
1245
+ } catch (error) {
1246
+ if (statusCodeOf(error) === 404) {
1247
+ return { pending: [], scanned_from_block: 0, scanned_to_block: 0 };
1248
+ }
1249
+ throw error;
1250
+ }
1251
+ },
1252
+ enabled: hasProviders && (options.enabled ?? true) && privateReadReady && !!privateReadAddress && !!chainId && !!client,
1253
+ // A 503 means discovery is not configured for this chain server-side —
1254
+ // polling can't fix that, so stop until a manual refetch succeeds.
1255
+ refetchInterval: (query2) => statusCodeOf(query2.state.error) === 503 ? false : options.refetchInterval ?? 3e4,
1256
+ staleTime: 25e3,
1257
+ retry: (failureCount, error) => {
1258
+ const status = statusCodeOf(error);
1259
+ if (status === 429 || status === 404 || status === 503) return false;
1260
+ return failureCount < 2;
1261
+ }
1262
+ });
1263
+ return {
1264
+ pending: query.data?.pending ?? [],
1265
+ scannedToBlock: query.data?.scanned_to_block,
1266
+ isFetching: query.isFetching,
1267
+ isError: query.isError,
1268
+ error: query.error,
1269
+ isRateLimited: statusCodeOf(query.error) === 429,
1270
+ isUnavailable: statusCodeOf(query.error) === 503,
1271
+ refetch: async () => (await query.refetch()).data
1272
+ };
1273
+ }
1373
1274
  function usePendingWithdrawals(options = {}) {
1374
1275
  const { address, isConnected } = useAccount();
1375
1276
  const { client, pollingInterval } = usePrivanaContext();
@@ -2029,7 +1930,8 @@ function TransactionErrorView({
2029
1930
  explorerLabel,
2030
1931
  onRetry,
2031
1932
  onDismiss,
2032
- isRetrying
1933
+ isRetrying,
1934
+ retryLabel = "Retry Verification"
2033
1935
  }) {
2034
1936
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
2035
1937
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
@@ -2068,7 +1970,7 @@ function TransactionErrorView({
2068
1970
  onClick: onRetry,
2069
1971
  disabled: isRetrying,
2070
1972
  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",
2071
- children: isRetrying ? "Retrying..." : "Retry Verification"
1973
+ children: isRetrying ? "Retrying..." : retryLabel
2072
1974
  }
2073
1975
  )
2074
1976
  ] })
@@ -3285,7 +3187,10 @@ function TokenSelectorView2({ selectedTokenId, onSelect }) {
3285
3187
  function CreditCardWidgetView({
3286
3188
  token,
3287
3189
  amount,
3288
- allowance
3190
+ allowance,
3191
+ onCredited,
3192
+ onLockSubmitted,
3193
+ onLockFailed
3289
3194
  }) {
3290
3195
  const moonpayCurrencyCode = token?.moonpayCurrencyCode;
3291
3196
  const containerRef = useRef(null);
@@ -3300,10 +3205,18 @@ function CreditCardWidgetView({
3300
3205
  {
3301
3206
  tokenId: token.id,
3302
3207
  currencyCode: moonpayCurrencyCode,
3303
- defaultBaseCurrencyAmount: amount || void 0,
3208
+ quoteCurrencyAmount: amount || void 0,
3209
+ lockAmount: true,
3304
3210
  variant: "embedded",
3305
3211
  autoStart: true,
3306
- theme: widgetTheme
3212
+ theme: widgetTheme,
3213
+ postDepositLock: allowance ? {
3214
+ maxAmount: BigInt(allowance.value),
3215
+ lockDuration: allowance.lockDuration
3216
+ } : void 0,
3217
+ onCredited,
3218
+ onLockSubmitted,
3219
+ onLockFailed
3307
3220
  }
3308
3221
  ) : /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
3309
3222
  token?.symbol ?? "This token",
@@ -3605,7 +3518,7 @@ function DepositView({
3605
3518
  className: "bg-secondary text-foreground hover:bg-secondary/80 cursor-pointer rounded px-3 py-2.5 text-xs font-semibold transition-colors",
3606
3519
  children: "MAX"
3607
3520
  }
3608
- ) : isCreditCard ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "USD" }) : selectedToken && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: selectedToken.symbol })
3521
+ ) : isCreditCard ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: selectedToken?.symbol ?? "USD" }) : selectedToken && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: selectedToken.symbol })
3609
3522
  ]
3610
3523
  }
3611
3524
  ),
@@ -3659,14 +3572,105 @@ function SummaryRow({ value, label }) {
3659
3572
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: label })
3660
3573
  ] });
3661
3574
  }
3575
+ var LISTENING_WINDOW_MS = 36e5;
3576
+ function remainingSeconds(deadline) {
3577
+ return Math.max(0, Math.round((deadline - Date.now()) / 1e3));
3578
+ }
3579
+ function useCountdown(deadline) {
3580
+ const [secondsLeft, setSecondsLeft] = useState(() => remainingSeconds(deadline));
3581
+ useEffect(() => {
3582
+ if (remainingSeconds(deadline) <= 0) return;
3583
+ const id = setInterval(() => {
3584
+ const left = remainingSeconds(deadline);
3585
+ setSecondsLeft(left);
3586
+ if (left <= 0) clearInterval(id);
3587
+ }, 1e3);
3588
+ return () => clearInterval(id);
3589
+ }, [deadline]);
3590
+ return secondsLeft;
3591
+ }
3592
+ function AwaitingDepositStatus({
3593
+ title,
3594
+ subtitle,
3595
+ remaining
3596
+ }) {
3597
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3 text-center", children: [
3598
+ /* @__PURE__ */ jsx("div", { className: "text-foreground", children: /* @__PURE__ */ jsx(Spinner, { size: 32 }) }),
3599
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3600
+ /* @__PURE__ */ jsx("h3", { className: "text-foreground text-xl leading-6 font-medium", children: title }),
3601
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm leading-[18px]", children: [
3602
+ subtitle,
3603
+ remaining && /* @__PURE__ */ jsxs(Fragment, { children: [
3604
+ /* @__PURE__ */ jsx("br", {}),
3605
+ "Remaining time: ",
3606
+ remaining
3607
+ ] })
3608
+ ] })
3609
+ ] })
3610
+ ] });
3611
+ }
3662
3612
  function ExternalDepositView({
3663
3613
  token,
3664
- amount
3614
+ amount,
3615
+ onCredited
3665
3616
  }) {
3666
3617
  const { getChainById: getChainById2 } = usePrivanaContext();
3667
3618
  const { depositAddress, isReady, isLoading } = useDepositAddress();
3668
3619
  const chain = token ? getChainById2(token.chainId) : void 0;
3669
3620
  const [copied, setCopied] = useState(false);
3621
+ const [deadline] = useState(() => Date.now() + LISTENING_WINDOW_MS);
3622
+ const secondsLeft = useCountdown(deadline);
3623
+ const listening = secondsLeft > 0;
3624
+ const [nothingFound, setNothingFound] = useState(false);
3625
+ const [credited, setCredited] = useState(null);
3626
+ const verification = useDepositVerification({
3627
+ onCredited: (txHash) => {
3628
+ if (onCredited) {
3629
+ onCredited();
3630
+ return;
3631
+ }
3632
+ setCredited({ txHash });
3633
+ }
3634
+ });
3635
+ const { isVerifying, verificationFailed, didTimeout, verify } = verification;
3636
+ const scanPaused = isVerifying || verificationFailed || didTimeout || !!credited;
3637
+ const {
3638
+ pending,
3639
+ isFetching: isScanning,
3640
+ isError: isScanError,
3641
+ isRateLimited,
3642
+ isUnavailable,
3643
+ refetch: refetchPendingDeposits
3644
+ } = usePendingDeposits({
3645
+ chainId: token?.chainId,
3646
+ enabled: isReady && !!depositAddress && !!token,
3647
+ refetchInterval: listening && !scanPaused ? 3e4 : false
3648
+ });
3649
+ const processedRef = useRef(/* @__PURE__ */ new Set());
3650
+ useEffect(() => {
3651
+ if (scanPaused) return;
3652
+ const next = [...pending].sort((a, b) => a.block_number - b.block_number).find((d) => !processedRef.current.has(`${d.tx_hash}:${d.log_index}`));
3653
+ if (!next) return;
3654
+ processedRef.current.add(`${next.tx_hash}:${next.log_index}`);
3655
+ void verify({
3656
+ hash: next.tx_hash,
3657
+ chainId: next.chain_id,
3658
+ amount: BigInt(next.amount),
3659
+ logIndex: next.log_index
3660
+ });
3661
+ }, [pending, scanPaused, verify]);
3662
+ const nothingFoundTimerRef = useRef(void 0);
3663
+ useEffect(() => () => clearTimeout(nothingFoundTimerRef.current), []);
3664
+ const handleRefresh = () => {
3665
+ processedRef.current.clear();
3666
+ void refetchPendingDeposits().then((result) => {
3667
+ if (!result || result.pending.length === 0) {
3668
+ setNothingFound(true);
3669
+ clearTimeout(nothingFoundTimerRef.current);
3670
+ nothingFoundTimerRef.current = setTimeout(() => setNothingFound(false), 4e3);
3671
+ }
3672
+ });
3673
+ };
3670
3674
  const handleCopy = () => {
3671
3675
  if (!depositAddress) return;
3672
3676
  void navigator.clipboard.writeText(depositAddress);
@@ -3704,6 +3708,55 @@ function ExternalDepositView({
3704
3708
  }
3705
3709
  )
3706
3710
  ] })
3711
+ ] }),
3712
+ credited ? /* @__PURE__ */ jsx(
3713
+ TransactionSuccessView,
3714
+ {
3715
+ title: "Deposit Credited",
3716
+ message: `Transfer ${shortenAddress(credited.txHash)} has been credited to your Privana balance.`,
3717
+ onDone: () => {
3718
+ setCredited(null);
3719
+ verification.reset();
3720
+ void refetchPendingDeposits();
3721
+ }
3722
+ }
3723
+ ) : verificationFailed || didTimeout ? /* @__PURE__ */ jsx(
3724
+ TransactionErrorView,
3725
+ {
3726
+ title: "Could not verify deposit",
3727
+ message: didTimeout ? "Verification timed out \u2014 your deposit may still be credited in the background." : verification.error?.message ?? "Your transfer was found but could not be verified.",
3728
+ onRetry: () => void verification.retryVerification(),
3729
+ onDismiss: () => verification.reset(),
3730
+ isRetrying: isVerifying
3731
+ }
3732
+ ) : isVerifying ? /* @__PURE__ */ jsx(
3733
+ AwaitingDepositStatus,
3734
+ {
3735
+ title: "Deposit detected",
3736
+ subtitle: "Crediting the incoming transaction to your Privana balance..."
3737
+ }
3738
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
3739
+ /* @__PURE__ */ jsx(
3740
+ AwaitingDepositStatus,
3741
+ {
3742
+ title: "Awaiting Deposit",
3743
+ subtitle: listening ? "We are listening for your incoming transaction for the next hour." : "We've stopped listening automatically. Use the button below to check for your deposit.",
3744
+ remaining: listening ? formatCountdown(secondsLeft) : void 0
3745
+ }
3746
+ ),
3747
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3748
+ /* @__PURE__ */ jsx(
3749
+ "button",
3750
+ {
3751
+ type: "button",
3752
+ onClick: handleRefresh,
3753
+ disabled: !depositAddress || isScanning,
3754
+ className: "bg-secondary text-foreground hover:bg-secondary/80 flex h-10 w-full 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",
3755
+ children: "Refresh deposit status"
3756
+ }
3757
+ ),
3758
+ isRateLimited ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Checked too recently \u2014 try again in a moment." }) : isUnavailable ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Deposit discovery is unavailable for this chain." }) : isScanError ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Chain temporarily unreachable \u2014 retrying automatically." }) : nothingFound ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", children: "No incoming transaction found yet." }) : null
3759
+ ] })
3707
3760
  ] })
3708
3761
  ] });
3709
3762
  }
@@ -3716,6 +3769,7 @@ function DepositModalContent({
3716
3769
  onConnectWallet,
3717
3770
  onDeposit,
3718
3771
  onDepositSuccess,
3772
+ onLockFailed,
3719
3773
  onClose,
3720
3774
  onCloseBlockedChange,
3721
3775
  onExit
@@ -3747,6 +3801,21 @@ function DepositModalContent({
3747
3801
  const [showSuccess, setShowSuccess] = useState(false);
3748
3802
  const [showTimeout, setShowTimeout] = useState(false);
3749
3803
  const [cancelled, setCancelled] = useState(false);
3804
+ const [isSubmittingLock, setIsSubmittingLock] = useState(false);
3805
+ const [lockFailedMessage, setLockFailedMessage] = useState(null);
3806
+ const finishDeposit = () => {
3807
+ setAmount("");
3808
+ if (onDepositSuccess) {
3809
+ resetDeposit();
3810
+ onDepositSuccess();
3811
+ } else {
3812
+ setShowSuccess(true);
3813
+ }
3814
+ };
3815
+ const finishCardPurchase = () => {
3816
+ setAmount("");
3817
+ onDepositSuccess?.();
3818
+ };
3750
3819
  const {
3751
3820
  txHash,
3752
3821
  isGettingAddress,
@@ -3761,14 +3830,24 @@ function DepositModalContent({
3761
3830
  retryVerification,
3762
3831
  reset: resetDeposit
3763
3832
  } = useDeposit({
3764
- onCredited: () => {
3765
- setAmount("");
3766
- if (onDepositSuccess) {
3767
- resetDeposit();
3768
- onDepositSuccess();
3769
- } else {
3770
- setShowSuccess(true);
3833
+ onCredited: (_txHash, _response, lockPending) => {
3834
+ if (lockPending) {
3835
+ setIsSubmittingLock(true);
3836
+ return;
3771
3837
  }
3838
+ finishDeposit();
3839
+ },
3840
+ onLockSubmitted: () => {
3841
+ setIsSubmittingLock(false);
3842
+ finishDeposit();
3843
+ },
3844
+ // The deposit credited; only the policy lock failed. Success must not
3845
+ // fire (the host would act on unlocked funds) — show the dedicated
3846
+ // error view and let the host re-prompt for a fresh lock.
3847
+ onLockFailed: (err) => {
3848
+ setIsSubmittingLock(false);
3849
+ setLockFailedMessage(err.message);
3850
+ onLockFailed?.(err);
3772
3851
  },
3773
3852
  onCheckTimeout: () => {
3774
3853
  setAmount("");
@@ -3801,7 +3880,11 @@ function DepositModalContent({
3801
3880
  setCancelled(false);
3802
3881
  deposit({
3803
3882
  tokenId: token.id,
3804
- amount: parseTokenAmount(args.amount, token.decimals)
3883
+ amount: parseTokenAmount(args.amount, token.decimals),
3884
+ postDepositLock: allowance ? {
3885
+ maxAmount: BigInt(allowance.value),
3886
+ lockDuration: allowance.lockDuration
3887
+ } : void 0
3805
3888
  }).catch((err) => {
3806
3889
  toast.error(err instanceof Error ? err.message : "Deposit failed");
3807
3890
  });
@@ -3811,26 +3894,32 @@ function DepositModalContent({
3811
3894
  const depositSteps = [
3812
3895
  {
3813
3896
  label: "Getting deposit address",
3814
- status: isGettingAddress ? "active" : isSwitchingChain || isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
3897
+ status: isGettingAddress ? "active" : isSwitchingChain || isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing || isSubmittingLock ? "completed" : "pending"
3815
3898
  },
3816
3899
  {
3817
3900
  label: `Switching to ${targetChain?.name ?? "deposit chain"}`,
3818
- status: isSwitchingChain ? "active" : isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
3901
+ status: isSwitchingChain ? "active" : isSendingTransaction || isWaitingForConfirmation || isWaitingForProcessing || isSubmittingLock ? "completed" : "pending"
3819
3902
  },
3820
3903
  {
3821
3904
  label: "Confirm in wallet",
3822
- status: isSendingTransaction ? "active" : isWaitingForConfirmation || isWaitingForProcessing ? "completed" : "pending"
3905
+ status: isSendingTransaction ? "active" : isWaitingForConfirmation || isWaitingForProcessing || isSubmittingLock ? "completed" : "pending"
3823
3906
  },
3824
3907
  {
3825
3908
  label: "Confirming transaction",
3826
- status: isWaitingForConfirmation ? "active" : isWaitingForProcessing ? "completed" : "pending"
3909
+ status: isWaitingForConfirmation ? "active" : isWaitingForProcessing || isSubmittingLock ? "completed" : "pending"
3827
3910
  },
3828
3911
  {
3829
3912
  label: "Verifying deposit \u2014 may take up to a few minutes",
3830
- status: isWaitingForProcessing ? "active" : "pending"
3831
- }
3913
+ status: isWaitingForProcessing ? "active" : isSubmittingLock ? "completed" : "pending"
3914
+ },
3915
+ ...allowance ? [
3916
+ {
3917
+ label: `Locking funds for ${appName}`,
3918
+ status: isSubmittingLock ? "active" : "pending"
3919
+ }
3920
+ ] : []
3832
3921
  ];
3833
- const flowView = showSuccess ? "deposit-success" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled ? "depositing" : null;
3922
+ const flowView = showSuccess ? "deposit-success" : lockFailedMessage ? "lock-error" : showTimeout ? "deposit-timeout" : verificationFailed ? "deposit-error" : isPending && !cancelled || isSubmittingLock ? "depositing" : null;
3834
3923
  const activeView = flowView ?? view;
3835
3924
  const handleDepositDone = () => {
3836
3925
  setShowSuccess(false);
@@ -3842,6 +3931,12 @@ function DepositModalContent({
3842
3931
  setCancelled(true);
3843
3932
  resetDeposit();
3844
3933
  };
3934
+ const handleLockFailedDone = () => {
3935
+ setLockFailedMessage(null);
3936
+ setAmount("");
3937
+ setCancelled(false);
3938
+ resetDeposit();
3939
+ };
3845
3940
  const handleDismissVerificationError = () => {
3846
3941
  setAmount("");
3847
3942
  setCancelled(false);
@@ -3895,6 +3990,14 @@ function DepositModalContent({
3895
3990
  onDone: handleDepositDone
3896
3991
  }
3897
3992
  ),
3993
+ activeView === "lock-error" && /* @__PURE__ */ jsx(
3994
+ TransactionWarningView,
3995
+ {
3996
+ title: "Deposit credited, lock failed",
3997
+ message: `Your deposit was credited but locking the funds for ${appName} failed: ${lockFailedMessage}`,
3998
+ onDone: handleLockFailedDone
3999
+ }
4000
+ ),
3898
4001
  activeView === "deposit-timeout" && /* @__PURE__ */ jsx(
3899
4002
  TransactionWarningView,
3900
4003
  {
@@ -3988,8 +4091,28 @@ function DepositModalContent({
3988
4091
  }
3989
4092
  }
3990
4093
  ),
3991
- activeView === "external-deposit" && /* @__PURE__ */ jsx(ExternalDepositView, { token: selectedToken, amount }),
3992
- activeView === "credit-card-widget" && /* @__PURE__ */ jsx(MoonPayGate, { enabled: true, children: /* @__PURE__ */ jsx(CreditCardWidgetView, { token: selectedToken, amount, allowance }) })
4094
+ activeView === "external-deposit" && /* @__PURE__ */ jsx(
4095
+ ExternalDepositView,
4096
+ {
4097
+ token: selectedToken,
4098
+ amount,
4099
+ onCredited: allowance ? void 0 : onDepositSuccess
4100
+ }
4101
+ ),
4102
+ activeView === "credit-card-widget" && /* @__PURE__ */ jsx(MoonPayGate, { enabled: true, children: /* @__PURE__ */ jsx(
4103
+ CreditCardWidgetView,
4104
+ {
4105
+ token: selectedToken,
4106
+ amount,
4107
+ allowance,
4108
+ onCredited: allowance ? void 0 : finishCardPurchase,
4109
+ onLockSubmitted: finishCardPurchase,
4110
+ onLockFailed: (err) => {
4111
+ setLockFailedMessage(err.message);
4112
+ onLockFailed?.(err);
4113
+ }
4114
+ }
4115
+ ) })
3993
4116
  ] });
3994
4117
  }
3995
4118
  function DepositModal({ open, onClose, ...handlers }) {
@@ -4432,7 +4555,7 @@ function WalletBalanceView({
4432
4555
  allowance,
4433
4556
  amount,
4434
4557
  onAmountChange,
4435
- onPlay,
4558
+ onFundSession,
4436
4559
  onAddFunds,
4437
4560
  onWithdraw
4438
4561
  }) {
@@ -4457,18 +4580,17 @@ function WalletBalanceView({
4457
4580
  const hasValidAmount = !!amount && parseFloat(amount) > 0;
4458
4581
  const tooManyDecimals = hasValidAmount && !!token && amount.includes(".") && amount.split(".")[1].length > token.decimals;
4459
4582
  const exceedsBalance = hasValidAmount && !tooManyDecimals && !!token && !isBalanceLoading && !isBalanceError && parseTokenAmount(amount, token.decimals) > availableWei;
4460
- const canPlay = hasValidAmount && !!token && !tooManyDecimals && !exceedsBalance && !isBalanceLoading && !isBalanceError;
4583
+ const canFundSession = hasValidAmount && !!token && !tooManyDecimals && !exceedsBalance && !isBalanceLoading && !isBalanceError;
4461
4584
  const handleMax = () => {
4462
4585
  const max = availableFormatted.replace(/\s/g, "");
4463
4586
  if (parseFloat(max) > 0) onAmountChange(max);
4464
4587
  };
4465
- const handlePlay = () => {
4466
- if (!token || !canPlay || !onPlay) return;
4467
- onPlay({ tokenId: token.id, amount });
4468
- onAmountChange("");
4588
+ const handleFundSession = () => {
4589
+ if (!token || !canFundSession || !onFundSession) return;
4590
+ onFundSession({ tokenId: token.id, amount });
4469
4591
  };
4470
4592
  const showInput = variant !== "fully-in-use";
4471
- const showInlinePlay = variant === "session-zero" || variant === "mixed";
4593
+ const showInlineFund = variant === "session-zero" || variant === "mixed";
4472
4594
  return /* @__PURE__ */ jsxs("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: [
4473
4595
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
4474
4596
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -4520,14 +4642,14 @@ function WalletBalanceView({
4520
4642
  }
4521
4643
  )
4522
4644
  ] }),
4523
- showInlinePlay && /* @__PURE__ */ jsx(
4645
+ showInlineFund && /* @__PURE__ */ jsx(
4524
4646
  "button",
4525
4647
  {
4526
4648
  type: "button",
4527
- disabled: !onPlay || !canPlay,
4528
- onClick: handlePlay,
4649
+ disabled: !onFundSession || !canFundSession,
4650
+ onClick: handleFundSession,
4529
4651
  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",
4530
- children: "Play"
4652
+ children: "Fund session"
4531
4653
  }
4532
4654
  )
4533
4655
  ] }),
@@ -4551,10 +4673,10 @@ function WalletBalanceView({
4551
4673
  "button",
4552
4674
  {
4553
4675
  type: "button",
4554
- disabled: !onPlay || !canPlay,
4555
- onClick: handlePlay,
4676
+ disabled: !onFundSession || !canFundSession,
4677
+ onClick: handleFundSession,
4556
4678
  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",
4557
- children: "Play"
4679
+ children: "Fund session"
4558
4680
  }
4559
4681
  ),
4560
4682
  /* @__PURE__ */ jsx(
@@ -4591,10 +4713,34 @@ function WalletBalanceView({
4591
4713
  ] }) })
4592
4714
  ] });
4593
4715
  }
4716
+ function invokeGuarded(runRef, invoke, handlers) {
4717
+ const run = ++runRef.current;
4718
+ const isCurrent = () => runRef.current === run;
4719
+ let result;
4720
+ try {
4721
+ result = invoke();
4722
+ } catch (err) {
4723
+ if (isCurrent()) handlers.onError(err);
4724
+ return;
4725
+ }
4726
+ if (result == null || typeof result.then !== "function") {
4727
+ handlers.onSyncResult();
4728
+ return;
4729
+ }
4730
+ handlers.onAsyncStart();
4731
+ result.then(
4732
+ () => {
4733
+ if (isCurrent()) handlers.onSuccess();
4734
+ },
4735
+ (err) => {
4736
+ if (isCurrent()) handlers.onError(err);
4737
+ }
4738
+ );
4739
+ }
4594
4740
  function WalletModalContent({
4595
4741
  session,
4596
4742
  allowance,
4597
- onPlay,
4743
+ onFundSession,
4598
4744
  onEndSession,
4599
4745
  onDepositSuccess,
4600
4746
  onClose,
@@ -4609,7 +4755,10 @@ function WalletModalContent({
4609
4755
  const [depositBlocked, setDepositBlocked] = useState(false);
4610
4756
  const [withdrawPending, setWithdrawPending] = useState(false);
4611
4757
  const [endSessionError, setEndSessionError] = useState(null);
4758
+ const [fundSessionError, setFundSessionError] = useState(null);
4612
4759
  const endSessionRunRef = useRef(0);
4760
+ const fundSessionRunRef = useRef(0);
4761
+ const [pendingFundSession, setPendingFundSession] = useState(null);
4613
4762
  const prevAddressRef = useRef(address);
4614
4763
  useEffect(() => {
4615
4764
  const prev = prevAddressRef.current;
@@ -4621,10 +4770,13 @@ function WalletModalContent({
4621
4770
  setDepositBlocked(false);
4622
4771
  setWithdrawPending(false);
4623
4772
  setEndSessionError(null);
4773
+ setFundSessionError(null);
4774
+ setPendingFundSession(null);
4624
4775
  endSessionRunRef.current++;
4776
+ fundSessionRunRef.current++;
4625
4777
  }
4626
4778
  }, [address, hostedAuthConfig]);
4627
- const closeBlocked = depositBlocked || withdrawPending || view === "ending-session";
4779
+ const closeBlocked = depositBlocked || withdrawPending || view === "funding-session" || view === "ending-session";
4628
4780
  useEffect(() => {
4629
4781
  onCloseBlockedChange?.(closeBlocked);
4630
4782
  }, [closeBlocked, onCloseBlockedChange]);
@@ -4638,20 +4790,16 @@ function WalletModalContent({
4638
4790
  };
4639
4791
  const startEndSession = () => {
4640
4792
  if (!onEndSession) return;
4641
- const run = ++endSessionRunRef.current;
4642
4793
  setEndSessionError(null);
4643
- setView("ending-session");
4644
- Promise.resolve().then(() => onEndSession()).then(
4645
- () => {
4646
- if (endSessionRunRef.current !== run) return;
4647
- setView("withdraw");
4648
- },
4649
- (err) => {
4650
- if (endSessionRunRef.current !== run) return;
4794
+ invokeGuarded(endSessionRunRef, () => onEndSession(), {
4795
+ onSyncResult: () => setView("withdraw"),
4796
+ onAsyncStart: () => setView("ending-session"),
4797
+ onSuccess: () => setView("withdraw"),
4798
+ onError: (err) => {
4651
4799
  setEndSessionError(err instanceof Error ? err.message : null);
4652
4800
  setView("end-session-error");
4653
4801
  }
4654
- );
4802
+ });
4655
4803
  };
4656
4804
  const handleWithdraw = () => {
4657
4805
  if (session && onEndSession) {
@@ -4665,6 +4813,36 @@ function WalletModalContent({
4665
4813
  setEndSessionError(null);
4666
4814
  setView("balance");
4667
4815
  };
4816
+ const fundSession = (args) => {
4817
+ if (!onFundSession) return;
4818
+ setFundSessionError(null);
4819
+ setPendingFundSession(args);
4820
+ setAmount("");
4821
+ invokeGuarded(fundSessionRunRef, () => onFundSession(args), {
4822
+ onSyncResult: () => {
4823
+ setPendingFundSession(null);
4824
+ setView("balance");
4825
+ },
4826
+ onAsyncStart: () => setView("funding-session"),
4827
+ onSuccess: () => {
4828
+ setPendingFundSession(null);
4829
+ setView("balance");
4830
+ },
4831
+ onError: (err) => {
4832
+ setFundSessionError(err instanceof Error ? err.message : null);
4833
+ setView("fund-session-error");
4834
+ }
4835
+ });
4836
+ };
4837
+ const dismissFundSessionError = () => {
4838
+ fundSessionRunRef.current++;
4839
+ setFundSessionError(null);
4840
+ if (pendingFundSession) {
4841
+ setAmount(pendingFundSession.amount);
4842
+ setPendingFundSession(null);
4843
+ }
4844
+ setView("balance");
4845
+ };
4668
4846
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4669
4847
  onClose && /* @__PURE__ */ jsx(
4670
4848
  "button",
@@ -4680,7 +4858,7 @@ function WalletModalContent({
4680
4858
  children: /* @__PURE__ */ jsx(CloseIcon, {})
4681
4859
  }
4682
4860
  ),
4683
- (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 }) }),
4861
+ (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 }) }),
4684
4862
  view === "balance" && /* @__PURE__ */ jsx(
4685
4863
  WalletBalanceView,
4686
4864
  {
@@ -4688,11 +4866,30 @@ function WalletModalContent({
4688
4866
  allowance,
4689
4867
  amount,
4690
4868
  onAmountChange: setAmount,
4691
- onPlay,
4869
+ onFundSession: onFundSession ? fundSession : void 0,
4692
4870
  onAddFunds: () => setView("deposit"),
4693
4871
  onWithdraw: handleWithdraw
4694
4872
  }
4695
4873
  ),
4874
+ view === "funding-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
4875
+ TransactionProgressView,
4876
+ {
4877
+ title: "Funding session...",
4878
+ steps: [{ label: "Waiting for the session to be funded", status: "active" }]
4879
+ }
4880
+ ) }),
4881
+ view === "fund-session-error" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
4882
+ TransactionErrorView,
4883
+ {
4884
+ title: "Could not fund session",
4885
+ message: fundSessionError ? `Your session could not be funded. (${fundSessionError})` : "Your session could not be funded.",
4886
+ onRetry: () => {
4887
+ if (pendingFundSession) fundSession(pendingFundSession);
4888
+ },
4889
+ onDismiss: dismissFundSessionError,
4890
+ retryLabel: "Try again"
4891
+ }
4892
+ ) }),
4696
4893
  view === "ending-session" && /* @__PURE__ */ jsx("div", { className: "bg-muted flex flex-col gap-6 rounded-[10px] p-5", children: /* @__PURE__ */ jsx(
4697
4894
  TransactionProgressView,
4698
4895
  {
@@ -4706,7 +4903,8 @@ function WalletModalContent({
4706
4903
  title: "Could not end session",
4707
4904
  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.",
4708
4905
  onRetry: startEndSession,
4709
- onDismiss: dismissEndSessionError
4906
+ onDismiss: dismissEndSessionError,
4907
+ retryLabel: "Try again"
4710
4908
  }
4711
4909
  ) }),
4712
4910
  view === "withdraw" && /* @__PURE__ */ jsx(WithdrawModalContent, { onBack: exitWithdraw, onPendingChange: setWithdrawPending }),
@@ -4780,6 +4978,6 @@ function WalletInlineModal({ className, ...handlers }) {
4780
4978
  );
4781
4979
  }
4782
4980
 
4783
- export { DepositInlineModal, DepositModal, LOCK_TYPES, MODIFY_LOCK_TYPES, PrivanaButton, PrivanaIcon, PrivanaInlineModal, PrivanaModal, TRANSFER_LOCKED_TYPES, TRANSFER_TYPES, WITHDRAW_FROM_LOCK_TYPES, WITHDRAW_TYPES, WalletInlineModal, WalletModal, WithdrawInlineModal, WithdrawModal, createDomain, createLockExpiry, getChainIcon, getTokenIcon, signLockMessage, signModifyLockMessage, signTransferLockedMessage, signTransferMessage, signWithdrawFromLockMessage, signWithdrawMessage, useBalance, useBatchBalances, useDeposit, useDepositAddress, useExpiredLocks, useHistory, useHostedRedirectAuth, useLockFunds, useLockedFunds, useModifyLock, usePendingWithdrawals, usePrivanaClient, useTokenInfo, useTokenList, useTotalLockedBalance, useTransfer, useUnlockFunds, useWithdraw };
4981
+ export { DepositInlineModal, DepositModal, PrivanaButton, PrivanaIcon, PrivanaInlineModal, PrivanaModal, WalletInlineModal, WalletModal, WithdrawInlineModal, WithdrawModal, getChainIcon, getTokenIcon, useBalance, useBatchBalances, useDeposit, useDepositAddress, useExpiredLocks, useHistory, useHostedRedirectAuth, useLockFunds, useLockedFunds, useModifyLock, usePendingDeposits, usePendingWithdrawals, usePrivanaClient, useTokenInfo, useTokenList, useTotalLockedBalance, useTransfer, useUnlockFunds, useWithdraw };
4784
4982
  //# sourceMappingURL=index.js.map
4785
4983
  //# sourceMappingURL=index.js.map