@oasisprotocol/privana-sdk 0.4.3 → 0.5.0

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.
@@ -1,16 +1,18 @@
1
1
  "use client";
2
2
  import { createContext, useContext, useRef, useCallback, useSyncExternalStore, useState, useEffect, useMemo } from 'react';
3
- import { WagmiContext } from 'wagmi';
3
+ import { WagmiContext, useConfig, useAccount } from 'wagmi';
4
4
  import { watchAccount, getAccount, getWalletClient } from 'wagmi/actions';
5
5
  import { createSiweMessage } from 'viem/siwe';
6
- import { jsx } from 'react/jsx-runtime';
7
- import { zeroAddress, hexToString } from 'viem';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+ import { parseAbiItem, zeroAddress, hexToString, parseUnits, decodeEventLog, formatUnits } from 'viem';
8
8
  import { useQueryClient } from '@tanstack/react-query';
9
9
  import { Slot } from '@radix-ui/react-slot';
10
10
  import { cva } from 'class-variance-authority';
11
11
  import { clsx } from 'clsx';
12
12
  import { twMerge } from 'tailwind-merge';
13
13
  import { getTransactionReceipt as getTransactionReceipt$1, waitForTransactionReceipt as waitForTransactionReceipt$1, getTransaction, call } from 'viem/actions';
14
+ import { Loader2, CircleCheckIcon } from 'lucide-react';
15
+ import { MoonPayBuyWidget } from '@moonpay/moonpay-react';
14
16
 
15
17
  // ../../shared/config.json
