@openfort/react 1.0.2 → 1.0.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.
Files changed (30) hide show
  1. package/build/components/Openfort/types.d.ts +3 -0
  2. package/build/components/Pages/AssetInventory/index.js +127 -48
  3. package/build/components/Pages/AssetInventory/index.js.map +1 -1
  4. package/build/components/Pages/CreateWallet/SolanaCreateWallet.js +11 -12
  5. package/build/components/Pages/CreateWallet/SolanaCreateWallet.js.map +1 -1
  6. package/build/components/Pages/LoadWallets/index.js +5 -2
  7. package/build/components/Pages/LoadWallets/index.js.map +1 -1
  8. package/build/components/Pages/SelectToken/styles.d.ts +13 -0
  9. package/build/components/Pages/SelectToken/styles.js +103 -1
  10. package/build/components/Pages/SelectToken/styles.js.map +1 -1
  11. package/build/components/Pages/SendConfirmation/index.js +7 -0
  12. package/build/components/Pages/SendConfirmation/index.js.map +1 -1
  13. package/build/constants/logos.d.ts +2 -0
  14. package/build/constants/logos.js +58 -0
  15. package/build/constants/logos.js.map +1 -0
  16. package/build/ethereum/hooks/useEthereumWalletAssets.d.ts +25 -11
  17. package/build/ethereum/hooks/useEthereumWalletAssets.js +134 -14
  18. package/build/ethereum/hooks/useEthereumWalletAssets.js.map +1 -1
  19. package/build/hooks/openfort/useUser.d.ts +2 -1
  20. package/build/hooks/openfort/useUser.js +3 -2
  21. package/build/hooks/openfort/useUser.js.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/openfort/CoreOpenfortProvider.js +5 -3
  24. package/build/openfort/CoreOpenfortProvider.js.map +1 -1
  25. package/build/utils/index.d.ts +1 -1
  26. package/build/version.d.ts +1 -1
  27. package/build/version.js +1 -1
  28. package/build/wagmi/embeddedConnector.js +6 -1
  29. package/build/wagmi/embeddedConnector.js.map +1 -1
  30. package/package.json +1 -1
@@ -372,6 +372,9 @@ export type Asset = {
372
372
  };
373
373
  raw?: getAssets.Erc20Asset;
374
374
  };
