@rhinestone/deposit-modal 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1955,6 +1955,7 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
1955
1955
  var INITIAL_POLL_INTERVAL = 3e3;
1956
1956
  var MAX_POLL_INTERVAL = 3e4;
1957
1957
  var BACKOFF_MULTIPLIER = 1.5;
1958
+ var PROCESS_TIMEOUT_MS = 10 * 60 * 1e3;
1958
1959
  function ProcessingStep({
1959
1960
  publicClient,
1960
1961
  smartAccount,
@@ -2010,6 +2011,13 @@ function ProcessingStep({
2010
2011
  }
2011
2012
  try {
2012
2013
  await publicClient.waitForTransactionReceipt({ hash: txHash });
2014
+ } catch (error) {
2015
+ const message = error instanceof Error ? error.message : "Transaction not confirmed";
2016
+ setState({ type: "error", message });
2017
+ onError?.(message, "PROCESS_ERROR");
2018
+ return;
2019
+ }
2020
+ try {
2013
2021
  const result = await service.processDeposit(smartAccount, {
2014
2022
  chainId: sourceChain,
2015
2023
  token: sourceToken,
@@ -2017,22 +2025,24 @@ function ProcessingStep({
2017
2025
  txHash,
2018
2026
  sender
2019
2027
  });
2020
- if (result.status === 202 || result.message.includes("Funds are already on the target chain")) {
2028
+ if (result.message.includes("Funds are already on the target chain")) {
2021
2029
  if (sameChainAndToken) {
2022
2030
  setState({ type: "complete" });
2023
2031
  onDepositComplete?.(txHash);
2024
- } else {
2025
- const message = `Processor returned same-chain response. Re-register the account for target chain ${targetChain}.`;
2026
- setState({ type: "error", message });
2027
- onError?.(message, "TARGET_CHAIN_MISMATCH");
2032
+ return;
2028
2033
  }
2034
+ setState({ type: "processing" });
2035
+ return;
2036
+ }
2037
+ if (result.status === 202) {
2038
+ setState({ type: "processing" });
2029
2039
  return;
2030
2040
  }
2031
2041
  setState({ type: "processing" });
2032
2042
  } catch (error) {
2033
2043
  const message = error instanceof Error ? error.message : "Process failed";
2034
- setState({ type: "error", message });
2035
- onError?.(message, "PROCESS_ERROR");
2044
+ setState({ type: "processing", warning: message });
2045
+ onError?.(message, "PROCESS_FALLBACK");
2036
2046
  }
2037
2047
  }
2038
2048
  triggerProcess();
@@ -2051,7 +2061,8 @@ function ProcessingStep({
2051
2061
  onError
2052
2062
  ]);
2053
2063
  const pollIntervalRef = (0, import_react6.useRef)(INITIAL_POLL_INTERVAL);
2054
- const timeoutRef = (0, import_react6.useRef)(null);
2064
+ const pollTimeoutRef = (0, import_react6.useRef)(null);
2065
+ const processTimeoutRef = (0, import_react6.useRef)(null);
2055
2066
  (0, import_react6.useEffect)(() => {
2056
2067
  if (state.type !== "processing") {
2057
2068
  pollIntervalRef.current = INITIAL_POLL_INTERVAL;
@@ -2099,7 +2110,7 @@ function ProcessingStep({
2099
2110
  }
2100
2111
  function scheduleNextPoll() {
2101
2112
  if (!isMounted) return;
2102
- timeoutRef.current = setTimeout(() => {
2113
+ pollTimeoutRef.current = setTimeout(() => {
2103
2114
  pollIntervalRef.current = Math.min(
2104
2115
  pollIntervalRef.current * BACKOFF_MULTIPLIER,
2105
2116
  MAX_POLL_INTERVAL
@@ -2110,8 +2121,8 @@ function ProcessingStep({
2110
2121
  pollStatus();
2111
2122
  return () => {
2112
2123
  isMounted = false;
2113
- if (timeoutRef.current) {
2114
- clearTimeout(timeoutRef.current);
2124
+ if (pollTimeoutRef.current) {
2125
+ clearTimeout(pollTimeoutRef.current);
2115
2126
  }
2116
2127
  };
2117
2128
  }, [
@@ -2124,6 +2135,26 @@ function ProcessingStep({
2124
2135
  onDepositComplete,
2125
2136
  onDepositFailed
2126
2137
  ]);
2138
+ (0, import_react6.useEffect)(() => {
2139
+ if (state.type !== "processing") {
2140
+ if (processTimeoutRef.current) {
2141
+ clearTimeout(processTimeoutRef.current);
2142
+ processTimeoutRef.current = null;
2143
+ }
2144
+ return;
2145
+ }
2146
+ processTimeoutRef.current = setTimeout(() => {
2147
+ const message = "We couldn't confirm your transfer. Please contact support if funds do not arrive.";
2148
+ setState({ type: "error", message });
2149
+ onError?.(message, "PROCESS_TIMEOUT");
2150
+ }, PROCESS_TIMEOUT_MS);
2151
+ return () => {
2152
+ if (processTimeoutRef.current) {
2153
+ clearTimeout(processTimeoutRef.current);
2154
+ processTimeoutRef.current = null;
2155
+ }
2156
+ };
2157
+ }, [state.type, onError]);
2127
2158
  const isError = state.type === "error" || state.type === "failed";
2128
2159
  const isComplete = state.type === "complete";
2129
2160
  const isProcessing = state.type === "triggering" || state.type === "processing";
@@ -2376,6 +2407,27 @@ function ProcessingStep({
2376
2407
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "rs-step-body rs-space-y-3", children: [
2377
2408
  isProcessing && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2378
2409
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "rs-progress-bar", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "rs-progress-bar-fill rs-progress-bar-fill--indeterminate" }) }),
2410
+ state.type === "processing" && state.warning && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "rs-alert rs-alert--warning", children: [
2411
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2412
+ "svg",
2413
+ {
2414
+ className: "rs-alert-icon",
2415
+ viewBox: "0 0 24 24",
2416
+ fill: "none",
2417
+ stroke: "currentColor",
2418
+ strokeWidth: "2",
2419
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2420
+ "path",
2421
+ {
2422
+ strokeLinecap: "round",
2423
+ strokeLinejoin: "round",
2424
+ d: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
2425
+ }
2426
+ )
2427
+ }
2428
+ ),
2429
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "rs-alert-text", children: "We couldn't reach the processor. We'll keep checking for a webhook update." })
2430
+ ] }),
2379
2431
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "rs-alert rs-alert--info", children: [
2380
2432
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2381
2433
  "svg",
@@ -2513,6 +2565,7 @@ function DepositFlow({
2513
2565
  amount: defaultAmount,
2514
2566
  recipient,
2515
2567
  signerAddress = DEFAULT_SIGNER_ADDRESS,
2568
+ forceRegister = true,
2516
2569
  waitForFinalTx = true,
2517
2570
  onRequestConnect,
2518
2571
  connectButtonLabel,
@@ -2529,6 +2582,16 @@ function DepositFlow({
2529
2582
  const [step, setStep] = (0, import_react7.useState)({ type: "setup" });
2530
2583
  const [totalBalanceUsd, setTotalBalanceUsd] = (0, import_react7.useState)(0);
2531
2584
  const targetChainObj = (0, import_react7.useMemo)(() => CHAIN_BY_ID[targetChain], [targetChain]);
2585
+ const lastTargetRef = (0, import_react7.useRef)(null);
2586
+ (0, import_react7.useEffect)(() => {
2587
+ const prev = lastTargetRef.current;
2588
+ if (prev && (prev.chain !== targetChain || prev.token.toLowerCase() !== targetToken.toLowerCase())) {
2589
+ if (step.type !== "processing") {
2590
+ setStep({ type: "setup" });
2591
+ }
2592
+ }
2593
+ lastTargetRef.current = { chain: targetChain, token: targetToken };
2594
+ }, [targetChain, targetToken, step.type]);
2532
2595
  const handleBackFromAmount = (0, import_react7.useCallback)(() => {
2533
2596
  setStep((prev) => {
2534
2597
  if (prev.type !== "amount") return prev;
@@ -2658,6 +2721,7 @@ function DepositFlow({
2658
2721
  targetToken,
2659
2722
  signerAddress,
2660
2723
  recipient,
2724
+ forceRegister,
2661
2725
  service,
2662
2726
  onSetupComplete: handleSetupComplete,
2663
2727
  onConnected: handleConnected,
@@ -2795,6 +2859,7 @@ function DepositModal({
2795
2859
  recipient,
2796
2860
  backendUrl = DEFAULT_BACKEND_URL,
2797
2861
  signerAddress = DEFAULT_SIGNER_ADDRESS,
2862
+ forceRegister = true,
2798
2863
  waitForFinalTx = true,
2799
2864
  onRequestConnect,
2800
2865
  connectButtonLabel,
@@ -2960,6 +3025,7 @@ function DepositModal({
2960
3025
  amount: defaultAmount,
2961
3026
  recipient,
2962
3027
  signerAddress,
3028
+ forceRegister,
2963
3029
  waitForFinalTx,
2964
3030
  onRequestConnect,
2965
3031
  connectButtonLabel,
@@ -3494,13 +3560,6 @@ var import_sdk3 = require("@rhinestone/sdk");
3494
3560
  // src/core/safe.ts
3495
3561
  var import_viem8 = require("viem");
3496
3562
  var SAFE_ABI = [
3497
- {
3498
- type: "function",
3499
- name: "nonce",
3500
- stateMutability: "view",
3501
- inputs: [],
3502
- outputs: [{ name: "", type: "uint256" }]
3503
- },
3504
3563
  {
3505
3564
  type: "function",
3506
3565
  name: "isOwner",
@@ -3545,20 +3604,88 @@ var SAFE_ABI = [
3545
3604
  anonymous: false
3546
3605
  }
3547
3606
  ];
3548
- var SAFE_TX_TYPES = {
3549
- SafeTx: [
3550
- { name: "to", type: "address" },
3551
- { name: "value", type: "uint256" },
3552
- { name: "data", type: "bytes" },
3553
- { name: "operation", type: "uint8" },
3554
- { name: "safeTxGas", type: "uint256" },
3555
- { name: "baseGas", type: "uint256" },
3556
- { name: "gasPrice", type: "uint256" },
3557
- { name: "gasToken", type: "address" },
3558
- { name: "refundReceiver", type: "address" },
3559
- { name: "nonce", type: "uint256" }
3560
- ]
3561
- };
3607
+ async function executeSafeEthTransfer(params) {
3608
+ const {
3609
+ walletClient,
3610
+ publicClient,
3611
+ safeAddress,
3612
+ recipient,
3613
+ amount,
3614
+ chainId
3615
+ } = params;
3616
+ const account = walletClient.account;
3617
+ const chain = walletClient.chain;
3618
+ if (!account || !chain) {
3619
+ throw new Error("Wallet not connected");
3620
+ }
3621
+ if (chain.id !== chainId) {
3622
+ throw new Error(`Switch to ${getChainName(chainId)} to sign`);
3623
+ }
3624
+ const isOwner = await publicClient.readContract({
3625
+ address: safeAddress,
3626
+ abi: SAFE_ABI,
3627
+ functionName: "isOwner",
3628
+ args: [account.address]
3629
+ });
3630
+ if (!isOwner) {
3631
+ throw new Error("Connected wallet is not a Safe owner");
3632
+ }
3633
+ const safeTx = {
3634
+ to: recipient,
3635
+ value: amount,
3636
+ data: "0x",
3637
+ operation: 0,
3638
+ safeTxGas: 0n,
3639
+ baseGas: 0n,
3640
+ gasPrice: 0n,
3641
+ gasToken: import_viem8.zeroAddress,
3642
+ refundReceiver: import_viem8.zeroAddress
3643
+ };
3644
+ const signature = (0, import_viem8.concat)([
3645
+ (0, import_viem8.pad)(account.address, { size: 32 }),
3646
+ (0, import_viem8.pad)((0, import_viem8.toHex)(0), { size: 32 }),
3647
+ (0, import_viem8.toHex)(1, { size: 1 })
3648
+ ]);
3649
+ const txHash = await walletClient.writeContract({
3650
+ account,
3651
+ chain,
3652
+ address: safeAddress,
3653
+ abi: SAFE_ABI,
3654
+ functionName: "execTransaction",
3655
+ args: [
3656
+ safeTx.to,
3657
+ safeTx.value,
3658
+ safeTx.data,
3659
+ safeTx.operation,
3660
+ safeTx.safeTxGas,
3661
+ safeTx.baseGas,
3662
+ safeTx.gasPrice,
3663
+ safeTx.gasToken,
3664
+ safeTx.refundReceiver,
3665
+ signature
3666
+ ]
3667
+ });
3668
+ const receipt = await publicClient.waitForTransactionReceipt({
3669
+ hash: txHash
3670
+ });
3671
+ const safeLogs = receipt.logs.filter(
3672
+ (log) => log.address.toLowerCase() === safeAddress.toLowerCase()
3673
+ );
3674
+ const parsed = (0, import_viem8.parseEventLogs)({
3675
+ abi: SAFE_ABI,
3676
+ logs: safeLogs,
3677
+ strict: false
3678
+ });
3679
+ const failed = parsed.find((log) => log.eventName === "ExecutionFailure");
3680
+ if (failed) {
3681
+ throw new Error("Safe transaction failed");
3682
+ }
3683
+ const succeeded = parsed.find((log) => log.eventName === "ExecutionSuccess");
3684
+ if (!succeeded) {
3685
+ throw new Error("Safe transaction status unavailable");
3686
+ }
3687
+ return { txHash };
3688
+ }
3562
3689
  async function executeSafeErc20Transfer(params) {
3563
3690
  const {
3564
3691
  walletClient,
@@ -3577,19 +3704,12 @@ async function executeSafeErc20Transfer(params) {
3577
3704
  if (chain.id !== chainId) {
3578
3705
  throw new Error(`Switch to ${getChainName(chainId)} to sign`);
3579
3706
  }
3580
- const [isOwner, nonce] = await Promise.all([
3581
- publicClient.readContract({
3582
- address: safeAddress,
3583
- abi: SAFE_ABI,
3584
- functionName: "isOwner",
3585
- args: [account.address]
3586
- }),
3587
- publicClient.readContract({
3588
- address: safeAddress,
3589
- abi: SAFE_ABI,
3590
- functionName: "nonce"
3591
- })
3592
- ]);
3707
+ const isOwner = await publicClient.readContract({
3708
+ address: safeAddress,
3709
+ abi: SAFE_ABI,
3710
+ functionName: "isOwner",
3711
+ args: [account.address]
3712
+ });
3593
3713
  if (!isOwner) {
3594
3714
  throw new Error("Connected wallet is not a Safe owner");
3595
3715
  }
@@ -3607,19 +3727,13 @@ async function executeSafeErc20Transfer(params) {
3607
3727
  baseGas: 0n,
3608
3728
  gasPrice: 0n,
3609
3729
  gasToken: import_viem8.zeroAddress,
3610
- refundReceiver: import_viem8.zeroAddress,
3611
- nonce
3730
+ refundReceiver: import_viem8.zeroAddress
3612
3731
  };
3613
- const signature = await walletClient.signTypedData({
3614
- account,
3615
- domain: {
3616
- chainId,
3617
- verifyingContract: safeAddress
3618
- },
3619
- types: SAFE_TX_TYPES,
3620
- primaryType: "SafeTx",
3621
- message: safeTx
3622
- });
3732
+ const signature = (0, import_viem8.concat)([
3733
+ (0, import_viem8.pad)(account.address, { size: 32 }),
3734
+ (0, import_viem8.pad)((0, import_viem8.toHex)(0), { size: 32 }),
3735
+ (0, import_viem8.toHex)(1, { size: 1 })
3736
+ ]);
3623
3737
  const txHash = await walletClient.writeContract({
3624
3738
  account,
3625
3739
  chain,
@@ -3713,6 +3827,7 @@ function WithdrawFlow({
3713
3827
  decimals
3714
3828
  };
3715
3829
  }, [sourceChain, sourceToken]);
3830
+ const isSourceNative = sourceToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
3716
3831
  const stepIndex = step.type === "form" ? 0 : 1;
3717
3832
  const currentBackHandler = void 0;
3718
3833
  (0, import_react10.useEffect)(() => {
@@ -3789,7 +3904,14 @@ function WithdrawFlow({
3789
3904
  });
3790
3905
  handleConnected(address, smartAccount);
3791
3906
  const amountUnits = (0, import_viem9.parseUnits)(amountValue, asset.decimals);
3792
- const result = await executeSafeErc20Transfer({
3907
+ const result = isSourceNative ? await executeSafeEthTransfer({
3908
+ walletClient,
3909
+ publicClient,
3910
+ safeAddress,
3911
+ recipient: smartAccount,
3912
+ amount: amountUnits,
3913
+ chainId: sourceChain
3914
+ }) : await executeSafeErc20Transfer({
3793
3915
  walletClient,
3794
3916
  publicClient,
3795
3917
  safeAddress,
@@ -3836,6 +3958,7 @@ function WithdrawFlow({
3836
3958
  sourceToken,
3837
3959
  sourceChain,
3838
3960
  onWithdrawSubmitted,
3961
+ isSourceNative,
3839
3962
  handleError
3840
3963
  ]
3841
3964
  );
@@ -3852,15 +3975,23 @@ function WithdrawFlow({
3852
3975
  [onWithdrawFailed]
3853
3976
  );
3854
3977
  const targetChainOptions = (0, import_react10.useMemo)(() => {
3978
+ if (isSourceNative) return SOURCE_CHAINS;
3855
3979
  return SOURCE_CHAINS.filter((chain) => Boolean(getUsdcAddress(chain.id)));
3856
- }, []);
3857
- const handleTargetChainChange = (0, import_react10.useCallback)((chainId) => {
3858
- setTargetChain(chainId);
3859
- const nextToken = getUsdcAddress(chainId);
3860
- if (nextToken) {
3861
- setTargetToken(nextToken);
3862
- }
3863
- }, []);
3980
+ }, [isSourceNative]);
3981
+ const handleTargetChainChange = (0, import_react10.useCallback)(
3982
+ (chainId) => {
3983
+ setTargetChain(chainId);
3984
+ if (isSourceNative) {
3985
+ setTargetToken(NATIVE_TOKEN_ADDRESS);
3986
+ return;
3987
+ }
3988
+ const nextToken = getUsdcAddress(chainId);
3989
+ if (nextToken) {
3990
+ setTargetToken(nextToken);
3991
+ }
3992
+ },
3993
+ [isSourceNative]
3994
+ );
3864
3995
  const handleTargetTokenChange = (0, import_react10.useCallback)((token) => {
3865
3996
  setTargetToken(token);
3866
3997
  }, []);
package/dist/index.d.cts CHANGED
@@ -77,6 +77,7 @@ interface DepositModalProps {
77
77
  recipient?: Address;
78
78
  backendUrl?: string;
79
79
  signerAddress?: Address;
80
+ forceRegister?: boolean;
80
81
  waitForFinalTx?: boolean;
81
82
  onRequestConnect?: () => void;
82
83
  connectButtonLabel?: string;
@@ -135,7 +136,7 @@ interface AssetOption {
135
136
  balanceUsd?: number;
136
137
  }
137
138
 
138
- declare function DepositModal({ walletClient, publicClient, address, targetChain: targetChainProp, targetToken, isOpen, onClose, inline, switchChain, sourceChain: sourceChainProp, sourceToken, defaultAmount, recipient, backendUrl, signerAddress, waitForFinalTx, onRequestConnect, connectButtonLabel, theme, branding, uiConfig, className, onReady, onConnected, onDepositSubmitted, onDepositComplete, onDepositFailed, onError, }: DepositModalProps): react_jsx_runtime.JSX.Element;
139
+ declare function DepositModal({ walletClient, publicClient, address, targetChain: targetChainProp, targetToken, isOpen, onClose, inline, switchChain, sourceChain: sourceChainProp, sourceToken, defaultAmount, recipient, backendUrl, signerAddress, forceRegister, waitForFinalTx, onRequestConnect, connectButtonLabel, theme, branding, uiConfig, className, onReady, onConnected, onDepositSubmitted, onDepositComplete, onDepositFailed, onError, }: DepositModalProps): react_jsx_runtime.JSX.Element;
139
140
  declare namespace DepositModal {
140
141
  var displayName: string;
141
142
  }
package/dist/index.d.ts CHANGED
@@ -77,6 +77,7 @@ interface DepositModalProps {
77
77
  recipient?: Address;
78
78
  backendUrl?: string;
79
79
  signerAddress?: Address;
80
+ forceRegister?: boolean;
80
81
  waitForFinalTx?: boolean;
81
82
  onRequestConnect?: () => void;
82
83
  connectButtonLabel?: string;
@@ -135,7 +136,7 @@ interface AssetOption {
135
136
  balanceUsd?: number;
136
137
  }
137
138
 
138
- declare function DepositModal({ walletClient, publicClient, address, targetChain: targetChainProp, targetToken, isOpen, onClose, inline, switchChain, sourceChain: sourceChainProp, sourceToken, defaultAmount, recipient, backendUrl, signerAddress, waitForFinalTx, onRequestConnect, connectButtonLabel, theme, branding, uiConfig, className, onReady, onConnected, onDepositSubmitted, onDepositComplete, onDepositFailed, onError, }: DepositModalProps): react_jsx_runtime.JSX.Element;
139
+ declare function DepositModal({ walletClient, publicClient, address, targetChain: targetChainProp, targetToken, isOpen, onClose, inline, switchChain, sourceChain: sourceChainProp, sourceToken, defaultAmount, recipient, backendUrl, signerAddress, forceRegister, waitForFinalTx, onRequestConnect, connectButtonLabel, theme, branding, uiConfig, className, onReady, onConnected, onDepositSubmitted, onDepositComplete, onDepositFailed, onError, }: DepositModalProps): react_jsx_runtime.JSX.Element;
139
140
  declare namespace DepositModal {
140
141
  var displayName: string;
141
142
  }
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/DepositModal.tsx
2
- import { useMemo as useMemo5, useEffect as useEffect7, useRef as useRef5, useState as useState7, useCallback as useCallback4 } from "react";
2
+ import { useMemo as useMemo5, useEffect as useEffect7, useRef as useRef6, useState as useState7, useCallback as useCallback4 } from "react";
3
3
 
4
4
  // src/components/ui/Modal.tsx
5
5
  import {
@@ -132,7 +132,7 @@ function Modal({
132
132
  Modal.displayName = "Modal";
133
133
 
134
134
  // src/DepositFlow.tsx
135
- import { useState as useState6, useCallback as useCallback3, useMemo as useMemo4, useEffect as useEffect6 } from "react";
135
+ import { useState as useState6, useCallback as useCallback3, useMemo as useMemo4, useEffect as useEffect6, useRef as useRef5 } from "react";
136
136
 
137
137
  // src/components/ui/Spinner.tsx
138
138
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -1915,6 +1915,7 @@ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-ru
1915
1915
  var INITIAL_POLL_INTERVAL = 3e3;
1916
1916
  var MAX_POLL_INTERVAL = 3e4;
1917
1917
  var BACKOFF_MULTIPLIER = 1.5;
1918
+ var PROCESS_TIMEOUT_MS = 10 * 60 * 1e3;
1918
1919
  function ProcessingStep({
1919
1920
  publicClient,
1920
1921
  smartAccount,
@@ -1970,6 +1971,13 @@ function ProcessingStep({
1970
1971
  }
1971
1972
  try {
1972
1973
  await publicClient.waitForTransactionReceipt({ hash: txHash });
1974
+ } catch (error) {
1975
+ const message = error instanceof Error ? error.message : "Transaction not confirmed";
1976
+ setState({ type: "error", message });
1977
+ onError?.(message, "PROCESS_ERROR");
1978
+ return;
1979
+ }
1980
+ try {
1973
1981
  const result = await service.processDeposit(smartAccount, {
1974
1982
  chainId: sourceChain,
1975
1983
  token: sourceToken,
@@ -1977,22 +1985,24 @@ function ProcessingStep({
1977
1985
  txHash,
1978
1986
  sender
1979
1987
  });
1980
- if (result.status === 202 || result.message.includes("Funds are already on the target chain")) {
1988
+ if (result.message.includes("Funds are already on the target chain")) {
1981
1989
  if (sameChainAndToken) {
1982
1990
  setState({ type: "complete" });
1983
1991
  onDepositComplete?.(txHash);
1984
- } else {
1985
- const message = `Processor returned same-chain response. Re-register the account for target chain ${targetChain}.`;
1986
- setState({ type: "error", message });
1987
- onError?.(message, "TARGET_CHAIN_MISMATCH");
1992
+ return;
1988
1993
  }
1994
+ setState({ type: "processing" });
1995
+ return;
1996
+ }
1997
+ if (result.status === 202) {
1998
+ setState({ type: "processing" });
1989
1999
  return;
1990
2000
  }
1991
2001
  setState({ type: "processing" });
1992
2002
  } catch (error) {
1993
2003
  const message = error instanceof Error ? error.message : "Process failed";
1994
- setState({ type: "error", message });
1995
- onError?.(message, "PROCESS_ERROR");
2004
+ setState({ type: "processing", warning: message });
2005
+ onError?.(message, "PROCESS_FALLBACK");
1996
2006
  }
1997
2007
  }
1998
2008
  triggerProcess();
@@ -2011,7 +2021,8 @@ function ProcessingStep({
2011
2021
  onError
2012
2022
  ]);
2013
2023
  const pollIntervalRef = useRef4(INITIAL_POLL_INTERVAL);
2014
- const timeoutRef = useRef4(null);
2024
+ const pollTimeoutRef = useRef4(null);
2025
+ const processTimeoutRef = useRef4(null);
2015
2026
  useEffect5(() => {
2016
2027
  if (state.type !== "processing") {
2017
2028
  pollIntervalRef.current = INITIAL_POLL_INTERVAL;
@@ -2059,7 +2070,7 @@ function ProcessingStep({
2059
2070
  }
2060
2071
  function scheduleNextPoll() {
2061
2072
  if (!isMounted) return;
2062
- timeoutRef.current = setTimeout(() => {
2073
+ pollTimeoutRef.current = setTimeout(() => {
2063
2074
  pollIntervalRef.current = Math.min(
2064
2075
  pollIntervalRef.current * BACKOFF_MULTIPLIER,
2065
2076
  MAX_POLL_INTERVAL
@@ -2070,8 +2081,8 @@ function ProcessingStep({
2070
2081
  pollStatus();
2071
2082
  return () => {
2072
2083
  isMounted = false;
2073
- if (timeoutRef.current) {
2074
- clearTimeout(timeoutRef.current);
2084
+ if (pollTimeoutRef.current) {
2085
+ clearTimeout(pollTimeoutRef.current);
2075
2086
  }
2076
2087
  };
2077
2088
  }, [
@@ -2084,6 +2095,26 @@ function ProcessingStep({
2084
2095
  onDepositComplete,
2085
2096
  onDepositFailed
2086
2097
  ]);
2098
+ useEffect5(() => {
2099
+ if (state.type !== "processing") {
2100
+ if (processTimeoutRef.current) {
2101
+ clearTimeout(processTimeoutRef.current);
2102
+ processTimeoutRef.current = null;
2103
+ }
2104
+ return;
2105
+ }
2106
+ processTimeoutRef.current = setTimeout(() => {
2107
+ const message = "We couldn't confirm your transfer. Please contact support if funds do not arrive.";
2108
+ setState({ type: "error", message });
2109
+ onError?.(message, "PROCESS_TIMEOUT");
2110
+ }, PROCESS_TIMEOUT_MS);
2111
+ return () => {
2112
+ if (processTimeoutRef.current) {
2113
+ clearTimeout(processTimeoutRef.current);
2114
+ processTimeoutRef.current = null;
2115
+ }
2116
+ };
2117
+ }, [state.type, onError]);
2087
2118
  const isError = state.type === "error" || state.type === "failed";
2088
2119
  const isComplete = state.type === "complete";
2089
2120
  const isProcessing = state.type === "triggering" || state.type === "processing";
@@ -2336,6 +2367,27 @@ function ProcessingStep({
2336
2367
  /* @__PURE__ */ jsxs9("div", { className: "rs-step-body rs-space-y-3", children: [
2337
2368
  isProcessing && /* @__PURE__ */ jsxs9(Fragment2, { children: [
2338
2369
  /* @__PURE__ */ jsx10("div", { className: "rs-progress-bar", children: /* @__PURE__ */ jsx10("div", { className: "rs-progress-bar-fill rs-progress-bar-fill--indeterminate" }) }),
2370
+ state.type === "processing" && state.warning && /* @__PURE__ */ jsxs9("div", { className: "rs-alert rs-alert--warning", children: [
2371
+ /* @__PURE__ */ jsx10(
2372
+ "svg",
2373
+ {
2374
+ className: "rs-alert-icon",
2375
+ viewBox: "0 0 24 24",
2376
+ fill: "none",
2377
+ stroke: "currentColor",
2378
+ strokeWidth: "2",
2379
+ children: /* @__PURE__ */ jsx10(
2380
+ "path",
2381
+ {
2382
+ strokeLinecap: "round",
2383
+ strokeLinejoin: "round",
2384
+ d: "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
2385
+ }
2386
+ )
2387
+ }
2388
+ ),
2389
+ /* @__PURE__ */ jsx10("p", { className: "rs-alert-text", children: "We couldn't reach the processor. We'll keep checking for a webhook update." })
2390
+ ] }),
2339
2391
  /* @__PURE__ */ jsxs9("div", { className: "rs-alert rs-alert--info", children: [
2340
2392
  /* @__PURE__ */ jsx10(
2341
2393
  "svg",
@@ -2473,6 +2525,7 @@ function DepositFlow({
2473
2525
  amount: defaultAmount,
2474
2526
  recipient,
2475
2527
  signerAddress = DEFAULT_SIGNER_ADDRESS,
2528
+ forceRegister = true,
2476
2529
  waitForFinalTx = true,
2477
2530
  onRequestConnect,
2478
2531
  connectButtonLabel,
@@ -2489,6 +2542,16 @@ function DepositFlow({
2489
2542
  const [step, setStep] = useState6({ type: "setup" });
2490
2543
  const [totalBalanceUsd, setTotalBalanceUsd] = useState6(0);
2491
2544
  const targetChainObj = useMemo4(() => CHAIN_BY_ID[targetChain], [targetChain]);
2545
+ const lastTargetRef = useRef5(null);
2546
+ useEffect6(() => {
2547
+ const prev = lastTargetRef.current;
2548
+ if (prev && (prev.chain !== targetChain || prev.token.toLowerCase() !== targetToken.toLowerCase())) {
2549
+ if (step.type !== "processing") {
2550
+ setStep({ type: "setup" });
2551
+ }
2552
+ }
2553
+ lastTargetRef.current = { chain: targetChain, token: targetToken };
2554
+ }, [targetChain, targetToken, step.type]);
2492
2555
  const handleBackFromAmount = useCallback3(() => {
2493
2556
  setStep((prev) => {
2494
2557
  if (prev.type !== "amount") return prev;
@@ -2618,6 +2681,7 @@ function DepositFlow({
2618
2681
  targetToken,
2619
2682
  signerAddress,
2620
2683
  recipient,
2684
+ forceRegister,
2621
2685
  service,
2622
2686
  onSetupComplete: handleSetupComplete,
2623
2687
  onConnected: handleConnected,
@@ -2755,6 +2819,7 @@ function DepositModal({
2755
2819
  recipient,
2756
2820
  backendUrl = DEFAULT_BACKEND_URL,
2757
2821
  signerAddress = DEFAULT_SIGNER_ADDRESS,
2822
+ forceRegister = true,
2758
2823
  waitForFinalTx = true,
2759
2824
  onRequestConnect,
2760
2825
  connectButtonLabel,
@@ -2769,10 +2834,10 @@ function DepositModal({
2769
2834
  onDepositFailed,
2770
2835
  onError
2771
2836
  }) {
2772
- const modalRef = useRef5(null);
2837
+ const modalRef = useRef6(null);
2773
2838
  const [currentStepIndex, setCurrentStepIndex] = useState7(0);
2774
2839
  const [totalBalanceUsd, setTotalBalanceUsd] = useState7(null);
2775
- const backHandlerRef = useRef5(void 0);
2840
+ const backHandlerRef = useRef6(void 0);
2776
2841
  const targetChain = getChainId(targetChainProp);
2777
2842
  const sourceChain = sourceChainProp ? getChainId(sourceChainProp) : void 0;
2778
2843
  const service = useMemo5(() => createDepositService(backendUrl), [backendUrl]);
@@ -2781,7 +2846,7 @@ function DepositModal({
2781
2846
  applyTheme(modalRef.current, theme);
2782
2847
  }
2783
2848
  }, [isOpen, theme]);
2784
- const hasCalledReady = useRef5(false);
2849
+ const hasCalledReady = useRef6(false);
2785
2850
  useEffect7(() => {
2786
2851
  if (isOpen && !hasCalledReady.current) {
2787
2852
  hasCalledReady.current = true;
@@ -2920,6 +2985,7 @@ function DepositModal({
2920
2985
  amount: defaultAmount,
2921
2986
  recipient,
2922
2987
  signerAddress,
2988
+ forceRegister,
2923
2989
  waitForFinalTx,
2924
2990
  onRequestConnect,
2925
2991
  connectButtonLabel,
@@ -2941,13 +3007,13 @@ function DepositModal({
2941
3007
  DepositModal.displayName = "DepositModal";
2942
3008
 
2943
3009
  // src/WithdrawModal.tsx
2944
- import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo8, useRef as useRef7, useState as useState10 } from "react";
3010
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo8, useRef as useRef8, useState as useState10 } from "react";
2945
3011
 
2946
3012
  // src/WithdrawFlow.tsx
2947
3013
  import { useCallback as useCallback6, useEffect as useEffect9, useMemo as useMemo7, useState as useState9 } from "react";
2948
3014
 
2949
3015
  // src/components/steps/WithdrawFormStep.tsx
2950
- import { useCallback as useCallback5, useEffect as useEffect8, useMemo as useMemo6, useRef as useRef6, useState as useState8 } from "react";
3016
+ import { useCallback as useCallback5, useEffect as useEffect8, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
2951
3017
  import { erc20Abi as erc20Abi3, formatUnits as formatUnits5, parseUnits as parseUnits3 } from "viem";
2952
3018
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2953
3019
  function useClickOutside(ref, onClose) {
@@ -2989,9 +3055,9 @@ function WithdrawFormStep({
2989
3055
  const [showChainDropdown, setShowChainDropdown] = useState8(false);
2990
3056
  const [showTokenDropdown, setShowTokenDropdown] = useState8(false);
2991
3057
  const [isSubmitting, setIsSubmitting] = useState8(false);
2992
- const hasAttemptedSwitch = useRef6(false);
2993
- const chainDropdownRef = useRef6(null);
2994
- const tokenDropdownRef = useRef6(null);
3058
+ const hasAttemptedSwitch = useRef7(false);
3059
+ const chainDropdownRef = useRef7(null);
3060
+ const tokenDropdownRef = useRef7(null);
2995
3061
  const publicClientChainId = publicClient.chain?.id;
2996
3062
  useClickOutside(chainDropdownRef, () => setShowChainDropdown(false));
2997
3063
  useClickOutside(tokenDropdownRef, () => setShowTokenDropdown(false));
@@ -3453,19 +3519,15 @@ import { walletClientToAccount as walletClientToAccount2 } from "@rhinestone/sdk
3453
3519
 
3454
3520
  // src/core/safe.ts
3455
3521
  import {
3522
+ concat,
3456
3523
  encodeFunctionData,
3457
3524
  erc20Abi as erc20Abi4,
3525
+ pad,
3458
3526
  parseEventLogs,
3527
+ toHex,
3459
3528
  zeroAddress as zeroAddress2
3460
3529
  } from "viem";
3461
3530
  var SAFE_ABI = [
3462
- {
3463
- type: "function",
3464
- name: "nonce",
3465
- stateMutability: "view",
3466
- inputs: [],
3467
- outputs: [{ name: "", type: "uint256" }]
3468
- },
3469
3531
  {
3470
3532
  type: "function",
3471
3533
  name: "isOwner",
@@ -3510,20 +3572,88 @@ var SAFE_ABI = [
3510
3572
  anonymous: false
3511
3573
  }
3512
3574
  ];
3513
- var SAFE_TX_TYPES = {
3514
- SafeTx: [
3515
- { name: "to", type: "address" },
3516
- { name: "value", type: "uint256" },
3517
- { name: "data", type: "bytes" },
3518
- { name: "operation", type: "uint8" },
3519
- { name: "safeTxGas", type: "uint256" },
3520
- { name: "baseGas", type: "uint256" },
3521
- { name: "gasPrice", type: "uint256" },
3522
- { name: "gasToken", type: "address" },
3523
- { name: "refundReceiver", type: "address" },
3524
- { name: "nonce", type: "uint256" }
3525
- ]
3526
- };
3575
+ async function executeSafeEthTransfer(params) {
3576
+ const {
3577
+ walletClient,
3578
+ publicClient,
3579
+ safeAddress,
3580
+ recipient,
3581
+ amount,
3582
+ chainId
3583
+ } = params;
3584
+ const account = walletClient.account;
3585
+ const chain = walletClient.chain;
3586
+ if (!account || !chain) {
3587
+ throw new Error("Wallet not connected");
3588
+ }
3589
+ if (chain.id !== chainId) {
3590
+ throw new Error(`Switch to ${getChainName(chainId)} to sign`);
3591
+ }
3592
+ const isOwner = await publicClient.readContract({
3593
+ address: safeAddress,
3594
+ abi: SAFE_ABI,
3595
+ functionName: "isOwner",
3596
+ args: [account.address]
3597
+ });
3598
+ if (!isOwner) {
3599
+ throw new Error("Connected wallet is not a Safe owner");
3600
+ }
3601
+ const safeTx = {
3602
+ to: recipient,
3603
+ value: amount,
3604
+ data: "0x",
3605
+ operation: 0,
3606
+ safeTxGas: 0n,
3607
+ baseGas: 0n,
3608
+ gasPrice: 0n,
3609
+ gasToken: zeroAddress2,
3610
+ refundReceiver: zeroAddress2
3611
+ };
3612
+ const signature = concat([
3613
+ pad(account.address, { size: 32 }),
3614
+ pad(toHex(0), { size: 32 }),
3615
+ toHex(1, { size: 1 })
3616
+ ]);
3617
+ const txHash = await walletClient.writeContract({
3618
+ account,
3619
+ chain,
3620
+ address: safeAddress,
3621
+ abi: SAFE_ABI,
3622
+ functionName: "execTransaction",
3623
+ args: [
3624
+ safeTx.to,
3625
+ safeTx.value,
3626
+ safeTx.data,
3627
+ safeTx.operation,
3628
+ safeTx.safeTxGas,
3629
+ safeTx.baseGas,
3630
+ safeTx.gasPrice,
3631
+ safeTx.gasToken,
3632
+ safeTx.refundReceiver,
3633
+ signature
3634
+ ]
3635
+ });
3636
+ const receipt = await publicClient.waitForTransactionReceipt({
3637
+ hash: txHash
3638
+ });
3639
+ const safeLogs = receipt.logs.filter(
3640
+ (log) => log.address.toLowerCase() === safeAddress.toLowerCase()
3641
+ );
3642
+ const parsed = parseEventLogs({
3643
+ abi: SAFE_ABI,
3644
+ logs: safeLogs,
3645
+ strict: false
3646
+ });
3647
+ const failed = parsed.find((log) => log.eventName === "ExecutionFailure");
3648
+ if (failed) {
3649
+ throw new Error("Safe transaction failed");
3650
+ }
3651
+ const succeeded = parsed.find((log) => log.eventName === "ExecutionSuccess");
3652
+ if (!succeeded) {
3653
+ throw new Error("Safe transaction status unavailable");
3654
+ }
3655
+ return { txHash };
3656
+ }
3527
3657
  async function executeSafeErc20Transfer(params) {
3528
3658
  const {
3529
3659
  walletClient,
@@ -3542,19 +3672,12 @@ async function executeSafeErc20Transfer(params) {
3542
3672
  if (chain.id !== chainId) {
3543
3673
  throw new Error(`Switch to ${getChainName(chainId)} to sign`);
3544
3674
  }
3545
- const [isOwner, nonce] = await Promise.all([
3546
- publicClient.readContract({
3547
- address: safeAddress,
3548
- abi: SAFE_ABI,
3549
- functionName: "isOwner",
3550
- args: [account.address]
3551
- }),
3552
- publicClient.readContract({
3553
- address: safeAddress,
3554
- abi: SAFE_ABI,
3555
- functionName: "nonce"
3556
- })
3557
- ]);
3675
+ const isOwner = await publicClient.readContract({
3676
+ address: safeAddress,
3677
+ abi: SAFE_ABI,
3678
+ functionName: "isOwner",
3679
+ args: [account.address]
3680
+ });
3558
3681
  if (!isOwner) {
3559
3682
  throw new Error("Connected wallet is not a Safe owner");
3560
3683
  }
@@ -3572,19 +3695,13 @@ async function executeSafeErc20Transfer(params) {
3572
3695
  baseGas: 0n,
3573
3696
  gasPrice: 0n,
3574
3697
  gasToken: zeroAddress2,
3575
- refundReceiver: zeroAddress2,
3576
- nonce
3698
+ refundReceiver: zeroAddress2
3577
3699
  };
3578
- const signature = await walletClient.signTypedData({
3579
- account,
3580
- domain: {
3581
- chainId,
3582
- verifyingContract: safeAddress
3583
- },
3584
- types: SAFE_TX_TYPES,
3585
- primaryType: "SafeTx",
3586
- message: safeTx
3587
- });
3700
+ const signature = concat([
3701
+ pad(account.address, { size: 32 }),
3702
+ pad(toHex(0), { size: 32 }),
3703
+ toHex(1, { size: 1 })
3704
+ ]);
3588
3705
  const txHash = await walletClient.writeContract({
3589
3706
  account,
3590
3707
  chain,
@@ -3678,6 +3795,7 @@ function WithdrawFlow({
3678
3795
  decimals
3679
3796
  };
3680
3797
  }, [sourceChain, sourceToken]);
3798
+ const isSourceNative = sourceToken.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
3681
3799
  const stepIndex = step.type === "form" ? 0 : 1;
3682
3800
  const currentBackHandler = void 0;
3683
3801
  useEffect9(() => {
@@ -3754,7 +3872,14 @@ function WithdrawFlow({
3754
3872
  });
3755
3873
  handleConnected(address, smartAccount);
3756
3874
  const amountUnits = parseUnits4(amountValue, asset.decimals);
3757
- const result = await executeSafeErc20Transfer({
3875
+ const result = isSourceNative ? await executeSafeEthTransfer({
3876
+ walletClient,
3877
+ publicClient,
3878
+ safeAddress,
3879
+ recipient: smartAccount,
3880
+ amount: amountUnits,
3881
+ chainId: sourceChain
3882
+ }) : await executeSafeErc20Transfer({
3758
3883
  walletClient,
3759
3884
  publicClient,
3760
3885
  safeAddress,
@@ -3801,6 +3926,7 @@ function WithdrawFlow({
3801
3926
  sourceToken,
3802
3927
  sourceChain,
3803
3928
  onWithdrawSubmitted,
3929
+ isSourceNative,
3804
3930
  handleError
3805
3931
  ]
3806
3932
  );
@@ -3817,15 +3943,23 @@ function WithdrawFlow({
3817
3943
  [onWithdrawFailed]
3818
3944
  );
3819
3945
  const targetChainOptions = useMemo7(() => {
3946
+ if (isSourceNative) return SOURCE_CHAINS;
3820
3947
  return SOURCE_CHAINS.filter((chain) => Boolean(getUsdcAddress(chain.id)));
3821
- }, []);
3822
- const handleTargetChainChange = useCallback6((chainId) => {
3823
- setTargetChain(chainId);
3824
- const nextToken = getUsdcAddress(chainId);
3825
- if (nextToken) {
3826
- setTargetToken(nextToken);
3827
- }
3828
- }, []);
3948
+ }, [isSourceNative]);
3949
+ const handleTargetChainChange = useCallback6(
3950
+ (chainId) => {
3951
+ setTargetChain(chainId);
3952
+ if (isSourceNative) {
3953
+ setTargetToken(NATIVE_TOKEN_ADDRESS);
3954
+ return;
3955
+ }
3956
+ const nextToken = getUsdcAddress(chainId);
3957
+ if (nextToken) {
3958
+ setTargetToken(nextToken);
3959
+ }
3960
+ },
3961
+ [isSourceNative]
3962
+ );
3829
3963
  const handleTargetTokenChange = useCallback6((token) => {
3830
3964
  setTargetToken(token);
3831
3965
  }, []);
@@ -3918,10 +4052,10 @@ function WithdrawModal({
3918
4052
  onWithdrawFailed,
3919
4053
  onError
3920
4054
  }) {
3921
- const modalRef = useRef7(null);
4055
+ const modalRef = useRef8(null);
3922
4056
  const [currentStepIndex, setCurrentStepIndex] = useState10(0);
3923
4057
  const [totalBalanceUsd, setTotalBalanceUsd] = useState10(null);
3924
- const backHandlerRef = useRef7(void 0);
4058
+ const backHandlerRef = useRef8(void 0);
3925
4059
  const targetChain = getChainId(targetChainProp);
3926
4060
  const sourceChain = getChainId(sourceChainProp);
3927
4061
  const service = useMemo8(() => createDepositService(backendUrl), [backendUrl]);
@@ -3930,7 +4064,7 @@ function WithdrawModal({
3930
4064
  applyTheme(modalRef.current, theme);
3931
4065
  }
3932
4066
  }, [isOpen, theme]);
3933
- const hasCalledReady = useRef7(false);
4067
+ const hasCalledReady = useRef8(false);
3934
4068
  useEffect10(() => {
3935
4069
  if (isOpen && !hasCalledReady.current) {
3936
4070
  hasCalledReady.current = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/deposit-modal",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "React modal component for Rhinestone cross-chain deposits",
5
5
  "author": "Rhinestone <dev@rhinestone.wtf>",
6
6
  "bugs": {