1010import 'package:solana/dto.dart' ;
1111import 'package:solana/solana.dart' ;
1212
13+ import '../../utilities/default_sol_tokens.dart' ;
14+
15+ /// A token mint discovered in a wallet, with its decimals when known.
16+ class DiscoveredSolMint {
17+ final String mint;
18+ final int ? decimals;
19+
20+ const DiscoveredSolMint ({this .mint = '' , this .decimals});
21+ }
22+
1323/// Exception for Solana token API errors.
1424class SolanaTokenApiException implements Exception {
1525 final String message;
@@ -338,19 +348,28 @@ class SolanaTokenAPI {
338348 try {
339349 _checkClient ();
340350
341- // TODO: Implement proper metadata PDA derivation when solana package
342- // exposes findProgramAddress() utilities.
343- //
344- // The Solana Token Metadata program (metaqbxxUerdq28cj1RbAqWwTRiWLs6nshmbbuP3xqb)
345- // stores token metadata at a PDA derived from the mint address using:
346- // findProgramAddress(
347- // ["metadata", metadataProgram, mintPubkey],
348- // metadataProgram
349- // )
350- //
351- // Until then, return null to allow users to enter custom token details.
351+ // Resolve name/symbol/logo from the bundled known token list when the
352+ // mint matches a well known token.
353+ for (final token in DefaultSolTokens .list) {
354+ if (token.address == mintAddress) {
355+ return SolanaTokenApiResponse <Map <String , dynamic >?>(
356+ value: {
357+ "name" : token.name,
358+ "symbol" : token.symbol,
359+ "decimals" : token.decimals,
360+ "logoUri" : token.logoUri,
361+ },
362+ );
363+ }
364+ }
352365
353- // Metadata PDA derivation not yet implemented
366+ // On-chain metadata lookup is not implemented here: it would require
367+ // deriving the Token Metadata program PDA
368+ // (metaqbxxUerdq28cj1RbAqWwTRiWLs6nshmbbuP3xqb) from the mint and
369+ // decoding the Metaplex account, which the solana package does not yet
370+ // expose helpers for. Returning null lets callers fall back to a
371+ // mint-derived placeholder name/symbol while still using the correct
372+ // on-chain decimals.
354373 return SolanaTokenApiResponse <Map <String , dynamic >?>(
355374 value: null ,
356375 );
@@ -362,6 +381,187 @@ class SolanaTokenAPI {
362381 }
363382 }
364383
384+ /// Discover all SPL token mints held by a wallet.
385+ ///
386+ /// Queries the wallet's token accounts for both the standard SPL Token
387+ /// program and the Token2022 program, then extracts the unique mint
388+ /// addresses from those accounts along with the number of decimals each
389+ /// mint is configured with. The decimals are read directly from the parsed
390+ /// token account data ('tokenAmount.decimals'), which mirrors the value
391+ /// stored on the mint account, so balances are scaled correctly.
392+ Future <SolanaTokenApiResponse <List <DiscoveredSolMint >>>
393+ discoverTokensForWallet ({
394+ required String walletAddress,
395+ }) async {
396+ try {
397+ _checkClient ();
398+
399+ const splTokenProgramId = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' ;
400+ const token2022ProgramId = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' ;
401+
402+ final splResponse = await _rpcClient! .getTokenAccountsByOwner (
403+ walletAddress,
404+ TokenAccountsFilter .byProgramId (splTokenProgramId),
405+ encoding: Encoding .jsonParsed,
406+ );
407+
408+ final token2022Response = await _rpcClient! .getTokenAccountsByOwner (
409+ walletAddress,
410+ TokenAccountsFilter .byProgramId (token2022ProgramId),
411+ encoding: Encoding .jsonParsed,
412+ );
413+
414+ final accounts = [
415+ ...splResponse.value,
416+ ...token2022Response.value,
417+ ];
418+
419+ final byMint = < String , DiscoveredSolMint > {};
420+ for (final account in accounts) {
421+ final extracted =
422+ _extractMintFromParsedTokenAccount (account.account.data);
423+ final mint = extracted.mint;
424+ if (mint.isEmpty) {
425+ continue ;
426+ }
427+
428+ // Prefer an entry that already has decimals resolved.
429+ final existing = byMint[mint];
430+ if (existing == null || existing.decimals == null ) {
431+ byMint[mint] = extracted;
432+ }
433+ }
434+
435+ // For any mint whose decimals could not be read from the token account
436+ // data, fetch the mint account directly and read its decimals.
437+ final resolved = < DiscoveredSolMint > [];
438+ for (final entry in byMint.values) {
439+ if (entry.decimals != null ) {
440+ resolved.add (entry);
441+ } else {
442+ final decimals = await _fetchMintDecimals (entry.mint);
443+ resolved.add (
444+ DiscoveredSolMint (mint: entry.mint, decimals: decimals),
445+ );
446+ }
447+ }
448+
449+ return SolanaTokenApiResponse <List <DiscoveredSolMint >>(value: resolved);
450+ } on Exception catch (e) {
451+ return SolanaTokenApiResponse <List <DiscoveredSolMint >>(
452+ exception: SolanaTokenApiException (
453+ 'Failed to discover tokens: ${e .toString ()}' ,
454+ originalException: e,
455+ ),
456+ );
457+ }
458+ }
459+
460+ /// Fetch the number of decimals configured on a token's mint account.
461+ ///
462+ /// Used as a fallback when the decimals could not be read from a parsed
463+ /// token account. Returns null if the mint account cannot be read or parsed.
464+ Future <int ?> _fetchMintDecimals (String mintAddress) async {
465+ try {
466+ final response = await _rpcClient! .getAccountInfo (
467+ mintAddress,
468+ encoding: Encoding .jsonParsed,
469+ );
470+
471+ final data = response.value? .data;
472+ if (data is ParsedAccountData ) {
473+ return data.when (
474+ splToken: (spl) => spl.when (
475+ account: (info, type, accountType) => null ,
476+ mint: (info, type, accountType) => info.decimals,
477+ unknown: (type) => null ,
478+ ),
479+ token2022: (token2022data) => token2022data.when (
480+ account: (info, type, accountType) => null ,
481+ mint: (info, type, accountType) => info.decimals,
482+ unknown: (type) => null ,
483+ ),
484+ stake: (_) => null ,
485+ unsupported: (_) => null ,
486+ );
487+ }
488+
489+ if (data is Map <String , dynamic >) {
490+ final parsed = data['parsed' ];
491+ if (parsed is Map <String , dynamic >) {
492+ final info = parsed['info' ];
493+ if (info is Map <String , dynamic >) {
494+ final decimals = info['decimals' ];
495+ if (decimals is int ) {
496+ return decimals;
497+ }
498+ return int .tryParse (decimals? .toString () ?? '' );
499+ }
500+ }
501+ }
502+ } catch (_) {
503+ // Ignore and report unknown decimals.
504+ }
505+
506+ return null ;
507+ }
508+
509+ /// Extract the mint address and decimals from a parsed token account's data.
510+ ///
511+ /// Handles both standard SPL Token and Token2022 account data. The decimals
512+ /// come from 'tokenAmount.decimals' on the holding, which matches the value
513+ /// stored on the mint account. Returns an empty mint when the data is not a
514+ /// token account or cannot be parsed, and null decimals when unavailable.
515+ DiscoveredSolMint _extractMintFromParsedTokenAccount (dynamic data) {
516+ try {
517+ if (data is ParsedAccountData ) {
518+ return data.when (
519+ splToken: (spl) => spl.when (
520+ account: (info, type, accountType) => DiscoveredSolMint (
521+ mint: info.mint,
522+ decimals: info.tokenAmount.decimals,
523+ ),
524+ mint: (info, type, accountType) => const DiscoveredSolMint (),
525+ unknown: (type) => const DiscoveredSolMint (),
526+ ),
527+ token2022: (token2022data) => token2022data.when (
528+ account: (info, type, accountType) => DiscoveredSolMint (
529+ mint: info.mint,
530+ decimals: info.tokenAmount.decimals,
531+ ),
532+ mint: (info, type, accountType) => const DiscoveredSolMint (),
533+ unknown: (type) => const DiscoveredSolMint (),
534+ ),
535+ stake: (_) => const DiscoveredSolMint (),
536+ unsupported: (_) => const DiscoveredSolMint (),
537+ );
538+ }
539+
540+ if (data is Map <String , dynamic >) {
541+ final parsed = data['parsed' ];
542+ if (parsed is Map <String , dynamic >) {
543+ final info = parsed['info' ];
544+ if (info is Map <String , dynamic >) {
545+ final mint = info['mint' ];
546+ if (mint is String ) {
547+ int ? decimals;
548+ final tokenAmount = info['tokenAmount' ];
549+ if (tokenAmount is Map ) {
550+ final d = tokenAmount['decimals' ];
551+ decimals = d is int ? d : int .tryParse (d? .toString () ?? '' );
552+ }
553+ return DiscoveredSolMint (mint: mint, decimals: decimals);
554+ }
555+ }
556+ }
557+ }
558+ } catch (_) {
559+ // Ignore parsing errors and treat as no mint found.
560+ }
561+
562+ return const DiscoveredSolMint ();
563+ }
564+
365565 /// Validate if a string is a valid Solana mint address.
366566 ///
367567 /// A valid Solana address must:
0 commit comments