mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
* 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>
1947 lines
62 KiB
Dart
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,
|
|
});
|
|
}
|