375
+ export type MultiChainAsset = Asset & {
376
+ chainId: number;
377
+ };
375
378
  export type SendFormState = {
376
379
  recipient: string;
377
380
  amount: string;
@@ -1,10 +1,14 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { motion } from 'framer-motion';
3
+ import { useState, useEffect, useMemo } from 'react';
2
4
  import { formatUnits } from 'viem';
5
+ import { TOKEN_LOGO, symbolToColor } from '../../../constants/logos.js';
3
6
  import { useEthereumWalletAssets } from '../../../ethereum/hooks/useEthereumWalletAssets.js';
7
+ import Chain from '../../Common/Chain/index.js';
4
8
  import { ModalHeading } from '../../Common/Modal/styles.js';
5
- import { EmptyState } from '../BuyProviderSelect/styles.js';
6
- import { SelectTokenContent, TokenList, TokenContainer, TokenInfo, TokenSymbol, TokenBalance, TokenName } from '../SelectToken/styles.js';
7
- import { getAssetSymbol, getAssetDecimals, formatBalanceWithSymbol } from '../Send/utils.js';
9
+ import { useOpenfort } from '../../Openfort/useOpenfort.js';
10
+ import { SelectTokenContent, EmptyState, ContentWrapper, ChainGroup, ChainGroupHeader, TokenPill, TokenPillSymbol, InfoLink, TokenList, TokenContainer, TokenLeftGroup, TokenInfo, TokenSymbol, TokenName, TokenBalance, TokenLogoArea, TokenLogoImg, TokenLogoFallback, ChainBadge } from '../SelectToken/styles.js';
11
+ import { getAssetSymbol, getAssetDecimals } from '../Send/utils.js';
8
12
 
9
13
  const ZERO = BigInt(0);
10
14
  const usdFormatter = new Intl.NumberFormat('en-US', {
@@ -13,53 +17,128 @@ const usdFormatter = new Intl.NumberFormat('en-US', {
13
17
  minimumFractionDigits: 2,
14
18
  maximumFractionDigits: 2,
15
19
  });
16
- const AssetInventory = () => {
17
- const { data: walletAssets, isLoading: isBalancesLoading } = useEthereumWalletAssets();
18
- // Show all tokens for both buy and send flows
19
- const selectableTokens = walletAssets || [];
20
- const renderContent = () => {
21
- if (!selectableTokens.length) {
22
- if (isBalancesLoading) {
23
- return jsx(EmptyState, { children: "Loading balances\u2026" });
20
+ const priceFormatter = new Intl.NumberFormat('en-US', {
21
+ style: 'currency',
22
+ currency: 'USD',
23
+ minimumFractionDigits: 2,
24
+ maximumFractionDigits: 4,
25
+ });
26
+ function getTokenLogoUrl(token) {
27
+ var _a;
28
+ const symbol = getAssetSymbol(token).toUpperCase();
29
+ return (_a = TOKEN_LOGO[symbol]) !== null && _a !== void 0 ? _a : null;
30
+ }
31
+ function TokenLogo({ token }) {
32
+ const [imgError, setImgError] = useState(false);
33
+ const symbol = getAssetSymbol(token);
34
+ const logoUrl = getTokenLogoUrl(token);
35
+ return (jsxs(TokenLogoArea, { children: [logoUrl && !imgError ? (jsx(TokenLogoImg, { src: logoUrl, alt: symbol, onError: () => setImgError(true) })) : (jsx(TokenLogoFallback, { "$bg": symbolToColor(symbol), children: symbol.charAt(0).toUpperCase() })), jsx(ChainBadge, { children: jsx(Chain, { id: token.chainId, unsupported: false, size: 14 }) })] }));
36
+ }
37
+ function renderTokenRow(token) {
38
+ var _a, _b, _c, _d;
39
+ const key = token.type === 'erc20' ? `${token.chainId}-${token.address}` : `${token.chainId}-native`;
40
+ const displaySymbol = getAssetSymbol(token);
41
+ const displayName = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.name) || displaySymbol || 'Unknown Token';
42
+ const decimals = getAssetDecimals(token);
43
+ const pricePerToken = (_c = (_b = token.metadata) === null || _b === void 0 ? void 0 : _b.fiat) === null || _c === void 0 ? void 0 : _c.value;
44
+ let usdValue = null;
45
+ let balanceNum = '';
46
+ let priceDisplay = null;
47
+ const isBalanceLoaded = token.balance !== undefined;
48
+ const hasZeroBalance = isBalanceLoaded && ((_d = token.balance) !== null && _d !== void 0 ? _d : ZERO) <= ZERO;
49
+ if (hasZeroBalance)
50
+ return null;
51
+ if (isBalanceLoaded && token.balance !== undefined) {
52
+ const amount = parseFloat(formatUnits(token.balance, decimals));
53
+ if (Number.isFinite(amount)) {
54
+ balanceNum = `${amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 4 })} ${displaySymbol}`;
55
+ if (pricePerToken !== undefined) {
56
+ const totalUsd = amount * pricePerToken;
57
+ if (totalUsd >= 0.01) {
58
+ usdValue = usdFormatter.format(totalUsd);
59
+ }
60
+ else if (totalUsd > 0) {
61
+ usdValue = '<$0.01';
62
+ }
63
+ else {
64
+ usdValue = usdFormatter.format(0);
65
+ }
66
+ priceDisplay = `@${priceFormatter.format(pricePerToken)}`;
24
67
  }
25
- return jsx(EmptyState, { children: "No supported tokens found for this network yet." });
26
68
  }
27
- return (jsx(TokenList, { children: selectableTokens.map((token) => {
28
- var _a, _b, _c, _d, _e;
29
- const key = token.type === 'erc20' ? token.address : 'native';
30
- const displaySymbol = getAssetSymbol(token);
31
- const displayName = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.name) || displaySymbol || 'Unknown Token';
32
- const decimals = getAssetDecimals(token);
33
- const pricePerToken = (_c = (_b = token.metadata) === null || _b === void 0 ? void 0 : _b.fiat) === null || _c === void 0 ? void 0 : _c.value;
34
- let usdValue = null;
35
- // Show loading state for balances
36
- const isBalanceLoaded = token.balance !== undefined;
37
- const balanceDisplay = isBalanceLoaded
38
- ? formatBalanceWithSymbol(token.balance, decimals, ((_d = token.metadata) === null || _d === void 0 ? void 0 : _d.symbol) || '')
39
- : 'Loading...';
40
- // Check if token has zero balance (for send flow opacity)
41
- const hasZeroBalance = isBalanceLoaded && ((_e = token.balance) !== null && _e !== void 0 ? _e : ZERO) <= ZERO;
42
- if (hasZeroBalance)
43
- return null;
44
- if (isBalanceLoaded && pricePerToken !== undefined && token.balance !== undefined) {
45
- const amount = parseFloat(formatUnits(token.balance, decimals));
46
- if (Number.isFinite(amount)) {
47
- const totalUsd = amount * pricePerToken;
48
- if (totalUsd >= 0.01) {
49
- usdValue = usdFormatter.format(totalUsd);
50
- }
51
- else if (totalUsd > 0) {
52
- usdValue = '<$0.01';
53
- }
54
- else {
55
- usdValue = usdFormatter.format(0);
56
- }
57
- }
58
- }
59
- return (jsxs(TokenContainer, { children: [jsx(TokenInfo, { children: jsx(TokenSymbol, { children: displayName }) }), jsxs(TokenInfo, { children: [jsx(TokenBalance, { children: balanceDisplay }), usdValue ? jsx(TokenName, { style: { textAlign: 'end' }, children: usdValue }) : null] })] }, key));
60
- }) }));
61
- };
62
- return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), renderContent()] }));
69
+ }
70
+ return (jsxs(TokenContainer, { children: [jsxs(TokenLeftGroup, { children: [jsx(TokenLogo, { token: token }), jsxs(TokenInfo, { style: { textAlign: 'left' }, children: [jsx(TokenSymbol, { children: displayName }), jsx(TokenName, { children: balanceNum || 'Loading...' })] })] }), jsxs(TokenInfo, { children: [usdValue ? jsx(TokenBalance, { children: usdValue }) : null, priceDisplay ? jsx(TokenName, { style: { textAlign: 'end' }, children: priceDisplay }) : null] })] }, key));
71
+ }
72
+ const PILL_LOGO_SIZE = 16;
73
+ function PillLogo({ symbol }) {
74
+ var _a;
75
+ const [imgError, setImgError] = useState(false);
76
+ const url = (_a = TOKEN_LOGO[symbol.toUpperCase()]) !== null && _a !== void 0 ? _a : null;
77
+ if (!url || imgError) {
78
+ return (jsx("span", { style: {
79
+ width: PILL_LOGO_SIZE,
80
+ height: PILL_LOGO_SIZE,
81
+ borderRadius: '50%',
82
+ background: symbolToColor(symbol),
83
+ display: 'inline-flex',
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ fontSize: 9,
87
+ fontWeight: 700,
88
+ color: '#fff',
89
+ flexShrink: 0,
90
+ }, children: symbol.charAt(0).toUpperCase() }));
91
+ }
92
+ return (jsx("img", { src: url, alt: symbol, onError: () => setImgError(true), style: {
93
+ width: PILL_LOGO_SIZE,
94
+ height: PILL_LOGO_SIZE,
95
+ borderRadius: '50%',
96
+ objectFit: 'cover',
97
+ flexShrink: 0,
98
+ } }));
99
+ }
100
+ const AssetInventory = () => {
101
+ var _a;
102
+ const { data, multiChain, isLoading: isBalancesLoading } = useEthereumWalletAssets({ multiChain: true });
103
+ const { triggerResize, chains } = useOpenfort();
104
+ const [showDetails, setShowDetails] = useState(false);
105
+ useEffect(() => {
106
+ if (!isBalancesLoading)
107
+ triggerResize();
108
+ }, [isBalancesLoading]);
109
+ useEffect(() => {
110
+ triggerResize();
111
+ }, [showDetails]);
112
+ const tokens = (_a = (multiChain ? data : null)) !== null && _a !== void 0 ? _a : [];
113
+ const hasBalance = tokens.some((t) => t.balance > ZERO);
114
+ const chainNameMap = useMemo(() => {
115
+ const map = new Map();
116
+ for (const c of chains)
117
+ map.set(c.id, c.name);
118
+ return map;
119
+ }, [chains]);
120
+ const groupedByChain = useMemo(() => {
121
+ var _a;
122
+ const groups = new Map();
123
+ for (const t of tokens) {
124
+ if (!groups.has(t.chainId))
125
+ groups.set(t.chainId, []);
126
+ groups.get(t.chainId).push({
127
+ symbol: getAssetSymbol(t),
128
+ name: ((_a = t.metadata) === null || _a === void 0 ? void 0 : _a.name) || getAssetSymbol(t),
129
+ });
130
+ }
131
+ return groups;
132
+ }, [tokens]);
133
+ if (isBalancesLoading) {
134
+ return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), jsx(EmptyState, { children: "Loading balances..." })] }));
135
+ }
136
+ if (showDetails) {
137
+ return (jsxs(SelectTokenContent, { onBack: () => {
138
+ setShowDetails(false);
139
+ }, children: [jsx(ModalHeading, { children: "Configured assets" }), jsx(motion.div, { initial: { opacity: 0, scale: 1.1 }, animate: { opacity: 1, scale: 1 }, transition: { duration: 0.2, ease: [0.26, 0.08, 0.25, 1] }, style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }, children: jsx(ContentWrapper, { style: { overflowY: 'auto', maxHeight: 400 }, children: Array.from(groupedByChain.entries()).map(([chainId, assets]) => (jsxs(ChainGroup, { children: [jsxs(ChainGroupHeader, { children: [jsx(Chain, { id: chainId, unsupported: false, size: 18 }), chainNameMap.get(chainId) || `Chain ${chainId}`] }), assets.map((a) => (jsxs(TokenPill, { children: [jsx(PillLogo, { symbol: a.symbol }), jsx(TokenPillSymbol, { children: a.symbol }), a.name !== a.symbol && a.name] }, `${chainId}-${a.symbol}`)))] }, chainId))) }) })] }, "details"));
140
+ }
141
+ return (jsxs(SelectTokenContent, { children: [jsx(ModalHeading, { children: "Your assets" }), jsxs(ContentWrapper, { children: [jsxs(InfoLink, { type: "button", onClick: () => setShowDetails(true), children: [jsxs("svg", { role: "img", "aria-label": "Info", width: "12", height: "12", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("circle", { cx: "7", cy: "7", r: "6", stroke: "currentColor", strokeWidth: "1.25" }), jsx("path", { d: "M7 6.25V10", stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round" }), jsx("circle", { cx: "7", cy: "4.25", r: "0.75", fill: "currentColor" })] }), "Only configured chains and tokens are shown"] }), jsx(TokenList, { children: hasBalance ? tokens.map(renderTokenRow) : jsx(EmptyState, { children: "No assets found" }) })] })] }, "assets"));
63
142
  };
64
143
 
65
144
  export { AssetInventory };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,10 +1,9 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { RecoveryMethod, EmbeddedState } from '@openfort/openfort-js';
2
+ import { RecoveryMethod } from '@openfort/openfort-js';
3
3
  import { motion } from 'framer-motion';
4
4
  import { useState, useEffect, useCallback, useMemo } from 'react';
5
5
  import { PhoneIcon, EmailIcon, FingerPrintIcon, KeyIcon, LockIcon } from '../../../assets/icons.js';
6
6
  import { OpenfortError } from '../../../types.js';
7
- import { useOpenfortCore } from '../../../openfort/useOpenfort.js';
8
7
  import { useRecoveryOTP } from '../../../shared/hooks/useRecoveryOTP.js';
9
8
  import { handleOtpRecoveryError } from '../../../shared/utils/otpError.js';
10
9
  import { useSolanaEmbeddedWallet } from '../../../solana/hooks/useSolanaEmbeddedWallet.js';
@@ -55,7 +54,6 @@ const OtherMethod = ({ currentMethod, onChangeMethod, }) => {
55
54
  };
56
55
  const SolanaCreateAutomatic = ({ onBack, logoutOnBack }) => {
57
56
  var _a;
58
- const { embeddedState } = useOpenfortCore();
59
57
  const { setRoute, triggerResize } = useOpenfort();
60
58
  const embeddedWallet = useSolanaEmbeddedWallet();
61
59
  const { isEnabled: isWalletRecoveryOTPEnabled, requestOTP } = useRecoveryOTP();
@@ -115,11 +113,12 @@ const SolanaCreateAutomatic = ({ onBack, logoutOnBack }) => {
115
113
  })();
116
114
  }, [shouldCreate]);
117
115
  const [canSendOtp, setCanSendOtp] = useState(true);
116
+ // Trigger creation on mount. We only land here when no Solana wallet exists.
117
+ // Don't gate on embeddedState — the user may have an EVM wallet (embeddedState=READY)
118
+ // but still need a Solana wallet.
118
119
  useEffect(() => {
119
- if (embeddedState === EmbeddedState.EMBEDDED_SIGNER_NOT_CONFIGURED) {
120
- setShouldCreate(true);
121
- }
122
- }, [embeddedState]);
120
+ setShouldCreate(true);
121
+ }, []);
123
122
  const handleResendClick = useCallback(() => {
124
123
  setOtpStatus('send-otp');
125
124
  setCanSendOtp(false);
@@ -145,7 +144,6 @@ const SolanaCreateAutomatic = ({ onBack, logoutOnBack }) => {
145
144
  const SolanaCreatePasskey = ({ onChangeMethod, onBack, logoutOnBack, }) => {
146
145
  const { triggerResize, setRoute } = useOpenfort();
147
146
  const embeddedWallet = useSolanaEmbeddedWallet();
148
- const { embeddedState } = useOpenfortCore();
149
147
  const [shouldCreate, setShouldCreate] = useState(false);
150
148
  const [recoveryError, setRecoveryError] = useState(null);
151
149
  useEffect(() => {
@@ -164,11 +162,12 @@ const SolanaCreatePasskey = ({ onChangeMethod, onBack, logoutOnBack, }) => {
164
162
  }
165
163
  })();
166
164
  }, [shouldCreate]);
165
+ // Trigger creation on mount. We only land here when no Solana wallet exists.
166
+ // Don't gate on embeddedState — the user may have an EVM wallet (embeddedState=READY)
167
+ // but still need a Solana wallet.
167
168
  useEffect(() => {
168
- if (embeddedState === EmbeddedState.EMBEDDED_SIGNER_NOT_CONFIGURED) {
169
- setShouldCreate(true);
170
- }
171
- }, [embeddedState]);
169
+ setShouldCreate(true);
170
+ }, []);
172
171
  useEffect(() => {
173
172
  if (recoveryError)
174
173
  triggerResize();
@@ -1 +1 @@
1
- {"version":3,"file":"SolanaCreateWallet.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"SolanaCreateWallet.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -29,7 +29,7 @@ const errorForChainRegistry = {
29
29
  }),
30
30
  };
31
31
  const LoadWallets = () => {
32
- const { chainType, user } = useOpenfortCore();
32
+ const { chainType, user, isLoadingAccounts } = useOpenfortCore();
33
33
  const { triggerResize, setRoute, setConnector, walletConfig } = useOpenfort();
34
34
  const ethereumWallet = useEthereumEmbeddedWallet();
35
35
  const solanaWallet = useSolanaEmbeddedWallet();
@@ -38,7 +38,10 @@ const LoadWallets = () => {
38
38
  const wallets = embeddedWallet.wallets;
39
39
  const isLoadingWallets = embeddedWallet.status === 'fetching-wallets' ||
40
40
  embeddedWallet.status === 'connecting' ||
41
- embeddedWallet.status === 'creating';
41
+ embeddedWallet.status === 'creating' ||
42
+ // For Solana, the hook never enters 'fetching-wallets' — accounts are derived from the
43
+ // core embeddedAccounts store. Wait for the core fetch to complete before routing.
44
+ isLoadingAccounts;
42
45
  const errorWallets = embeddedWallet.status === 'error' ? new Error(embeddedWallet.error) : undefined;
43
46
  useEffect(() => {
44
47
  let timeout;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -12,4 +12,17 @@ export declare const TokenInfo: import("styled-components").StyledComponent<"div
12
12
  export declare const TokenSymbol: import("styled-components").StyledComponent<"span", any, {}, never>;
13
13
  export declare const TokenName: import("styled-components").StyledComponent<"span", any, {}, never>;
14
14
  export declare const TokenBalance: import("styled-components").StyledComponent<"span", any, {}, never>;
15
+ export declare const TokenLeftGroup: import("styled-components").StyledComponent<"div", any, {}, never>;
16
+ export declare const TokenLogoArea: import("styled-components").StyledComponent<"div", any, {}, never>;
17
+ export declare const TokenLogoFallback: import("styled-components").StyledComponent<"div", any, {
18
+ $bg: string;
19
+ }, never>;
20
+ export declare const TokenLogoImg: import("styled-components").StyledComponent<"img", any, {}, never>;
21
+ export declare const ChainBadge: import("styled-components").StyledComponent<"div", any, {}, never>;
15
22
  export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
23
+ export declare const InfoLink: import("styled-components").StyledComponent<"button", any, {}, never>;
24
+ export declare const ContentWrapper: import("styled-components").StyledComponent<"div", any, {}, never>;
25
+ export declare const ChainGroup: import("styled-components").StyledComponent<"div", any, {}, never>;
26
+ export declare const ChainGroupHeader: import("styled-components").StyledComponent<"div", any, {}, never>;
27
+ export declare const TokenPill: import("styled-components").StyledComponent<"div", any, {}, never>;
28
+ export declare const TokenPillSymbol: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -67,12 +67,114 @@ const TokenBalance = styled.span `
67
67
  font-weight: 600;
68
68
  color: var(--ck-body-color);
69
69
  `;
70
+ const TokenLeftGroup = styled.div `
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 10px;
74
+ min-width: 0;
75
+ `;
76
+ const TokenLogoArea = styled.div `
77
+ position: relative;
78
+ width: 40px;
79
+ height: 40px;
80
+ flex-shrink: 0;
81
+ `;
82
+ const TokenLogoFallback = styled.div `
83
+ width: 40px;
84
+ height: 40px;
85
+ border-radius: 50%;
86
+ background: ${(p) => p.$bg};
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ font-size: 16px;
91
+ font-weight: 700;
92
+ color: #fff;
93
+ user-select: none;
94
+ `;
95
+ const TokenLogoImg = styled.img `
96
+ width: 40px;
97
+ height: 40px;
98
+ border-radius: 50%;
99
+ object-fit: cover;
100
+ `;
101
+ const ChainBadge = styled.div `
102
+ position: absolute;
103
+ bottom: -2px;
104
+ right: -2px;
105
+ width: 18px;
106
+ height: 18px;
107
+ border-radius: 50%;
108
+ background: var(--ck-body-background);
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ box-shadow: 0 0 0 0.7px var(--ck-body-background);
113
+ `;
70
114
  const EmptyState = styled.div `
71
115
  margin-top: 28px;
72
116
  font-size: 13px;
73
117
  color: var(--ck-body-color-muted);
74
118
  text-align: center;
75
119
  `;
120
+ const InfoLink = styled.button `
121
+ all: unset;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 5px;
126
+ width: 100%;
127
+ font-size: 13px;
128
+ font-weight: 400;
129
+ color: var(--ck-body-color-muted, rgba(255, 255, 255, 0.4));
130
+ padding: 0 0 8px;
131
+ cursor: pointer;
132
+ transition: color 0.15s ease;
133
+ svg { opacity: 0.6; }
134
+ &:hover {
135
+ color: var(--ck-body-color, #fff);
136
+ svg { opacity: 1; }
137
+ }
138
+ `;
139
+ const ContentWrapper = styled.div `
140
+ position: relative;
141
+ display: flex;
142
+ flex-direction: column;
143
+ flex: 1;
144
+ min-height: 0;
145
+ `;
146
+ const ChainGroup = styled.div `
147
+ &:not(:first-child) {
148
+ margin-top: 16px;
149
+ }
150
+ `;
151
+ const ChainGroupHeader = styled.div `
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ font-size: 14px;
156
+ font-weight: 600;
157
+ color: var(--ck-body-color, #fff);
158
+ margin-bottom: 8px;
159
+ `;
160
+ const TokenPill = styled.div `
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 6px;
164
+ padding: 6px 10px;
165
+ border-radius: 10px;
166
+ background: var(--ck-body-background-secondary, rgba(255, 255, 255, 0.06));
167
+ font-size: 13px;
168
+ font-weight: 500;
169
+ color: var(--ck-body-color-muted, rgba(255, 255, 255, 0.6));
170
+ &:not(:last-child) {
171
+ margin-bottom: 4px;
172
+ }
173
+ `;
174
+ const TokenPillSymbol = styled.span `
175
+ color: var(--ck-body-color, #fff);
176
+ font-weight: 600;
177
+ `;
76
178
 
77
- export { EmptyState, SelectTokenContent, TokenBalance, TokenButton, TokenContainer, TokenInfo, TokenList, TokenName, TokenSymbol };
179
+ export { ChainBadge, ChainGroup, ChainGroupHeader, ContentWrapper, EmptyState, InfoLink, SelectTokenContent, TokenBalance, TokenButton, TokenContainer, TokenInfo, TokenLeftGroup, TokenList, TokenLogoArea, TokenLogoFallback, TokenLogoImg, TokenName, TokenPill, TokenPillSymbol, TokenSymbol };
78
180
  //# sourceMappingURL=styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"styles.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -116,6 +116,7 @@ const SendConfirmation = () => {
116
116
  const [isPollingBalance, setIsPollingBalance] = useState(false);
117
117
  const originalBalanceRef = useRef(undefined);
118
118
  const pollingIntervalRef = useRef(null);
119
+ const submittingRef = useRef(false);
119
120
  // Inline transaction state management (replaces useEthereumSendTransaction + useEthereumWriteContract)
120
121
  const [nativeTxHash, setNativeTxHash] = useState(undefined);
121
122
  const [isNativePending, setIsNativePending] = useState(false);
@@ -255,8 +256,11 @@ const SendConfirmation = () => {
255
256
  }
256
257
  }, [isPollingBalance, currentBalance]);
257
258
  const handleConfirm = async () => {
259
+ if (submittingRef.current)
260
+ return;
258
261
  if (!recipientAddress || !parsedAmount || parsedAmount <= BigInt(0) || insufficientBalance)
259
262
  return;
263
+ submittingRef.current = true;
260
264
  try {
261
265
  if (token.type === 'native') {
262
266
  await sendTransactionAsync({
@@ -278,6 +282,9 @@ const SendConfirmation = () => {
278
282
  catch (_error) {
279
283
  // Errors are surfaced through mutation hooks
280
284
  }
285
+ finally {
286
+ submittingRef.current = false;
287
+ }
281
288
  };
282
289
  const handleCancel = () => {
283
290
  // Keep the current token, amount, and recipient when going back - don't reset
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,2 @@
1
+ export declare const TOKEN_LOGO: Record<string, string>;
2
+ export declare function symbolToColor(symbol: string): string;
@@ -0,0 +1,58 @@
1
+ const TW = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains';
2
+ // Unified symbol -> logo URL map (works for both native and ERC20)
3
+ const TOKEN_LOGO = {
4
+ // Native tokens (chain logos from Trust Wallet info/)
5
+ ETH: `${TW}/ethereum/info/logo.png`,
6
+ BNB: `${TW}/smartchain/info/logo.png`,
7
+ TBNB: `${TW}/smartchain/info/logo.png`,
8
+ MATIC: `${TW}/polygon/info/logo.png`,
9
+ POL: `${TW}/polygon/info/logo.png`,
10
+ AVAX: `${TW}/avalanchec/info/logo.png`,
11
+ FTM: `${TW}/fantom/info/logo.png`,
12
+ CELO: `${TW}/celo/info/logo.png`,
13
+ FIL: `${TW}/filecoin/info/logo.png`,
14
+ METIS: `${TW}/metis/info/logo.png`,
15
+ IOTX: `${TW}/iotex/info/logo.png`,
16
+ EVMOS: `${TW}/evmos/info/logo.png`,
17
+ XDAI: `${TW}/xdai/info/logo.png`,
18
+ FLR: `${TW}/flare/info/logo.png`,
19
+ TLOS: `${TW}/telos/info/logo.png`,
20
+ // ERC20 tokens (using mainnet Ethereum addresses)
21
+ USDC: `${TW}/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png`,
22
+ USDT: `${TW}/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png`,
23
+ DAI: `${TW}/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png`,
24
+ WETH: `${TW}/ethereum/assets/0xC02aaA39b223FE8D0A0e5c4F27eAD9083C756Cc2/logo.png`,
25
+ WBTC: `${TW}/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png`,
26
+ LINK: `${TW}/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png`,
27
+ UNI: `${TW}/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png`,
28
+ AAVE: `${TW}/ethereum/assets/0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9/logo.png`,
29
+ MKR: `${TW}/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png`,
30
+ CRV: `${TW}/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png`,
31
+ LDO: `${TW}/ethereum/assets/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32/logo.png`,
32
+ SHIB: `${TW}/ethereum/assets/0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE/logo.png`,
33
+ ARB: `${TW}/arbitrum/assets/0x912CE59144191C1204E64559FE8253a0e49E6548/logo.png`,
34
+ OP: `${TW}/optimism/assets/0x4200000000000000000000000000000000000042/logo.png`,
35
+ STETH: `${TW}/ethereum/assets/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84/logo.png`,
36
+ CBETH: `${TW}/ethereum/assets/0xBe9895146f7AF43049ca1c1AE358B0541Ea49704/logo.png`,
37
+ RETH: `${TW}/ethereum/assets/0xae78736Cd615f374D3085123A210448E74Fc6393/logo.png`,
38
+ GRT: `${TW}/ethereum/assets/0xc944E90C64B2c07662A292be6244BDf05Cda44a7/logo.png`,
39
+ SNX: `${TW}/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png`,
40
+ COMP: `${TW}/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png`,
41
+ PEPE: `${TW}/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png`,
42
+ SUSHI: `${TW}/ethereum/assets/0x6B3595068778DD592e39A122f4f5a5cF09C90fE2/logo.png`,
43
+ DYDX: `${TW}/ethereum/assets/0x92D6C1e31e14520e676a687F0a93788B716BEff5/logo.png`,
44
+ BEAM: `${TW}/ethereum/assets/0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE/logo.png`,
45
+ EUL: `${TW}/ethereum/assets/0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b/logo.png`,
46
+ };
47
+ // Deterministic color from symbol string (only used if no logo is found)
48
+ function symbolToColor(symbol) {
49
+ let hash = 0;
50
+ for (let i = 0; i < symbol.length; i++) {
51
+ hash = symbol.charCodeAt(i) + ((hash << 5) - hash);
52
+ }
53
+ const h = ((hash % 360) + 360) % 360;
54
+ return `hsl(${h}, 55%, 50%)`;
55
+ }
56
+
57
+ export { TOKEN_LOGO, symbolToColor };
58
+ //# sourceMappingURL=logos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logos.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,29 +1,43 @@
1
- import type { Asset } from '../../components/Openfort/types';
1
+ import type { Asset, MultiChainAsset } from '../../components/Openfort/types';
2
2
  import { OpenfortError } from '../../core/errors';
3
3
  import type { EthereumConfig } from '../../ethereum/types';
4
4
  type UseEthereumWalletAssetsOptions = {
5
5
  assets?: EthereumConfig['assets'];
6
+ /** When true, fetches assets for all configured chains and returns MultiChainAsset[]. */
7
+ multiChain?: boolean;
6
8
  staleTime?: number;
7
9
  };
10
+ type WalletAssetsReturnBase = {
11
+ isLoading: boolean;
12
+ isError: boolean;
13
+ isSuccess: boolean;
14
+ isIdle: boolean;
15
+ error: OpenfortError | undefined;
16
+ refetch: () => Promise<unknown>;
17
+ };
18
+ type UseEthereumWalletAssetsResult = (WalletAssetsReturnBase & {
19
+ multiChain: true;
20
+ data: readonly MultiChainAsset[] | null;
21
+ }) | (WalletAssetsReturnBase & {
22
+ multiChain: false;
23
+ data: readonly Asset[] | null;
24
+ });
8
25
  /**
9
26
  * Returns wallet assets (tokens, NFTs) for the connected Ethereum address.
10
27
  * Uses ERC-7811 via Openfort's authenticated RPC proxy.
11
28
  *
12
- * @param options - Optional custom assets config and staleTime
29
+ * When `multiChain` is true, fetches assets across all configured chains
30
+ * via `wallet_getAssets` and returns `MultiChainAsset[]` (assets tagged with `chainId`).
31
+ *
32
+ * @param options - Optional custom assets config, multiChain flag, and staleTime
13
33
  * @returns assets, isLoading, error, refetch
14
34
  *
15
35
  * @example
16
36
  * ```tsx
17
37
  * const { data: assets, isLoading } = useEthereumWalletAssets()
38
+ * // Multi-chain:
39
+ * const { data, multiChain } = useEthereumWalletAssets({ multiChain: true })
18
40
  * ```
19
41
  */
20
- export declare const useEthereumWalletAssets: ({ assets: hookCustomAssets, staleTime, }?: UseEthereumWalletAssetsOptions) => {
21
- data: readonly Asset[] | null;
22
- isLoading: boolean;
23
- isError: boolean;
24
- isSuccess: boolean;
25
- isIdle: boolean;
26
- error: OpenfortError | undefined;
27
- refetch: () => Promise<readonly Asset[] | undefined>;
28
- };
42
+ export declare const useEthereumWalletAssets: ({ assets: hookCustomAssets, multiChain, staleTime, }?: UseEthereumWalletAssetsOptions) => UseEthereumWalletAssetsResult;
29
43
  export {};
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useMemo } from 'react';
2
- import { custom, createWalletClient, numberToHex } from 'viem';
2
+ import { numberToHex, custom, createWalletClient, formatUnits } from 'viem';
3
3
  import { erc7811Actions } from 'viem/experimental';
4
4
  import { useOpenfort } from '../../components/Openfort/useOpenfort.js';
5
5
  import { OpenfortError, OpenfortReactErrorType } from '../../types.js';
@@ -8,20 +8,34 @@ import { openfortKeys } from '../../query/queryKeys.js';
8
8
  import { useAsyncData } from '../../shared/hooks/useAsyncData.js';
9
9
  import { useEthereumEmbeddedWallet } from './useEthereumEmbeddedWallet.js';
10
10
 
11
+ function getUsdValue(asset) {
12
+ var _a, _b, _c;
13
+ const fiat = (_a = asset.metadata) === null || _a === void 0 ? void 0 : _a.fiat;
14
+ if (!(fiat === null || fiat === void 0 ? void 0 : fiat.value) || asset.balance === undefined)
15
+ return 0;
16
+ const decimals = (_c = (_b = asset.metadata) === null || _b === void 0 ? void 0 : _b.decimals) !== null && _c !== void 0 ? _c : 18;
17
+ const amount = Number.parseFloat(formatUnits(asset.balance, decimals));
18
+ return Number.isFinite(amount) ? amount * fiat.value : 0;
19
+ }
11
20
  /**
12
21
  * Returns wallet assets (tokens, NFTs) for the connected Ethereum address.
13
22
  * Uses ERC-7811 via Openfort's authenticated RPC proxy.
14
23
  *
15
- * @param options - Optional custom assets config and staleTime
24
+ * When `multiChain` is true, fetches assets across all configured chains
25
+ * via `wallet_getAssets` and returns `MultiChainAsset[]` (assets tagged with `chainId`).
26
+ *
27
+ * @param options - Optional custom assets config, multiChain flag, and staleTime
16
28
  * @returns assets, isLoading, error, refetch
17
29
  *
18
30
  * @example
19
31
  * ```tsx
20
32
  * const { data: assets, isLoading } = useEthereumWalletAssets()
33
+ * // Multi-chain:
34
+ * const { data, multiChain } = useEthereumWalletAssets({ multiChain: true })
21
35
  * ```
22
36
  */
23
- const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000, } = {}) => {
24
- var _a;
37
+ const useEthereumWalletAssets = ({ assets: hookCustomAssets, multiChain = false, staleTime = 30000, } = {}) => {
38
+ var _a, _b;
25
39
  const wallet = useEthereumEmbeddedWallet();
26
40
  const isConnected = wallet.status === 'connected';
27
41
  const address = isConnected ? wallet.address : undefined;
@@ -29,6 +43,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
29
43
  const { walletConfig, publishableKey, overrides, thirdPartyAuth, chains } = useOpenfort();
30
44
  const { getAccessToken } = useUser();
31
45
  const chain = chains.find((c) => c.id === chainId);
46
+ const backendUrl = (overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl) || 'https://api.openfort.io';
32
47
  const buildHeaders = useCallback(async () => {
33
48
  if (thirdPartyAuth) {
34
49
  const accessToken = await thirdPartyAuth.getAccessToken();
@@ -51,10 +66,25 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
51
66
  };
52
67
  return headers;
53
68
  }, [publishableKey, getAccessToken, thirdPartyAuth]);
69
+ /** For multiChain: walletConfig.ethereum.assets as backend assetFilter format (hex chainId -> [{ address, type }]). */
70
+ const customAssetsMultiChain = useMemo(() => {
71
+ var _a;
72
+ if (!multiChain)
73
+ return undefined;
74
+ const configAssets = (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets;
75
+ if (!configAssets)
76
+ return undefined;
77
+ const mapped = {};
78
+ for (const [cid, addresses] of Object.entries(configAssets)) {
79
+ const hexChainId = numberToHex(Number(cid));
80
+ mapped[hexChainId] = addresses.map((addr) => ({ address: addr, type: 'erc20' }));
81
+ }
82
+ return Object.keys(mapped).length > 0 ? mapped : undefined;
83
+ }, [multiChain, (_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets]);
54
84
  const customTransport = useMemo(() => () => {
55
85
  return custom({
56
86
  async request({ method, params }) {
57
- const res = await fetch(`${(overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl) || 'https://api.openfort.io'}/rpc`, {
87
+ const res = await fetch(`${backendUrl}/rpc`, {
58
88
  method: 'POST',
59
89
  headers: await buildHeaders(),
60
90
  body: JSON.stringify({
@@ -71,7 +101,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
71
101
  return data.result;
72
102
  },
73
103
  });
74
- }, [buildHeaders, overrides === null || overrides === void 0 ? void 0 : overrides.backendUrl]);
104
+ }, [buildHeaders, backendUrl]);
75
105
  const customAssetsToFetch = useMemo(() => {
76
106
  var _a;
77
107
  if (!chainId)
@@ -80,11 +110,101 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
80
110
  const assetsFromHook = hookCustomAssets ? hookCustomAssets[chainId] || [] : [];
81
111
  const allAssets = [...assetsFromConfig, ...assetsFromHook];
82
112
  return allAssets;
83
- }, [(_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets, hookCustomAssets, chainId]);
113
+ }, [(_b = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _b === void 0 ? void 0 : _b.assets, hookCustomAssets, chainId]);
84
114
  const { data, error, isLoading, refetch } = useAsyncData({
85
- queryKey: [...openfortKeys.walletAssets(chainId, customAssetsToFetch, address)],
115
+ queryKey: multiChain
116
+ ? ['wallet-assets', 'multi', address, customAssetsMultiChain]
117
+ : [...openfortKeys.walletAssets(chainId, customAssetsToFetch, address)],
86
118
  queryFn: async () => {
87
- var _a, _b, _c, _d;
119
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
120
+ if (multiChain) {
121
+ if (!address) {
122
+ throw new OpenfortError('No wallet address available', OpenfortReactErrorType.UNEXPECTED_ERROR);
123
+ }
124
+ const headers = await buildHeaders();
125
+ const defaultRequest = fetch(`${backendUrl}/rpc`, {
126
+ method: 'POST',
127
+ headers,
128
+ body: JSON.stringify({
129
+ method: 'wallet_getAssets',
130
+ params: { account: address },
131
+ id: 1,
132
+ jsonrpc: '2.0',
133
+ }),
134
+ });
135
+ const customRequest = customAssetsMultiChain
136
+ ? fetch(`${backendUrl}/rpc`, {
137
+ method: 'POST',
138
+ headers,
139
+ body: JSON.stringify({
140
+ method: 'wallet_getAssets',
141
+ params: { account: address, assetFilter: customAssetsMultiChain },
142
+ id: 2,
143
+ jsonrpc: '2.0',
144
+ }),
145
+ })
146
+ : null;
147
+ const responses = await Promise.all([defaultRequest, customRequest].filter(Boolean));
148
+ const [defaultData, customData] = await Promise.all(responses.map((r) => r.json()));
149
+ const result = { ...((_a = defaultData.result) !== null && _a !== void 0 ? _a : {}) };
150
+ if ((customData === null || customData === void 0 ? void 0 : customData.result) && typeof customData.result === 'object') {
151
+ for (const [chainKey, assets] of Object.entries(customData.result)) {
152
+ if (!Array.isArray(assets))
153
+ continue;
154
+ if (!result[chainKey]) {
155
+ result[chainKey] = assets;
156
+ }
157
+ else {
158
+ const existing = new Map(result[chainKey].map((a) => { var _a; return [(_a = a.address) !== null && _a !== void 0 ? _a : '', a]; }));
159
+ for (const asset of assets) {
160
+ existing.set((_b = asset.address) !== null && _b !== void 0 ? _b : '', asset);
161
+ }
162
+ result[chainKey] = Array.from(existing.values());
163
+ }
164
+ }
165
+ }
166
+ const allAssets = [];
167
+ for (const [chainIdKey, assets] of Object.entries(result)) {
168
+ const cid = Number(chainIdKey);
169
+ if (!Array.isArray(assets))
170
+ continue;
171
+ for (const a of assets) {
172
+ if (a.type === 'erc20') {
173
+ const asset = {
174
+ type: 'erc20',
175
+ address: ((_c = a.address) !== null && _c !== void 0 ? _c : '0x0'),
176
+ balance: BigInt((_d = a.balance) !== null && _d !== void 0 ? _d : 0),
177
+ metadata: {
178
+ name: ((_e = a.metadata) === null || _e === void 0 ? void 0 : _e.name) || 'Unknown Token',
179
+ symbol: ((_f = a.metadata) === null || _f === void 0 ? void 0 : _f.symbol) || 'UNKNOWN',
180
+ decimals: (_g = a.metadata) === null || _g === void 0 ? void 0 : _g.decimals,
181
+ fiat: (_h = a.metadata) === null || _h === void 0 ? void 0 : _h.fiat,
182
+ },
183
+ raw: a,
184
+ };
185
+ allAssets.push({ ...asset, chainId: cid });
186
+ }
187
+ else if (a.type === 'native') {
188
+ const meta = ((_j = a.metadata) !== null && _j !== void 0 ? _j : {});
189
+ const asset = {
190
+ type: 'native',
191
+ address: 'native',
192
+ balance: BigInt((_k = a.balance) !== null && _k !== void 0 ? _k : 0),
193
+ metadata: {
194
+ symbol: meta.symbol || 'ETH',
195
+ decimals: meta.decimals,
196
+ fiat: (_l = meta.fiat) !== null && _l !== void 0 ? _l : { value: 0, currency: 'USD' },
197
+ },
198
+ raw: a,
199
+ };
200
+ allAssets.push({ ...asset, chainId: cid });
201
+ }
202
+ }
203
+ }
204
+ allAssets.sort((a, b) => getUsdValue(b) - getUsdValue(a));
205
+ return allAssets;
206
+ }
207
+ // Single-chain path
88
208
  if (!address || !chainId || !chain) {
89
209
  throw new OpenfortError('Wallet not connected', OpenfortReactErrorType.UNEXPECTED_ERROR, {
90
210
  error: new Error('Address, chainId, or chain not available'),
@@ -115,8 +235,8 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
115
235
  // ERC-7811 response keys may be hex (e.g. "0x14a34") or numeric depending on the RPC
116
236
  const rawByChain = defaultAssetsRaw;
117
237
  const customByChain = customAssets;
118
- const rawChainAssets = (_b = (_a = rawByChain[hexChainId]) !== null && _a !== void 0 ? _a : rawByChain[String(chainId)]) !== null && _b !== void 0 ? _b : [];
119
- const customChainAssets = (_d = (_c = customByChain[hexChainId]) !== null && _c !== void 0 ? _c : customByChain[String(chainId)]) !== null && _d !== void 0 ? _d : [];
238
+ const rawChainAssets = (_o = (_m = rawByChain[hexChainId]) !== null && _m !== void 0 ? _m : rawByChain[String(chainId)]) !== null && _o !== void 0 ? _o : [];
239
+ const customChainAssets = (_q = (_p = customByChain[hexChainId]) !== null && _p !== void 0 ? _p : customByChain[String(chainId)]) !== null && _q !== void 0 ? _q : [];
120
240
  const defaultAssets = rawChainAssets.map((a) => {
121
241
  var _a;
122
242
  let asset;
@@ -153,7 +273,6 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
153
273
  const mergedAssets = [...defaultAssets];
154
274
  const customAssetsForChain = customChainAssets.flatMap((asset) => {
155
275
  var _a, _b;
156
- // Custom assets are explicitly requested as erc20; skip if the API returns something unexpected.
157
276
  if (asset.type !== 'erc20')
158
277
  return [];
159
278
  if (!((_a = walletConfig === null || walletConfig === void 0 ? void 0 : walletConfig.ethereum) === null || _a === void 0 ? void 0 : _a.assets))
@@ -178,7 +297,7 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
178
297
  });
179
298
  return mergedAssets;
180
299
  },
181
- enabled: isConnected && !!chainId && !!chain && !!address,
300
+ enabled: multiChain ? isConnected && !!address : isConnected && !!chainId && !!chain && !!address,
182
301
  staleTime,
183
302
  });
184
303
  const mappedError = useMemo(() => {
@@ -191,10 +310,11 @@ const useEthereumWalletAssets = ({ assets: hookCustomAssets, staleTime = 30000,
191
310
  }, [error]);
192
311
  return {
193
312
  data: data !== null && data !== void 0 ? data : null,
313
+ multiChain,
194
314
  isLoading,
195
315
  isError: !!error,
196
316
  isSuccess: !!data && !error,
197
- isIdle: !isConnected || !chainId || !chain,
317
+ isIdle: multiChain ? !address : !isConnected || !chainId || !chain,
198
318
  error: mappedError,
199
319
  refetch,
200
320
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useEthereumWalletAssets.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"useEthereumWalletAssets.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @remarks Client-only. Use in a Client Component (e.g. add `"use client"` in Next.js App Router).
8
8
  *
9
- * @returns user, linkedAccounts, isAuthenticated, isConnected, getAccessToken, validateAndRefreshToken
9
+ * @returns user, linkedAccounts, isLoading, isAuthenticated, isConnected, getAccessToken, validateAndRefreshToken
10
10
  *
11
11
  * @example
12
12
  * ```tsx
@@ -21,6 +21,7 @@
21
21
  export declare function useUser(): {
22
22
  user: import("@openfort/openfort-js").User | null;
23
23
  linkedAccounts: import("@openfort/openfort-js").UserAccount[];
24
+ isLoading: boolean;
24
25
  isAuthenticated: boolean;
25
26
  isConnected: boolean;
26
27
  getAccessToken: () => Promise<string | null>;
@@ -11,7 +11,7 @@ import { handleOAuthConfigError } from '../../utils/oauthErrorHandler.js';
11
11
  *
12
12
  * @remarks Client-only. Use in a Client Component (e.g. add `"use client"` in Next.js App Router).
13
13
  *
14
- * @returns user, linkedAccounts, isAuthenticated, isConnected, getAccessToken, validateAndRefreshToken
14
+ * @returns user, linkedAccounts, isLoading, isAuthenticated, isConnected, getAccessToken, validateAndRefreshToken
15
15
  *
16
16
  * @example
17
17
  * ```tsx
@@ -24,7 +24,7 @@ import { handleOAuthConfigError } from '../../utils/oauthErrorHandler.js';
24
24
  * ```
25
25
  */
26
26
  function useUser() {
27
- const { user, client, embeddedState, linkedAccounts, activeEmbeddedAddress } = useOpenfortCore();
27
+ const { user, client, embeddedState, linkedAccounts, activeEmbeddedAddress, isLoading } = useOpenfortCore();
28
28
  const isAuthenticated = embeddedState !== EmbeddedState.NONE && embeddedState !== EmbeddedState.UNAUTHENTICATED;
29
29
  const isConnected = embeddedState === EmbeddedState.READY && !!activeEmbeddedAddress;
30
30
  const getAccessTokenAndUpdate = useCallback(async () => {
@@ -50,6 +50,7 @@ function useUser() {
50
50
  return {
51
51
  user,
52
52
  linkedAccounts,
53
+ isLoading,
53
54
  isAuthenticated,
54
55
  isConnected,
55
56
  getAccessToken: getAccessTokenAndUpdate,
@@ -1 +1 @@
1
- {"version":3,"file":"useUser.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"useUser.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/build/index.d.ts CHANGED
@@ -44,7 +44,7 @@ export { default as Avatar } from './components/Common/Avatar';
44
44
  export { default as ChainIcon } from './components/Common/Chain';
45
45
  export { OpenfortButton } from './components/ConnectButton';
46
46
  export { OpenfortProvider } from './components/Openfort/OpenfortProvider';
47
- export type { CustomizableRoutes } from './components/Openfort/types';
47
+ export type { CustomizableRoutes, MultiChainAsset } from './components/Openfort/types';
48
48
  export { LinkWalletOnSignUpOption, UIAuthProvider as AuthProvider } from './components/Openfort/types';
49
49
  export { embeddedWalletId } from './constants/openfort';
50
50
  export { OpenfortError, OpenfortReactErrorType, OpenfortReactErrorType as OpenfortErrorType, } from './core/errors';
@@ -1,5 +1,5 @@
1
1
  import { ChainTypeEnum, EmbeddedState } from '@openfort/openfort-js';
2
- import { useContext, useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect, createElement, Fragment } from 'react';
2
+ import { useContext, useMemo, useState, useEffect, useRef, useLayoutEffect, useCallback, createElement, Fragment } from 'react';
3
3
  import { useStore } from 'zustand';
4
4
  import { routes } from '../components/Openfort/types.js';
5
5
  import { useOpenfort } from '../components/Openfort/useOpenfort.js';
@@ -99,8 +99,10 @@ const CoreOpenfortProvider = ({ children, onConnect, onDisconnect, openfortConfi
99
99
  });
100
100
  });
101
101
  }, []);
102
- // Sync chainType from UI context into the store
103
- useEffect(() => {
102
+ // Sync chainType from UI context into the store — useLayoutEffect so the store
103
+ // is updated before the next paint, preventing a one-render-cycle race where
104
+ // the strategy context (synchronous) sees SVM but the store still shows EVM.
105
+ useLayoutEffect(() => {
104
106
  store.getState().setChainType(chainType);
105
107
  }, [store, chainType]);
106
108
  // Recompute isLoading when bridge address changes (bridge connects/disconnects)
@@ -1 +1 @@
1
- {"version":3,"file":"CoreOpenfortProvider.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CoreOpenfortProvider.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -9,4 +9,4 @@ declare function flattenChildren(children: React.ReactNode): ReactChildArray;
9
9
  export declare const isWalletConnectConnector: (connectorId?: string) => connectorId is "walletConnect";
10
10
  export declare const isCoinbaseWalletConnector: (connectorId?: string) => connectorId is "coinbaseWalletSDK";
11
11
  export declare const isInjectedConnector: (connectorId?: string) => connectorId is "injected";
12
- export { nFormatter, truncateEthAddress, truncateSolanaAddress, isMobile, isAndroid, detectBrowser, flattenChildren };
12
+ export { detectBrowser, flattenChildren, isAndroid, isMobile, nFormatter, truncateEthAddress, truncateSolanaAddress };
@@ -1 +1 @@
1
- export declare const OPENFORT_VERSION = "1.0.2";
1
+ export declare const OPENFORT_VERSION = "1.0.4";
package/build/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const OPENFORT_VERSION = '1.0.2';
1
+ const OPENFORT_VERSION = '1.0.4';
2
2
 
3
3
  export { OPENFORT_VERSION };
4
4
  //# sourceMappingURL=version.js.map
@@ -9,7 +9,12 @@ function setEmbeddedWalletProvider(p) {
9
9
  function embeddedWalletConnector() {
10
10
  return createConnector((config) => {
11
11
  const accountsChangedHandler = (accs) => {
12
- config.emitter.emit('change', { accounts: accs });
12
+ // Filter out non-EVM addresses (e.g. Solana base58) that the SDK may emit
13
+ // when recover() is called for a Solana wallet — sdk bug, defensive guard.
14
+ const valid = accs.filter((a) => /^0x[0-9a-fA-F]{40}$/i.test(a));
15
+ if (valid.length === 0)
16
+ return;
17
+ config.emitter.emit('change', { accounts: valid });
13
18
  };
14
19
  const chainChangedHandler = (chain) => {
15
20
  config.emitter.emit('change', { chainId: Number(chain) });
@@ -1 +1 @@
1
- {"version":3,"file":"embeddedConnector.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"embeddedConnector.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfort/react",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "author": "Openfort (https://www.openfort.io)",
5
5
  "license": "BSD-2-Clause license",
6
6
  "description": "The easiest way to integrate Openfort to your project.",