Files
hash-wallet/cw_solana/lib/solana_client.dart
T
acf64a2468 feat: Auto fetch wallet tokens in solana wallets (#2983)
* CW-1203-New-Design - Nav bar (#2619)

* Add new main actions navbar

* new navBar animation

* refactor: Optimize gas fee calculations for Base chain transactions (#2613)

* refactor: Optimize gas fee calculations for Base chain transactions

* feat: Added an abstract method for priority fee calculations in EVMChainWallet class, and implemented it across the evm wallets.

* fix: Estimated fee on send screen for base wallet different from fee on swipe to send bottomsheet

* cleaning reown previous folder

* fix: Fix issue resulting in tron and solana not displaying estimated fees, also modify estimated fees to display directly instead of formatted display (e.g 0.00005237ETH instead of 5.237e-5 ETH) (#2640)

* Fix untappable area of wallets page (#2636)

* Fix untappable area of wallets page

* Cleanup

* refactor: Enhance node switching logic to prioritize unused active nodes (#2639)

* CW-1190: Minor Fixes (#2614)

* fix: Enhance WalletConnect URI handling and validation, better handle errors

* fix: Disable swipe to send if wallet is not synchronized

* fix: Handle session expiry gracefully

* fix: Disable swipe to send if wallet is not synced

* CW-1238-Solana-Fixes (#2620)

* refactor: optimize SPL token transaction and balance fetching with batch processing

* fix: SPL tokens not populating all fields

* fix:Solana Fixes

- Optimize SPL token transactions and balance fetching
- Auto Populate all fields when fetching SPL tokens
- Gracefully handle errors when sending Solana tokens

* fix: update wallet compatibility check in LinkViewModel

* feat: add Moralis API key to secrets

* Update

* feat: Add error handling for invalid associated token accounts in Solana transactions

* fix: Add cleaning for reown to linux ci

* refactor: Update transactions after eveey batch fetch

* fix: make error messages more readable

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update estimated fee on currency change (#2644)

* refactor: Optimize gas fee calculations for Base chain transactions

* feat: Added an abstract method for priority fee calculations in EVMChainWallet class, and implemented it across the evm wallets.

* fix: Estimated fee on send screen for base wallet different from fee on swipe to send bottomsheet

* fix: Update currency selection to calculate estimated fees upon selection in SendCard widget

* CW-1208: Pay Anything EVM enahancements (#2600)

* feat: Pay Anything EVM enahancements

* ifx: Add baseEth to address validator switch case

* fix: Error detecting other wallet types

* feat: Preserve QR amount and other data through evm chain selection flow

* Update container color to surfaceContainer

* feat: Add support for Bitcoin Lightning Network detection

- Identify Base QR codes when scanned
- Display all available currencies for network
- Enhance detector to recognize lightning addresses

* fix: Update token selection for currently selected wallet

* fix: Improve token matching logic for EVM networks

* feat: Update selected currency when wallet is switched

* ui: Add chain badge to the swap confirmation bottomsheet

* fix: Asks to switch wallets even if the current wallet type is the same as the QR. Fix merge conflicts too

* fix: Handle matching logic internally and general cleanups

* fix: Terminate flow if address is not a valid parsed address

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>

* fix: Sync bar ETA (#2515)

* fix: Sync bar ETA

* fix: Smooth ETA - WIP

* fix: Show disconnected status when sync disconnects

* feat: Display blocks remaining for the first five seconds and then switch to percentage

* Update

* fix: Extend duration for showing blocks remaining during sync from 5 to 15 seconds; update connectivity check to handle multiple connectivity results, and handle disconnected state while syncing

* feat: Initially display block if the a reconnection is triggered while syncing

* fix: More improvements to syncing

* Fixes to Arbitrum Wallet (#2646)

* feat: initialize cw_arbitrum package with essential files

- Implement ArbitrumClient for transaction handling
- Add ArbitrumWallet and related classes for wallet management
- Establish transaction history and info classes for Arbitrum transactions
- Add ArbitrumWalletService for wallet operations and management

* feat: Add arbitrum related secrets to workflow

* feat: Add nodes for arbitrum

* feat: Add Arbitrum support to cryptocurrency handling

* refactor: Update Arbitrum currency references from 'ARB' to 'ETH'

* feat: Add Arbitrum support to wallet type and transaction handling

* feat: Add Arbitrum URL schemes to Android and iOS configurations

* feat: Add Arbitrum SVG icons and a WebP image

* feat: Setup Arbitrum proxy

* fix: Add localization

* feat: Integrate Arbitrum support across various configurations and files

- Updated cakewallet.bat to include Arbitrum in app configuration.
- Added Arbitrum node list to pubspec_base.yaml.
- Modified Android, iOS, Linux, and macOS app configuration scripts to support Arbitrum.
- Introduced Arbitrum output path and generation logic in configure.dart.
- Enhanced wallet type and pubspec generation to include Arbitrum.
- Added Arbitrum-related secrets in secret_key.dart.

* feat: Enhance Arbitrum integration with new URI class and updates across various components

- Introduced ArbitrumURI class for handling Arbitrum payment URIs.
- Updated seed validator, wallet creation service, and other components to support Arbitrum.
- Added Arbitrum node handling in default settings migration and node list management.
- Enhanced preferences and settings store to include Arbitrum-specific configurations.
- Updated QR utility and token utilities to accommodate Arbitrum functionalities.

* feat: Add Arbitrum support to various buy providers

- Updated DFXBuyProvider, KryptonimBuyProvider, OnRamperBuyProvider, and RobinhoodBuyProvider to include Arbitrum wallet type and blockchain references.
- Enhanced integration for Arbitrum across multiple components to ensure consistent support.

* feat: Implement Arbitrum support in wallet selection and settings

- Added Arbitrum wallet type handling in common test flows and wallet services.
- Integrated Arbitrum icons in desktop wallet selection and menu widgets.
- Updated privacy settings to include ArbiScan options.
- Enhanced key service to support Arbitrum private and public key retrieval.

* feat: Expand Arbitrum support across various view models and components

- Integrated Arbitrum wallet type handling in advanced privacy settings, transaction details, and wallet management.
- Added Arbitrum-specific methods for transaction and wallet creation, restoration, and fee management.
- Updated dashboard and home settings to accommodate Arbitrum functionalities, including token management and transaction display.
- Enhanced exchange and send view models to support Arbitrum transactions and currency handling.
- Included Arbitrum in wallet address list and privacy settings for improved user experience.

* refactor: Simplify token initialization in Arbitrum wallet service

- Removed the migration flag from the addInitialTokens method in ArbitrumWalletService.
- Updated addInitialTokens method in ArbitrumWallet to handle existing tokens more efficiently.
- Enhanced initial token retrieval logic in DefaultArbitrumErc20Tokens to prioritize iconPath assignment.
- Improved token update logic in BaseWallet for consistency across wallet services.

* chore: Update Dart SDK constraints in pubspec.yaml

* fix: Update file paths for arbitrum assets

* fix: Correct box name formatting in Arbitrum wallet initialization

* Update .github/workflows/pr_test_build_android.yml [skip ci

* Update .github/workflows/pr_test_build_android.yml

* fix: Add missing evm params

* fix: Remove duplicate method declaration

* refactor: simplify isSwapsXyzSendingEVMTokenSwap check

* fix: Update arbitrum configs with sql storage update

* feat: Add estimated fee retrieval for Arbitrum and revamp priority fee handling for EVMChainWallet

* refactor: Update transaction priority handling and estimated fee calculations across EVMChain chains, also remove priority fee levels for Arbitrum

* fix: Address validation for Arbitrum

* Update scripts/android/pubspec_gen.sh [skip ci

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor node form UI

* refactor node form UI to use new ListItem model

* add new list item row types and widgets

* add style wrapper

* remove print [skip ci]

* Integrate new design mockup with ViewModel (#2653)

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* integrate homepage from new ui mockup

* fix import

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* integrate homepage from new ui mockup

* fix import

* add new-ui dir to pubspec_base

* minor layout fixes

* cleanup navbar logic

* Modify navbar behaviour

* smooth color change when selecting navbar options

* open old ui pages with new ui action buttons

* feat: working navbar in new ui

* fix: minor sizing tweaks

* CW-1266-integrate-bitcoin-lightning-through-spark-sdk (#2623)

* feat: add Lightning Network support for Bitcoin wallets

* refactor: rename `fiatConvertationStore` to `fiatConversionStore` for consistency and update related occurrences across codebase

* feat: enhance address validation with Lightning Network invoice support for BTC & refactor wallet type/token checks in view model

* feat: add support for Lightning invoice detection, refactor MWEB deposit/withdraw actions, and integrate Lightning transaction creation with updated priority handling

* feat: add method to retrieve unused Spark deposit address for Bitcoin wallets

* feat: add Breez API key support and update secrets handling for Bitcoin Lightning wallet integration in workflows

* chore: update Breez SDK dependency to version 0.3.4 in pubspec files

* Add bitcoin secrets config [skip ci]

* feat: extend Lightning wallet functionality with transaction history fetching

* feat: add LNURL-pay address detection and support in address parsing flow for Bitcoin Lightning integration

* refactor: simplify `ReceivePageOption` logic

* refactor: centralize `PaymentURI` generation logic across wallet types

* feat: enhance `PaymentURI` handling with asynchronous support and Lightning-specific functionality

* refactor: streamline `PaymentURI` logic and remove redundant URI implementations across wallet types

* refactor: remove redundant debug print statement from `bitcoin_wallet_addresses.dart`

* refactor: improve consistency in widget styling and centralized label logic, add Bitcoin Lightning deposit/withdraw support

* feat: reload balance and tx history after sending a lightning transaction

* feat: improve address formatting for human-readable addresses and update the default LNURL domain

* fix: merge conflicts

* feat: add error handling for LightningWallet initialization and adjust transaction direction logic

* feat: enable private transactions by default in LightningWallet and update Breez SDK version to 0.4.2

* minor fixes [skip ci]

* chore: fix some minor issues in comments (#2654)

Signed-off-by: black5box <black5box@outlook.com>

* fix: handle send-all functionality for LightningWallet transactions and adjust amount calculation logic

* fix-german (#2659)

* chore: update German localization strings for consistency and accuracy

* chore: update German localization strings for consistency and accuracy [skip-ci]

* feat: add LNURL support for address validation and LightningWallet compatibility, enhance error handling for OpenCryptoPay

* fix: adjust LightningWallet amount parsing from 9 to 8 decimal places

* feat: add LNURL support in LightningPaymentRequest and LightningWallet

* Fix navigation gradient (#2657)

* Fix navigation gradient

* Fix CONFIG_ARGS formatting in app_config.sh (#2660)

* fix: block wrongly parsed addresses (#2656)

* fix: block wrongly parsed addresses

* fix: move parsed address check into handlePaymentFlow method

* fix: Handle QR URLs separately for pay anything flow

* feat: add electrum seed support for Lightning

* refactor: improve `parseFixed` logic and add comprehensive unit tests for edge cases (#2661)

* Update cw_bitcoin/lib/bitcoin_wallet.dart [skip ci]

* resolve conflict issue

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com>

* remove old code [skip ci]

* fix: page widgets rebuilt every time

* chore: formatting

* remove unused navbar

* fix: lightning balance on balance card in lightning mode

* fix: return if uri == null

* feat: trade history in new ui

* feat: enhance balance model with secondary asset handling

* feat: unique card colors and icons for coins

* wip: card customization

* fix: merge conflict

* feat: customizable balance card

* Fix some spacings and test sizing

* Fix formatting

* feat: add Lightning Network support for send flow and fee handling adjustments

* refactor: remove unnecessary debug print statement in send flow for Lightning Network balance

* feat: integrate OpenCryptoPay QR handling in send flow and improve scan action handling logic

* feat: add maxDepositClaimFee parameter for Lightning wallet configuration

* Switch balance card to RoundedSuperellipse and set color list to Wrap

* Fix

* Switch to scroll view

* refactor: update navigation route from `receive` to `addressPage` in coin action row

* feat: bloc-based viewmodel for card customizer

* feat: mobx-independent balance card customizer vm

* minor fixes

* copyWith constructor for cleaner state changes

* refactor: wrap `HistorySection` with `Observer` for reactive state updates, update navigation for "Buy" action, and fix incorrect balance property

* feat: enhance Bitcoin receive flow with Lightning and SegWit options, update address page registration, and localize coin action labels

* feat: add helper methods for Lightning and SegWit receive page options in Bitcoin configuration

* feat: add Lightning transaction type handling and enable navigation to transaction details from history

* Add more special card types

* feat: new ui settings page

* feat: add Lightning actions to balance cards, refactor card rendering logic

* refactor: update settings page localization, hide unused settings items

* feat: add secrets generation for cw_bitcoin, including breezApiKey

* Lightning switcher animation (#2725)

* Properly animate bitcoin/lightning switcher

* Cleanup and fix sizing

* Final cleanup

* Fix action button colors (#2732)

* feat: add hardware wallet type handling and improve parsed address reset flow

* Refactor settings page to use generic row items (#2745)

* fix android back button in settings on android, force iphone-style transitions on all platforms

* move settings page to generic row impl

* fix imports

* Balance card fixes (#2729)

* Update balance card sizing

* Fix spacing

* Initial font changes (#2731)

* Initial font changes

* More font changes

* feat: integrate Lightning transaction support and improve transaction history UX

- Enhanced dashboard to compute confirmations using the first and last transactions.
- Updated `BitcoinWallet` to subscribe for and manage Lightning transactions.
- Improved `HistoryTile` layout and introduced new leading icons for Lightning transactions.

* feat: add unclaimed deposit actions and enhance Lightning event handling

* release changes [skip ci]

* Enhanced card customizer (#2744)

* enhanced card customizer

* Fix conflicts

* Add missing icons + adjust spacing

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>

* feat: enhance Lightning invoice handling and address validation

- Added support for identifying non-zero amount Lightning invoices.
- Refined invoice matching with updated regex patterns.
- Improved UI to conditionally display addresses in sending confirmations.
- Adjusted logic to handle zero-value Lightning invoices.

* feat: add `isPayjoinAvailable` computed property and update Payjoin visibility logic

* feat: improve Lightning wallet handling and block explorer logic

* update patch version [skip ci]

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: David Adegoke <64401859+Blazebrain@users.noreply.github.com>

* minor new ui fixes

* padding for new row separators

* remove debug prints

* New design improve nav bar (#2763)

* tweak navigation bar touch targets and sizing

* feat: add tests for identifying zero and non-zero amount Lightning invoices (#2772)

* feat: add tests for identifying zero and non-zero amount Lightning invoices

- Added unit tests to validate recognition of Bolt11 zero-amount and non-zero-amount invoices.
- Enhanced `isBolt11ZeroInvoice` function to handle `lightning:` prefixed invoices.

* fix: correct currency selection logic for Lightning wallets in Send ViewModel

* feat: improve Lightning wallet initialization and address generation

- Updated `init` method in `LightningWallet` to return a boolean for initialization success, adding error handling for failed setups.
- Integrated dynamic name generation using `generateName` for Lightning wallet addresses.

* refactor: relocate `generate_name` to `cw_core` and update imports

* chore: update breez sdk (#2789)

* Wire tokens and NFTs to new Home page (#2804)

* wiring for assets on home page

* fix imports

* wire nft page in new ui

* fix nft display in new ui

* fix totalWidth calc

* add tokens button + bugfixes

* add tokens button + bugfixes

* fix late initialization

* fix history section exception

* fixes

* Add haptic feedback (#2813)

* Add haptic feedback

* Add haptic feedback to correct ActionButton

* Return bottom gradient (#2808)

* Add IgnorePointer to bottom nav gradient (#2822)

* New design send page (#2791)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* Update lib/view_model/unspent_coins/unspent_coins_list_view_model.dart [skip ci]

* Update lib/view_model/send/send_view_model.dart [skip ci]

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Fix conflicts with dev

* Fix conflicts with dev

* Add zcash receive options to new design flow

* wire in missing settings in new ui

* up migration version

* formatting

* translations

* lint.sh

* New design receive page (#2834)

* feat: viewmodel on new receive page (wip)

* feat: address type selector for new ui receive page

* fix: minor fixes to touch targets and tap anims

* minor ui fix

* wip receive page changes

* receive page + address selection page

* receive page + address selection page

* add font

* add svg

* minor ui fixes

* receive page fixes/improvements

* change receive page modal design

* further ui polish

* icons for infobox

* fix restricted import

* fix closing label modal

* - fix balance card appearance on address page
- fix search bar position
- fix mweb position in address type selector

* receive page fixes

* fix anonpay

* remove restricted import

* fixes

* fix receive on zec

* simplify chain id

* lint fix

* fix migration numbering

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix migration version

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix settings

* readability

* fix formatting

* move dismiss infobox to vm

* disable addr rotation on zcash

* remove _setEffects

* translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* New UI Sync bar (#2831)

* syncbar

* small styling fixes

* modify duration

* lint.sh

* syncHeavy wallet types

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* update translations, add pull-down refresh, separate top bar components to files

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* alphabetize

* New design balance card icons (#2824)

* Add the other crypto card icons

* Update Zano, add Zcash and Arbitrum

* Update default card style and available gradients

* Fix broken preview

* Fix animation

* Color combinations fix

* Minor updates

* Add decred icon

* if(!mounted) return;

* New design minor fixes (#2842)

* More minor fixes

* Update modal sheet behaviour

* Update missing currency image links

* List row and navigation bar tweaks

* Minor navbar fix

* New UI Swap page (#2829)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* swap page

* fixes

* further fixes

* merge

* png -> svg

* minor condition changes

* lint.sh

* fix swapsxyz isCentralized

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* translations, readability improvements, remove debug rows

* cleanup

* alphabetize strings

* small bugfixes

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix new ui coin control page layout (#2843)

* New design lightning flows (#2821)

* new ui send page (wip)

* coin control page improvements

* fee settings, new send confirmation modal

* minor aesthetic fixes

* load network name without cw_bitcoin import

* recipient dot cleanup

* minor logic fixes

* ui polish

* - add aliaspay support
- fix wording for max button
- fix back button action in modals

* Sizing changes

* dismiss transaction on modal close

* sizing fix

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix remove recipient icon
- properly round amounts
- fix coin control page spacing

* - fix button sizing
- add description for fees page

* lightning flow wip

* lightning flow wip #2

* lightning withdraw flow

* lightning deposit flow (wip, currently works but really janky)

* send from external + bugfixes

* merge fixes

* merge fixes

* fixes

* Update lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart

---------

Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix covered create wallet button

* fix: close modal after transaction commitment in Send ViewModel (#2858)

* New design tweaks (#2847)

* Add more haptic feedback

* Fix background gradients and modify SafeArea in home_page.dart

* Fix history padding, fix divider color, disable Charts, update base_page.dart appbar style, fix nav bar border + height on iOS

* Lightning Username setup flow for new UI (#2845)

* lightning username edit/create flow

* fix button color

* fix layout

* fixes

* remove character limitation for internal builds

* remove dependency on cw_bitcoin

* fix username requirements

* fixes

* condition fix

* Update lib/new-ui/pages/lightning_username_page.dart

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* keyboard hide overlay for iphone

* New design UI tweaks (#2870)

* Fix nav height on Android

* Fix blue gradient

* Update transactions view and minor UI tweaks

* CW1011-Allow-users-to-view-bitcoin-amounts-in-sats (#2678)

* feat: introduce `formatFixed` integration and sats display preference

* feat: enable display of Bitcoin amounts in satoshis and refactor amount formatting logic

* refactor: consolidate fee formatting logic and add support for MWEB availability check

* feat: add support for satoshi-based amount display and enhance fiat conversion logic

* feat: add support for satoshi-based amount parsing, formatting, and display preferences across buy/sell flow and UI adjustments

* feat: implement `AmountParsingProxy` with unit tests for parsing and formatting crypto amounts in various display modes

* feat: replace `preferBalanceInSats` with `displayAmountsInSatoshi` and introduce `AmountParsingProxy` for unified crypto amount handling across the app

* refactor: streamline amount formatting logic and enhance unspent coins handling with `AmountParsingProxy` integration for unified crypto processing

* refactor: replace direct bitcoin amount formatting with `AmountParsingProxy` and remove debug print in `exchange_view_model`

* refactor: replace `getIt<AmountParsingProxy>` with `_appStore.amountParsingProxy`, remove redundant dependencies across view models and adjust related imports

* refactor: remove redundant `AmountParsingProxy` registration in DI setup to simplify dependency management [skip ci]

* feat: add getCryptoSymbol to simplify the Symbol selection between Sats and Bitcoin

* fix: merge conflict

* Revert "fix: merge conflict"

This reverts commit 6debdaa0466747d6fcc85d6bd0be115e6fb4d856.

* fix import [skip ci]

* feat: remove number formatter from exchangeVM

* refactor: replace `SettingsStore` with `AppStore` across view models, integrate `AmountParsingProxy` for amount formatting, and clean up redundant imports

* refactor: remove debug print statements from `getCryptoOutputAmount` in `AmountParsingProxy`

* feat: enhance fee and deposit amount formatting with crypto symbols and improved parsing

* feat: integrate `AmountParsingProxy` for improved Bitcoin amount parsing in `PaymentURI` and fiat-to-crypto conversion

* feat: append crypto symbols to Bitcoin transaction amounts in dashboard view

* feat: improve crypto amount parsing in SendCard widget and Output view model using `AmountParsingProxy`

* feat: conditionally show Bitcoin amount display setting for Bitcoin wallets and refactor `DisplaySettingsViewModel` to use `AppStore`

* refactor: clean up redundant imports in `bitcoin_wallet.dart` and `electrum_wallet.dart`

* feat: append crypto symbols to fee values in send and cake pay card widgets for improved clarity

* refactor: replace `currencyTitle` with `currency` across widgets and integrate `AmountParsingProxy` for improved crypto amount formatting and parsing

* refactor: improve validation logic and amount parsing in SendCard and Output widgets

* refactor: standardize amount parsing and formatting logic with `AmountParsingProxy` across ViewModels and widgets

* refactor: `AppStore` remove debug prints

* refactor: rename amount parsing/formatting methods for better clarity in tests [skip ci]

* fix: AmountParsingProxy and optimize `cryptoAmount` handling in BuySellViewModel

* fix: adjust balance calculation for Monero and Wownero to exclude unlocked amount from full balance

* fix: correct `_appStore` usage and format adjustments in `SendViewModel`

* feat: enhance satoshi on new-ui

* feat: add computed property for `amountParsingProxy` in AppStore

* feat: localize Bitcoin amount display option and update asset history UI

* feat: improve exchange amount processing and refactor fiat conversion values

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

* New design unconfirmed balance widget (#2861)

* unconfirmed balance display widget for new ui

* unconfirmed balance display widget for new ui

* merge

* alphabetize translations

* New UI fixes (#2860)

* fix asset top bar

* improve wordmark in large qr mode on receive

* receive option fixes

* exchange -> swap

* send page fixes

* update pubspec base

* go to home on wallet change

* nicer card customizer

* don't show account name for single-account wallets

* special card design for lightning

* fiat amount bar improvement

* update translations

* add sending indicator

* fix non-number input to receive amount

* de-jank fiat input

* nicer balance card animation when switching btc-ln

* nicer balance card animation when switching btc-ln

* fix backup password input

* display time+date in history

* fix error on buy

* fix incorrect wallet showing in wallets tab

* fix wallet list layout

* only show coin control option on hasCoinControl

* better error on send page

* fiat value in coin control

* add zano to isSyncHeeavy

* fix keys in settings

* fix assets condition

* fix import

* remove BasePage dependency in RescanPage

* add missing settings

* show send/receive in swap confirm sheet

* fix receive background on ios

* fix assets spacing

* fix trade tile sizing

* fix padding

* fix icon in wallet select modal

* change text on swap initial modal

* spacing

* l2 send external modal styling

* fix null on swap confirm sheet

* change infobox text

* add chain icon to asset widgets

* add chain icon to asset widgets

* disable swap page reset

* fix wallet list page layout

* revert merge conflict issue [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Account Customizer for new UI Monero (#2859)

* new ui monero account management (wip)

* account reordering wip

* account reordering wip

* account reordering (works)

* cards view compact mode

* format

* translation

* name generation

* fix for non-xmr wallets

* }

* allow reset order, fallback on improperly saved order

* translation

* fix for pre-reorder wallet loading

* fix loading

* - save cards on modal slide-down
- always load active account as first

* fix card customizer close anim

* fixes

* fix condition

* merge

* fix migration versions

* use updated flutter rust bridge in lightning and payjoin

* mvp fixes

* new ui mweb mask/unmask

* fallback for balance card amount format

* New UI tweaks (#2872)

* Update lightning transaction history view

* Add BNB card design

* Color, padding,  and radius fixes

* Fix wallet list bottom section & gradient

*  Update Apps page

* reformat

* nicer sat picker

* fix keyboard

* chain badges

* enhance styling for card customizer

* mweb settings icon

* mweb chain badge

* update wording in settings

* bnb chain icon

* always round send amount

* always round all amount

* token placeholder

* Fix ModernButton colors (#2888)

* Fix ModernButton colors

* Minor

* guard for unconfirmed balance widget

* fix error on wallet switch

* New design mvp fixes (#2890)

* mvp fixes

* fallback for balance card amount format

* reformat

* nicer sat picker

* fix keyboard

* chain badges

* enhance styling for card customizer

* bnb chain icon

* always round send amount

* always round all amount

* guard for unconfirmed balance widget

* fix error on wallet switch

* Update lib/new-ui/pages/account_customizer.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fixes

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Install Protoc in reusable build workflow

Added step to install Protoc before building.

* Fix protobuf-compiler installation step

Update package list before installing protobuf-compiler.

* Modify Protoc installation command to handle update errors

Change the install command for Protoc to avoid failure on update.

* Streamline Lightning wallet balance management and crypto handling (#2893)

* refactor: streamline crypto amount handling and improve Lightning balance management

- Removed redundancy in balance calculations for Lightning wallets.
- Centralized crypto symbol creation with `selectedCryptoCurrencySymbol`.
- Enhanced crypto amount handling and formatting with `AmountParsingProxy`.
- Simplified logic for displaying Lightning balances across UI components.

* refactor: remove sats display logic and unused formatting from BalanceCard widget

* chore: update breez-sdk-spark-flutter to latest commit

* feat: enhance crypto amount formatting with localized separators and max decimals

* fixes

* fix: handle null balance records in Lightning wallet cards

* New design add validators (#2894)

* add validation, fix node form

* Add validator to send amount input field

Added a validator to the amount input field.

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Disable 'addresses' button on receive screen for BTC LN (#2896)

* feat: Disable addresses button on receive screen for BTC LN

* chore: remove debug print in uri computation

* feat: introduce swap action toggle and UI updates

* feat: conditionally display Zcash card setting for Zcash wallets in Display Settings

* Fix leading button color (#2898)

* fix amount format

* fix loadCards

* pre-beta fixes

* cyjan: fix: order being null for certain index

* pre-beta fixes v2 (#2905)

* minor context check [skip ci]

* persist fee priority for zcash (#2907)

* Add support for Zcash address detection and amount logic refactor (#2906)

* refactor: consolidate crypto amount handling with `displayAmount` and `cryptoCurrencySymbol`, improve fiat conversion logic, and optimize token currency management

* refactor: simplify `displayAmount` logic and remove unused `_rawAmount` property

* feat: add support for Zcash transparent address detection and simplify amount conversion logic

* Improve Lightning invoice handling and crypto formatting (#2908)

* fix: update Lightning address regex to support additional formats and prefixes

* fix: conditionally render advanced settings dropdown based on fees or coin control availability

* feat: enhance address detection, Lightning invoice handling, and crypto formatting

* feat: improve Lightning invoice parsing and crypto amount formatting on send page

* chore: remove unnecessary exception printing in `getBolt11Amount` function

* Fix lightning swap

* import fix (hide Mac;)

* parse -> tryParse

* Transaction Commited -> Transaction Sent!

* feat: enable clipboard paste for deposit and receive addresses in swap page (#2909)

* fix: resolving LNURLS (#2911)

* new UI pre-beta fixes (#2910)

* fixes

* parse -> tryParse

* change sat display settings style

* minor fix until hive is removed [skip ci]

* Remove redundant logic and improve send page features (#2913)

* fix: update crypto amount parsing logic in send confirmation sheet

* feat: Added checkbox to toggle Litecoin MWEB coins on the send page.
fix: LNUrlPayRecord

* fix: remove redundant `.withLocalSeperator()` from fiat balance formatting logic

* fix: update Bitcoin amount display titles to align with consistent formatting standards

* force boolean to update (#2916)

* force boolean to update

* fix bnb balance

* fixes #4 (#2917)

* only display sats for btc wallets in card customizer

* fix: resolve async handling of Lightning transaction history in `fetchTransactions` method (#2919)

* New design pre beta fixes 5 (#2918)

* improve l2 deposit/withdraw flow layouts

* add copied indicator

* SafeArea

* fix addr rotation

* fix styling of lightning switcher

* no raster graphics in new ui please

* optimization for assets/history section

* remove unused bloc provider

* optimize dashboard + fix duplicated name

* disable lightning mode on wallet change

* remove broken animation

* safearea around nodes page

* fix rescan page layout

* fix exception on really fast switching

* remove broken blur

* card customizer autosave

* remove debug print

* add better fallbacks for currency titles

* revert due to broken animation

* fix send page crash

* fix chain title for arb

* fix cupertino date picker

* Update lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* feat: add focus management and external address validation to send page forms

- Introduced `focusNode` to `NewSendAddressInput` for better input handling.
- Enhanced address validation by synchronizing with parsed external addresses.
- Updated logic for non-MWEB unspent coin type in send actions.
- Suppressed unnecessary exception logs in Bitcoin wallet transactions.

* New design swap page fixes (#2923)

* swap page fixes

* add scroll controller

* comment

* tweak refresh pulldown (#2924)

* add copy button to l2 send external modal (#2925)

* account customizer padding

* feat: lightning transaction fetching and Lightning wallet integration (#2927)

- Allow nullable `height` in `ElectrumTransactionInfo`.
- Add `fromDate` support for filtered Lightning transaction history retrieval.
- Simplify Lightning address handling logic in wallet addresses.
- Optimize Lightning wallet transaction syncing based on the last Lightning transaction date.
- Clean up imports in `electrum_transaction_history.dart`.

* New balance handeling (#2931)

* feat: lightning transaction fetching and Lightning wallet integration

- Allow nullable `height` in `ElectrumTransactionInfo`.
- Add `fromDate` support for filtered Lightning transaction history retrieval.
- Simplify Lightning address handling logic in wallet addresses.
- Optimize Lightning wallet transaction syncing based on the last Lightning transaction date.
- Clean up imports in `electrum_transaction_history.dart`.

* feat: add support for Lightning balance in wallet snapshot and initialization

* Disable Lightning switcher for hardware wallets (#2932)

* fix: disable Lightning switcher for hardware wallets

* fix: update wallet type check to use `isSoftwareWallet` for Lightning support

* New design pre beta fixes 6 (#2929)

* fix swaps from ln

* prevent crash if ln balance isn't loaded yet

* fix currency icons disappearing on swap confirm sheet

* fix swap fiat validation

* display ln at top on swap currency selector in btc wallets

* update icon for card options

* display default fiat and crypto at top of currency picker

* fix fiat amount bar formatting

* add modal scroll controller to provider logs

* revert premature optimization (sizing issue)

* New design pre beta fixes 7 (#2935)

* fix swaps from ln

* prevent crash if ln balance isn't loaded yet

* fix currency icons disappearing on swap confirm sheet

* fix swap fiat validation

* display ln at top on swap currency selector in btc wallets

* update icon for card options

* display default fiat and crypto at top of currency picker

* fix fiat amount bar formatting

* add modal scroll controller to provider logs

* revert premature optimization (sizing issue)

* copyable trade id

* disable swaptrade on ln

* set isFixedRate properly

* update color

* display trocador provider on swap confirmation sheet

* add "exchange rate is fixed" text

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* updated l2 modal (#2934)

* display names for send addr input (#2936)

* fix: MWEB toggle logic on send page (#2938)

* feat: add retry logic to `getAddress` in Lightning wallet (#2939)

* Enhance crypto address handling and add Lightning invoice support (#2937)

* feat: enhance crypto address handling and add Lightning invoice support

- Added `_overrideFromCryptoCurrency` and `_overrideToCryptoCurrency` for Zcash unified and shielded addresses.
- Improved Lightning invoice generation with `getLightningInvoice` for Bitcoin wallets.
- Updated error handling in exchange provider to accommodate API changes.
- Introduced Lightning invoice fallback for deposit and receive addresses in ExchangeViewModel.

* feat: Added `_overrideFromCryptoCurrency` and `_overrideToCryptoCurrency` for Zcash unified and shielded addresses.

* fix pay anything on paste button (#2940)

* Prevent multiple paste triggering any pay flow

* fixup fiat input (#2941)

* New design small fixes (#2920)

* Fix optical symmetry for swipe to send text

* Update nav bar icons

* Top bar and card tweaks

* Update paddings

---------

Co-authored-by: malik1004x <malikowskirobert@gmail.com>

* fix erc20 balance display (#2944)

* fix erc20 balance display

* route through cw_evm

* New design beta fixes (#2943)

* fix asset tile layout

* fix card customizer keyboard jumping

* fix cardsview exception

* fix swap modal height

* remove syncbar percentage

* add copy icon to trade id

* fix trade confirmation appearing twice

* remove useless mweb setting

* add tag to contact currency selector text

* prettify picker for ln

* fix receive crash

* always route to NewSendPage

* handle ln amount on paste

* handle ln amount on payment request creation

* prettify ln errors

* readd "blocks" word

* register deeplinks

* Enhance bitcoin error message [skip ci]

* fix support chat page (#2946)

* new-design-fix-confirm-sheet-parsing-error (#2950)

* feat: use bigint instead of doubles

* feat: use bigint instead of doubles

* feat: use bigint instead of doubles

* fix: also show sats in confirm sheet

* fix: also show sats in confirm sheet

* feat: localize amounts and cap amount in tx history (#2951)

* fix: linux CI

* feat: localize token balances (#2954)

* fix: send amount hww (#2955)

* chore: use latest breez sdk (#2953)

* fix addAddressWord

* fixes (#2957)

* receive fixes (#2959)

* fix: stabilize android build by increasing jvm heap size (#2958)

* fix: lighting by having the new SDK Save into a new location (#2961)

* New design rc fixes (#2960)

* additional safeguards for isSyncHeavy check

* fix account name getting reset

* update base icon

* fix units in swap

* readd memos to zec

* New design minor UI (#2956)

* Wrap text for list item widgets

* Update lightning address and QR icons

* Update near intents icon

* Fix share issue

* Fix (#2963)

* fix tx amount formatting

* rename currency

* change wording for zec memo

* fix base icon

* fix app icon script [skip ci]

* New design rc fixes 2 (#2965)

* fix account name changes

* fix spacing on addresses page

* add payjoin copy modal

* improve memo ui

* potential fix for ci

* Set default address for zcash and bitcoin for buy flow (#2979)

* new monero.com icons (#2980)

* parse and validate deposit amount from api (#2967)

* generic fixes

* minor context fix [skip ci]

* Generic fixes (#2982)

- solana send all bug
- pasting SOL address triggering payanything flow
- add more address types for payanything detection

* rescan fix

* about page (#2974)

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* update settings icons/wording (#2973)

* Changelog modal (New UI) (#2971)

* add new changelog modal

* typo

* print -> printV

* Update lib/new-ui/widgets/changelog_modal.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix node form (#2970)

* feat: Auto fetch wallet tokens in solana wallets

* fix: Merge conflicts

* fix: Merge conflicts

* refactor: replace SvgPicture with CakeImageWidget

---------

Signed-off-by: black5box <black5box@outlook.com>
Co-authored-by: Serhii <borodenko.sv@gmail.com>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: tuxsudo <tuxsudo@tux.pizza>
Co-authored-by: malik1004x <malikowskirobert@gmail.com>
Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: black5box <black5box@outlook.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: Konstantin Ullrich <konstantin@cakewallet.com>
2026-05-04 19:55:18 +02:00

1947 lines
62 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/solana_rpc_http_service.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_solana/pending_solana_transaction.dart';
import 'package:cw_solana/solana_balance.dart';
import 'package:cw_solana/solana_exceptions.dart';
import 'package:cw_solana/solana_transaction_model.dart';
import 'package:cw_core/spl_token.dart';
import 'package:on_chain/solana/solana.dart';
import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart';
import 'package:on_chain/solana/src/models/pda/pda.dart';
import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta.dart';
import '.secrets.g.dart' as secrets;
/// Result object containing both parsed transactions and token mints
class TransactionFetchResult {
final List<SolanaTransactionModel> transactions;
final List<String> tokenMints;
TransactionFetchResult({
required this.transactions,
required this.tokenMints,
});
}
class SolanaWalletClient {
// Minimum amount in SOL to consider a transaction valid (to filter spam)
static const double minValidAmount = 0.00000003;
late final client = ProxyWrapper().getHttpIOClient();
SolanaRPC? _provider;
bool connect(Node node) {
try {
String formattedUrl;
String protocolUsed = node.isSSL ? "https" : "http";
if (node.uriRaw == 'rpc.ankr.com') {
String ankrApiKey = secrets.ankrApiKey;
formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
} else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
String chainStackApiKey = secrets.chainStackApiKey;
formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
} else {
formattedUrl = '$protocolUsed://${node.uriRaw}';
}
_provider = SolanaRPC(SolanaRPCHTTPService(url: formattedUrl));
return true;
} catch (e) {
return false;
}
}
Future<double> getBalance(String walletAddress, {bool throwOnError = false}) async {
try {
final balance = await _provider!.requestWithContext(
SolanaRPCGetBalance(
account: SolAddress(walletAddress),
),
);
final balInLamp = balance.result.toDouble();
final solBalance = balInLamp / SolanaUtils.lamportsPerSol;
return solBalance;
} catch (_) {
if (throwOnError) {
rethrow;
}
return 0.0;
}
}
Future<List<TokenAccountResponse>?> getSPLTokenAccounts(
String mintAddress, String publicKey) async {
try {
final result = await _provider!.request(
SolanaRPCGetTokenAccountsByOwner(
account: SolAddress(publicKey),
mint: SolAddress(mintAddress),
commitment: Commitment.confirmed,
encoding: SolanaRPCEncoding.base64,
),
);
return result;
} catch (e) {
return null;
}
}
Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String walletAddress,
{bool throwOnError = false}) async {
try {
// Fetch the token accounts (a token can have multiple accounts for various uses)
final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress);
// Handle scenario where there is no token account
if (tokenAccounts == null || tokenAccounts.isEmpty) {
return null;
}
// Sum raw amounts and ui amounts across all token accounts
BigInt totalRaw = BigInt.zero;
double totalUi = 0.0;
for (var tokenAccount in tokenAccounts) {
final tokenAmountResult = await _provider!.request(
SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey),
);
final raw = BigInt.tryParse(tokenAmountResult.amount) ?? BigInt.zero;
totalRaw += raw;
final ui = tokenAmountResult.uiAmount ??
(double.tryParse(tokenAmountResult.uiAmountString ?? '0') ?? 0.0);
totalUi += ui;
}
return SolanaBalance.forToken(totalRaw, totalUi);
} catch (_) {
if (throwOnError) {
rethrow;
}
return null;
}
}
Future<double> getFeeForMessage(String message, Commitment commitment) async {
try {
final feeForMessage = await _provider!.request(
SolanaRPCGetFeeForMessage(
encodedMessage: message,
commitment: commitment,
),
);
final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol;
return fee;
} catch (_) {
return 0.0;
}
}
Future<double> getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async {
final message = await _getMessageForNativeTransaction(
publicKey: publicKey,
destinationAddress: publicKey.toAddress().address,
lamports: SolanaUtils.lamportsPerSol,
commitment: commitment,
);
final estimatedFee = await _getFeeFromCompiledMessage(
message,
commitment,
);
return estimatedFee;
}
Future<List<SolanaTransactionModel>?> parseTransaction({
VersionedTransactionResponse? txResponse,
required String walletAddress,
String? splTokenSymbol,
}) async {
if (txResponse == null) return null;
try {
final blockTime = txResponse.blockTime;
final meta = txResponse.meta;
final transaction = txResponse.transaction;
if (meta == null || transaction == null) return null;
final int fee = meta.fee;
final feeInSol = fee / SolanaUtils.lamportsPerSol;
final message = transaction.message;
final instructions = message.compiledInstructions;
String signature = (txResponse.transaction?.signatures.isEmpty ?? true)
? ""
: Base58Encoder.encode(txResponse.transaction!.signatures.first);
// We need to check if this is a swap transaction (both native SOL and SPL token balance changes)
final isSwap = _isSwapTransaction(meta, message, walletAddress);
if (isSwap) {
// We parse it separately, because we want to extract two separate transactions, the outgoing and incoming side of the swap
final swapTransactions = await _parseSwapTransaction(
message: message,
meta: meta,
fee: fee,
feeInSol: feeInSol,
walletAddress: walletAddress,
signature: signature,
blockTime: blockTime,
instructions: instructions,
splTokenSymbol: splTokenSymbol,
);
if (swapTransactions.isNotEmpty) return swapTransactions;
}
for (final instruction in instructions) {
final programId = message.accountKeys[instruction.programIdIndex];
if (programId == SystemProgramConst.programId ||
programId == ComputeBudgetConst.programId) {
// For native solana transactions
if (instruction.accounts.length < 2) continue;
// Get the fee payer index based on transaction type
// For legacy transfers, the first account is usually the fee payer
// For versioned, the first account in instruction is usually the fee payer
final feePayerIndex =
txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0];
final transactionModel = await _parseNativeTransaction(
message: message,
meta: meta,
fee: fee,
feeInSol: feeInSol,
feePayerIndex: feePayerIndex,
walletAddress: walletAddress,
signature: signature,
blockTime: blockTime,
);
if (transactionModel != null) {
return [transactionModel];
}
} else if (programId == SPLTokenProgramConst.tokenProgramId) {
// For SPL Token transactions
if (instruction.accounts.length < 2) continue;
final transactionModel = await _parseSPLTokenTransaction(
message: message,
meta: meta,
fee: fee,
feeInSol: feeInSol,
instruction: instruction,
walletAddress: walletAddress,
signature: signature,
blockTime: blockTime,
splTokenSymbol: splTokenSymbol,
);
if (transactionModel != null) {
return [transactionModel];
}
} else if (programId == AssociatedTokenAccountProgramConst.associatedTokenProgramId) {
// For ATA program, we need to check if this is a create account transaction
// or if it's part of a normal token transfer
// We skip this transaction if this is the only instruction (this means that it's a create account transaction)
if (instructions.length == 1) {
return null;
}
// We look for a token transfer instruction in the same transaction
bool hasTokenTransfer = false;
for (final otherInstruction in instructions) {
final otherProgramId = message.accountKeys[otherInstruction.programIdIndex];
if (otherProgramId == SPLTokenProgramConst.tokenProgramId) {
hasTokenTransfer = true;
break;
}
}
// If there's no token transfer instruction, it means this is just an ATA creation transaction
if (!hasTokenTransfer) {
return null;
}
continue;
} else {
continue;
}
}
} catch (e, s) {
printV("Error parsing transaction: $e\n$s");
}
return null;
}
/// Detects if a transaction is a swap by checking that the wallet both sends one asset and receives another.
///
/// A simple token transfer is NOT a swap (wallet only sends or receives, not both).
bool _isSwapTransaction(
ConfirmedTransactionMeta meta,
VersionedMessage message,
String walletAddress,
) {
final fee = meta.fee;
final preBalances = meta.preBalances;
final postBalances = meta.postBalances;
final accountKeys = message.accountKeys;
bool walletSentSol = false;
bool walletReceivedSol = false;
if (preBalances.isNotEmpty && postBalances.isNotEmpty) {
final maxLength = [
accountKeys.length,
preBalances.length,
postBalances.length,
].reduce((a, b) => a < b ? a : b);
for (int i = 0; i < maxLength; i++) {
if (accountKeys[i].address != walletAddress) {
continue;
}
final change = postBalances[i] - preBalances[i];
if (change > BigInt.zero) {
walletReceivedSol = true;
} else if (change < BigInt.zero) {
// Only count as sent if the decrease
// exceeds the fee (otherwise it's just fees).
final netDecrease = change.abs() - BigInt.from(fee);
if (netDecrease > BigInt.zero) {
walletSentSol = true;
}
}
break;
}
}
bool walletSentToken = false;
bool walletReceivedToken = false;
final preTokenBalances = meta.preTokenBalances;
final postTokenBalances = meta.postTokenBalances;
if (preTokenBalances != null && postTokenBalances != null) {
// Check wallet-owned balances that exist in pre
for (final preBal in preTokenBalances) {
if (preBal.owner?.address != walletAddress) {
continue;
}
final mint = preBal.mint.address;
final preAmt = preBal.uiTokenAmount.uiAmount ?? 0.0;
double postAmt = preAmt;
for (final postBal in postTokenBalances) {
if (postBal.owner?.address == walletAddress && postBal.mint.address == mint) {
postAmt = postBal.uiTokenAmount.uiAmount ?? 0.0;
break;
}
}
final diff = postAmt - preAmt;
if (diff < 0) {
walletSentToken = true;
} else if (diff > 0) {
walletReceivedToken = true;
}
}
// Check for tokens the wallet received into
// a newly created ATA (no pre-balance entry).
for (final postBal in postTokenBalances) {
if (postBal.owner?.address != walletAddress) {
continue;
}
final postAmt = postBal.uiTokenAmount.uiAmount ?? 0.0;
if (postAmt <= 0) continue;
final mint = postBal.mint.address;
final existsInPre = preTokenBalances.any(
(p) => p.owner?.address == walletAddress && p.mint.address == mint,
);
if (!existsInPre) {
walletReceivedToken = true;
}
}
}
// A swap requires the wallet to both send and receive across different assets.
final walletSent = walletSentSol || walletSentToken;
final walletReceived = walletReceivedSol || walletReceivedToken;
return walletSent && walletReceived;
}
/// Parses a swap transaction and creates dual entries (outgoing and incoming)
Future<List<SolanaTransactionModel>> _parseSwapTransaction({
required VersionedMessage message,
required ConfirmedTransactionMeta meta,
required int fee,
required double feeInSol,
required String walletAddress,
required String signature,
required BigInt? blockTime,
required List<CompiledInstruction> instructions,
String? splTokenSymbol,
}) async {
final List<SolanaTransactionModel> swapTransactions = [];
final preBalances = meta.preBalances;
final postBalances = meta.postBalances;
final accountKeys = message.accountKeys;
final preTokenBalances = meta.preTokenBalances;
final postTokenBalances = meta.postTokenBalances;
String? decreasedMintForWallet;
String? increasedMintForWallet;
if (preTokenBalances != null && postTokenBalances != null) {
for (final preTokenBal in preTokenBalances) {
final owner = preTokenBal.owner?.address ?? '';
if (owner != walletAddress) continue;
final mint = preTokenBal.mint.address;
final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0;
double postAmount = preAmount;
for (final postTokenBal in postTokenBalances) {
final postOwner = postTokenBal.owner?.address ?? '';
final postMint = postTokenBal.mint.address;
if (postOwner == walletAddress && postMint == mint) {
postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0;
break;
}
}
final diff = postAmount - preAmount;
if (diff < 0 && decreasedMintForWallet == null) {
decreasedMintForWallet = mint;
} else if (diff > 0 && increasedMintForWallet == null) {
increasedMintForWallet = mint;
}
}
}
final bool isSplToSplSwap = decreasedMintForWallet != null &&
increasedMintForWallet != null &&
decreasedMintForWallet != increasedMintForWallet;
// Parse outgoing side (what was sent)
double outgoingAmount = 0.0;
String outgoingTokenSymbol = '';
String? outgoingMintAddress;
String? outgoingFrom;
String? outgoingTo;
// First we check if there are any native SOL balance changes for the wallet.
// For pure SPL → SPL swaps, SOL changes are just fees, so we ignore them.
if (!isSplToSplSwap && preBalances.isNotEmpty && postBalances.isNotEmpty) {
final maxLength =
accountKeys.length < preBalances.length ? accountKeys.length : preBalances.length;
for (int i = 0; i < maxLength && i < postBalances.length; i++) {
final accountKey = accountKeys[i];
final accountAddress = accountKey.address;
if (accountAddress == walletAddress) {
final preBalance = preBalances[i];
final postBalance = postBalances[i];
final balanceChange = preBalance - postBalance;
if (balanceChange > BigInt.zero) {
// The wallet sent SOL
outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
outgoingTokenSymbol = 'SOL';
outgoingMintAddress = null;
outgoingFrom = walletAddress;
// We find the intermediate account or swap program account
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
final firstAccountIndex = instructions[0].accounts[0];
if (firstAccountIndex < accountKeys.length) {
outgoingTo = accountKeys[firstAccountIndex].address;
}
}
outgoingTo ??= walletAddress;
break;
}
}
}
}
// If no SOL outgoing, we check if there are any SPL token balance changes for the wallet
if (outgoingAmount == 0.0 && preTokenBalances != null) {
for (final preTokenBal in preTokenBalances) {
final owner = preTokenBal.owner?.address ?? '';
if (owner == walletAddress) {
final mint = preTokenBal.mint.address;
// For SPL → SPL swaps, we only treat the decreased mint as outgoing
if (isSplToSplSwap && mint != decreasedMintForWallet) {
continue;
}
final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0;
// We find the corresponding post balance
for (final postTokenBal in postTokenBalances ?? []) {
final postOwner = postTokenBal.owner?.address ?? '';
final postMint = postTokenBal.mint.address;
final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0;
if (postOwner == walletAddress && postMint == mint) {
final diff = preAmount - postAmount;
if (diff > 0) {
// The wallet sent tokens
outgoingAmount = diff.toDouble();
outgoingMintAddress = mint;
final token = await getTokenInfo(mint);
outgoingTokenSymbol = token?.symbol ?? 'TOKEN';
outgoingFrom = walletAddress;
// We find the intermediate account
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
final firstAccountIndex = instructions[0].accounts[0];
if (firstAccountIndex < accountKeys.length) {
outgoingTo = accountKeys[firstAccountIndex].address;
}
}
outgoingTo ??= walletAddress;
break;
}
}
}
if (outgoingAmount > 0) break;
}
}
}
// Parse incoming side (what was received)
double incomingAmount = 0.0;
String incomingTokenSymbol = '';
String? incomingMintAddress;
String? incomingFrom;
String? incomingTo;
// We check if there are any native SOL balance changes for the wallet
if (preBalances.isNotEmpty && postBalances.isNotEmpty) {
final maxLength =
accountKeys.length < preBalances.length ? accountKeys.length : preBalances.length;
for (int i = 0; i < maxLength && i < postBalances.length; i++) {
final accountKey = accountKeys[i];
final accountAddress = accountKey.address;
if (accountAddress == walletAddress) {
final preBalance = preBalances[i];
final postBalance = postBalances[i];
final balanceChange = postBalance - preBalance;
if (balanceChange > BigInt.zero) {
// The wallet received SOL
incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
incomingTokenSymbol = 'SOL';
incomingMintAddress = null;
incomingTo = walletAddress;
// We find the intermediate account
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
final firstAccountIndex = instructions[0].accounts[0];
if (firstAccountIndex < accountKeys.length) {
incomingFrom = accountKeys[firstAccountIndex].address;
}
}
incomingFrom ??= walletAddress;
break;
}
}
}
}
// If no SOL incoming, check SPL token incoming using ATA derivation
if (incomingAmount == 0.0 && preTokenBalances != null && postTokenBalances != null) {
// Collect all unique mints from token balances (excluding wrapped SOL)
final mints = <String>{};
for (final tokenBal in preTokenBalances) {
final mint = tokenBal.mint.address;
if (mint != 'So11111111111111111111111111111111111111112') {
mints.add(mint);
}
}
for (final tokenBal in postTokenBalances) {
final mint = tokenBal.mint.address;
if (mint != 'So11111111111111111111111111111111111111112') {
mints.add(mint);
}
}
// For each mint, we derive the wallet's ATA address and check for balance changes
for (final mint in mints) {
try {
final walletSolAddress = SolAddress(walletAddress);
final mintSolAddress = SolAddress(mint);
final ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintSolAddress,
owner: walletSolAddress,
);
final ataAddress = ata.address.address;
// We check if this ATA address appears in the account keys
int? ataAccountIndex;
for (int i = 0; i < accountKeys.length; i++) {
final accountKey = accountKeys[i];
if (accountKey.address == ataAddress) {
ataAccountIndex = i;
break;
}
}
// If ATA is in the transaction, we check for balance changes
if (ataAccountIndex != null) {
double preAmount = 0.0;
double postAmount = 0.0;
// We find the pre balance
for (final preTokenBal in preTokenBalances) {
final accountIndex = preTokenBal.accountIndex;
final tokenMint = preTokenBal.mint.address;
if (accountIndex == ataAccountIndex && tokenMint == mint) {
preAmount = preTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
break;
}
}
// We find the post balance
for (final postTokenBal in postTokenBalances) {
final accountIndex = postTokenBal.accountIndex;
final tokenMint = postTokenBal.mint.address;
if (accountIndex == ataAccountIndex && tokenMint == mint) {
postAmount = postTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
break;
}
}
final diff = postAmount - preAmount;
if (diff > 0) {
// The wallet received tokens
incomingAmount = diff.toDouble();
incomingMintAddress = mint;
final token = await getTokenInfo(mint);
incomingTokenSymbol = token?.symbol ?? 'TOKEN';
incomingTo = walletAddress;
// We find the intermediate account
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
final firstAccountIndex = instructions[0].accounts[0];
if (firstAccountIndex < accountKeys.length) {
incomingFrom = accountKeys[firstAccountIndex].address;
}
}
incomingFrom ??= walletAddress;
break;
}
}
} catch (e) {
// We skip if the ATA derivation fails
continue;
}
}
}
// Outgoing transaction model
if (outgoingAmount > 0.0 && outgoingFrom != null && outgoingTo != null) {
final outgoingId =
'${signature}_outgoing'; // We create a composite ID for the outgoing transaction
swapTransactions.add(SolanaTransactionModel(
isOutgoingTx: true,
from: outgoingFrom,
to: outgoingTo,
id: outgoingId,
amount: outgoingAmount,
programId: outgoingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
blockTimeInInt: blockTime?.toInt() ?? 0,
tokenSymbol: outgoingTokenSymbol,
fee: feeInSol,
));
}
// Incoming transaction model
if (incomingAmount > 0.0 && incomingFrom != null && incomingTo != null) {
final incomingId =
'${signature}_incoming'; // We create a composite ID for the incoming transaction
swapTransactions.add(SolanaTransactionModel(
isOutgoingTx: false,
from: incomingFrom,
to: incomingTo,
id: incomingId,
amount: incomingAmount,
programId: incomingMintAddress == null
? SystemProgramConst.programId.address
: SPLTokenProgramConst.tokenProgramId.address,
blockTimeInInt: blockTime?.toInt() ?? 0,
tokenSymbol: incomingTokenSymbol,
fee: 0.0, // Fee only charged on outgoing side
));
}
return swapTransactions;
}
Future<SolanaTransactionModel?> _parseNativeTransaction({
required VersionedMessage message,
required ConfirmedTransactionMeta meta,
required int fee,
required double feeInSol,
required int feePayerIndex,
required String walletAddress,
required String signature,
required BigInt? blockTime,
}) async {
final accountKeys = message.accountKeys;
final preBalances = meta.preBalances;
final postBalances = meta.postBalances;
final maxLen = [
accountKeys.length,
preBalances.length,
postBalances.length,
].reduce((a, b) => a < b ? a : b);
// Find the wallet's own balance change.
int walletIndex = -1;
for (int i = 0; i < maxLen; i++) {
if (accountKeys[i].address == walletAddress) {
walletIndex = i;
break;
}
}
if (walletIndex < 0) return null;
final walletPre = preBalances[walletIndex];
final walletPost = postBalances[walletIndex];
// Positive = wallet lost SOL, negative = wallet gained.
final walletChange = walletPre - walletPost;
final bool walletPaidFee =
feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress;
// Net transfer amount excluding the fee.
final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange;
final isOutgoing = netChange > BigInt.zero;
final amountLamports = netChange.abs();
final amountInSol = amountLamports.toDouble() / SolanaUtils.lamportsPerSol;
if (amountInSol < minValidAmount) return null;
// Find the most likely receiver, the account that has the largest opposite balance change.
String? receiver;
BigInt bestChange = BigInt.zero;
for (int i = 0; i < maxLen; i++) {
if (i == walletIndex) continue;
final change = postBalances[i] - preBalances[i];
if (isOutgoing && change > bestChange) {
bestChange = change;
receiver = accountKeys[i].address;
} else if (!isOutgoing && change < BigInt.zero && change.abs() > bestChange) {
bestChange = change.abs();
receiver = accountKeys[i].address;
}
}
if (receiver == null) return null;
return SolanaTransactionModel(
isOutgoingTx: isOutgoing,
from: isOutgoing ? walletAddress : receiver,
to: isOutgoing ? receiver : walletAddress,
id: signature,
amount: amountInSol,
programId: SystemProgramConst.programId.address,
tokenSymbol: 'SOL',
blockTimeInInt: blockTime?.toInt() ?? 0,
fee: feeInSol,
);
}
Future<SolanaTransactionModel?> _parseSPLTokenTransaction({
required VersionedMessage message,
required ConfirmedTransactionMeta meta,
required int fee,
required double feeInSol,
required CompiledInstruction instruction,
required String walletAddress,
required String signature,
required BigInt? blockTime,
String? splTokenSymbol,
}) async {
final preTokenBalances = meta.preTokenBalances;
final postTokenBalances = meta.postTokenBalances;
final accountKeys = message.accountKeys;
final accounts = instruction.accounts;
// TransferChecked has 4 accounts:
// [0] source, [1] mint, [2] destination, [3] owner
// Transfer has 3 accounts:
// [0] source, [1] destination, [2] owner
final isTransferChecked = accounts.length >= 4;
final sourceAccountIndex = accounts[0];
final destinationAccountIndex = isTransferChecked ? accounts[2] : accounts[1];
String? mintAddress;
if (isTransferChecked) {
mintAddress = accountKeys[accounts[1]].address;
}
double userPreAmount = 0.0;
double userPostAmount = 0.0;
if (preTokenBalances != null) {
for (final preBal in preTokenBalances) {
final idx = preBal.accountIndex;
if (idx == sourceAccountIndex || idx == destinationAccountIndex) {
if (preBal.owner?.address == walletAddress) {
if (mintAddress != null && preBal.mint.address != mintAddress) {
continue;
}
mintAddress ??= preBal.mint.address;
userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
break;
}
}
}
}
if (postTokenBalances != null) {
for (final postBal in postTokenBalances) {
final idx = postBal.accountIndex;
if (idx == sourceAccountIndex || idx == destinationAccountIndex) {
if (postBal.owner?.address == walletAddress) {
if (mintAddress != null && postBal.mint.address != mintAddress) {
continue;
}
mintAddress ??= postBal.mint.address;
userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
break;
}
}
}
}
final diff = userPreAmount - userPostAmount;
final rawAmount = diff.abs();
final amountInString = rawAmount.toStringAsFixed(6);
final amount = double.parse(amountInString);
final isOutgoing = diff > 0;
// Resolve sender/receiver from token balance owners
String? senderOwner;
String? receiverOwner;
final allBalances = [
...?preTokenBalances,
...?postTokenBalances,
];
for (final bal in allBalances) {
if (mintAddress != null && bal.mint.address != mintAddress) {
continue;
}
if (bal.accountIndex == sourceAccountIndex) {
senderOwner ??= bal.owner?.address;
}
if (bal.accountIndex == destinationAccountIndex) {
receiverOwner ??= bal.owner?.address;
}
if (senderOwner != null && receiverOwner != null) {
break;
}
}
final sender = senderOwner ?? accountKeys[sourceAccountIndex].address;
final receiver = receiverOwner ?? accountKeys[destinationAccountIndex].address;
String? tokenSymbol = splTokenSymbol;
if (tokenSymbol == null && mintAddress != null) {
final token = await getTokenInfo(mintAddress);
tokenSymbol = token?.symbol;
}
return SolanaTransactionModel(
isOutgoingTx: isOutgoing,
from: sender,
to: receiver,
id: signature,
amount: amount,
programId: SPLTokenProgramConst.tokenProgramId.address,
blockTimeInInt: blockTime?.toInt() ?? 0,
tokenSymbol: tokenSymbol ?? '',
fee: feeInSol,
);
}
/// Fetches a specific transaction by signature and parses it
/// It returns a TransactionFetchResult object containing both transactions and token mints extracted from the transaction or null if the transaction is not found or cannot be parsed
Future<TransactionFetchResult?> fetchTransactionBySignature({
required String signature,
required String walletAddress,
String? splTokenSymbol,
}) async {
try {
final txResponse = await _provider!.request(
SolanaRPCGetTransaction(
transactionSignature: signature,
encoding: SolanaRPCEncoding.jsonParsed,
maxSupportedTransactionVersion: 1,
skipVerification: true,
),
);
final versionedResponse = txResponse as VersionedTransactionResponse?;
if (versionedResponse == null) return null;
final tokenMints = _extractTokenMintsFromMeta(versionedResponse.meta);
final parsed = await parseTransaction(
txResponse: versionedResponse,
walletAddress: walletAddress,
splTokenSymbol: splTokenSymbol,
);
if (parsed == null) return null;
return TransactionFetchResult(
transactions: parsed,
tokenMints: tokenMints,
);
} catch (e) {
printV('Error fetching transaction by signature: $e');
return null;
}
}
/// Extracts token mint addresses from transaction metadata
/// It returns a list of unique token mint addresses (excluding wrapped SOL)
List<String> _extractTokenMintsFromMeta(ConfirmedTransactionMeta? meta) {
if (meta == null) return [];
final preTokenBalances = meta.preTokenBalances;
final postTokenBalances = meta.postTokenBalances;
final mints = <String>{};
if (preTokenBalances != null) {
for (final tokenBal in preTokenBalances) {
final mint = tokenBal.mint.address;
if (mint != 'So11111111111111111111111111111111111111112') {
mints.add(mint);
}
}
}
if (postTokenBalances != null) {
for (final tokenBal in postTokenBalances) {
final mint = tokenBal.mint.address;
if (mint != 'So11111111111111111111111111111111111111112') {
mints.add(mint);
}
}
}
return mints.toList();
}
/// Load the Address's transactions into the account
Future<List<SolanaTransactionModel>> fetchTransactions(
SolAddress address, {
String? splTokenSymbol,
int? splTokenDecimal,
Commitment? commitment,
SolAddress? walletAddress,
required void Function(List<SolanaTransactionModel>) onUpdate,
}) async {
List<SolanaTransactionModel> transactions = [];
try {
final signatures = await _provider!.request(
SolanaRPCGetSignaturesForAddress(
account: address,
commitment: commitment,
),
);
// The maximum concurrent batch size.
const int batchSize = 10;
for (int i = 0; i < signatures.length; i += batchSize) {
final batch = signatures.skip(i).take(batchSize).toList();
final batchResponses = await Future.wait(batch.map((signature) async {
try {
return await _provider!.request(
SolanaRPCGetTransaction(
transactionSignature: signature['signature'],
encoding: SolanaRPCEncoding.jsonParsed,
maxSupportedTransactionVersion: 1,
skipVerification: true,
),
);
} catch (e) {
return null;
}
}));
final versionedBatchResponses = batchResponses.whereType<VersionedTransactionResponse>();
final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction(
txResponse: tx,
splTokenSymbol: splTokenSymbol,
walletAddress: walletAddress?.address ?? address.address,
));
final parsedTransactionsLists = await Future.wait(parsedTransactionsFutures);
// We flatten the list of lists into a single list
for (final parsedList in parsedTransactionsLists) {
if (parsedList != null) {
transactions.addAll(parsedList);
}
}
// Only update UI if we have new valid transactions
if (transactions.isNotEmpty) {
onUpdate(List<SolanaTransactionModel>.from(transactions));
}
if (i + batchSize < signatures.length) {
await Future.delayed(const Duration(milliseconds: 100));
}
}
return transactions;
} catch (err, s) {
printV('Error fetching transactions: $err \n$s');
return [];
}
}
Future<List<SolanaTransactionModel>> getSPLTokenTransfers({
required String mintAddress,
required String splTokenSymbol,
required int splTokenDecimal,
required SolanaPrivateKey privateKey,
required void Function(List<SolanaTransactionModel>) onUpdate,
}) async {
ProgramDerivedAddress? associatedTokenAccount;
final ownerWalletAddress = privateKey.publicKey().toAddress();
try {
associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
payerPrivateKey: privateKey,
mintAddress: SolAddress(mintAddress),
ownerAddress: ownerWalletAddress,
shouldCreateATA: false,
);
} catch (e, s) {
printV('$e \n $s');
}
if (associatedTokenAccount == null) return [];
final accountPublicKey = associatedTokenAccount.address;
final tokenTransactions = await fetchTransactions(
accountPublicKey,
splTokenSymbol: splTokenSymbol,
splTokenDecimal: splTokenDecimal,
walletAddress: ownerWalletAddress,
onUpdate: onUpdate,
);
return tokenTransactions;
}
final Map<String, SPLToken?> tokenInfoCache = {};
Future<SPLToken?> getTokenInfo(String mintAddress) async {
if (tokenInfoCache.containsKey(mintAddress)) {
return tokenInfoCache[mintAddress];
} else {
final token = await fetchSPLTokenInfo(mintAddress);
if (token != null) {
tokenInfoCache[mintAddress] = token;
}
return token;
}
}
Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
try {
final uri = Uri.https(
'solana-gateway.moralis.io',
'/token/mainnet/$mintAddress/metadata',
);
final response = await client.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String;
final name = decodedResponse['name'] ?? '';
final decimal = decodedResponse['decimals'] ?? '0';
final iconPath = decodedResponse['logo'] ?? '';
String filteredTokenSymbol =
symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
return SPLToken(
name: name,
mint: symbol,
symbol: filteredTokenSymbol,
mintAddress: mintAddress,
iconPath: iconPath,
decimal: int.tryParse(decimal) ?? 0,
);
} catch (e, s) {
printV('Error fetching token info: $e \n $s');
try {
final programAddress =
MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress));
final token = await _provider!.request(
SolanaRPCGetMetadataAccount(
account: programAddress.address,
commitment: Commitment.confirmed,
),
);
if (token == null) return null;
final metadata = token.data;
String? iconPath;
//TODO(Further explore fetching images)
// try {
// iconPath = await _client.getIconImageFromTokenUri(metadata.uri);
// } catch (_) {}
String filteredTokenSymbol =
metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
return SPLToken.fromMetadata(
name: metadata.name,
mint: metadata.symbol,
symbol: filteredTokenSymbol,
mintAddress: token.mint.address,
iconPath: iconPath,
);
} catch (_) {}
return null;
}
}
void stop() {}
SolanaRPC? get getSolanaProvider => _provider;
Future<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required SolanaPrivateKey ownerPrivateKey,
required bool isSendAll,
required double solBalance,
String? tokenMint,
List<String> references = const [],
}) async {
const commitment = Commitment.confirmed;
if (tokenTitle == CryptoCurrency.sol.title) {
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerPrivateKey: ownerPrivateKey,
commitment: commitment,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingNativeTokenTransaction;
} else {
final pendingSPLTokenTransaction = _signSPLTokenTransaction(
tokenDecimals: tokenDecimals,
tokenMint: tokenMint!,
inputAmount: inputAmount,
ownerPrivateKey: ownerPrivateKey,
destinationAddress: destinationAddress,
commitment: commitment,
solBalance: solBalance,
);
return pendingSPLTokenTransaction;
}
}
Future<SolAddress> _getLatestBlockhash(Commitment commitment) async {
final latestBlockhash = await _provider!.request(
const SolanaRPCGetLatestBlockhash(),
);
return latestBlockhash.blockhash;
}
Future<Message> _getMessageForNativeTransaction({
required SolanaPublicKey publicKey,
required String destinationAddress,
required int lamports,
required Commitment commitment,
}) async {
final instructions = [
SystemProgram.transfer(
from: publicKey.toAddress(),
layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
to: SolAddress(destinationAddress),
),
];
final latestBlockhash = await _getLatestBlockhash(commitment);
final message = Message.compile(
transactionInstructions: instructions,
payer: publicKey.toAddress(),
recentBlockhash: latestBlockhash,
);
return message;
}
Future<Message> _getMessageForSPLTokenTransaction({
required SolAddress ownerAddress,
required SolAddress destinationAddress,
required int tokenDecimals,
required SolAddress mintAddress,
required SolAddress sourceAccount,
required int amount,
required Commitment commitment,
required SolAddress tokenProgramId,
}) async {
final instructions = [
SPLTokenProgram.transferChecked(
layout: SPLTokenTransferCheckedLayout(
amount: BigInt.from(amount),
decimals: tokenDecimals,
),
mint: mintAddress,
source: sourceAccount,
destination: destinationAddress,
owner: ownerAddress,
)
];
final latestBlockhash = await _getLatestBlockhash(commitment);
final message = Message.compile(
transactionInstructions: instructions,
payer: ownerAddress,
recentBlockhash: latestBlockhash,
);
return message;
}
Future<double> _getFeeFromCompiledMessage(Message message, Commitment commitment) async {
final base64Message = base64Encode(message.serialize());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
}
Future<bool> hasSufficientFundsLeftForRent({
required double inputAmount,
required double solBalance,
required double fee,
}) async {
final rent = await _provider!.request(
SolanaRPCGetMinimumBalanceForRentExemption(
size: SolanaTokenAccountUtils.accountSize,
),
);
final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble();
final remnant = solBalance - (inputAmount + fee);
if (remnant > rentInSol) return true;
return false;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required double inputAmount,
required String destinationAddress,
required SolanaPrivateKey ownerPrivateKey,
required Commitment commitment,
required bool isSendAll,
required double solBalance,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt();
Message message = await _getMessageForNativeTransaction(
publicKey: ownerPrivateKey.publicKey(),
destinationAddress: destinationAddress,
lamports: lamports,
commitment: commitment,
);
SolAddress latestBlockhash = await _getLatestBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
commitment,
);
if (!isSendAll) {
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: inputAmount,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignNativeTokenTransactionRentException();
}
}
String serializedTransaction;
if (isSendAll) {
final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt();
final updatedLamports = lamports - feeInLamports;
final transaction = _constructNativeTransaction(
ownerPrivateKey: ownerPrivateKey,
destinationAddress: destinationAddress,
latestBlockhash: latestBlockhash,
lamports: updatedLamports,
);
serializedTransaction = await _signTransactionInternal(
ownerPrivateKey: ownerPrivateKey,
transaction: transaction,
);
} else {
final transaction = _constructNativeTransaction(
ownerPrivateKey: ownerPrivateKey,
destinationAddress: destinationAddress,
latestBlockhash: latestBlockhash,
lamports: lamports,
);
serializedTransaction = await _signTransactionInternal(
ownerPrivateKey: ownerPrivateKey,
transaction: transaction,
);
}
sendTx() async => await sendTransaction(
serializedTransaction: serializedTransaction,
commitment: commitment,
);
final pendingTransaction = PendingSolanaTransaction(
amount: inputAmount,
serializedTransaction: serializedTransaction,
destinationAddress: destinationAddress,
sendTransaction: sendTx,
fee: fee,
);
return pendingTransaction;
}
SolanaTransaction _constructNativeTransaction({
required SolanaPrivateKey ownerPrivateKey,
required String destinationAddress,
required SolAddress latestBlockhash,
required int lamports,
}) {
final owner = ownerPrivateKey.publicKey().toAddress();
/// Create a transfer instruction to move funds from the owner to the receiver.
final transferInstruction = SystemProgram.transfer(
from: owner,
layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
to: SolAddress(destinationAddress),
);
/// Construct a Solana transaction with the transfer instruction.
return SolanaTransaction(
instructions: [transferInstruction],
recentBlockhash: latestBlockhash,
payerKey: ownerPrivateKey.publicKey().toAddress(),
type: TransactionType.v0,
);
}
/// Creates a transferChecked instruction with a custom token program ID.
/// This supports both standard SPL Token and Token-2022.
TransactionInstruction _createTransferCheckedInstruction({
required SolAddress tokenProgramId,
required SolAddress source,
required SolAddress destination,
required SolAddress mint,
required SolAddress owner,
required BigInt amount,
required int decimals,
}) {
// TransferChecked instruction format:
// - Instruction discriminator: 12 (u8)
// - Amount: 8 bytes (u64, little-endian)
// - Decimals: 1 byte (u8)
// Convert BigInt to 8-byte little-endian array
final amountBytes = <int>[];
var amountValue = amount.toUnsigned(64);
for (int i = 0; i < 8; i++) {
amountBytes.add((amountValue & BigInt.from(0xFF)).toInt());
amountValue = amountValue >> 8;
}
final instructionData = <int>[12, ...amountBytes, decimals];
// Account order for transferChecked:
// 0. source (writable)
// 1. mint (readonly)
// 2. destination (writable)
// 3. owner (signer)
final accounts = [
AccountMeta(
publicKey: source,
isWritable: true,
isSigner: false,
),
AccountMeta(
publicKey: mint,
isWritable: false,
isSigner: false,
),
AccountMeta(
publicKey: destination,
isWritable: true,
isSigner: false,
),
AccountMeta(
publicKey: owner,
isWritable: false,
isSigner: true,
),
];
return TransactionInstruction.fromBytes(
programId: tokenProgramId,
instructionBytes: instructionData,
keys: accounts,
);
}
/// Gets the token program ID for a given mint address.
/// Returns the standard SPL Token program ID if the mint account cannot be fetched.
Future<SolAddress> _getTokenProgramId(SolAddress mintAddress) async {
try {
final mintAccountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: mintAddress,
commitment: Commitment.confirmed,
),
);
// Determine the token program ID from the mint account owner
if (mintAccountInfo != null) {
return mintAccountInfo.owner;
}
} catch (e) {
// If we can't fetch mint info, default to standard SPL Token program
printV('Warning: Could not fetch mint account info: $e');
}
return SPLTokenProgramConst.tokenProgramId;
}
Future<ProgramDerivedAddress?> _getOrCreateAssociatedTokenAccount({
required SolanaPrivateKey payerPrivateKey,
required SolAddress ownerAddress,
required SolAddress mintAddress,
required bool shouldCreateATA,
}) async {
// For transaction history loading (shouldCreateATA: false), try standard token program first
// to avoid unnecessary RPC call. Only fetch token program ID when creating accounts.
SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId;
if (shouldCreateATA) {
// Only fetch token program ID when we need to create an account
tokenProgramId = await _getTokenProgramId(mintAddress);
}
// Try with standard token program first (most common case)
var associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: ownerAddress,
tokenProgramId: SPLTokenProgramConst.tokenProgramId,
);
SolanaAccountInfo? accountInfo;
try {
accountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: associatedTokenAccount.address,
commitment: Commitment.confirmed,
),
);
} catch (e) {
accountInfo = null;
}
// If account exists with standard program, return it
if (accountInfo != null) return associatedTokenAccount;
// If not found and we're not creating, try Token-2022 as fallback
if (!shouldCreateATA) {
try {
final token2022ProgramId = await _getTokenProgramId(mintAddress);
if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: ownerAddress,
tokenProgramId: token2022ProgramId,
);
try {
accountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: associatedTokenAccount.address,
commitment: Commitment.confirmed,
),
);
if (accountInfo != null) return associatedTokenAccount;
} catch (_) {}
}
} catch (_) {}
return null;
}
// For account creation, use the detected token program ID
associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: ownerAddress,
tokenProgramId: tokenProgramId,
);
final payerAddress = payerPrivateKey.publicKey().toAddress();
final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount(
payer: payerAddress,
associatedToken: associatedTokenAccount.address,
owner: ownerAddress,
mint: mintAddress,
tokenProgramId: tokenProgramId,
);
final blockhash = await _getLatestBlockhash(Commitment.confirmed);
final transaction = SolanaTransaction(
payerKey: payerAddress,
instructions: [createAssociatedTokenAccount],
recentBlockhash: blockhash,
type: TransactionType.v0,
);
final serializedTransaction = await _signTransactionInternal(
ownerPrivateKey: payerPrivateKey,
transaction: transaction,
);
await sendTransaction(
serializedTransaction: serializedTransaction,
commitment: Commitment.confirmed,
);
// Wait for confirmation
await Future.delayed(const Duration(seconds: 2));
return associatedTokenAccount;
}
Future<PendingSolanaTransaction> _signSPLTokenTransaction({
required int tokenDecimals,
required String tokenMint,
required double inputAmount,
required String destinationAddress,
required SolanaPrivateKey ownerPrivateKey,
required Commitment commitment,
required double solBalance,
}) async {
final mintAddress = SolAddress(tokenMint);
// Input by the user
final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt();
final tokenProgramId = await _getTokenProgramId(mintAddress);
ProgramDerivedAddress? associatedSenderAccount;
SolAddress senderTokenProgramId = tokenProgramId;
try {
associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: ownerPrivateKey.publicKey().toAddress(),
tokenProgramId: tokenProgramId,
);
// Verify the account exists and get the actual program ID that owns it
final accountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: associatedSenderAccount.address,
commitment: Commitment.confirmed,
),
);
if (accountInfo != null) {
senderTokenProgramId = accountInfo.owner;
} else {
associatedSenderAccount = null;
}
} catch (e) {
associatedSenderAccount = null;
}
// If account doesn't exist with detected program ID, try standard token program as fallback
if (associatedSenderAccount == null &&
tokenProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
try {
associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: ownerPrivateKey.publicKey().toAddress(),
tokenProgramId: SPLTokenProgramConst.tokenProgramId,
);
final accountInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: associatedSenderAccount.address,
commitment: Commitment.confirmed,
),
);
if (accountInfo != null) {
senderTokenProgramId = accountInfo.owner;
} else {
associatedSenderAccount = null;
}
} catch (_) {
associatedSenderAccount = null;
}
}
if (associatedSenderAccount == null) {
throw SolanaNoAssociatedTokenAccountException(
ownerPrivateKey.publicKey().toAddress().address,
mintAddress.address,
);
}
// Get or create recipient account using the sender's token program ID
// This ensures both accounts use the same program
ProgramDerivedAddress? associatedRecipientAccount;
try {
// First, try to get/create with the sender's actual program ID
final recipientPDA = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
mint: mintAddress,
owner: SolAddress(destinationAddress),
tokenProgramId: senderTokenProgramId,
);
// Check if account exists with correct program
SolanaAccountInfo? recipientInfo;
try {
recipientInfo = await _provider!.request(
SolanaRPCGetAccountInfo(
account: recipientPDA.address,
commitment: Commitment.confirmed,
),
);
} catch (_) {
recipientInfo = null;
}
if (recipientInfo != null && recipientInfo.owner.address == senderTokenProgramId.address) {
associatedRecipientAccount = recipientPDA;
} else {
// Create the account with the correct program ID
final createATA = AssociatedTokenAccountProgram.associatedTokenAccount(
payer: ownerPrivateKey.publicKey().toAddress(),
associatedToken: recipientPDA.address,
owner: SolAddress(destinationAddress),
mint: mintAddress,
tokenProgramId: senderTokenProgramId,
);
final blockhash = await _getLatestBlockhash(Commitment.confirmed);
final createTransaction = SolanaTransaction(
payerKey: ownerPrivateKey.publicKey().toAddress(),
instructions: [createATA],
recentBlockhash: blockhash,
type: TransactionType.v0,
);
final serializedCreateTx = await _signTransactionInternal(
ownerPrivateKey: ownerPrivateKey,
transaction: createTransaction,
);
await sendTransaction(
serializedTransaction: serializedCreateTx,
commitment: Commitment.confirmed,
);
await Future.delayed(const Duration(seconds: 2));
associatedRecipientAccount = recipientPDA;
}
} catch (e) {
throw SolanaCreateAssociatedTokenAccountException(e.toString());
}
// Create transferChecked instruction with the correct token program ID
final transferInstructions = _createTransferCheckedInstruction(
tokenProgramId: senderTokenProgramId,
source: associatedSenderAccount.address,
destination: associatedRecipientAccount.address,
mint: mintAddress,
owner: ownerPrivateKey.publicKey().toAddress(),
amount: BigInt.from(amount),
decimals: tokenDecimals,
);
final latestBlockHash = await _getLatestBlockhash(commitment);
final transaction = SolanaTransaction(
payerKey: ownerPrivateKey.publicKey().toAddress(),
instructions: [transferInstructions],
recentBlockhash: latestBlockHash,
);
final message = await _getMessageForSPLTokenTransaction(
ownerAddress: ownerPrivateKey.publicKey().toAddress(),
tokenDecimals: tokenDecimals,
mintAddress: mintAddress,
destinationAddress: associatedRecipientAccount.address,
sourceAccount: associatedSenderAccount.address,
amount: amount,
commitment: commitment,
tokenProgramId: tokenProgramId,
);
final fee = await _getFeeFromCompiledMessage(message, commitment);
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: 0,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignSPLTokenTransactionRentException();
}
final serializedTransaction = await _signTransactionInternal(
ownerPrivateKey: ownerPrivateKey,
transaction: transaction,
);
sendTx() async => await sendTransaction(
serializedTransaction: serializedTransaction,
commitment: commitment,
);
final pendingTransaction = PendingSolanaTransaction(
amount: inputAmount,
serializedTransaction: serializedTransaction,
destinationAddress: destinationAddress,
sendTransaction: sendTx,
fee: fee,
);
return pendingTransaction;
}
Future<String> _signTransactionInternal({
required SolanaPrivateKey ownerPrivateKey,
required SolanaTransaction transaction,
}) async {
/// Sign the transaction with the owner's private key.
final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
/// Serialize the transaction.
final serializedTransaction = transaction.serializeString();
return serializedTransaction;
}
Future<String> sendTransaction({
required String serializedTransaction,
required Commitment commitment,
}) async {
try {
/// Send the transaction to the Solana network.
final signature = await _provider!.request(
SolanaRPCSendTransaction(
encodedTransaction: serializedTransaction,
commitment: commitment,
),
);
return signature;
} catch (e) {
throw Exception(e);
}
}
Future<String?> getIconImageFromTokenUri(String uri) async {
if (uri.isEmpty || uri == '…') return null;
try {
final client = ProxyWrapper().getHttpIOClient();
final response = await client.get(Uri.parse(uri));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300) {
return jsonResponse['image'];
} else {
return null;
}
} catch (e) {
printV('Error occurred while fetching token image: \n${e.toString()}');
return null;
}
}
Future<List<MoralisSolanaTokenBalance>> fetchWalletTokensFromMoralis(
String address,
) async {
try {
if (secrets.moralisApiKey.isEmpty) {
printV('Moralis API key is empty, cannot fetch wallet tokens');
return [];
}
final uri = Uri.https(
'solana-gateway.moralis.io',
'/account/mainnet/$address/tokens',
);
final response = await client.get(
uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
printV(
'Moralis Solana API returned status: '
'${response.statusCode}',
);
return [];
}
final decodedResponse = jsonDecode(response.body) as List;
final List<MoralisSolanaTokenBalance> tokens = [];
for (final item in decodedResponse) {
final tokenData = item as Map<String, dynamic>;
final amountStr = tokenData['amount'] as String? ?? '0';
final amount = double.tryParse(amountStr) ?? 0.0;
if (amount <= 0) continue;
final mint = tokenData['mint'] as String? ?? '';
if (mint.isEmpty) continue;
final amountRaw = tokenData['amountRaw'] as String? ?? '0';
final decimals = tokenData['decimals'] as int? ?? 0;
final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? '';
tokens.add(
MoralisSolanaTokenBalance(
mint: mint,
amount: amount,
amountRaw: amountRaw,
decimals: decimals,
associatedTokenAddress: associatedTokenAddress,
),
);
}
return tokens;
} catch (e) {
printV('Error fetching wallet tokens from Moralis: ${e.toString()}');
return [];
}
}
}
class MoralisSolanaTokenBalance {
final String mint;
final double amount;
final String amountRaw;
final int decimals;
final String associatedTokenAddress;
const MoralisSolanaTokenBalance({
required this.mint,
required this.amount,
required this.amountRaw,
required this.decimals,
required this.associatedTokenAddress,
});
}