Skip to content

Commit efc2eb1

Browse files
Agilulfo1820claude
andauthored
feat(account-modal): tabbed Assets with token detail, history, staking cards (#612)
* feat(account-modal): tabbed assets with token detail, history, and staking cards Refactor AccountModal Assets into a full portfolio view: balance header with Send/Swap/History actions, Token/Staking tabs, per-token detail screen with Swap/Send/Receive, aggregated transfer history backed by the VeChain indexer, transaction detail screen, and protocol-specific staking cards for Stargate, VeBetterDAO Navigators, and BetterSwap LP positions. Adds VVET (wrapped VET) as a tracked token priced 1:1 with VET, and folds staking positions into useTotalBalance so the portfolio total reflects the user's full holdings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(account-modal): polish assets layout, icons, and staking cards - Move Manage Custom Tokens button back inline with the search input; shrink the search input height for a tighter Assets header. - Trim AssetsTabs to just the tab labels. - Use the bundled BetterSwapLogo for the BetterSwap LP card; switch Stargate to the actual product logo; route VVET to the VeChain token registry icon and rename the Navigators card to "VeBetter". - Remove the redundant external-link icon next to the protocol name on staking cards and tighten the "Go to platform" button styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transfer-history): use eventType=VET for native VET filter The indexer doesn't accept the VET sentinel address as a tokenAddress filter, so filtering by VET was fetching a mixed page and post-filtering client-side, leaving only a handful of rows per page. Switch to the indexer's eventType=VET parameter so the page contains only VET transfers and pagination is meaningful. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(prices): 24h price change from oracle ValueUpdate events Add useOraclePriceChanges24h: reads OracleVechainEnergy's current values for each supported feed and scans ValueUpdate events in a ~5h window centered on the block ~24h ago, taking the most recent observation per feedId as the baseline. Filters by contract address only (the id field is non-indexed) and decodes args client-side. useTokenPrices exposes a priceChanges map keyed by token address (VOT3 and veDelegate inherit B3TR's change; VVET inherits VET's). Plumbs priceChange24hPct through useTokensWithValues and a USD-weighted portfolio-level change through useTotalBalance. UI: new common PriceChangeBadge renders +/-X.XX% in semantic green/red/muted with the "24h" suffix in neutral tertiary text. Surfaces in AssetButton, the restyled card-shaped asset row, TokenDetail header, AssetsHeader, and the main BalanceSection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(history): resolve .vet domains and add copy buttons - New AddressOrDomainLabel wraps useVechainDomain and renders the .vet domain when available, falling back to humanAddress otherwise. Used for the From/To subtitle on HistoryItemRow and the From/To rows on TransactionDetailContent. - New CopyIconButton wraps the SSR-safe copyToClipboard with a Copy -> Check icon transition. Added next to From, To, and Hash on the transaction detail screen so users can copy the full underlying address / tx ID even when the label is truncated or a domain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): dedupe and Map-group transfers to avoid duplicate React keys groupByDay used a sequential lastKey check, so any non-contiguous same-day items (e.g. when paginating overlaps a day boundary) produced multiple groups with the same label and React threw a duplicate-key error on Load more. Switch to a Map keyed by day label and a Set keyed by item.id so a transfer can never produce two groups or two rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transfer-history): use page param instead of offset/limit The VeChain indexer ignores offset/limit and only honors a 0-indexed `page` parameter (returning ~20 items per page). Previously every Load More click hit the same page-0 response, and the row dedupe quietly swallowed the duplicate IDs, so the UI appeared frozen. - Drop PAGE_SIZE and the offset/limit params. - Send only `page=<n>`. - getNextPageParam returns allPages.length (0 -> 1 -> 2 ...) when the server reports hasNext. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transfer-history): scope eventType=VET to explicit VET filter only isVetTokenAddress treated 'undefined tokenAddress' the same as the VET sentinel, so opening unfiltered history forced eventType=VET on the indexer and the user only saw native VET rows. Trigger the VET filter only when the caller explicitly passes the VET sentinel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): resolve unknown ERC-20 symbols and mirror sparkles icon - useTransferHistory now collects every ERC-20 address per page that isn't in the known token set (custom tokens + VET/VTHO/B3TR/VOT3/ veDelegate/VVET), fetches getTokenInfo for them in parallel, and uses the real symbol + decimals. Fixes rows that previously displayed as e.g. "+100 0x680f" (LP tokens from BetterSwap and other DeFi protocols) and corrects the amount when the contract isn't 18 decimals. - TransactionDetailContent now mirrors HistoryItemRow's behaviour: when the transfer was received from 0x000...000 it renders the LuSparkles placeholder circle instead of the token logo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(staking): add Juicy Finance card and fix history row overflow - New useJuicyPosition hook reads the Juicy Finance pool (Aave v3 fork at 0x00Bd212704A8816264607a7110cCabe70219D5aB on mainnet): enumerates reserves, batches getReserveData + balanceOf on each aToken / variableDebt / stableDebt token, and computes supplied + borrowed per asset and net USD value. Health factor read from getUserAccountData (null when there's no debt). - New JuicyFinanceCard renders a supplied list, optional borrowed list, and a colored health-rate tag; routes to www.juicyfinance.io. - StakingTab gates on hasPosition; useTotalBalance folds the net Juicy position into the portfolio total. - Local JuicyPoolAbi added in staking/abis.ts (canonical Aave v3 subset from vechain/b32). - HistoryItemRow: split amount and token symbol onto two lines, with flexShrink/minW/maxW constraints so long token names (jVechainVTHO, variableDebtVechainVTHO, etc.) truncate with an ellipsis instead of overflowing the row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build): satisfy stricter tsc on staking + oracle hooks CI's @vechain/vechain-kit:build runs tsc with stricter options than `yarn typecheck` and surfaced four type errors: - CustomTokenInfo.decimals is typed as string, not number — drop the inline cast in useTransferHistory and useJuicyPosition and coerce with Number(info.decimals) (fallback 18) instead. - oracle.read.getLatestValue requires `0x\${string}` — cast the PRICE_FEED_IDS value at the call site. - getEventLogs requires nodeUrl in GetEventsProps — pass network.nodeUrl in useOraclePriceChanges24h. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(i18n): translate remaining aria-labels and Copy prefix - aria-label on the action buttons in AssetsHeader and TokenDetail now uses the translated string (matches the visible text), not the raw English key. - Row's CopyIconButton aria-label uses t('Copy') instead of a hardcoded English prefix; Row pulls its own useTranslation hook. - Add the 'Copy' key to en.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n: translate new assets/history/staking keys across all 16 locales Add translations for the 37 keys introduced by the AccountModal refactor (Token, Staking, History, Sent, Received, Supplied, Borrowed, Health rate, Go to platform, Copy, etc.) to every supported locale: de, es, fr, hi, it, ja, ko, nl, pt, ro, ru, sv, tr, tw, vi, zh. Also alphabetises all language files (including en.json) case- insensitively so the per-locale set of keys stays in sync going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(history): thread onBack through transaction-detail / history screens Token Detail -> tap a history-preview row -> Transaction Detail. The back button was hardcoded to navigate to the aggregated history screen instead of returning to Token Detail. Add an optional onBack prop to both TransactionHistoryContent and TransactionDetailContent, and pass: - TokenDetail.handleHistoryItem -> onBack returns to Token Detail. - TokenDetail.handleSeeAll -> onBack returns to Token Detail. - TransactionHistory.handleItemClick -> onBack re-dispatches the same history screen with its tokenFilter + parent onBack so further back presses still chain correctly. History screen's back button uses onBack if provided, else falls back to the Assets list (existing behaviour). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(review): address valid CodeRabbit findings - useTransferHistory: only pre-fill symbolByAddress for known 18-decimal tokens (VET, VTHO, B3TR, VOT3, veDelegate, VVET). Custom tokens flow through the existing lazy getTokenInfo path so their real decimals are honoured. - useBetterSwapLpPositions: validate factoryAddress / pairs with viem's isAddress; build the queryKey from a pairs fingerprint (first + last + length) so different sets with equal length get distinct cache entries; use the validated pair list throughout the queryFn. - config: checksum the testnet stargate/stargate-NFT/navigator-registry addresses and the mainnet navigator address (EIP-55) to match the surrounding style. - CopyIconButton: store the setTimeout id in a useRef and clear it on rapid re-click and on unmount. Other CodeRabbit suggestions were verified against current code and are either already covered (oracle feedId cast, prices lowercase mirror, ERC20 balance hook's internal address guard), pre-existing patterns across the kit, or no-ops given the surrounding constraints — left unchanged to keep this diff minimal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(charts): 24h sparklines from oracle ValueUpdate events - Refactor the oracle hook into a single useOracleHistory24h query that scans 24h of ValueUpdate events once and exposes: * useOraclePriceChanges24h -> percent change per feed * useTokenPriceHistory24h(token) -> sorted PricePoint[] The current spot value is pinned as the closing point so the chart always extends to "now" even between sparse oracle updates. - New usePortfolioPriceHistory24h derives a portfolio sparkline by walking the union of feed timestamps and weighting each feed by the wallet's current liquid holdings (VVET counted as VET; VOT3 + veDelegate counted on the B3TR feed). Cheap and accurate enough for a 24h window where holdings rarely change. - New PriceChart: dependency-free SVG sparkline with a green/red/neutral tone, gradient fill, configurable opacity, and vector-effect non-scaling-stroke so the line stays crisp regardless of container width. - TokenDetail screen shows the chart full-opacity between balance and actions; VVET / VOT3 / veDelegate piggy-back on VET / B3TR. - BalanceSection (main wallet view) overlays the portfolio chart at ~18% opacity behind the balance text, tinted by the day's direction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(chart): smooth sparkline with Catmull-Rom-to-Bezier curves The straight-line polyline produced sharp corners between sparse oracle points. Replace M/L segments with a Catmull-Rom-derived cubic-Bezier spline: each segment's control points are derived from the surrounding two points so the curve glides through every observation without overshooting. The filled area + the existing strokeLinejoin/Linecap =round still apply. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chart): switch to monotone cubic interpolation to stop overshoot The Catmull-Rom-to-Bezier smoothing produced control points that overshot adjacent points whenever neighbouring oracle observations jumped sharply, causing the line to backtrack on itself. Replace with Fritsch-Carlson monotone cubic: tangents at each point are the average of neighbouring slopes (zero at local extrema), then clamped so the curve is provably monotone within each segment. The curve still passes through every observation but is guaranteed not to overshoot or reverse direction between points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chart): show portfolio sparkline on Assets page AssetsHeader now renders the same monotone-cubic portfolio sparkline between the balance row and the Send/Swap/History actions, mirroring the per-token chart on TokenDetail. Reuses usePortfolioPriceHistory24h and PriceChart, tinted by the day's direction. Hidden when balances are hidden (showAssets=false) or when there's only one observation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(chart): drop the sparkline underlay from BalanceSection The chart still ships on Assets and TokenDetail; on the main wallet view (BalanceSection) it competed with the AssetIcons row and didn't add enough signal at 18% opacity. Removes the underlay layer, the usePortfolioPriceHistory24h call, the Box wrapper and the now-unused imports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chart): hover/touch tooltip + swap Assets header button order - PriceChart: new interactive prop. When enabled the chart accepts mouse/touch input, snaps to the nearest data point, and renders a vertical guide line, a marker dot at the active point, and a position-aware tooltip with the formatted value and timestamp. New formatValue prop lets callers override the value formatter (defaults to a 4-decimal USD price; AssetsHeader passes formatCompactCurrency so portfolio values render as $4.30K-style). - TokenDetail and AssetsHeader charts opt in via interactive. - AssetsHeader actions reordered to [Swap, Send, History] so they match the [Swap, Send, Receive] order on TokenDetail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chart): fold staking positions into the portfolio sparkline The chart was only summing liquid token balances, so for wallets where most value sits in Stargate / Navigators / Juicy / BetterSwap LP, the sparkline reported a value an order of magnitude smaller than useTotalBalance's headline number. usePortfolioPriceHistory24h now also reads useStargatePositions, useNavigatorPosition, useJuicyPosition, useBetterSwapLpPositions and maps each into the appropriate price feed: - Stargate VET supplied -> VET feed amount. - Navigator staked B3TR / delegated B3TR -> B3TR feed amount. - Juicy supplied/borrowed -> per-asset net into VET (via VVET), VTHO, or B3TR feeds. - BetterSwap LP positions -> flat USD offset (their underlying split varies per pair, so attributing amounts to specific feeds isn't meaningful; the current USD value still contributes to the total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e6e9ae5 commit efc2eb1

69 files changed

Lines changed: 11973 additions & 7294 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/vechain-kit/src/components/AccountModal/AccountModal.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
FAQContent,
1616
ProfileContent,
1717
AssetsContent,
18+
TokenDetailContent,
19+
TransactionHistoryContent,
20+
TransactionDetailContent,
1821
LanguageSettingsContent,
1922
TermsAndPrivacyContent,
2023
GasTokenSettingsContent,
@@ -74,8 +77,20 @@ export const AccountModal = ({
7477
return (
7578
<SendTokenSummaryContent {...currentContent.props} />
7679
);
80+
case 'token-detail':
81+
return <TokenDetailContent {...currentContent.props} />;
82+
case 'transaction-history':
83+
return (
84+
<TransactionHistoryContent {...currentContent.props} />
85+
);
86+
case 'transaction-detail':
87+
return (
88+
<TransactionDetailContent {...currentContent.props} />
89+
);
7790
case 'swap-token':
7891
return <SwapTokenContent {...currentContent.props} />;
92+
case 'receive-token':
93+
return <ReceiveTokenContent {...currentContent.props} />;
7994
case 'choose-name':
8095
return <ChooseNameContent {...currentContent.props} />;
8196
case 'choose-name-search':

packages/vechain-kit/src/components/AccountModal/Components/BalanceSection.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
Button,
88
useToken,
99
} from '@chakra-ui/react';
10-
import { useRefreshBalances, useWallet, useTotalBalance, LocalStorageKey, useLocalStorage } from '@/hooks';
10+
import {
11+
useRefreshBalances,
12+
useWallet,
13+
useTotalBalance,
14+
LocalStorageKey,
15+
useLocalStorage,
16+
} from '@/hooks';
17+
import { PriceChangeBadge } from '@/components/common';
1118
import { useState } from 'react';
1219
import { useTranslation } from 'react-i18next';
1320
import { LuRefreshCw } from 'react-icons/lu';
@@ -26,7 +33,7 @@ export const BalanceSection = ({
2633
}) => {
2734
const { t } = useTranslation();
2835
const { account } = useWallet();
29-
const { formattedBalance, isLoading } = useTotalBalance({
36+
const { formattedBalance, isLoading, priceChange24hPct } = useTotalBalance({
3037
address: account?.address ?? '',
3138
});
3239

@@ -109,9 +116,18 @@ export const BalanceSection = ({
109116
mt={4}
110117
mb={4}
111118
>
112-
<Heading size={'2xl'} fontWeight={'700'}>
113-
{showAssets ? formattedBalance : '$****'}
114-
</Heading>
119+
<HStack spacing={3} align="baseline">
120+
<Heading size={'2xl'} fontWeight={'700'}>
121+
{showAssets ? formattedBalance : '$****'}
122+
</Heading>
123+
{showAssets && (
124+
<PriceChangeBadge
125+
valuePct={priceChange24hPct}
126+
showSuffix
127+
fontSize="sm"
128+
/>
129+
)}
130+
</HStack>
115131

116132
<HStack
117133
w={'full'}

packages/vechain-kit/src/components/AccountModal/Contents/Assets/AssetsContent.tsx

Lines changed: 58 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
import {
22
Container,
3-
HStack,
4-
IconButton,
5-
Input,
6-
InputGroup,
7-
InputLeftElement,
83
ModalBody,
94
ModalCloseButton,
105
ModalHeader,
11-
Tooltip,
126
VStack,
13-
useToken,
147
} from '@chakra-ui/react';
15-
import { useWallet, useTokensWithValues, TokenWithValue } from '@/hooks';
8+
import { useState } from 'react';
9+
import { useTranslation } from 'react-i18next';
1610
import {
17-
AssetButton,
1811
ModalBackButton,
1912
StickyHeaderContainer,
2013
} from '@/components/common';
2114
import { useVeChainKitConfig } from '@/providers';
22-
import { useTranslation } from 'react-i18next';
23-
import { LuPencil } from 'react-icons/lu';
24-
import { AccountModalContentTypes } from '../../Types';
25-
import { LuSearch } from 'react-icons/lu';
26-
import { useState } from 'react';
27-
import { useCurrency } from '@/hooks';
28-
import { SupportedCurrency } from '@/utils/currencyUtils';
29-
import { NON_TRANSFERABLE_TOKEN_SYMBOLS } from '@/utils';
3015
import { useAccountModalOptions } from '@/hooks/modals/useAccountModalOptions';
16+
import { TokenWithValue } from '@/hooks';
17+
import { AccountModalContentTypes } from '../../Types';
18+
import { AssetsHeader } from './Components/AssetsHeader';
19+
import {
20+
AssetsTabIndex,
21+
AssetsTabs,
22+
} from './Components/AssetsTabs';
23+
import { TokensTab } from './Components/TokensTab';
24+
import { StakingTab } from './Components/StakingTab';
3125

3226
export type AssetsContentProps = {
3327
setCurrentContent: React.Dispatch<
@@ -36,31 +30,44 @@ export type AssetsContentProps = {
3630
};
3731

3832
export const AssetsContent = ({ setCurrentContent }: AssetsContentProps) => {
39-
const { account } = useWallet();
40-
const { sortedTokens } = useTokensWithValues({ address: account?.address });
41-
const { allowCustomTokens, darkMode } = useVeChainKitConfig();
42-
const { currentCurrency } = useCurrency();
43-
44-
const textTertiary = useToken('colors', 'vechain-kit-text-tertiary');
4533
const { t } = useTranslation();
34+
const { allowCustomTokens } = useVeChainKitConfig();
4635
const { isolatedView } = useAccountModalOptions();
47-
const [searchQuery, setSearchQuery] = useState('');
36+
const [tabIndex, setTabIndex] = useState<AssetsTabIndex>(0);
4837

4938
const handleTokenSelect = (token: TokenWithValue) => {
39+
setCurrentContent({
40+
type: 'token-detail',
41+
props: { setCurrentContent, token },
42+
});
43+
};
44+
45+
const handleSend = () => {
5046
setCurrentContent({
5147
type: 'send-token',
5248
props: {
5349
setCurrentContent,
54-
preselectedToken: token,
5550
onBack: () => setCurrentContent('assets'),
5651
},
5752
});
5853
};
5954

60-
// Filter tokens by search query
61-
const filteredTokens = sortedTokens.filter(({ symbol }) =>
62-
symbol.toLowerCase().includes(searchQuery.toLowerCase()),
63-
);
55+
const handleSwap = () => {
56+
setCurrentContent({
57+
type: 'swap-token',
58+
props: {
59+
setCurrentContent,
60+
onBack: () => setCurrentContent('assets'),
61+
},
62+
});
63+
};
64+
65+
const handleHistory = () => {
66+
setCurrentContent({
67+
type: 'transaction-history',
68+
props: { setCurrentContent },
69+
});
70+
};
6471

6572
return (
6673
<>
@@ -76,65 +83,31 @@ export const AssetsContent = ({ setCurrentContent }: AssetsContentProps) => {
7683

7784
<Container h={['540px', 'auto']} p={0}>
7885
<ModalBody>
79-
<HStack spacing={2}>
80-
<InputGroup size="lg" flex={1}>
81-
<Input
82-
placeholder="Search token"
83-
bg={darkMode ? '#00000038' : 'gray.50'}
84-
borderRadius="xl"
85-
height="56px"
86-
pl={12}
87-
value={searchQuery}
88-
onChange={(e) => setSearchQuery(e.target.value)}
89-
data-testid="search-token-input"
90-
/>
91-
<InputLeftElement h="56px" w="56px" pl={4}>
92-
<LuSearch
93-
color={textTertiary}
94-
/>
95-
</InputLeftElement>
96-
</InputGroup>
97-
{allowCustomTokens && (
98-
<Tooltip label={t('Manage Custom Tokens')}>
99-
<IconButton
100-
aria-label={t('Manage Custom Tokens')}
101-
icon={<LuPencil />}
102-
variant="vechainKitSecondary"
103-
size="lg"
104-
height="56px"
105-
width="56px"
106-
minW="56px"
107-
borderRadius="xl"
108-
onClick={() =>
109-
setCurrentContent('add-custom-token')
110-
}
111-
/>
112-
</Tooltip>
113-
)}
114-
</HStack>
115-
116-
<VStack spacing={2} align="stretch" mt={2}>
117-
{filteredTokens.map((token) => {
118-
const hasBalance = Number(token.balance) > 0;
119-
const isNonTransferable =
120-
NON_TRANSFERABLE_TOKEN_SYMBOLS.includes(
121-
token.symbol,
122-
);
86+
<VStack spacing={6} align="stretch" w="full">
87+
<AssetsHeader
88+
onSend={handleSend}
89+
onSwap={handleSwap}
90+
onHistory={handleHistory}
91+
/>
12392

124-
return (
125-
<AssetButton
126-
key={token.address}
127-
symbol={token.symbol}
128-
amount={Number(token.balance)}
129-
currencyValue={token.valueInCurrency}
130-
currentCurrency={
131-
currentCurrency as SupportedCurrency
93+
<AssetsTabs
94+
tabIndex={tabIndex}
95+
onTabChange={setTabIndex}
96+
tokenPanel={
97+
<TokensTab
98+
onSelect={handleTokenSelect}
99+
onManageTokens={
100+
allowCustomTokens
101+
? () =>
102+
setCurrentContent(
103+
'add-custom-token',
104+
)
105+
: undefined
132106
}
133-
onClick={() => handleTokenSelect(token)}
134-
isDisabled={!hasBalance || isNonTransferable}
135107
/>
136-
);
137-
})}
108+
}
109+
stakingPanel={<StakingTab />}
110+
/>
138111
</VStack>
139112
</ModalBody>
140113
</Container>

0 commit comments

Comments
 (0)