16
18
  var config_default = {
@@ -18,12 +20,14 @@ var config_default = {
18
20
  {
19
21
  id: 84532,
20
22
  name: "Base Sepolia",
21
- explorerUrl: "https://sepolia.basescan.org"
23
+ explorerUrl: "https://sepolia.basescan.org",
24
+ explorerName: "BaseScan"
22
25
  },
23
26
  {
24
27
  id: 11155111,
25
28
  name: "Ethereum Sepolia",
26
- explorerUrl: "https://sepolia.etherscan.io"
29
+ explorerUrl: "https://sepolia.etherscan.io",
30
+ explorerName: "Etherscan"
27
31
  }
28
32
  ],
29
33
  networks: {
@@ -74,11 +78,16 @@ function normalizeAddress(value) {
74
78
  var SUPPORTED_CHAINS = config_default.chains.map((chain) => ({
75
79
  id: chain.id,
76
80
  name: chain.name,
77
- explorerUrl: chain.explorerUrl
81
+ explorerUrl: chain.explorerUrl,
82
+ explorerName: chain.explorerName
78
83
  }));
79
84
  function getChainById(chainId) {
80
85
  return SUPPORTED_CHAINS.find((c) => c.id === chainId);
81
86
  }
87
+ function getExplorerLabel(chainId) {
88
+ const chain = getChainById(chainId);
89
+ return `View on ${chain?.explorerName ?? "Explorer"}`;
90
+ }
82
91
  function getExplorerAddressUrl(chainId, address) {
83
92
  const chain = getChainById(chainId);
84
93
  if (!chain) return void 0;
@@ -209,6 +218,11 @@ function isHostedAuthRefreshActive(session, now = Date.now(), skewMs = HOSTED_AU
209
218
  return session.refreshExpiresAt > now + skewMs;
210
219
  }
211
220
 
221
+ // src/sdk/auth/siwe.ts
222
+ function buildSiweStatement(chainId) {
223
+ return `Sign in to Privana on chain ${chainId}`;
224
+ }
225
+
212
226
  // src/sdk/client/errors.ts
213
227
  var AccountingApiError = class _AccountingApiError extends Error {
214
228
  constructor(message, statusCode, detail) {
@@ -663,15 +677,13 @@ function deleteCachedPrivateReadToken(scopeKey) {
663
677
  cache.delete(scopeKey);
664
678
  }
665
679
  var DEFAULT_SIWE_VALIDITY_MS = 24 * 60 * 60 * 1e3;
666
- var DEFAULT_STATEMENT = "Sign in to access your private account data.";
667
680
  var AUTH_REFRESH_SKEW_MS = 3e4;
668
681
  var SiweAuthContext = createContext(null);
669
682
  function SiweAuthProvider({
670
683
  children,
671
684
  client,
672
685
  networkConfig,
673
- autoLogin = true,
674
- statement
686
+ autoLogin = true
675
687
  }) {
676
688
  const wagmiContext = useContext(WagmiContext);
677
689
  const { address, isConnected, status } = useSafeAccount();
@@ -730,7 +742,7 @@ function SiweAuthProvider({
730
742
  uri,
731
743
  version: "1",
732
744
  nonce: nonceRes.nonce,
733
- statement: statement ?? DEFAULT_STATEMENT,
745
+ statement: buildSiweStatement(networkConfig.chainId),
734
746
  issuedAt,
735
747
  expirationTime
736
748
  });
@@ -766,7 +778,7 @@ function SiweAuthProvider({
766
778
  setIsLoading(false);
767
779
  loginInFlight.current = false;
768
780
  }
769
- }, [wagmiContext, address, client, networkConfig.chainId, networkConfig.apiUrl, statement]);
781
+ }, [wagmiContext, address, client, networkConfig.chainId, networkConfig.apiUrl]);
770
782
  const refreshAccessToken = useCallback(async () => {
771
783
  const data = refreshDataRef.current;
772
784
  if (!data || refreshInFlight.current) return;
@@ -838,6 +850,15 @@ function useSiweAuth() {
838
850
  if (!ctx) throw new Error("useSiweAuth must be used within SiweAuthProvider");
839
851
  return ctx;
840
852
  }
853
+
854
+ // src/sdk/moonpay-currency-codes.ts
855
+ var MOONPAY_CURRENCY_CODE_BY_TOKEN_ID = {
856
+ // Sandbox MPT test token (Ethereum Sepolia).
857
+ "0xbd3a41ffd21be1cfcdca7a4e7755842a5b78c9443fb7ea008e6a7314f0caea87": "usdc"
858
+ };
859
+ function resolveMoonpayCurrencyCode(tokenId) {
860
+ return MOONPAY_CURRENCY_CODE_BY_TOKEN_ID[tokenId.toLowerCase()];
861
+ }
841
862
  var PrivanaContext = createContext(null);
842
863
  function readStoredHostedAuthSession(storage, hostedAuthStorageKey, now = Date.now()) {
843
864
  const raw = storage.getItem(hostedAuthStorageKey);
@@ -878,6 +899,8 @@ function PrivanaProvider({
878
899
  chains,
879
900
  pollingInterval = 1e4,
880
901
  serviceAddress,
902
+ serviceName,
903
+ serviceIcon,
881
904
  hostedAuth,
882
905
  siweAuth
883
906
  }) {
@@ -929,7 +952,8 @@ function PrivanaProvider({
929
952
  decimals: t.decimals,
930
953
  contract: t.token_address ?? zeroAddress,
931
954
  name: t.name,
932
- chainId: t.chain_id
955
+ chainId: t.chain_id,
956
+ moonpayCurrencyCode: resolveMoonpayCurrencyCode(t.token_id)
933
957
  }))
934
958
  );
935
959
  setTokensStatus("ready");
@@ -1096,6 +1120,8 @@ function PrivanaProvider({
1096
1120
  tokensError,
1097
1121
  pollingInterval,
1098
1122
  serviceAddress,
1123
+ serviceName,
1124
+ serviceIcon,
1099
1125
  hostedAuthConfig,
1100
1126
  hostedAuthSession,
1101
1127
  setHostedAuthSession,
@@ -1113,6 +1139,8 @@ function PrivanaProvider({
1113
1139
  tokensError,
1114
1140
  pollingInterval,
1115
1141
  serviceAddress,
1142
+ serviceName,
1143
+ serviceIcon,
1116
1144
  hostedAuthConfig,
1117
1145
  hostedAuthSession,
1118
1146
  setHostedAuthSession,
@@ -1125,8 +1153,7 @@ function PrivanaProvider({
1125
1153
  {
1126
1154
  client,
1127
1155
  networkConfig,
1128
- autoLogin: siweAuth.autoLogin,
1129
- statement: siweAuth.statement,
1156
+ autoLogin: typeof siweAuth === "object" ? siweAuth.autoLogin : void 0,
1130
1157
  children
1131
1158
  }
1132
1159
  ) : children });
@@ -1144,7 +1171,6 @@ function useSafePrivanaContext() {
1144
1171
  var INITIAL_AUTH_BACKOFF_MS = 5e3;
1145
1172
  var MAX_AUTH_BACKOFF_MS = 6e4;
1146
1173
  var DEFAULT_SIWE_AUTH_VALIDITY_MS = 24 * 60 * 60 * 1e3;
1147
- var PRIVATE_READ_STATEMENT = "Sign in to Privana to access private account data.";
1148
1174
  var privateReadFailureCache = /* @__PURE__ */ new Map();
1149
1175
  var privateReadInflight = /* @__PURE__ */ new Map();
1150
1176
  async function executeHostedAuthPrivateReadRequest({
@@ -1261,12 +1287,12 @@ function usePrivateReadRequest() {
1261
1287
  const uri = typeof window !== "undefined" && window.location.origin ? window.location.origin : apiUrl;
1262
1288
  const message = createSiweMessage({
1263
1289
  address: walletAddress,
1264
- chainId: walletClient.chain?.id ?? networkConfig.chainId,
1290
+ chainId: networkConfig.chainId,
1265
1291
  domain,
1266
1292
  expirationTime,
1267
1293
  issuedAt,
1268
1294
  nonce: nonceResponse.nonce,
1269
- statement: PRIVATE_READ_STATEMENT,
1295
+ statement: buildSiweStatement(networkConfig.chainId),
1270
1296
  uri,
1271
1297
  version: "1"
1272
1298
  });
@@ -1657,7 +1683,7 @@ function Skeleton({ className, ...props }) {
1657
1683
  "div",
1658
1684
  {
1659
1685
  "data-slot": "skeleton",
1660
- className: cn("bg-accent animate-pulse rounded-md", className),
1686
+ className: cn("bg-secondary animate-pulse rounded-md", className),
1661
1687
  ...props
1662
1688
  }
1663
1689
  );
@@ -1674,7 +1700,10 @@ function getAction(client, actionFn, name) {
1674
1700
  return (params) => actionFn(client, params);
1675
1701
  }
1676
1702
 
1677
- // ../../node_modules/@wagmi/core/dist/esm/actions/getTransactionReceipt.js
1703
+ // ../../node_modules/@wagmi/core/dist/esm/actions/getChainId.js
1704
+ function getChainId2(config) {
1705
+ return config.state.chainId;
1706
+ }
1678
1707
  async function getTransactionReceipt(config, parameters) {
1679
1708
  const { chainId, ...rest } = parameters;
1680
1709
  const client = config.getClient({ chainId });
@@ -1708,12 +1737,1102 @@ async function waitForTransactionReceipt(config, parameters) {
1708
1737
  chainId: client.chain.id
1709
1738
  };
1710
1739
  }
1711
-
1712
- // ../../node_modules/@wagmi/core/dist/esm/actions/getChainId.js
1713
- function getChainId2(config) {
1714
- return config.state.chainId;
1740
+ var DEFAULT_DELIVERY_TIMEOUT_MS = 12e4;
1741
+ var DEFAULT_VERIFICATION_TIMEOUT_MS = 10 * 6e4;
1742
+ var DEFAULT_FINALITY_RETRY_INTERVAL_MS = 15e3;
1743
+ var ERC20_TRANSFER_EVENT = parseAbiItem(
1744
+ "event Transfer(address indexed from, address indexed to, uint256 value)"
1745
+ );
1746
+ function useFiatOnRamp(options) {
1747
+ const { tokenId, onCredited, onError, onDebugEvent } = options;
1748
+ const deliveryTimeout = options.deliveryTimeout ?? DEFAULT_DELIVERY_TIMEOUT_MS;
1749
+ const deliveryPollInterval = options.deliveryPollInterval ?? 3e3;
1750
+ const verificationTimeout = options.verificationTimeout ?? DEFAULT_VERIFICATION_TIMEOUT_MS;
1751
+ const finalityRetryInterval = options.finalityRetryInterval ?? DEFAULT_FINALITY_RETRY_INTERVAL_MS;
1752
+ const { client, enabledTokens } = usePrivanaContext();
1753
+ const { executePrivateRead, privateReadReady } = usePrivateReadRequest();
1754
+ const wagmiConfig = useConfig();
1755
+ const selectedToken = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
1756
+ const [status, setStatus] = useState("idle");
1757
+ const [pending, setPending] = useState([]);
1758
+ const [error, setError] = useState(null);
1759
+ const [depositAddress, setDepositAddress] = useState();
1760
+ const [minDepositBaseUnits, setMinDepositBaseUnits] = useState();
1761
+ const [activeIntentId, setActiveIntentId] = useState(null);
1762
+ const [activeVerificationId, setActiveVerificationId] = useState(null);
1763
+ const [finalityProgress, setFinalityProgress] = useState({});
1764
+ const onCreditedRef = useRef(onCredited);
1765
+ const onErrorRef = useRef(onError);
1766
+ const onDebugEventRef = useRef(onDebugEvent);
1767
+ const statusRef = useRef(status);
1768
+ const activeIntentIdRef = useRef(null);
1769
+ const activeVerificationRecordRef = useRef(null);
1770
+ const activeVerificationKeyRef = useRef(null);
1771
+ const triggeredVerificationKeysRef = useRef(/* @__PURE__ */ new Set());
1772
+ const closeReconcilePromiseRef = useRef(null);
1773
+ const purchaseInitiatedRef = useRef(false);
1774
+ useEffect(() => {
1775
+ onCreditedRef.current = onCredited;
1776
+ onErrorRef.current = onError;
1777
+ onDebugEventRef.current = onDebugEvent;
1778
+ }, [onCredited, onError, onDebugEvent]);
1779
+ useEffect(() => {
1780
+ statusRef.current = status;
1781
+ }, [status]);
1782
+ useEffect(() => {
1783
+ activeIntentIdRef.current = activeIntentId;
1784
+ }, [activeIntentId]);
1785
+ useEffect(() => {
1786
+ activeIntentIdRef.current = null;
1787
+ setActiveIntentId(null);
1788
+ }, [tokenId]);
1789
+ const emitDebug = useCallback(
1790
+ (event, payload) => {
1791
+ onDebugEventRef.current?.({
1792
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1793
+ event,
1794
+ status: statusRef.current,
1795
+ tokenId,
1796
+ payload
1797
+ });
1798
+ },
1799
+ [tokenId]
1800
+ );
1801
+ useEffect(() => {
1802
+ emitDebug("private-read-state", { privateReadReady });
1803
+ }, [emitDebug, privateReadReady]);
1804
+ useEffect(() => {
1805
+ if (!privateReadReady) {
1806
+ emitDebug("deposit-address:skip", { reason: "private-read-not-ready" });
1807
+ setDepositAddress(void 0);
1808
+ setMinDepositBaseUnits(void 0);
1809
+ return;
1810
+ }
1811
+ let cancelled = false;
1812
+ void (async () => {
1813
+ try {
1814
+ emitDebug("deposit-address:request");
1815
+ const resp = await executePrivateRead(() => client.getDepositAddress());
1816
+ if (cancelled) return;
1817
+ setDepositAddress(resp.deposit_address);
1818
+ const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
1819
+ const mins = token ? resp.min_deposit?.[String(token.chainId)] : void 0;
1820
+ if (mins?.erc20) setMinDepositBaseUnits(BigInt(mins.erc20));
1821
+ emitDebug("deposit-address:success", {
1822
+ depositAddress: resp.deposit_address,
1823
+ selectedToken: token ? summariseToken(token) : null,
1824
+ minDepositBaseUnits: mins?.erc20 ?? null
1825
+ });
1826
+ } catch (err) {
1827
+ if (!cancelled) {
1828
+ emitDebug("deposit-address:error", errorPayload(err));
1829
+ console.warn("Failed to fetch Privana deposit address:", err);
1830
+ }
1831
+ }
1832
+ })();
1833
+ return () => {
1834
+ cancelled = true;
1835
+ };
1836
+ }, [client, emitDebug, enabledTokens, executePrivateRead, privateReadReady, tokenId]);
1837
+ const refreshPending = useCallback(async () => {
1838
+ if (!privateReadReady) {
1839
+ emitDebug("pending:skip", { reason: "private-read-not-ready" });
1840
+ setPending([]);
1841
+ return;
1842
+ }
1843
+ try {
1844
+ emitDebug("pending:request");
1845
+ const { pending: rows } = await executePrivateRead(() => client.getPendingOnRamps());
1846
+ setPending(rows);
1847
+ emitDebug("pending:success", {
1848
+ count: rows.length,
1849
+ rows: rows.map(summariseOnRampRecord)
1850
+ });
1851
+ } catch (err) {
1852
+ emitDebug("pending:error", errorPayload(err));
1853
+ console.warn("Failed to load pending on-ramps:", err);
1854
+ }
1855
+ }, [client, emitDebug, executePrivateRead, privateReadReady]);
1856
+ useEffect(() => {
1857
+ refreshPending();
1858
+ }, [refreshPending]);
1859
+ const clearActiveVerification = useCallback(() => {
1860
+ const key = activeVerificationKeyRef.current;
1861
+ if (key) triggeredVerificationKeysRef.current.delete(key);
1862
+ activeVerificationKeyRef.current = null;
1863
+ activeVerificationRecordRef.current = null;
1864
+ setActiveVerificationId(null);
1865
+ }, []);
1866
+ const { verify } = useDepositVerification({
1867
+ pollTimeout: verificationTimeout,
1868
+ pollInterval: options.verificationPollInterval,
1869
+ finalityRetryInterval,
1870
+ onCheckRetry: (message) => {
1871
+ const record = activeVerificationRecordRef.current;
1872
+ if (!record) return;
1873
+ emitDebug("verification:check-retry", {
1874
+ message,
1875
+ record: summariseOnRampRecord(record)
1876
+ });
1877
+ setFinalityProgress((prev) => ({ ...prev, [record.transaction_id]: message }));
1878
+ },
1879
+ onCredited: (depositTxHash) => {
1880
+ const record = activeVerificationRecordRef.current;
1881
+ emitDebug("verification:credited", {
1882
+ depositTxHash,
1883
+ record: record ? summariseOnRampRecord(record) : null
1884
+ });
1885
+ if (record && activeIntentIdRef.current === record.transaction_id) {
1886
+ setStatus("credited");
1887
+ }
1888
+ if (record) {
1889
+ setFinalityProgress((prev) => {
1890
+ if (!(record.transaction_id in prev)) return prev;
1891
+ const next = { ...prev };
1892
+ delete next[record.transaction_id];
1893
+ return next;
1894
+ });
1895
+ }
1896
+ void (async () => {
1897
+ try {
1898
+ if (record && depositTxHash.startsWith("0x")) {
1899
+ emitDebug("onramp:mark-deposit-triggered-request", {
1900
+ transactionId: record.transaction_id,
1901
+ depositTxHash
1902
+ });
1903
+ const updated = await executePrivateRead(
1904
+ () => client.updateOnRamp(record.transaction_id, {
1905
+ deposit_tx_hash: depositTxHash
1906
+ })
1907
+ );
1908
+ emitDebug("onramp:mark-deposit-triggered-success", {
1909
+ record: summariseOnRampRecord(updated)
1910
+ });
1911
+ }
1912
+ } catch (err) {
1913
+ emitDebug("onramp:mark-deposit-triggered-error", errorPayload(err));
1914
+ console.warn("Failed to mark on-ramp row complete:", err);
1915
+ } finally {
1916
+ await refreshPending();
1917
+ clearActiveVerification();
1918
+ if (record && activeIntentIdRef.current === record.transaction_id) {
1919
+ activeIntentIdRef.current = null;
1920
+ setActiveIntentId(null);
1921
+ }
1922
+ }
1923
+ })();
1924
+ onCreditedRef.current?.(depositTxHash);
1925
+ },
1926
+ onCheckTimeout: (depositTxHash) => {
1927
+ const record = activeVerificationRecordRef.current;
1928
+ const err = new Error(
1929
+ "Privana verification is still pending. Retry from the pending on-ramp list if it does not complete."
1930
+ );
1931
+ emitDebug("verification:timeout", { depositTxHash, message: err.message });
1932
+ clearActiveVerification();
1933
+ if (!record || activeIntentIdRef.current === record.transaction_id) {
1934
+ setStatus("failed");
1935
+ setError(err);
1936
+ }
1937
+ void refreshPending();
1938
+ onErrorRef.current?.(err);
1939
+ },
1940
+ onError: (err) => {
1941
+ const record = activeVerificationRecordRef.current;
1942
+ emitDebug("verification:error", errorPayload(err));
1943
+ clearActiveVerification();
1944
+ if (!record || activeIntentIdRef.current === record.transaction_id) {
1945
+ setStatus("failed");
1946
+ setError(err);
1947
+ }
1948
+ onErrorRef.current?.(err);
1949
+ }
1950
+ });
1951
+ const prepareOnRampIntent = useCallback(
1952
+ async ({
1953
+ currencyCode,
1954
+ baseCurrencyCode,
1955
+ baseCurrencyAmount
1956
+ }) => {
1957
+ try {
1958
+ setError(null);
1959
+ purchaseInitiatedRef.current = false;
1960
+ const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
1961
+ if (!token) throw new Error(`Unknown token: ${tokenId}`);
1962
+ if (!depositAddress) throw new Error("Privana deposit address is not ready");
1963
+ emitDebug("intent:create-request", {
1964
+ tokenId,
1965
+ chainId: token.chainId,
1966
+ currencyCode,
1967
+ baseCurrencyCode: baseCurrencyCode ?? null,
1968
+ baseCurrencyAmount: baseCurrencyAmount ?? null,
1969
+ depositAddress
1970
+ });
1971
+ const record = await executePrivateRead(
1972
+ () => client.createOnRampIntent({
1973
+ wallet_address: depositAddress,
1974
+ token_id: tokenId,
1975
+ chain_id: token.chainId,
1976
+ moonpay_currency_code: currencyCode,
1977
+ base_currency_code: baseCurrencyCode,
1978
+ base_currency_amount: baseCurrencyAmount
1979
+ })
1980
+ );
1981
+ activeIntentIdRef.current = record.transaction_id;
1982
+ setActiveIntentId(record.transaction_id);
1983
+ emitDebug("intent:create-success", {
1984
+ record: summariseOnRampRecord(record)
1985
+ });
1986
+ return record;
1987
+ } catch (err) {
1988
+ const e = err instanceof Error ? err : new Error("Failed to create on-ramp intent");
1989
+ setStatus("failed");
1990
+ setError(e);
1991
+ emitDebug("intent:create-error", errorPayload(e));
1992
+ onErrorRef.current?.(e);
1993
+ throw e;
1994
+ }
1995
+ },
1996
+ [client, depositAddress, emitDebug, enabledTokens, executePrivateRead, tokenId]
1997
+ );
1998
+ const registerOnRampTokenMapping = useCallback(
1999
+ async (moonpayTransactionId) => {
2000
+ const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
2001
+ if (!token) {
2002
+ emitDebug("register-token-mapping:skip", {
2003
+ moonpayTransactionId,
2004
+ reason: "selected-token-not-found"
2005
+ });
2006
+ return;
2007
+ }
2008
+ const transactionId = activeIntentIdRef.current ?? moonpayTransactionId;
2009
+ try {
2010
+ emitDebug("register-token-mapping:request", {
2011
+ transactionId,
2012
+ moonpayTransactionId,
2013
+ tokenId,
2014
+ chainId: token.chainId
2015
+ });
2016
+ const record = await executePrivateRead(
2017
+ () => client.updateOnRamp(transactionId, {
2018
+ token_id: tokenId,
2019
+ chain_id: token.chainId,
2020
+ moonpay_transaction_id: transactionId === moonpayTransactionId ? void 0 : moonpayTransactionId
2021
+ })
2022
+ );
2023
+ emitDebug("register-token-mapping:success", {
2024
+ transactionId,
2025
+ moonpayTransactionId,
2026
+ record: summariseOnRampRecord(record)
2027
+ });
2028
+ } catch (err) {
2029
+ emitDebug("register-token-mapping:error", {
2030
+ transactionId,
2031
+ moonpayTransactionId,
2032
+ ...errorPayload(err)
2033
+ });
2034
+ console.warn("Failed to register on-ramp token mapping:", err);
2035
+ }
2036
+ },
2037
+ [client, emitDebug, enabledTokens, executePrivateRead, tokenId]
2038
+ );
2039
+ const handleTransactionCreated = useCallback(
2040
+ async (props) => {
2041
+ emitDebug("moonpay:onTransactionCreated", summariseMoonPayEventProps(props));
2042
+ purchaseInitiatedRef.current = true;
2043
+ await registerOnRampTokenMapping(props.id);
2044
+ },
2045
+ [emitDebug, registerOnRampTokenMapping]
2046
+ );
2047
+ const signUrl = useCallback(
2048
+ async (url) => {
2049
+ setError(null);
2050
+ try {
2051
+ emitDebug("moonpay:onUrlSignatureRequested", summariseMoonPayUrl(url));
2052
+ const { signature } = await executePrivateRead(() => client.signOnRampUrl({ url }));
2053
+ setStatus("awaiting-purchase");
2054
+ emitDebug("sign-url:success", {
2055
+ signatureLength: signature.length
2056
+ });
2057
+ return signature;
2058
+ } catch (err) {
2059
+ const e = err instanceof Error ? err : new Error("Failed to sign on-ramp URL");
2060
+ setStatus("failed");
2061
+ setError(e);
2062
+ emitDebug("sign-url:error", errorPayload(e));
2063
+ onErrorRef.current?.(e);
2064
+ throw err;
2065
+ }
2066
+ },
2067
+ [client, emitDebug, executePrivateRead]
2068
+ );
2069
+ const waitForOnChainHash = useCallback(
2070
+ async (transactionId) => {
2071
+ const startTime = Date.now();
2072
+ emitDebug("delivery-poll:start", {
2073
+ transactionId,
2074
+ deliveryTimeout,
2075
+ deliveryPollInterval
2076
+ });
2077
+ while (Date.now() - startTime < deliveryTimeout) {
2078
+ try {
2079
+ const { pending: rows } = await executePrivateRead(() => client.getPendingOnRamps());
2080
+ setPending(rows);
2081
+ const record = rows.find((r) => matchesOnRampTransaction(r, transactionId));
2082
+ emitDebug("delivery-poll:tick", {
2083
+ transactionId,
2084
+ count: rows.length,
2085
+ matchingRecord: record ? summariseOnRampRecord(record) : null
2086
+ });
2087
+ if (record?.on_chain_tx_hash && record.quote_currency_amount) {
2088
+ emitDebug("delivery-poll:success", {
2089
+ transactionId,
2090
+ record: summariseOnRampRecord(record)
2091
+ });
2092
+ return record;
2093
+ }
2094
+ } catch (err) {
2095
+ emitDebug("delivery-poll:error", {
2096
+ transactionId,
2097
+ ...errorPayload(err)
2098
+ });
2099
+ console.warn("Polling pending on-ramps failed:", err);
2100
+ }
2101
+ await new Promise((r) => setTimeout(r, deliveryPollInterval));
2102
+ }
2103
+ emitDebug("delivery-poll:timeout", { transactionId });
2104
+ return null;
2105
+ },
2106
+ [client, deliveryPollInterval, deliveryTimeout, emitDebug, executePrivateRead]
2107
+ );
2108
+ const triggerVerification = useCallback(
2109
+ async (record) => {
2110
+ const verificationKey = getOnRampVerificationKey(record);
2111
+ if (triggeredVerificationKeysRef.current.has(verificationKey)) {
2112
+ emitDebug("verification:skip-duplicate", {
2113
+ verificationKey,
2114
+ record: summariseOnRampRecord(record)
2115
+ });
2116
+ return;
2117
+ }
2118
+ triggeredVerificationKeysRef.current.add(verificationKey);
2119
+ activeVerificationKeyRef.current = verificationKey;
2120
+ activeVerificationRecordRef.current = record;
2121
+ setActiveVerificationId(record.transaction_id);
2122
+ setFinalityProgress((prev) => {
2123
+ if (!(record.transaction_id in prev)) return prev;
2124
+ const next = { ...prev };
2125
+ delete next[record.transaction_id];
2126
+ return next;
2127
+ });
2128
+ emitDebug("verification:start", {
2129
+ verificationKey,
2130
+ record: summariseOnRampRecord(record)
2131
+ });
2132
+ try {
2133
+ if (!record.on_chain_tx_hash || !record.quote_currency_amount) {
2134
+ throw new Error("On-ramp record missing on-chain tx hash or delivered amount");
2135
+ }
2136
+ if (record.chain_id === void 0 || !record.wallet_address) {
2137
+ throw new Error("On-ramp record missing chain id or wallet address");
2138
+ }
2139
+ const recordTokenId = record.token_id;
2140
+ if (!recordTokenId) {
2141
+ throw new Error("On-ramp record missing token id");
2142
+ }
2143
+ const token = enabledTokens.find((t) => t.id.toLowerCase() === recordTokenId.toLowerCase());
2144
+ if (!token) throw new Error(`Unknown token: ${recordTokenId}`);
2145
+ if (token.chainId !== record.chain_id) {
2146
+ throw new Error(
2147
+ `Token ${recordTokenId} is on chain ${token.chainId} but record is on chain ${record.chain_id}`
2148
+ );
2149
+ }
2150
+ const amount = await resolveDeliveredAmount({
2151
+ onChainTxHash: record.on_chain_tx_hash,
2152
+ chainId: record.chain_id,
2153
+ walletAddress: record.wallet_address,
2154
+ token,
2155
+ fallbackAmount: record.quote_currency_amount,
2156
+ wagmiConfig,
2157
+ emitDebug
2158
+ });
2159
+ if (minDepositBaseUnits !== void 0 && amount < minDepositBaseUnits) {
2160
+ emitDebug("verification:below-minimum", {
2161
+ quoteCurrencyAmount: record.quote_currency_amount,
2162
+ minDepositBaseUnits: String(minDepositBaseUnits)
2163
+ });
2164
+ throw new Error(
2165
+ `Delivered amount (${record.quote_currency_amount}) is below the minimum deposit.`
2166
+ );
2167
+ }
2168
+ if (activeIntentIdRef.current === record.transaction_id) {
2169
+ setStatus("verifying");
2170
+ }
2171
+ emitDebug("verification:check-deposit-request", {
2172
+ hash: record.on_chain_tx_hash,
2173
+ chainId: record.chain_id,
2174
+ amount: amount.toString()
2175
+ });
2176
+ await verify({
2177
+ hash: record.on_chain_tx_hash,
2178
+ chainId: record.chain_id,
2179
+ amount
2180
+ });
2181
+ } catch (err) {
2182
+ triggeredVerificationKeysRef.current.delete(verificationKey);
2183
+ if (activeVerificationKeyRef.current === verificationKey) {
2184
+ activeVerificationKeyRef.current = null;
2185
+ activeVerificationRecordRef.current = null;
2186
+ setActiveVerificationId(null);
2187
+ }
2188
+ throw err;
2189
+ }
2190
+ },
2191
+ [emitDebug, enabledTokens, minDepositBaseUnits, verify, wagmiConfig]
2192
+ );
2193
+ const handleTransactionCompleted = useCallback(
2194
+ async (props) => {
2195
+ emitDebug("moonpay:onTransactionCompleted", summariseMoonPayEventProps(props));
2196
+ try {
2197
+ setStatus("awaiting-delivery");
2198
+ await registerOnRampTokenMapping(props.id);
2199
+ const transactionId = activeIntentIdRef.current ?? props.id;
2200
+ const record = await waitForOnChainHash(transactionId);
2201
+ if (!record) {
2202
+ const err = new Error(
2203
+ "Backend has not yet confirmed delivery. You can finish from the pending list."
2204
+ );
2205
+ emitDebug("moonpay:completed-without-backend-row", {
2206
+ transactionId,
2207
+ moonpayTransactionId: props.id,
2208
+ message: err.message
2209
+ });
2210
+ setStatus("failed");
2211
+ setError(err);
2212
+ onErrorRef.current?.(err);
2213
+ return;
2214
+ }
2215
+ await triggerVerification(record);
2216
+ } catch (err) {
2217
+ const e = err instanceof Error ? err : new Error("Verification failed");
2218
+ emitDebug("moonpay:onTransactionCompleted-error", errorPayload(e));
2219
+ setStatus("failed");
2220
+ setError(e);
2221
+ onErrorRef.current?.(e);
2222
+ }
2223
+ },
2224
+ [emitDebug, registerOnRampTokenMapping, triggerVerification, waitForOnChainHash]
2225
+ );
2226
+ const handleWidgetClosed = useCallback(async () => {
2227
+ if (closeReconcilePromiseRef.current) return closeReconcilePromiseRef.current;
2228
+ closeReconcilePromiseRef.current = (async () => {
2229
+ const previousStatus = statusRef.current;
2230
+ const transactionId = activeIntentIdRef.current;
2231
+ emitDebug("moonpay:widget-closed-reconcile", {
2232
+ previousStatus,
2233
+ transactionId
2234
+ });
2235
+ if (!transactionId) {
2236
+ await refreshPending();
2237
+ return;
2238
+ }
2239
+ if (!purchaseInitiatedRef.current) {
2240
+ emitDebug("moonpay:widget-closed-without-purchase", {
2241
+ previousStatus,
2242
+ transactionId
2243
+ });
2244
+ await refreshPending();
2245
+ if (previousStatus === "awaiting-purchase" || previousStatus === "awaiting-delivery") {
2246
+ setStatus("idle");
2247
+ }
2248
+ return;
2249
+ }
2250
+ try {
2251
+ setStatus("awaiting-delivery");
2252
+ const record = await waitForOnChainHash(transactionId);
2253
+ if (record) {
2254
+ await triggerVerification(record);
2255
+ return;
2256
+ }
2257
+ await refreshPending();
2258
+ if (previousStatus === "awaiting-purchase" || previousStatus === "awaiting-delivery") {
2259
+ setStatus("idle");
2260
+ }
2261
+ } catch (err) {
2262
+ const e = err instanceof Error ? err : new Error("On-ramp reconciliation failed");
2263
+ emitDebug("moonpay:widget-closed-reconcile-error", errorPayload(e));
2264
+ setStatus("failed");
2265
+ setError(e);
2266
+ onErrorRef.current?.(e);
2267
+ }
2268
+ })();
2269
+ try {
2270
+ await closeReconcilePromiseRef.current;
2271
+ } finally {
2272
+ closeReconcilePromiseRef.current = null;
2273
+ }
2274
+ }, [emitDebug, refreshPending, triggerVerification, waitForOnChainHash]);
2275
+ const finishPendingVerification = useCallback(
2276
+ async (record) => {
2277
+ try {
2278
+ emitDebug("pending:finish-verification", {
2279
+ record: summariseOnRampRecord(record)
2280
+ });
2281
+ await triggerVerification(record);
2282
+ } catch (err) {
2283
+ const e = err instanceof Error ? err : new Error("Verification failed");
2284
+ emitDebug("pending:finish-verification-error", errorPayload(e));
2285
+ setStatus("failed");
2286
+ setError(e);
2287
+ onErrorRef.current?.(e);
2288
+ throw e;
2289
+ }
2290
+ },
2291
+ [emitDebug, triggerVerification]
2292
+ );
2293
+ const triggerVerificationRef = useRef(triggerVerification);
2294
+ useEffect(() => {
2295
+ triggerVerificationRef.current = triggerVerification;
2296
+ }, [triggerVerification]);
2297
+ useEffect(() => {
2298
+ let cancelled = false;
2299
+ void (async () => {
2300
+ for (const record of pending) {
2301
+ if (cancelled) break;
2302
+ if (!record.on_chain_tx_hash || !record.quote_currency_amount) continue;
2303
+ const key = getOnRampVerificationKey(record);
2304
+ if (triggeredVerificationKeysRef.current.has(key)) continue;
2305
+ try {
2306
+ await triggerVerificationRef.current(record);
2307
+ } catch {
2308
+ }
2309
+ }
2310
+ })();
2311
+ return () => {
2312
+ cancelled = true;
2313
+ };
2314
+ }, [pending]);
2315
+ return {
2316
+ status,
2317
+ activeIntentId,
2318
+ pending,
2319
+ activeVerificationId,
2320
+ error,
2321
+ finalityProgress,
2322
+ depositAddress,
2323
+ minDepositBaseUnits,
2324
+ selectedToken,
2325
+ prepareOnRampIntent,
2326
+ signUrl,
2327
+ handleTransactionCreated,
2328
+ handleTransactionCompleted,
2329
+ finishPendingVerification,
2330
+ handleWidgetClosed,
2331
+ refreshPending
2332
+ };
2333
+ }
2334
+ function summariseToken(token) {
2335
+ return {
2336
+ tokenId: token.id,
2337
+ chainId: token.chainId,
2338
+ symbol: token.symbol ?? null,
2339
+ decimals: token.decimals ?? null
2340
+ };
2341
+ }
2342
+ function summariseOnRampRecord(record) {
2343
+ return {
2344
+ transaction_id: record.transaction_id,
2345
+ external_transaction_id: record.external_transaction_id ?? null,
2346
+ moonpay_transaction_id: record.moonpay_transaction_id ?? null,
2347
+ status: record.status,
2348
+ wallet_address: record.wallet_address,
2349
+ token_id: record.token_id,
2350
+ chain_id: record.chain_id,
2351
+ moonpay_currency_code: record.moonpay_currency_code ?? null,
2352
+ quote_currency_amount: record.quote_currency_amount ?? null,
2353
+ on_chain_tx_hash: record.on_chain_tx_hash ?? null,
2354
+ deposit_tx_hash: record.deposit_tx_hash ?? null,
2355
+ deposit_triggered_at: record.deposit_triggered_at ?? null,
2356
+ credited_at: record.credited_at ?? null
2357
+ };
2358
+ }
2359
+ async function resolveDeliveredAmount({
2360
+ onChainTxHash,
2361
+ chainId,
2362
+ walletAddress,
2363
+ token,
2364
+ fallbackAmount,
2365
+ wagmiConfig,
2366
+ emitDebug
2367
+ }) {
2368
+ if (token.contract === zeroAddress) {
2369
+ return parseUnits(fallbackAmount, token.decimals);
2370
+ }
2371
+ let receiptError;
2372
+ try {
2373
+ const receipt = await waitForTransactionReceipt(wagmiConfig, {
2374
+ hash: onChainTxHash,
2375
+ chainId,
2376
+ timeout: 6e4,
2377
+ pollingInterval: 4e3
2378
+ });
2379
+ let delivered = 0n;
2380
+ for (const log of receipt.logs) {
2381
+ if (log.address.toLowerCase() !== token.contract.toLowerCase()) continue;
2382
+ try {
2383
+ const decoded = decodeEventLog({
2384
+ abi: [ERC20_TRANSFER_EVENT],
2385
+ data: log.data,
2386
+ topics: log.topics
2387
+ });
2388
+ if (decoded.eventName !== "Transfer") continue;
2389
+ const to = decoded.args.to.toLowerCase();
2390
+ if (to !== walletAddress.toLowerCase()) continue;
2391
+ delivered += decoded.args.value;
2392
+ } catch {
2393
+ }
2394
+ }
2395
+ if (delivered > 0n) {
2396
+ emitDebug("verification:amount-from-receipt", {
2397
+ amount: delivered.toString(),
2398
+ tokenAddress: token.contract,
2399
+ walletAddress,
2400
+ moonpayQuoteCurrencyAmount: fallbackAmount
2401
+ });
2402
+ return delivered;
2403
+ }
2404
+ emitDebug("verification:amount-from-receipt-missing", {
2405
+ tokenAddress: token.contract,
2406
+ walletAddress,
2407
+ moonpayQuoteCurrencyAmount: fallbackAmount
2408
+ });
2409
+ } catch (err) {
2410
+ emitDebug("verification:amount-from-receipt-error", errorPayload(err));
2411
+ receiptError = err;
2412
+ }
2413
+ const errorDetail = receiptError instanceof Error ? receiptError.message : receiptError === void 0 ? `no ${token.symbol} Transfer to ${walletAddress} found` : String(receiptError);
2414
+ throw new Error(
2415
+ `Unable to derive delivered ${token.symbol} amount from ${onChainTxHash}: ${errorDetail}`
2416
+ );
2417
+ }
2418
+ function matchesOnRampTransaction(record, transactionId) {
2419
+ return record.transaction_id === transactionId || record.external_transaction_id === transactionId || record.moonpay_transaction_id === transactionId;
2420
+ }
2421
+ function getOnRampVerificationKey(record) {
2422
+ return record.on_chain_tx_hash ?? record.transaction_id;
2423
+ }
2424
+ function summariseMoonPayEventProps(props) {
2425
+ return {
2426
+ id: props.id,
2427
+ externalTransactionId: props.externalTransactionId,
2428
+ status: props.status,
2429
+ walletAddress: props.walletAddress,
2430
+ walletAddressTag: props.walletAddressTag,
2431
+ baseCurrencyAmount: props.baseCurrencyAmount,
2432
+ quoteCurrencyAmount: props.quoteCurrencyAmount,
2433
+ baseCurrency: props.baseCurrency,
2434
+ quoteCurrency: props.quoteCurrency,
2435
+ createdAt: props.createdAt
2436
+ };
2437
+ }
2438
+ function summariseMoonPayUrl(url) {
2439
+ try {
2440
+ const parsed = new URL(url);
2441
+ const params = parsed.searchParams;
2442
+ return {
2443
+ origin: parsed.origin,
2444
+ pathname: parsed.pathname,
2445
+ apiKeyPrefix: params.get("apiKey")?.slice(0, 8) ?? null,
2446
+ currencyCode: params.get("currencyCode"),
2447
+ baseCurrencyCode: params.get("baseCurrencyCode"),
2448
+ baseCurrencyAmount: params.get("baseCurrencyAmount"),
2449
+ walletAddress: params.get("walletAddress"),
2450
+ externalCustomerId: params.get("externalCustomerId"),
2451
+ externalTransactionId: params.get("externalTransactionId"),
2452
+ redirectURL: params.get("redirectURL"),
2453
+ signaturePresent: params.has("signature")
2454
+ };
2455
+ } catch {
2456
+ return { parseError: true, length: url.length };
2457
+ }
2458
+ }
2459
+ function errorPayload(err) {
2460
+ if (err instanceof Error) {
2461
+ return {
2462
+ name: err.name,
2463
+ message: err.message,
2464
+ stack: err.stack?.split("\n").slice(0, 4).join("\n")
2465
+ };
2466
+ }
2467
+ return { message: String(err) };
2468
+ }
2469
+ function useMoonPayBuyWidget({
2470
+ variant,
2471
+ visible,
2472
+ autoStart,
2473
+ canBuy,
2474
+ openWidget,
2475
+ refreshPending,
2476
+ theme,
2477
+ themeId,
2478
+ colorCode,
2479
+ baseCurrencyCode,
2480
+ baseCurrencyAmount,
2481
+ lockAmount,
2482
+ paymentMethod,
2483
+ currencyCode,
2484
+ depositAddress,
2485
+ externalCustomerId,
2486
+ externalTransactionId,
2487
+ onClose,
2488
+ onCloseOverlay,
2489
+ onReady,
2490
+ onUrlSignatureRequested,
2491
+ onTransactionCreated,
2492
+ onTransactionCompleted
2493
+ }) {
2494
+ const autoStartedRef = useRef(false);
2495
+ useEffect(() => {
2496
+ if (!autoStart || autoStartedRef.current || !canBuy) return;
2497
+ autoStartedRef.current = true;
2498
+ void openWidget();
2499
+ }, [autoStart, canBuy, openWidget]);
2500
+ useEffect(() => {
2501
+ if (variant !== "embedded" || !visible) return;
2502
+ const id = setInterval(() => void refreshPending(), 5e3);
2503
+ return () => clearInterval(id);
2504
+ }, [variant, visible, refreshPending]);
2505
+ const callbacksRef = useRef({
2506
+ onClose,
2507
+ onCloseOverlay,
2508
+ onReady,
2509
+ onUrlSignatureRequested,
2510
+ onTransactionCreated,
2511
+ onTransactionCompleted
2512
+ });
2513
+ useEffect(() => {
2514
+ callbacksRef.current = {
2515
+ onClose,
2516
+ onCloseOverlay,
2517
+ onReady,
2518
+ onUrlSignatureRequested,
2519
+ onTransactionCreated,
2520
+ onTransactionCompleted
2521
+ };
2522
+ });
2523
+ const overlayNode = useMemo(
2524
+ () => variant === "overlay" ? buildOverlayNode() : void 0,
2525
+ [variant]
2526
+ );
2527
+ return useMemo(() => {
2528
+ if (!visible || !depositAddress || !externalTransactionId) return null;
2529
+ return /* @__PURE__ */ jsx(
2530
+ MoonPayBuyWidget,
2531
+ {
2532
+ variant,
2533
+ visible: true,
2534
+ theme,
2535
+ themeId,
2536
+ colorCode,
2537
+ overlayNode,
2538
+ baseCurrencyCode,
2539
+ baseCurrencyAmount,
2540
+ lockAmount: lockAmount ? "true" : void 0,
2541
+ paymentMethod,
2542
+ currencyCode,
2543
+ walletAddress: depositAddress,
2544
+ externalCustomerId,
2545
+ externalTransactionId,
2546
+ onClose: () => callbacksRef.current.onClose(),
2547
+ onCloseOverlay: () => callbacksRef.current.onCloseOverlay(),
2548
+ onReady: () => callbacksRef.current.onReady(),
2549
+ onUrlSignatureRequested: (url) => callbacksRef.current.onUrlSignatureRequested(url),
2550
+ onTransactionCreated: (props) => callbacksRef.current.onTransactionCreated(props),
2551
+ onTransactionCompleted: (props) => callbacksRef.current.onTransactionCompleted(props)
2552
+ }
2553
+ );
2554
+ }, [
2555
+ visible,
2556
+ depositAddress,
2557
+ externalTransactionId,
2558
+ variant,
2559
+ theme,
2560
+ themeId,
2561
+ colorCode,
2562
+ overlayNode,
2563
+ baseCurrencyCode,
2564
+ baseCurrencyAmount,
2565
+ lockAmount,
2566
+ paymentMethod,
2567
+ currencyCode,
2568
+ externalCustomerId
2569
+ ]);
2570
+ }
2571
+ function buildOverlayNode() {
2572
+ if (typeof document === "undefined") return void 0;
2573
+ const wrap = document.createElement("div");
2574
+ wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:12px";
2575
+ wrap.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="139" height="20.61" viewBox="0 0 139 20.61"><path d="M15.08,20.61L10.49,20.61L10.49,0.00L17.96,0.00L18.81,0.02L19.62,0.10L20.38,0.22L21.11,0.39L21.79,0.60L22.43,0.87L23.03,1.18L23.58,1.53L24.08,1.92L24.53,2.35L24.93,2.82L25.29,3.33L25.59,3.88L25.84,4.47L26.03,5.09L26.17,5.75L26.25,6.44L26.28,7.17L26.28,7.17L26.28,7.62L26.25,8.33L26.17,9.01L26.03,9.66L25.84,10.27L25.59,10.86L25.29,11.41L24.93,11.93L24.53,12.40L24.08,12.83L23.58,13.23L23.03,13.58L22.43,13.89L21.79,14.15L21.11,14.37L20.38,14.54L19.62,14.66L18.81,14.73L17.96,14.76L17.96,14.76L15.08,14.76L15.08,20.61ZM18.19,3.98L15.08,3.98L15.08,10.78L18.19,10.78L18.53,10.77L18.87,10.73L19.18,10.68L19.48,10.59L19.76,10.49L20.02,10.36L20.27,10.21L20.50,10.04L20.70,9.85L20.89,9.64L21.06,9.42L21.21,9.17L21.34,8.91L21.45,8.63L21.53,8.34L21.59,8.04L21.62,7.72L21.63,7.39L21.63,7.39L21.62,7.05L21.59,6.72L21.53,6.40L21.45,6.10L21.34,5.82L21.21,5.56L21.06,5.31L20.89,5.09L20.70,4.88L20.50,4.69L20.27,4.53L20.02,4.38L19.76,4.26L19.48,4.16L19.18,4.08L18.87,4.02L18.53,3.99L18.19,3.98L18.19,3.98ZM31.76,20.61L27.16,20.61L27.16,0.00L35.20,0.00L36.03,0.02L36.82,0.09L37.58,0.19L38.30,0.34L38.98,0.53L39.62,0.77L40.23,1.05L40.78,1.37L41.28,1.73L41.74,2.13L42.15,2.57L42.51,3.05L42.82,3.58L43.07,4.15L43.26,4.76L43.40,5.42L43.49,6.12L43.52,6.86L43.52,6.86L43.52,7.31L43.49,8.04L43.40,8.73L43.26,9.38L43.07,9.98L42.82,10.54L42.51,11.06L42.51,11.06L42.15,11.53L41.75,11.97L41.30,12.36L40.80,12.72L40.26,13.03L39.68,13.30L39.68,13.30L44.89,20.61L39.57,20.61L35.12,14.06L31.76,14.06L31.76,20.61ZM35.56,3.89L31.76,3.89L31.76,10.44L35.56,10.44L35.89,10.43L36.21,10.40L36.51,10.34L36.79,10.26L37.06,10.16L37.31,10.04L37.55,9.89L37.77,9.73L37.97,9.55L38.15,9.35L38.32,9.13L38.46,8.89L38.59,8.64L38.69,8.37L38.77,8.09L38.82,7.79L38.86,7.49L38.87,7.17L38.86,6.85L38.82,6.54L38.77,6.25L38.69,5.97L38.59,5.70L38.46,5.45L38.32,5.21L38.15,4.99L37.97,4.79L37.77,4.61L37.55,4.44L37.31,4.30L37.06,4.17L36.79,4.07L36.51,3.99L36.21,3.94L35.89,3.90L35.56,3.89L35.56,3.89ZM50.25,20.61L45.66,20.61L45.66,0.17L50.25,0.17L50.25,20.61ZM64.88,20.61L57.41,20.61L51.19,0.17L55.92,0.17L60.71,16.88L61.66,16.88L66.09,0.17L70.68,0.17L64.88,20.61ZM71.44,20.61L66.85,20.61L73.60,0.17L81.02,0.17L88.02,20.61L83.26,20.61L81.61,15.54L73.07,15.54L71.44,20.61ZM74.27,11.73L80.35,11.73L77.80,3.92L76.76,3.92L74.27,11.73ZM92.94,20.61L88.68,20.61L88.68,0.17L96.21,0.17L103.91,16.88L104.30,16.88L104.30,0.17L108.62,0.17L108.62,20.61L101.03,20.61L93.33,3.89L92.94,3.89L92.94,20.61ZM113.87,20.61L109.28,20.61L116.02,0.17L123.44,0.17L130.44,20.61L125.68,20.61L124.03,15.54L115.49,15.54L113.87,20.61ZM116.70,11.73L122.77,11.73L120.22,3.92L119.19,3.92L116.70,11.73Z" fill="#ffffff"/></svg><span>Your secure checkout is loading</span>`;
2576
+ return wrap;
2577
+ }
2578
+ function FiatOnRampForm({
2579
+ tokenId,
2580
+ currencyCode,
2581
+ baseCurrencyCode = "usd",
2582
+ defaultBaseCurrencyAmount = "100",
2583
+ tokenSymbol,
2584
+ theme,
2585
+ themeId,
2586
+ colorCode,
2587
+ variant = "overlay",
2588
+ autoStart = false,
2589
+ lockAmount,
2590
+ paymentMethod,
2591
+ onCredited,
2592
+ onError,
2593
+ onDebugEvent
2594
+ }) {
2595
+ const { address } = useAccount();
2596
+ const [visible, setVisible] = useState(false);
2597
+ const [isPreparing, setIsPreparing] = useState(false);
2598
+ const [rowError, setRowError] = useState(null);
2599
+ const {
2600
+ status,
2601
+ activeIntentId,
2602
+ pending,
2603
+ activeVerificationId,
2604
+ error,
2605
+ finalityProgress,
2606
+ depositAddress,
2607
+ minDepositBaseUnits,
2608
+ selectedToken,
2609
+ prepareOnRampIntent,
2610
+ signUrl,
2611
+ handleTransactionCreated,
2612
+ handleTransactionCompleted,
2613
+ finishPendingVerification,
2614
+ handleWidgetClosed,
2615
+ refreshPending
2616
+ } = useFiatOnRamp({ tokenId, onCredited, onError, onDebugEvent });
2617
+ const decimals = selectedToken?.decimals;
2618
+ const displaySymbol = tokenSymbol ?? selectedToken?.symbol ?? currencyCode.toUpperCase();
2619
+ const emitFormDebug = useCallback(
2620
+ (event, payload) => {
2621
+ onDebugEvent?.({
2622
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2623
+ event,
2624
+ status,
2625
+ tokenId,
2626
+ payload
2627
+ });
2628
+ },
2629
+ [onDebugEvent, status, tokenId]
2630
+ );
2631
+ const minFiatGate = minDepositBaseUnits !== void 0 && decimals !== void 0 ? Number(formatUnits(minDepositBaseUnits, decimals)) * 1.05 : void 0;
2632
+ const isBelowMin = minFiatGate !== void 0 && Number(defaultBaseCurrencyAmount) < minFiatGate;
2633
+ const isBusy = isPreparing || status === "awaiting-purchase";
2634
+ const isInitializing = !!address && !depositAddress;
2635
+ const isPrePurchase = status === "idle" || status === "awaiting-purchase";
2636
+ const isVerifying = status === "awaiting-delivery" || status === "verifying";
2637
+ const blockReasons = useMemo(
2638
+ () => [
2639
+ !address ? "wallet-not-connected" : null,
2640
+ !depositAddress ? "deposit-address-not-loaded" : null,
2641
+ isBusy ? `busy:${isPreparing ? "preparing" : status}` : null,
2642
+ visible ? "widget-open" : null,
2643
+ isBelowMin ? "below-minimum" : null
2644
+ ].filter((reason) => Boolean(reason)),
2645
+ [address, depositAddress, isBelowMin, isBusy, isPreparing, status, visible]
2646
+ );
2647
+ const canBuy = blockReasons.length === 0;
2648
+ const handleOpen = useCallback(async () => {
2649
+ if (!canBuy) {
2650
+ emitFormDebug("form:open-blocked", {
2651
+ reasons: blockReasons,
2652
+ currencyCode,
2653
+ tokenSymbol: displaySymbol,
2654
+ tokenDecimals: decimals ?? null,
2655
+ baseCurrencyCode,
2656
+ defaultBaseCurrencyAmount,
2657
+ depositAddress: depositAddress ?? null,
2658
+ walletAddress: address ?? null,
2659
+ status
2660
+ });
2661
+ return;
2662
+ }
2663
+ setIsPreparing(true);
2664
+ emitFormDebug("form:open-click", {
2665
+ currencyCode,
2666
+ tokenSymbol: displaySymbol,
2667
+ tokenDecimals: decimals ?? null,
2668
+ baseCurrencyCode,
2669
+ defaultBaseCurrencyAmount,
2670
+ depositAddress: depositAddress ?? null,
2671
+ walletConnected: Boolean(address)
2672
+ });
2673
+ try {
2674
+ const intent = await prepareOnRampIntent({
2675
+ currencyCode,
2676
+ baseCurrencyCode,
2677
+ baseCurrencyAmount: defaultBaseCurrencyAmount
2678
+ });
2679
+ emitFormDebug("form:intent-ready", {
2680
+ transactionId: intent.transaction_id,
2681
+ externalTransactionId: intent.external_transaction_id ?? null
2682
+ });
2683
+ setVisible(true);
2684
+ } catch (err) {
2685
+ const error2 = err instanceof Error ? err : new Error("Failed to prepare MoonPay on-ramp");
2686
+ emitFormDebug("form:intent-error", {
2687
+ name: error2.name,
2688
+ message: error2.message
2689
+ });
2690
+ } finally {
2691
+ setIsPreparing(false);
2692
+ }
2693
+ }, [
2694
+ address,
2695
+ baseCurrencyCode,
2696
+ blockReasons,
2697
+ canBuy,
2698
+ currencyCode,
2699
+ decimals,
2700
+ displaySymbol,
2701
+ defaultBaseCurrencyAmount,
2702
+ depositAddress,
2703
+ emitFormDebug,
2704
+ prepareOnRampIntent,
2705
+ status
2706
+ ]);
2707
+ const handleClose = useCallback(async () => {
2708
+ emitFormDebug("moonpay:onClose");
2709
+ setVisible(false);
2710
+ await handleWidgetClosed();
2711
+ }, [emitFormDebug, handleWidgetClosed]);
2712
+ const handleCloseOverlay = useCallback(async () => {
2713
+ emitFormDebug("moonpay:onCloseOverlay");
2714
+ setVisible(false);
2715
+ await handleWidgetClosed();
2716
+ }, [emitFormDebug, handleWidgetClosed]);
2717
+ const handleReady = useCallback(async () => {
2718
+ emitFormDebug("moonpay:onReady");
2719
+ }, [emitFormDebug]);
2720
+ const widgetElement = useMoonPayBuyWidget({
2721
+ variant,
2722
+ visible,
2723
+ autoStart,
2724
+ canBuy,
2725
+ openWidget: handleOpen,
2726
+ refreshPending,
2727
+ theme,
2728
+ themeId,
2729
+ colorCode,
2730
+ baseCurrencyCode,
2731
+ baseCurrencyAmount: defaultBaseCurrencyAmount,
2732
+ lockAmount,
2733
+ paymentMethod,
2734
+ currencyCode,
2735
+ depositAddress,
2736
+ externalCustomerId: address?.toLowerCase(),
2737
+ externalTransactionId: activeIntentId,
2738
+ onClose: handleClose,
2739
+ onCloseOverlay: handleCloseOverlay,
2740
+ onReady: handleReady,
2741
+ onUrlSignatureRequested: signUrl,
2742
+ onTransactionCreated: handleTransactionCreated,
2743
+ onTransactionCompleted: handleTransactionCompleted
2744
+ });
2745
+ useEffect(() => {
2746
+ if (variant !== "embedded" || !visible) return;
2747
+ if (isVerifying || status === "credited") {
2748
+ setVisible(false);
2749
+ void refreshPending();
2750
+ }
2751
+ }, [variant, visible, isVerifying, status, refreshPending]);
2752
+ return /* @__PURE__ */ jsxs("div", { "data-privana": true, className: "flex flex-col gap-4", children: [
2753
+ pending.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2754
+ /* @__PURE__ */ jsx("p", { className: "text-foreground text-sm font-medium", children: "Validating purchases" }),
2755
+ pending.map((record) => {
2756
+ const progress = parseFinalityProgress(finalityProgress[record.transaction_id]);
2757
+ const hasProgress = !!finalityProgress[record.transaction_id];
2758
+ const isStalled = !hasProgress && Date.now() / 1e3 - (record.updated_at ?? 0) > 60;
2759
+ const isActivelyVerifying = record.transaction_id === activeVerificationId;
2760
+ const showRetry = rowError?.id === record.transaction_id || isStalled && !isActivelyVerifying;
2761
+ return /* @__PURE__ */ jsxs(
2762
+ "div",
2763
+ {
2764
+ className: "border-border flex flex-col gap-1 rounded-md border p-2",
2765
+ children: [
2766
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
2767
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex items-center gap-1 text-xs", children: [
2768
+ /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin", "aria-hidden": true }),
2769
+ progress ?? "Verifying\u2026"
2770
+ ] }),
2771
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
2772
+ record.quote_currency_amount ?? "?",
2773
+ " ",
2774
+ displaySymbol
2775
+ ] })
2776
+ ] }),
2777
+ rowError?.id === record.transaction_id && /* @__PURE__ */ jsx("p", { className: "text-destructive text-xs", children: rowError.message }),
2778
+ showRetry && /* @__PURE__ */ jsx(
2779
+ Button,
2780
+ {
2781
+ type: "button",
2782
+ variant: "outline",
2783
+ size: "sm",
2784
+ onClick: async () => {
2785
+ setRowError(null);
2786
+ try {
2787
+ await finishPendingVerification(record);
2788
+ } catch (err) {
2789
+ setRowError({
2790
+ id: record.transaction_id,
2791
+ message: err instanceof Error ? err.message : "Verification failed"
2792
+ });
2793
+ }
2794
+ },
2795
+ children: "Retry"
2796
+ }
2797
+ )
2798
+ ]
2799
+ },
2800
+ record.transaction_id
2801
+ );
2802
+ })
2803
+ ] }),
2804
+ status === "credited" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 py-8 text-center", children: [
2805
+ /* @__PURE__ */ jsx(CircleCheckIcon, { className: "text-primary size-8", "aria-hidden": true }),
2806
+ /* @__PURE__ */ jsx("p", { className: "text-foreground text-sm font-medium", children: "Purchase credited" }),
2807
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
2808
+ "Your ",
2809
+ displaySymbol,
2810
+ " deposit is now available in your balance."
2811
+ ] })
2812
+ ] }),
2813
+ autoStart ? !visible && isPrePurchase && /* @__PURE__ */ jsx(Skeleton, { className: "h-[656px] w-full rounded-md" }) : isInitializing ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full rounded-md" }) : /* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleOpen, disabled: !canBuy, children: [
2814
+ (isBusy || visible) && /* @__PURE__ */ jsx(Loader2, { className: "animate-spin", "aria-hidden": true }),
2815
+ "Buy"
2816
+ ] }),
2817
+ widgetElement,
2818
+ error && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", role: "alert", children: error.message }),
2819
+ isBelowMin && minFiatGate !== void 0 && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", role: "alert", children: [
2820
+ "Minimum purchase is ~$",
2821
+ minFiatGate.toFixed(2),
2822
+ "."
2823
+ ] }),
2824
+ isVerifying && pending.length === 0 && /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex items-center gap-2 text-sm", children: [
2825
+ /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
2826
+ "Verifying your purchase\u2026"
2827
+ ] })
2828
+ ] });
2829
+ }
2830
+ function parseFinalityProgress(message) {
2831
+ if (!message) return null;
2832
+ const match = message.match(/(\d+\/\d+)\s+confirmations/i);
2833
+ return match ? `${match[1]} confirmations` : null;
1715
2834
  }
1716
2835
 
1717
- export { AccountingApiError, Button, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, NETWORK_CONFIG, NetworkError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, ValidationError, applyRefreshResponse, buildHostedAuthSession, buttonVariants, clearHostedAuthPendingTransaction, cn, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createPkceChallenge, createPkceVerifier, formatTimeRemaining, formatTokenAmount, getAccountingContract, getApiUrl, getChainById, getChainId, getChainId2, getExplorerAddressUrl, getTransactionReceipt, isHostedAuthRefreshActive, isHostedAuthSessionActive, normalizeAddress, normalizeHex, parseHostedAuthCallback, parseTokenAmount, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, shortenAddress, stripHostedAuthCallbackParams, syncHostedAuthSessionToClient, useDepositVerification, usePrivanaContext, usePrivateReadRequest, useSafeAccount, useSafePrivanaContext, useSiweAuth, waitForTransactionReceipt };
1718
- //# sourceMappingURL=chunk-2AHJHISR.js.map
1719
- //# sourceMappingURL=chunk-2AHJHISR.js.map
2836
+ export { AccountingApiError, Button, FiatOnRampForm, HOSTED_AUTH_CLOCK_SKEW_MS, HostedAuthError, HostedAuthRequiredError, HostedAuthStateMismatchError, HttpClient, NETWORK_CONFIG, NetworkError, PrivanaClient, PrivanaProvider, SUPPORTED_CHAINS, SiweAuthProvider, Skeleton, ValidationError, applyRefreshResponse, buildHostedAuthSession, buildSiweStatement, buttonVariants, clearHostedAuthPendingTransaction, cn, createHostedAuthPendingStorageKey, createHostedAuthState, createHostedAuthStorageKey, createPkceChallenge, createPkceVerifier, formatTimeRemaining, formatTokenAmount, getAccountingContract, getApiUrl, getChainById, getChainId, getChainId2, getExplorerAddressUrl, getExplorerLabel, getTransactionReceipt, isHostedAuthRefreshActive, isHostedAuthSessionActive, normalizeAddress, normalizeHex, parseHostedAuthCallback, parseTokenAmount, persistHostedAuthPendingTransaction, readHostedAuthPendingTransaction, readStoredHostedAuthSession, shortenAddress, stripHostedAuthCallbackParams, syncHostedAuthSessionToClient, useDepositVerification, useFiatOnRamp, usePrivanaContext, usePrivateReadRequest, useSafeAccount, useSafePrivanaContext, useSiweAuth, waitForTransactionReceipt };
2837
+ //# sourceMappingURL=chunk-54FC6TO3.js.map
2838
+ //# sourceMappingURL=chunk-54FC6TO3.js.map