feat(spl): implement balance fetching and fix ticker/symbol use in ui
What changed, and why it matters
This commit replaces placeholder/mock Solana token balances with real on-chain balance lookups and shows the correct token name and symbol in the user interface. It is a normal feature-completion change, not a security fix. There are no clear signs of malicious code, but the new code silently returns a zero balance whenever anything goes wrong, which could mislead users about their actual holdings.
Treat as a routine feature commit. For hardening, consider surfacing RPC/fetch errors in the UI instead of silently showing a zero balance, and validate that DefaultSplTokens metadata matches on-chain metadata where possible. No immediate security action is required based solely on this diff.
Security signals we found
Silent failure pattern: every error/empty/mismatch path in the balance provider returns a zero balance, which could hide outages, RPC tampering, or account issues from users.
UI now displays real token symbols/tickers instead of hard-coded placeholders, reducing user confusion but not a vulnerability.
No explicit security claims, CVE references, or researcher attribution in the commit message or diff.
Evidence from the diff
The patch removes mock Solana SPL token balance data and implements real RPC-based balance fetching via SolanaTokenAPI. It wires the FutureProvider result into several UI widgets, handles loading/error states, and looks up token metadata (name, symbol, decimals) from a hard-coded DefaultSplTokens list. Error paths all fall back to a zero balance without surfacing the failure to the user. No input validation, authentication, cryptography, or network trust changes are visible in the diff.
Changed components
lib/wallets/isar/providers/solana/sol_token_balance_provider.dartlib/pages/token_view/sol_token_view.dartlib/pages/token_view/sub_widgets/sol_token_select_item.dartlib/pages/token_view/sub_widgets/token_summary_sol.dartlib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dartInspect captured patch +479 / −155
diff --git a/lib/pages/token_view/sol_token_view.dart b/lib/pages/token_view/sol_token_view.dart
index 44ce450..8bcb5bd 100644
--- a/lib/pages/token_view/sol_token_view.dart
+++ b/lib/pages/token_view/sol_token_view.dart
@@ -16,6 +16,7 @@ import '../../services/event_bus/events/global/wallet_sync_status_changed_event.
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
+import '../../utilities/default_spl_tokens.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
import '../../wallets/wallet/impl/sub_wallets/solana_token_wallet.dart';
@@ -69,19 +70,39 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
/// Initialize the Solana token wallet for this token view.
///
- /// Creates a mock SolanaTokenWallet and sets it as the current token wallet
- /// in the provider so that UI widgets can access it.
+ /// Creates a SolanaTokenWallet with token data from DefaultSplTokens
+ /// and sets it as the current token wallet in the provider so that UI widgets can access it.
+ ///
+ /// If the token is not found in DefaultSplTokens, sets the token wallet to null
+ /// so the UI can display an error message.
+ ///
+ /// TODO: Implement token data lookup for tokens not on the default list.
void _initializeSolanaTokenWallet() {
- // Create a mock Solana token wallet with placeholder data
- // In a real implementation, this would load actual token data from the Solana API
+ dynamic tokenInfo;
+ try {
+ tokenInfo = DefaultSplTokens.list.firstWhere(
+ (token) => token.address == widget.tokenMint,
+ );
+ } catch (e) {
+ // Token not found in DefaultSplTokens.
+ tokenInfo = null;
+ }
+
+ if (tokenInfo == null) {
+ ref.read(solanaTokenServiceStateProvider.state).state = null;
+ debugPrint(
+ 'ERROR: Token not found in DefaultSplTokens: ${widget.tokenMint}',
+ );
+ return;
+ }
+
final solanaTokenWallet = SolanaTokenWallet(
tokenMint: widget.tokenMint,
- tokenName: "Solana Token", // TODO: Load actual token name.
- tokenSymbol: "SOL", // TODO: Load actual token symbol.
- tokenDecimals: 6, // TODO: Load actual token decimals.
+ tokenName: "${tokenInfo.name}",
+ tokenSymbol: "${tokenInfo.symbol}",
+ tokenDecimals: tokenInfo.decimals as int,
);
- // Set the wallet in the provider so that it can be accessed by widgets.
ref.read(solanaTokenServiceStateProvider.state).state = solanaTokenWallet;
}
@@ -105,8 +126,9 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
},
child: Background(
child: Scaffold(
- backgroundColor:
- Theme.of(context).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@@ -118,31 +140,34 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
},
),
centerTitle: true,
- title: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.min,
- children: [
- Expanded(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SolTokenIcon(
- mintAddress: widget.tokenMint,
- size: 24,
- ),
- const SizedBox(width: 10),
- Flexible(
- child: Text(
- "Token Name", // TODO: Replace with actual token name from SplToken.
- style: STextStyles.navBarTitle(context),
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.center,
- ),
+ title: Consumer(
+ builder: (context, ref, _) {
+ final tokenWallet = ref.watch(pCurrentSolanaTokenWallet);
+ final tokenName = tokenWallet?.tokenName ?? "Token";
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SolTokenIcon(mintAddress: widget.tokenMint, size: 24),
+ const SizedBox(width: 10),
+ Flexible(
+ child: Text(
+ tokenName,
+ style: STextStyles.navBarTitle(context),
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
),
- ],
- ),
- ),
- ],
+ ),
+ ],
+ );
+ },
),
actions: [
Padding(
@@ -152,10 +177,9 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
child: AppBarIconButton(
icon: SvgPicture.asset(
Assets.svg.verticalEllipsis,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.topNavIconPrimary,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.topNavIconPrimary,
),
onPressed: () {
// TODO: Implement token details navigation for Solana.
@@ -195,10 +219,9 @@ class _SolTokenViewState extends ConsumerState<SolTokenView> {
Text(
"Transactions",
style: STextStyles.itemSubtitle(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textDark3,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark3,
),
),
CustomTextButton(
diff --git a/lib/pages/token_view/sub_widgets/sol_token_select_item.dart b/lib/pages/token_view/sub_widgets/sol_token_select_item.dart
index 774109d..24d3f10 100644
--- a/lib/pages/token_view/sub_widgets/sol_token_select_item.dart
+++ b/lib/pages/token_view/sub_widgets/sol_token_select_item.dart
@@ -17,6 +17,7 @@ import '../../../themes/stack_colors.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
+import '../../../wallets/isar/providers/solana/sol_token_balance_provider.dart';
import '../../../widgets/icon_widgets/sol_token_icon.dart';
import '../../../widgets/rounded_white_container.dart';
import '../sol_token_view.dart';
@@ -88,6 +89,33 @@ class _SolTokenSelectItemState extends ConsumerState<SolTokenSelectItem> {
Expanded(
child: Consumer(
builder: (_, ref, __) {
+ // Fetch the balance.
+ final balanceAsync = ref.watch(
+ pSolanaTokenBalance(
+ (
+ walletId: widget.walletId,
+ tokenMint: widget.token.address,
+ fractionDigits: widget.token.decimals,
+ ),
+ ),
+ );
+
+ // Format the balance.
+ String balanceString = "0.00 ${widget.token.symbol}";
+ balanceAsync.when(
+ data: (balance) {
+ // Format the amount with the token symbol.
+ final decimalValue = balance.total.decimal.toStringAsFixed(widget.token.decimals);
+ balanceString = "$decimalValue ${widget.token.symbol}";
+ },
+ loading: () {
+ balanceString = "... ${widget.token.symbol}";
+ },
+ error: (error, stackTrace) {
+ balanceString = "0.00 ${widget.token.symbol}";
+ },
+ );
+
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -109,7 +137,7 @@ class _SolTokenSelectItemState extends ConsumerState<SolTokenSelectItem> {
),
const Spacer(),
Text(
- "0.00", // TODO [prio=high]: Replace with actual Solana token balance.
+ balanceString,
style:
isDesktop
? STextStyles.desktopTextExtraSmall(
diff --git a/lib/pages/token_view/sub_widgets/token_summary_sol.dart b/lib/pages/token_view/sub_widgets/token_summary_sol.dart
index 707906c..af57641 100644
--- a/lib/pages/token_view/sub_widgets/token_summary_sol.dart
+++ b/lib/pages/token_view/sub_widgets/token_summary_sol.dart
@@ -19,7 +19,6 @@ import '../../../providers/global/prefs_provider.dart';
import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart';
-import '../../../utilities/amount/amount_formatter.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart';
@@ -69,8 +68,14 @@ class SolanaTokenSummary extends ConsumerWidget {
);
}
- final balance = ref.watch(
- pSolanaTokenBalance((walletId: walletId, tokenMint: tokenMint)),
+ final balanceAsync = ref.watch(
+ pSolanaTokenBalance(
+ (
+ walletId: walletId,
+ tokenMint: tokenMint,
+ fractionDigits: tokenWallet.tokenDecimals,
+ ),
+ ),
);
Decimal? price;
@@ -85,42 +90,138 @@ class SolanaTokenSummary extends ConsumerWidget {
RoundedContainer(
color: Theme.of(context).extension<StackColors>()!.tokenSummaryBG,
padding: const EdgeInsets.all(24),
- child: Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ child: balanceAsync.when(
+ data: (balance) {
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ Assets.svg.walletDesktop,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ width: 12,
+ height: 12,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.w500_12(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ balance.total.decimal.toStringAsFixed(tokenWallet.tokenDecimals),
+ style: STextStyles.pageTitleH1(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextPrimary,
+ ),
+ ),
+ const SizedBox(width: 10),
+ CoinTickerTag(
+ ticker: tokenWallet.tokenSymbol,
+ ),
+ ],
+ ),
+ if (price != null) const SizedBox(height: 6),
+ if (price != null)
+ Text(
+ "${(balance.total.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
+ style: STextStyles.subtitle500(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextPrimary,
+ ),
+ ),
+ const SizedBox(height: 20),
+ SolanaTokenWalletOptions(
+ walletId: walletId,
+ tokenMint: tokenMint,
+ ),
+ ],
+ );
+ },
+ loading: () {
+ return Column(
children: [
- SvgPicture.asset(
- Assets.svg.walletDesktop,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.tokenSummaryTextSecondary,
- width: 12,
- height: 12,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ Assets.svg.walletDesktop,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ width: 12,
+ height: 12,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.w500_12(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ ),
+ ),
+ ],
),
- const SizedBox(width: 6),
+ const SizedBox(height: 6),
Text(
- ref.watch(pWalletName(walletId)),
- style: STextStyles.w500_12(context).copyWith(
+ "Loading balance...",
+ style: STextStyles.pageTitleH1(context).copyWith(
color: Theme.of(
context,
- ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ ).extension<StackColors>()!.tokenSummaryTextPrimary,
),
),
+ const SizedBox(height: 20),
+ SolanaTokenWalletOptions(
+ walletId: walletId,
+ tokenMint: tokenMint,
+ ),
],
- ),
- const SizedBox(height: 6),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ );
+ },
+ error: (error, stackTrace) {
+ return Column(
children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ Assets.svg.walletDesktop,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ width: 12,
+ height: 12,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ ref.watch(pWalletName(walletId)),
+ style: STextStyles.w500_12(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.tokenSummaryTextSecondary,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
Text(
- ref
- .watch(
- pAmountFormatter(
- Solana(CryptoCurrencyNetwork.main),
- ),
- )
- .format(balance.total),
+ "0.00",
style: STextStyles.pageTitleH1(context).copyWith(
color: Theme.of(
context,
@@ -131,24 +232,14 @@ class SolanaTokenSummary extends ConsumerWidget {
CoinTickerTag(
ticker: tokenWallet.tokenSymbol,
),
- ],
- ),
- if (price != null) const SizedBox(height: 6),
- if (price != null)
- Text(
- "${(balance.total.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
- style: STextStyles.subtitle500(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.tokenSummaryTextPrimary,
+ const SizedBox(height: 20),
+ SolanaTokenWalletOptions(
+ walletId: walletId,
+ tokenMint: tokenMint,
),
- ),
- const SizedBox(height: 20),
- SolanaTokenWalletOptions(
- walletId: walletId,
- tokenMint: tokenMint,
- ),
- ],
+ ],
+ );
+ },
),
),
Positioned(
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
index 45204e3..eb31dd1 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_sol_token_view.dart
@@ -17,8 +17,11 @@ import '../../../providers/providers.dart';
import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
+import '../../../utilities/default_spl_tokens.dart';
import '../../../utilities/text_styles.dart';
+import '../../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../wallets/wallet/impl/sub_wallets/solana_token_wallet.dart';
import '../../../widgets/coin_ticker_tag.dart';
import '../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart';
@@ -56,11 +59,52 @@ class _DesktopTokenViewState extends ConsumerState<DesktopSolTokenView> {
@override
void initState() {
+ // Initialize the Solana token wallet.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _initializeSolanaTokenWallet();
+ });
// TODO: Integrate Solana token refresh status when available.
initialSyncStatus = WalletSyncStatus.synced;
super.initState();
}
+ /// Initialize the Solana token wallet.
+ ///
+ /// Creates a SolanaTokenWallet with token data from DefaultSplTokens
+ /// and sets it as the current token wallet in the provider so that UI widgets can access it.
+ ///
+ /// If the token is not found in DefaultSplTokens, sets the token wallet to null
+ /// so the UI can display an error message.
+ void _initializeSolanaTokenWallet() {
+ // Look up the actual token from DefaultSplTokens.
+ dynamic tokenInfo;
+ try {
+ tokenInfo = DefaultSplTokens.list.firstWhere(
+ (token) => token.address == widget.tokenMint,
+ );
+ } catch (e) {
+ // Token not found in DefaultSplTokens.
+ tokenInfo = null;
+ }
+
+ if (tokenInfo == null) {
+ ref.read(solanaTokenServiceStateProvider.state).state = null;
+ debugPrint(
+ 'ERROR: Token not found in DefaultSplTokens: ${widget.tokenMint}',
+ );
+ return;
+ }
+
+ final solanaTokenWallet = SolanaTokenWallet(
+ tokenMint: widget.tokenMint,
+ tokenName: "${tokenInfo.name}",
+ tokenSymbol: "${tokenInfo.symbol}",
+ tokenDecimals: tokenInfo.decimals as int,
+ );
+
+ ref.read(solanaTokenServiceStateProvider.state).state = solanaTokenWallet;
+ }
+
@override
void dispose() {
super.dispose();
@@ -101,21 +145,21 @@ class _DesktopTokenViewState extends ConsumerState<DesktopSolTokenView> {
),
center: Expanded(
flex: 4,
- child: Row(
- children: [
- SolTokenIcon(mintAddress: widget.tokenMint, size: 32),
- const SizedBox(width: 12),
- Text(
- "Token Name", // TODO: Replace with actual token name from SplToken.
- style: STextStyles.desktopH3(context),
- ),
- const SizedBox(width: 12),
- CoinTickerTag(
- ticker: ref.watch(
- pWalletCoin(widget.walletId).select((s) => s.ticker),
- ),
- ),
- ],
+ child: Consumer(
+ builder: (context, ref, _) {
+ final tokenWallet = ref.watch(pCurrentSolanaTokenWallet);
+ final tokenName = tokenWallet?.tokenName ?? "Token";
+ final tokenSymbol = tokenWallet?.tokenSymbol ?? "SOL";
+ return Row(
+ children: [
+ SolTokenIcon(mintAddress: widget.tokenMint, size: 32),
+ const SizedBox(width: 12),
+ Text(tokenName, style: STextStyles.desktopH3(context)),
+ const SizedBox(width: 12),
+ CoinTickerTag(ticker: tokenSymbol),
+ ],
+ );
+ },
),
),
useSpacers: false,
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
index ce9134e..688044f 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart
@@ -28,6 +28,8 @@ import '../../../../wallets/crypto_currency/crypto_currency.dart'
show CryptoCurrency;
import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../../../../wallets/isar/providers/eth/token_balance_provider.dart';
+import '../../../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
+import '../../../../wallets/isar/providers/solana/sol_token_balance_provider.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import 'desktop_balance_toggle_button.dart';
@@ -78,31 +80,50 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
prefsChangeNotifierProvider.select((value) => value.currency),
);
- // For Ethereum tokens, get the token contract; for Solana tokens, show placeholder.
+ // For Ethereum tokens, get the token contract; for Solana tokens, get the token wallet.
dynamic tokenContract;
+ dynamic solanaTokenWallet;
if (widget.isToken) {
try {
tokenContract = ref.watch(
pCurrentTokenWallet.select((value) => value!.tokenContract),
);
} catch (_) {
- // Solana token or token wallet not yet loaded.
+ // Ethereum token not found, check for Solana.
tokenContract = null;
}
+
+ // Check for Solana token wallet if Ethereum token not found.
+ if (tokenContract == null) {
+ try {
+ solanaTokenWallet = ref.watch(pCurrentSolanaTokenWallet);
+ } catch (_) {
+ solanaTokenWallet = null;
+ }
+ }
}
- final price =
- widget.isToken && tokenContract != null
- ? ref.watch(
- priceAnd24hChangeNotifierProvider.select(
- (value) => value.getTokenPrice((tokenContract as dynamic).address as String),
+ final price = widget.isToken && tokenContract != null
+ ? ref.watch(
+ priceAnd24hChangeNotifierProvider.select(
+ (value) => value.getTokenPrice(
+ (tokenContract as dynamic).address as String,
),
- )
- : ref.watch(
- priceAnd24hChangeNotifierProvider.select(
- (value) => value.getPrice(coin),
+ ),
+ )
+ : widget.isToken && solanaTokenWallet != null
+ ? ref.watch(
+ priceAnd24hChangeNotifierProvider.select(
+ (value) => value.getTokenPrice(
+ "${(solanaTokenWallet as dynamic).tokenMint}",
),
- );
+ ),
+ )
+ : ref.watch(
+ priceAnd24hChangeNotifierProvider.select(
+ (value) => value.getPrice(coin),
+ ),
+ );
final _showAvailable =
ref.watch(walletBalanceToggleStateProvider.state).state ==
@@ -122,15 +143,38 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
break;
}
} else {
- final Balance balance =
- widget.isToken && tokenContract != null
- ? ref.watch(
- pTokenBalance((
- walletId: walletId,
- contractAddress: (tokenContract as dynamic).address as String,
- )),
- )
- : ref.watch(pWalletBalance(walletId));
+ final Balance balance;
+ if (widget.isToken && tokenContract != null) {
+ // Ethereum token balance
+ balance = ref.watch(
+ pTokenBalance((
+ walletId: walletId,
+ contractAddress: (tokenContract as dynamic).address as String,
+ )),
+ );
+ } else if (widget.isToken && solanaTokenWallet != null) {
+ // Solana token balance - handle async value.
+ final balanceAsync = ref.watch(
+ pSolanaTokenBalance((
+ walletId: walletId,
+ tokenMint: (solanaTokenWallet as dynamic).tokenMint,
+ fractionDigits: (solanaTokenWallet as dynamic).tokenDecimals,
+ )),
+ );
+ // Extract the balance from AsyncValue, defaulting to zero if not loaded.
+ final decimals = (solanaTokenWallet as dynamic).tokenDecimals as int;
+ balance =
+ balanceAsync.whenData((b) => b).value ??
+ Balance(
+ total: Amount.zeroWith(fractionDigits: decimals),
+ spendable: Amount.zeroWith(fractionDigits: decimals),
+ blockedTotal: Amount.zeroWith(fractionDigits: decimals),
+ pendingSpendable: Amount.zeroWith(fractionDigits: decimals),
+ );
+ } else {
+ // Regular wallet balance.
+ balance = ref.watch(pWalletBalance(walletId));
+ }
balanceToShow = _showAvailable ? balance.spendable : balance.total;
}
@@ -146,9 +190,18 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
- ref
- .watch(pAmountFormatter(coin))
- .format(balanceToShow, ethContract: tokenContract != null ? tokenContract as EthContract? : null),
+ widget.isToken && solanaTokenWallet != null
+ ? "${balanceToShow.decimal.toStringAsFixed(
+ (solanaTokenWallet as dynamic).tokenDecimals as int,
+ )} ${(solanaTokenWallet as dynamic).tokenSymbol}"
+ : ref
+ .watch(pAmountFormatter(coin))
+ .format(
+ balanceToShow,
+ ethContract: tokenContract != null
+ ? tokenContract as EthContract?
+ : null,
+ ),
style: STextStyles.desktopH3(context),
),
),
@@ -156,10 +209,9 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
SelectableText(
"${Amount.fromDecimal(price.value * balanceToShow.decimal, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle1,
),
),
// if (coin is Firo)
@@ -180,10 +232,9 @@ class _WDesktopWalletSummaryState extends ConsumerState<DesktopWalletSummary> {
WalletRefreshButton(
walletId: walletId,
initialSyncStatus: widget.initialSyncStatus,
- tokenContractAddress:
- widget.isToken && tokenContract != null
- ? (tokenContract as EthContract).address
- : null,
+ tokenContractAddress: widget.isToken && tokenContract != null
+ ? (tokenContract as EthContract).address
+ : null,
),
const SizedBox(width: 8),
diff --git a/lib/wallets/isar/providers/solana/sol_token_balance_provider.dart b/lib/wallets/isar/providers/solana/sol_token_balance_provider.dart
index 30b0691..ad36db8 100644
--- a/lib/wallets/isar/providers/solana/sol_token_balance_provider.dart
+++ b/lib/wallets/isar/providers/solana/sol_token_balance_provider.dart
@@ -2,31 +2,118 @@ import 'package:decimal/decimal.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/balance.dart';
+import '../../../../providers/global/wallets_provider.dart';
+import '../../../../services/solana/solana_token_api.dart';
import '../../../../utilities/amount/amount.dart';
+import '../../../../wallets/wallet/impl/solana_wallet.dart';
/// Provider family for Solana token balance.
-///
-/// Currently returns mock data while API is a WIP.
+///
+/// Fetches the token balance from the Solana blockchain via RPC.
///
/// Example usage in UI:
/// final balance = ref.watch(
-/// pSolanaTokenBalance((walletId: 'wallet1', tokenMint: 'EPjFWaJUwYUoRwzwkH4H8gNB7zHW9tLT6NCKB8S4yh6h'))
+/// pSolanaTokenBalance((walletId: 'wallet1', tokenMint: 'EPjFWaJUwYUoRwzwkH4H8gNB7zHW9tLT6NCKB8S4yh6h', fractionDigits: 6))
/// );
-final pSolanaTokenBalance = Provider.family<
+final pSolanaTokenBalance = FutureProvider.family<
Balance,
- ({String walletId, String tokenMint})>((ref, params) {
- // Mock data for UI development.
- // TODO: when API is ready, this should fetch real balance from SolanaAPI.
- return Balance(
- total: Amount.fromDecimal(
- Decimal.parse("1000.00"),
- fractionDigits: 6,
- ),
- spendable: Amount.fromDecimal(
- Decimal.parse("1000.00"),
- fractionDigits: 6,
- ),
- blockedTotal: Amount.zeroWith(fractionDigits: 6),
- pendingSpendable: Amount.zeroWith(fractionDigits: 6),
- );
+ ({String walletId, String tokenMint, int fractionDigits})>((ref, params) async {
+ // Get the wallet from the wallets provider.
+ final wallets = ref.watch(pWallets);
+ final wallet = wallets.getWallet(params.walletId);
+
+ if (wallet == null || wallet is! SolanaWallet) {
+ // Return zero balance if wallet not found or not Solana.
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
+
+ try {
+ // Initialize the SolanaTokenAPI with the RPC client.
+ final tokenApi = SolanaTokenAPI();
+ final rpcClient = wallet.getRpcClient();
+
+ if (rpcClient == null) {
+ // Return zero balance if RPC client not available.
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
+
+ tokenApi.initializeRpcClient(rpcClient);
+
+ // Get the wallet address.
+ final addressObj = await wallet.getCurrentReceivingAddress();
+ if (addressObj == null) {
+ // Return zero balance if address not found.
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
+
+ final walletAddress = addressObj.value;
+
+ // Get token accounts for this wallet and mint.
+ final accountsResponse = await tokenApi.getTokenAccountsByOwner(
+ walletAddress,
+ mint: params.tokenMint,
+ );
+
+ if (accountsResponse.isError || accountsResponse.value == null || accountsResponse.value!.isEmpty) {
+ // Return zero balance if no token accounts found.
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
+
+ // Get the balance of the first token account.
+ final tokenAccountAddress = accountsResponse.value!.first;
+ final balanceResponse = await tokenApi.getTokenAccountBalance(tokenAccountAddress);
+
+ if (balanceResponse.isError || balanceResponse.value == null) {
+ // Return zero balance if balance fetch failed.
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
+
+ // Convert the BigInt balance to an Amount with the token's fractional digits.
+ final balanceBigInt = balanceResponse.value!;
+ final balanceAmount = Amount(
+ rawValue: balanceBigInt,
+ fractionDigits: params.fractionDigits,
+ );
+
+ return Balance(
+ total: balanceAmount,
+ spendable: balanceAmount,
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ } catch (e) {
+ // Return zero balance if any error occurs.
+ print('Error fetching Solana token balance: $e');
+ return Balance(
+ total: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ spendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ blockedTotal: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ pendingSpendable: Amount.zeroWith(fractionDigits: params.fractionDigits),
+ );
+ }
});
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.