feat(account-modal): tabbed Assets with token detail, history, staking cards#612
Conversation
…aking 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) <[email protected]>
- 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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
- 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) <[email protected]>
… 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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
- 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) <[email protected]>
- 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) <[email protected]>
📝 WalkthroughWalkthroughThis PR adds comprehensive token detail browsing and transaction history viewing to the AccountModal, refactors the Assets tab into a tabbed interface separating tokens and staking positions, and integrates multi-protocol staking position tracking with 24h oracle-based price change visualization across the entire system. ChangesAccount Modal: Token Details, Transaction History, and Staking Positions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
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) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (9)
packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts (3)
131-131: 💤 Low valueRemove unused SCALE constant.
The
SCALEconstant is defined but only referenced via avoidstatement, indicating incomplete code cleanup.🧹 Cleanup suggestion
- // Keep both raw values around in case future callers want them; - // for now the percentage is what we expose. - void SCALE;If the constant was intended for future use, consider removing it until needed or add a comment explaining the intended use case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts` at line 131, The unused constant SCALE is defined but only referenced via a no-op "void SCALE;" which should be removed or documented; delete the SCALE declaration and the "void SCALE;" line inside useOraclePriceChanges24h (or, if SCALE is intended for future use, replace the void usage with a comment explaining its planned purpose) so there are no dead symbols left in the hook.
56-64: ⚡ Quick winConsider logging contract call failures.
Silent error catching in the
getLatestValuecalls could mask misconfigurations (wrong feed IDs, network issues, or contract errors). Consider logging failures to aid debugging.💡 Suggested improvement
try { const res = await oracle.read.getLatestValue(feedId as `0x${string}`); const raw = (res as readonly bigint[])[0]; return [token, raw] as const; } catch { + console.warn(`Failed to fetch latest price for ${token} (feed ${feedId})`); return [token, null] as const; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts` around lines 56 - 64, The map callback silently swallows errors from oracle.read.getLatestValue; update the catch block in the feedEntries.map(async ([token, feedId]) => { ... }) callback to log the failure (including token and feedId and the caught error) before returning [token, null]; use the project's logger if available (e.g., processLogger or a provided logger), otherwise fallback to console.error, and keep the behavior of returning [token, null] unchanged.
95-113: 💤 Low valueConsider logging event decoding failures.
Silent error catching during event decoding could hide malformed events or ABI mismatches. Logging would help diagnose oracle integration issues.
💡 Suggested improvement
try { const decoded = viemDecodeEventLog({ abi: OracleVechainEnergy__factory.abi, data: event.data.toString() as ViemHex, topics: event.topics.map((t) => t.toString()) as Topics, }); if (decoded.eventName !== 'ValueUpdate') continue; const args = decoded.args as unknown as { id: string; value: bigint; }; const idKey = args.id.toLowerCase(); if (!pastByFeedId.has(idKey)) { pastByFeedId.set(idKey, args.value); } - } catch { + } catch (err) { + console.warn('Failed to decode ValueUpdate event:', err); // ignore malformed entries }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts` around lines 95 - 113, The event-decoding loop swallows all decoding errors, hiding malformed events or ABI mismatches; update the catch block around viemDecodeEventLog(...) in the loop (where OracleVechainEnergy__factory.abi, events, and pastByFeedId are used) to log the failure details and the error (include event.data, event.topics and the caught error). Use the existing logger (e.g., processLogger or a module logger) if available, otherwise fallback to console.warn, so decoding failures are visible for debugging while still continuing to ignore that single malformed entry.packages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionDetailContent.tsx (2)
102-102: 💤 Low valueConsider extracting ZERO_ADDRESS to a shared constants file.
The
ZERO_ADDRESSconstant is defined inline here but represents a well-known Ethereum constant that may be used elsewhere. Consider moving it to a shared constants file (e.g., alongsideVET_TOKEN_SENTINELandVTHO_TOKEN_ADDRESSmentioned in the transfer history types layer).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionDetailContent.tsx` at line 102, Extract the inline ZERO_ADDRESS into the shared constants module used by the transfer history layer (the same place that exports VET_TOKEN_SENTINEL and VTHO_TOKEN_ADDRESS); add and export a ZERO_ADDRESS constant there, then replace the inline declaration in TransactionDetailContent (referencing ZERO_ADDRESS) with an import from that shared constants module so all components use the single canonical value.
43-50: 💤 Low valueConsider extracting date formatting helpers to a shared utility.
The
formatFullDatehelper is defined inline here, and a similarformatDayLabelexists inTransactionHistoryContent.tsx(lines 35-40). Extracting these to a shareddateUtils.tsor similar would reduce duplication and ensure consistent date formatting across the application.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionDetailContent.tsx` around lines 43 - 50, Extract the inline date helpers into a shared utility by creating a new module (e.g., dateUtils.ts) that exports functions matching the existing helpers' signatures—move formatFullDate and formatDayLabel into it, retain the locale and timestamp parameters and Intl.DateTimeFormat options, then update TransactionDetailContent (replace the local formatFullDate) and TransactionHistoryContent (replace formatDayLabel) to import these helpers; ensure existing behavior and formats are preserved and run tests/compile to confirm no type or import issues.packages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/TokenDetailContent.tsx (1)
42-42: ⚡ Quick winUse the centralized VET_TOKEN_SENTINEL constant.
The
VET_SENTINELconstant is defined here as'0x', but according to the review context, aVET_TOKEN_SENTINELconstant already exists in the transfer history types layer (packages/vechain-kit/src/hooks/api/transferHistory/types.ts). Import and use the existing constant to avoid duplication.♻️ Proposed fix
+import { VET_TOKEN_SENTINEL } from '@/hooks'; import { ModalBackButton, PriceChangeBadge, @@ -39,8 +40,6 @@ token: TokenWithValue; }; -const VET_SENTINEL = '0x'; - const ActionIconButton = ({ icon, label,Then replace all uses of
VET_SENTINELwithVET_TOKEN_SENTINEL(lines 106, 145, 211).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/TokenDetailContent.tsx` at line 42, Replace the local VET_SENTINEL constant with the centralized VET_TOKEN_SENTINEL: remove the const VET_SENTINEL = '0x' and import VET_TOKEN_SENTINEL from the transfer-history types module where it is declared, then replace all usages of VET_SENTINEL inside the TokenDetailContent component (the places that check or compare token sentinel values) with VET_TOKEN_SENTINEL to avoid duplication.packages/vechain-kit/src/hooks/api/staking/useStargatePositions.ts (1)
36-50: ⚡ Quick winAvoid hardcoded VET sentinel; use shared constant.
'0x'as an inline sentinel is brittle. Import the canonical VET token key/sentinel used across wallet/history hooks to keep pricing lookup consistent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/hooks/api/staking/useStargatePositions.ts` around lines 36 - 50, Replace the inline sentinel const VET_ADDRESS = '0x' in useStargatePositions with the shared canonical VET constant used by the wallet/history hooks: import that exported VET token key (the shared VET constant) at the top of the file and use it in the pricing lookup (vetPriceUsd = prices[SHARED_VET_CONSTANT] || 0). Update any references to VET_ADDRESS in this module (e.g., vetPriceUsd) to the imported constant so the stargate price lookup matches the rest of the app.packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/NavigatorsCard.tsx (1)
39-43: ⚡ Quick winClarify valueInCurrency fallback logic.
Both rows use a fallback of
1whenstakedB3TR + delegatedAmount === 0, which assigns the fulltotalValueInCurrencyto that row. If this denominator-zero case is possible, the fallback produces an arbitrary value (whichever row renders first gets the full value). Consider:
- If the denominator can never be zero (flags guarantee non-zero amounts), document this assumption or remove the fallback.
- If zero amounts are possible, fallback to
0instead of1to avoid assigning unearned value:♻️ Proposed fix
valueInCurrency={totalValueInCurrency * ( (stakedB3TR + delegatedAmount) > 0 ? stakedB3TR / (stakedB3TR + delegatedAmount) - : 1 + : 0 )}Apply the same change at line 61.
Also applies to: 57-61
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/NavigatorsCard.tsx` around lines 39 - 43, The valueInCurrency calculation uses a fallback of 1 when (stakedB3TR + delegatedAmount) === 0 which assigns the full totalValueInCurrency arbitrarily; update both occurrences where valueInCurrency is computed (the expression using totalValueInCurrency * ((stakedB3TR + delegatedAmount) > 0 ? stakedB3TR / (stakedB3TR + delegatedAmount) : 1) and the similar delegatedAmount expression) to use 0 as the fallback instead of 1, or alternatively remove the fallback if you can guarantee the denominator is never zero and document that assumption in the component (NavigatorsCard / the valueInCurrency calculations referencing stakedB3TR, delegatedAmount and totalValueInCurrency).packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/TokensTab.tsx (1)
84-86: ⚡ Quick winAvoid unsafe type assertion.
The
currentCurrencyis being cast toSupportedCurrencywithout runtime validation. IfcurrentCurrencyis not actually a validSupportedCurrencyvalue, this could lead to runtime errors.Consider removing the type assertion if
useCurrency()already returnsSupportedCurrency, or add runtime validation:🛡️ Safer approach without type assertion
If
currentCurrencyis already typed asSupportedCurrency, remove the assertion:- currentCurrency={ - currentCurrency as SupportedCurrency - } + currentCurrency={currentCurrency}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/TokensTab.tsx` around lines 84 - 86, The prop currentCurrency is being unsafely cast to SupportedCurrency in TokensTab.tsx; either remove the type assertion if useCurrency() already returns SupportedCurrency (i.e., ensure the hook's return type is corrected), or add a runtime guard before passing it down (create/use an isSupportedCurrency(value) check or validate against the SupportedCurrency enum/set and handle the fallback case) so you only pass a validated SupportedCurrency into the component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/AssetsTabs.tsx`:
- Line 31: The onChange handler currently casts Chakra's idx to AssetsTabIndex
which skips runtime validation; change the onChange passed to Tabs so it
validates idx before calling onTabChange: check that idx is a number and within
the valid range for AssetsTabIndex (e.g., 0 <= idx < numberOfTabs or validate
against a known array of allowed indices) and only then call onTabChange(idx as
AssetsTabIndex), otherwise call onTabChange(0) as the safe fallback; reference
the onChange prop on the Tabs component, the onTabChange handler, and the
AssetsTabIndex type when making this change.
In
`@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/JuicyFinanceCard.tsx`:
- Around line 95-98: The code unsafely casts currentCurrency to
SupportedCurrency before calling formatCompactCurrency (used in
JuicyFinanceCard), which can pass invalid values; add a type guard (e.g.,
isSupportedCurrency(value): value is SupportedCurrency) and validate
currentCurrency before passing it to formatCompactCurrency, and handle the
fallback case (use a default currency or render a safe placeholder) so both the
formatCompactCurrency call at the currentCurrency usage and the duplicate
occurrence later in the component use the validated currency instead of an
unchecked cast.
In
`@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/TokensTab.tsx`:
- Line 82: The code is unsafely converting token.balance to Number which can
lose precision if TokenWithValue.balance is a string or BigInt; inspect the
TokenWithValue type declaration to confirm the balance type and then fix either:
1) adjust TokensTab.tsx (the place using token.balance and the amount prop) to
accept and handle bigint/string balances using a safe big-number library (e.g.,
BigInt, Big.js, or ethers.BigNumber) and pass a properly formatted
decimal/string to the child component, or 2) change the child component’s amount
prop type to accept string/BigInt and update its internal arithmetic/display to
use big-number handling; ensure TokenWithValue.balance and any consumers are
type-consistent so no implicit Number() conversion occurs.
In `@packages/vechain-kit/src/components/common/AssetButton.tsx`:
- Line 19: The import in AssetButton.tsx uses a relative path for
PriceChangeBadge; update the import to use the repository path alias for
components (e.g., `@components/`...) so it follows the project's alias-import
convention; locate the import statement referencing PriceChangeBadge in
AssetButton (symbol: PriceChangeBadge) and replace the relative path with the
appropriate alias path for the components folder.
In `@packages/vechain-kit/src/components/common/CopyIconButton.tsx`:
- Around line 20-25: The copied-state timer in CopyIconButton's handleClick
schedules a new timeout on every click without clearing prior timers; modify
handleClick to clear any existing timer before creating a new one (store the
timer id in a ref, e.g. timeoutRef.current), then setCopied(true) and assign
timeoutRef.current = setTimeout(...). Also add a useEffect cleanup that clears
timeoutRef.current on unmount to avoid stray timers; update references in
CopyIconButton, handleClick, and the cleanup effect accordingly.
In `@packages/vechain-kit/src/config/testnet.ts`:
- Around line 91-94: Update the three testnet contract address values
(stargateContractAddress, stargateNftContractAddress,
navigatorRegistryContractAddress) to use EIP-55 checksummed addresses instead of
all-lowercase; compute and replace each string with the checksummed form (e.g.,
via ethers.utils.getAddress or another EIP-55 routine) so the values match the
checksummed format used in mainnet.ts.
In `@packages/vechain-kit/src/hooks/api/staking/useBetterSwapLpPositions.ts`:
- Around line 90-114: The hook useBetterSwapLpPositions enables and calls
contracts without validating addresses; update the enabled condition and the
queryFn to use isAddress() checks: ensure address and factoryAddress pass
isAddress() before setting enabled (replace !!address/!!factoryAddress with
isAddress(address) && isAddress(factoryAddress)), and in the pairs handling
filter/validate each pair with isAddress() before mapping into
executeMultipleClausesCall (skip invalid entries or return [] early). Keep the
same symbols (enabled, queryFn, executeMultipleClausesCall, pairs,
UniswapV2Pair__factory) and make sure any address casts to `0x${string}` only
occur after validation.
- Around line 93-99: The query key in useBetterSwapLpPositions uses only
pairs.length which can lead to cache collisions when the pairs list changes but
its length stays the same; update the queryKey (in useBetterSwapLpPositions) to
include a stable representation of the pairs themselves (for example a
deterministic identifier array like pairs.map(p => p.id) or
JSON.stringify(pairs) or similar) instead of or in addition to pairs.length so
React Query discriminates different pair sets and returns correct LP positions.
In `@packages/vechain-kit/src/hooks/api/staking/useNavigatorPosition.ts`:
- Around line 34-38: The code currently does prices[b3trAddress] which can miss
matches if keys are normalized (e.g., lowercased); update useNavigatorPosition
logic to normalize the b3trAddress key the same way the prices map uses (for
example lowercase or checksum) before lookup—specifically normalize the
b3trAddress variable and then use prices[normalizedB3trAddress] (or fallback to
alternate normalizations) to compute b3trPriceUsd; ensure the normalization step
is applied where b3trAddress is read and referenced in this file (symbols:
b3trAddress, prices, b3trPriceUsd).
In `@packages/vechain-kit/src/hooks/api/transferHistory/useTransferHistory.ts`:
- Around line 56-61: The code is hardcoding decimals to 18 when populating
symbolByAddress, causing wrong formatting; update the population logic in
useTransferHistory where symbolByAddress.set(...) is called to use the actual
token decimals (e.g., use the decimals property from the balance object
`b.decimals` or a reliable token metadata lookup) and fall back to a safe
default only if decimals is undefined; ensure this change feeds into the same
path used by formatUnits so amounts are formatted with the correct decimals.
In `@packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts`:
- Line 58: The TypeScript error is caused by passing a plain string `feedId`
into the contract call; update the call site where
`oracle.read.getLatestValue(feedId)` is invoked to cast `feedId` to the typed
address pattern (feedId as `0x${string}`) so the argument matches the contract
signature, and ensure any method name literals used with `oracle.read` are typed
with `as const` per guidelines; specifically locate the `feedId` variable and
the `oracle.read.getLatestValue` invocation and apply the cast and `as const`
typing where appropriate.
In `@packages/vechain-kit/src/hooks/api/wallet/useTokenBalances.ts`:
- Around line 29-33: The VVET balance query is currently only gated by
config.vvetContractAddress and can run when address is missing; update the
query's enabled condition in the useGetErc20Balance call (the destructured
result vvetBalance / vvetLoading) to require both the contract address and the
wallet address (e.g., enabled: !!config.vvetContractAddress && !!address) so the
hook only runs when all required params are present.
In `@packages/vechain-kit/src/languages/en.json`:
- Around line 458-461: Remove the duplicate JSON translation keys by deleting
the repeated entries "From", "To", and "View on explorer" in the newly added
block; keep the original occurrences earlier in the file and ensure only the
earlier definitions of the keys "From", "To", and "View on explorer" remain so
the JSON object has unique keys and no later overwrites occur.
---
Nitpick comments:
In
`@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/NavigatorsCard.tsx`:
- Around line 39-43: The valueInCurrency calculation uses a fallback of 1 when
(stakedB3TR + delegatedAmount) === 0 which assigns the full totalValueInCurrency
arbitrarily; update both occurrences where valueInCurrency is computed (the
expression using totalValueInCurrency * ((stakedB3TR + delegatedAmount) > 0 ?
stakedB3TR / (stakedB3TR + delegatedAmount) : 1) and the similar delegatedAmount
expression) to use 0 as the fallback instead of 1, or alternatively remove the
fallback if you can guarantee the denominator is never zero and document that
assumption in the component (NavigatorsCard / the valueInCurrency calculations
referencing stakedB3TR, delegatedAmount and totalValueInCurrency).
In
`@packages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/TokensTab.tsx`:
- Around line 84-86: The prop currentCurrency is being unsafely cast to
SupportedCurrency in TokensTab.tsx; either remove the type assertion if
useCurrency() already returns SupportedCurrency (i.e., ensure the hook's return
type is corrected), or add a runtime guard before passing it down (create/use an
isSupportedCurrency(value) check or validate against the SupportedCurrency
enum/set and handle the fallback case) so you only pass a validated
SupportedCurrency into the component.
In
`@packages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/TokenDetailContent.tsx`:
- Line 42: Replace the local VET_SENTINEL constant with the centralized
VET_TOKEN_SENTINEL: remove the const VET_SENTINEL = '0x' and import
VET_TOKEN_SENTINEL from the transfer-history types module where it is declared,
then replace all usages of VET_SENTINEL inside the TokenDetailContent component
(the places that check or compare token sentinel values) with VET_TOKEN_SENTINEL
to avoid duplication.
In
`@packages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionDetailContent.tsx`:
- Line 102: Extract the inline ZERO_ADDRESS into the shared constants module
used by the transfer history layer (the same place that exports
VET_TOKEN_SENTINEL and VTHO_TOKEN_ADDRESS); add and export a ZERO_ADDRESS
constant there, then replace the inline declaration in TransactionDetailContent
(referencing ZERO_ADDRESS) with an import from that shared constants module so
all components use the single canonical value.
- Around line 43-50: Extract the inline date helpers into a shared utility by
creating a new module (e.g., dateUtils.ts) that exports functions matching the
existing helpers' signatures—move formatFullDate and formatDayLabel into it,
retain the locale and timestamp parameters and Intl.DateTimeFormat options, then
update TransactionDetailContent (replace the local formatFullDate) and
TransactionHistoryContent (replace formatDayLabel) to import these helpers;
ensure existing behavior and formats are preserved and run tests/compile to
confirm no type or import issues.
In `@packages/vechain-kit/src/hooks/api/staking/useStargatePositions.ts`:
- Around line 36-50: Replace the inline sentinel const VET_ADDRESS = '0x' in
useStargatePositions with the shared canonical VET constant used by the
wallet/history hooks: import that exported VET token key (the shared VET
constant) at the top of the file and use it in the pricing lookup (vetPriceUsd =
prices[SHARED_VET_CONSTANT] || 0). Update any references to VET_ADDRESS in this
module (e.g., vetPriceUsd) to the imported constant so the stargate price lookup
matches the rest of the app.
In `@packages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.ts`:
- Line 131: The unused constant SCALE is defined but only referenced via a no-op
"void SCALE;" which should be removed or documented; delete the SCALE
declaration and the "void SCALE;" line inside useOraclePriceChanges24h (or, if
SCALE is intended for future use, replace the void usage with a comment
explaining its planned purpose) so there are no dead symbols left in the hook.
- Around line 56-64: The map callback silently swallows errors from
oracle.read.getLatestValue; update the catch block in the feedEntries.map(async
([token, feedId]) => { ... }) callback to log the failure (including token and
feedId and the caught error) before returning [token, null]; use the project's
logger if available (e.g., processLogger or a provided logger), otherwise
fallback to console.error, and keep the behavior of returning [token, null]
unchanged.
- Around line 95-113: The event-decoding loop swallows all decoding errors,
hiding malformed events or ABI mismatches; update the catch block around
viemDecodeEventLog(...) in the loop (where OracleVechainEnergy__factory.abi,
events, and pastByFeedId are used) to log the failure details and the error
(include event.data, event.topics and the caught error). Use the existing logger
(e.g., processLogger or a module logger) if available, otherwise fallback to
console.warn, so decoding failures are visible for debugging while still
continuing to ignore that single malformed entry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d430c376-73c2-4fec-bfcf-ba3e902371ed
📒 Files selected for processing (51)
packages/vechain-kit/src/components/AccountModal/AccountModal.tsxpackages/vechain-kit/src/components/AccountModal/Components/BalanceSection.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/AssetsContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/AssetsHeader.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/AssetsTabs.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/BetterSwapLpCard.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/JuicyFinanceCard.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/NavigatorsCard.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/StakingCard.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingCards/StargateCard.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/StakingTab.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Assets/Components/TokensTab.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Receive/ReceiveTokenContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/Swap/SwapTokenContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/Components/TokenHistoryPreview.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/TokenDetailContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TokenDetail/index.tspackages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/Components/HistoryItemRow.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionDetailContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/TransactionHistoryContent.tsxpackages/vechain-kit/src/components/AccountModal/Contents/TransactionHistory/index.tspackages/vechain-kit/src/components/AccountModal/Contents/index.tspackages/vechain-kit/src/components/AccountModal/Types/Types.tspackages/vechain-kit/src/components/common/AddressOrDomainLabel.tsxpackages/vechain-kit/src/components/common/AssetButton.tsxpackages/vechain-kit/src/components/common/CopyIconButton.tsxpackages/vechain-kit/src/components/common/PriceChangeBadge.tsxpackages/vechain-kit/src/components/common/index.tspackages/vechain-kit/src/config/index.tspackages/vechain-kit/src/config/mainnet.tspackages/vechain-kit/src/config/solo.tspackages/vechain-kit/src/config/testnet.tspackages/vechain-kit/src/hooks/api/index.tspackages/vechain-kit/src/hooks/api/staking/abis.tspackages/vechain-kit/src/hooks/api/staking/index.tspackages/vechain-kit/src/hooks/api/staking/useBetterSwapLpPositions.tspackages/vechain-kit/src/hooks/api/staking/useJuicyPosition.tspackages/vechain-kit/src/hooks/api/staking/useNavigatorPosition.tspackages/vechain-kit/src/hooks/api/staking/useStargatePositions.tspackages/vechain-kit/src/hooks/api/transferHistory/index.tspackages/vechain-kit/src/hooks/api/transferHistory/types.tspackages/vechain-kit/src/hooks/api/transferHistory/useTokenTransferHistory.tspackages/vechain-kit/src/hooks/api/transferHistory/useTransferHistory.tspackages/vechain-kit/src/hooks/api/wallet/index.tspackages/vechain-kit/src/hooks/api/wallet/useOraclePriceChanges24h.tspackages/vechain-kit/src/hooks/api/wallet/useTokenBalances.tspackages/vechain-kit/src/hooks/api/wallet/useTokenPrices.tspackages/vechain-kit/src/hooks/api/wallet/useTokensWithValues.tspackages/vechain-kit/src/hooks/api/wallet/useTotalBalance.tspackages/vechain-kit/src/languages/en.jsonpackages/vechain-kit/src/utils/constants.tsx
- 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) <[email protected]>
|
Size Change: +586 kB (+7.42%) 🔍 Total Size: 8.47 MB
ℹ️ View Unchanged
|
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) <[email protected]>
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) <[email protected]>
- 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) <[email protected]>
- 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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
- 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) <[email protected]>
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) <[email protected]>
Overview
Turns the AccountModal Assets screen into a full portfolio view: a balance header with quick actions, Token and Staking tabs, per-token detail pages, an aggregated transaction history, and protocol-specific staking cards. Adds the supporting data layer — on-chain price-change derivation, VeChain indexer pagination,
.vetdomain resolution — and a new tracked token (VVET).What's new
🎨 Assets surface
Tabs, local state).AssetButtonrows: card-shaped, two-line layout (symbol + amount on the left, currency value + 24h badge on the right).🪙 Token detail screen
📜 Transaction history & detail
.vetdomain resolution for From/To, withhumanAddressfallback.0x000…000(mints/rewards), in both list and detail.variableDebtVechainVTHO) truncate cleanly instead of overflowing.🥩 Staking tab
Cards render only when the wallet has positions in that protocol.
balanceOf, one row per pair.StakingCardshell with logo, total, optional tag, rows, and a compact Go to platform button.📈 24h price change (on-chain only)
useOraclePriceChanges24hscansOracleVechainEnergyValueUpdateevents in a ~5h window centered on the block ~24h ago and computes ±% per supported feed.useTokenPricesexposes apriceChangesmap keyed by token address (VOT3/veDelegate inherit B3TR; VVET inherits VET; lowercase mirror for indexer responses).PriceChangeBadgerenders ±X.XX% with red/green/muted color and a muted "24h" suffix.AssetButton,TokenDetail,AssetsHeader, andBalanceSection.🪙 VVET (Veiled VET / wrapped VET)
AppConfigand the token registry, priced 1:1 with VET.💰 Total balance
useTotalBalancenow sums liquid token values plus Stargate, Navigators, BetterSwap LP, and net Juicy (supplied − borrowed).priceChange24hPctas a USD-weighted average across holdings.Configuration
New
AppConfigfields (mainnet values shown; testnet/solo are populated where available, blank otherwise — cards self-disable):vvetContractAddress0x45429A2255e7248e57fce99E7239aED3f84B7a53stargateContractAddress0x03C557bE98123fdb6faD325328AC6eB77de7248CstargateNftContractAddress0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7navigatorRegistryContractAddress0xef238e33fc78ecc79beaf8386254a0fc67d048e0betterSwapFactoryAddress0x5970DcBeBAc33e75eFf315C675f1d2654f7bF1f5juicyPoolAddress0x00Bd212704A8816264607a7110cCabe70219D5aBRegistrazione.schermo.2026-05-14.alle.17.10.37.mov
Closes #560
Closes #564
Closes #562
Closes #565
Closes #561
Closes #566
Summary by CodeRabbit