diff --git a/.github/actions/validate-build/action.yml b/.github/actions/validate-build/action.yml index 7618c39617..733543f477 100644 --- a/.github/actions/validate-build/action.yml +++ b/.github/actions/validate-build/action.yml @@ -9,18 +9,21 @@ runs: run: | SDK_KDF_ASSETS_DIR=build/web/assets/packages/komodo_defi_framework - # Check that the web build folder contains a wasm file in the format build/web/kdf/*.wasm - if [ ! -f build/web/kdf/kdf/bin/*.wasm ]; then + # Check that the web build folder contains a wasm file in the format build/web/kdf/kdf/bin/*.wasm + shopt -s nullglob + wasm_files=(build/web/kdf/kdf/bin/*.wasm) + if [ ${#wasm_files[@]} -eq 0 ]; then echo "Error: Web build failed. No wasm file found in build/web/kdf/kdf/bin" # List files for debugging - echo "Listing files in build/web recursively" - ls -R build/web + echo "Listing files in build/web/kdf recursively" + ls -R build/web/kdf 2>/dev/null || echo "build/web/kdf directory does not exist" - echo "Listing files in web recursively" - ls -R web + echo "Listing files in web/kdf recursively" + ls -R web/kdf 2>/dev/null || echo "web/kdf directory does not exist" exit 1 fi + echo "Found wasm files: ${wasm_files[@]}" # Check that the index.html is present and that it is equal to the source index.html if ! cmp -s web/index.html build/web/index.html; then diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 7cf76d4bcb..3084962c59 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -48,7 +48,7 @@ jobs: with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - bundle-id: "com.komodo.komodowallet" + bundle-id: "com.komodo.wallet" profile-type: "MAC_APP_DEVELOPMENT" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_KEY_ID }} @@ -73,6 +73,10 @@ jobs: - name: Fetch packages, generate assets, and build for ${{ matrix.platform }} env: GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Disable code signing for macOS PR builds + CODE_SIGNING_ALLOWED: ${{ matrix.platform == 'macos' && github.event_name == 'pull_request' && 'NO' || '' }} + CODE_SIGNING_REQUIRED: ${{ matrix.platform == 'macos' && github.event_name == 'pull_request' && 'NO' || '' }} + EXPANDED_CODE_SIGN_IDENTITY: ${{ matrix.platform == 'macos' && github.event_name == 'pull_request' && '-' || '' }} uses: ./.github/actions/generate-assets with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d40a175d03..f8f9a9994d 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,7 @@ venv/ /macos/build AGENTS_1.md # .dmg Release -dist/ \ No newline at end of file +dist/ + +# KDF generated binaries +web/kdf/kdf/bin/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b8314a006..3c59c5ab33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,103 +1,225 @@ # Komodo Wallet v0.9.3 Release Notes -This release sharpens sign-in convenience, analytics coverage, and operational resilience ahead of the 0.9.3 cut. Highlights include one-click remember-me login, a dual analytics pipeline with Matomo support, a hardened feedback workflow, and improved compliance guardrails across platforms. +This release delivers significant performance improvements, enhanced analytics capabilities, and a comprehensive overhaul of authentication and wallet management. Key highlights include real-time portfolio streaming, a dual analytics pipeline with persistent queueing, one-click sign-in, Z-HTLC support, and extensive optimisations that reduce RPC usage while improving responsiveness across all platforms. ## 🚀 New Features -- **One-Click Remember Me Sign-In** ([@CharlVS], #3041) - Adds a quick login toggle that remembers hashed wallet metadata, auto-detects password manager autofill, and lets you resume the last wallet in one tap across desktop and mobile. -- **Feedback Portal Overhaul** ([@CharlVS], #3017) - Rebuilds the in-app feedback flow with provider plugins, optional contact opt-out, log attachments capped under 10 MB, and screenshot scrubbing for sensitive dialogs. -- **Dual Analytics Pipeline** ([@CharlVS], #2932) - Runs Firebase and Matomo providers side by side with persistent event queues, CI-aware disables, and configurable Matomo dimensions. -- **Wallet Import Renaming & Validation** ([@CharlVS], #2792) - Validates wallet names on creation or import and lets you rename imports before they enter the manager. -- **Version Insight Panel** ([@takenagain], #3109) - Adds a bloc-driven settings panel that surfaces app, mm2, and coin-config commits with periodic polling. -- **ZHTLC (Zcash HTLC) Coin Support** [SDK] ([@takenagain], [SDK#227](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/227)) - Full integration of ZHTLC coins with enhanced activation strategy, task-based RPCs, Zcash params downloader, orderbook v2 support, and activation progress estimation. -- **Custom Token Support in Coin Config** [SDK] ([@takenagain], [SDK#225](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/225)) - Adds custom token storage and management within the coin config system with Hive-based persistence and recovery mechanisms. -- **CoinPaprika Market Data Provider** [SDK] ([@takenagain], [SDK#215](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/215)) - Integrates CoinPaprika API as a fallback provider for CEX market data, improving data availability and resilience. -- **External SDK Logging Support** [SDK] ([@CharlVS], [SDK#222](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/222)) - Enables passing external logger functions to the SDK for custom logging with managed subscriptions and improved callback error handling. -- **Trading-Related RPCs** [SDK] ([@CharlVS], [SDK#191](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/191)) - Implements comprehensive trading, orderbook, and Lightning RPCs with type-safe interfaces and SDK-level swap manager functionality. -- **Flutter Web WASM Support** [SDK] ([@CharlVS], [SDK#176](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/176)) - Comprehensive WASM support with OPFS interop extensions and unified cross-compatible storage implementation. - -## 🎨 UI & UX - -- **Asset List Loading Guards** ([@takenagain], #3134) - Hides portfolio rows until fiat pricing lands and shows placeholders instead of flickering zeroes. -- **Token Parent Labeling** ([@smk762], #2988) - Marks parent chains as native, adopts SDK display names, and keeps network suffixes visible across wallet views. -- **DEX Address Pill Consistency** ([@smk762], #2974) - Aligns the address pill style between list and detail flows for clearer swap confirmations. -- **Coin Detail Fiat Per Address** ([@takenagain], #3049) - Restores fiat balance context for individual addresses on the coin detail screen. -- **Memo Field Contextualization** ([@smk762], #2998) - Only renders memo inputs for Tendermint and ZHTLC assets so other withdrawals stay clutter-free. -- **Parent Display Name Suffix** [SDK] ([@CharlVS], [SDK#213](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/213)) - Adds token standard suffix support via `CoinSubClass.tokenStandardSuffix` for clearer parent chain identification. +- **Realtime Portfolio Streaming** ([@CharlVS], #3253) - Live balance updates throughout the app via `CoinsBloc` streaming, eliminating the need for manual refreshes +- **One-Click "Remember Me" Sign-In** ([@CharlVS], #3041) - Securely cache wallet metadata for instant access with improved post-login routing +- **Dual Analytics Pipeline** ([@CharlVS], #2932) - Firebase and Matomo integration with persistent event queueing, CI toggles, and comprehensive event tracking +- **Z-HTLC Support** ([@Francois], #3158) - Full support for privacy-preserving Hash Time Locked Contracts with configurable activation toggles and optional sync parameters +- **Enhanced Feedback System** ([@CharlVS], #3017) - Comprehensive feedback portal overhaul with provider plugins, opt-out contact handling, screenshot scrubbing, and analytics integration +- **Geo-blocking Bouncer** ([@CharlVS], #3150) - Privacy coin restrictions with regulated build overrides for compliance +- **Transaction Broadcast Details** ([@smk762], #3308) - View transaction details immediately after broadcasting withdrawals +- **Market Maker Mobile Improvements** ([@Francois], #3220) - Status indicators and start/stop controls now available in mobile view +- **Swap Data Export** ([@Kadan], #3220) - Copy and export swap data for reference and debugging +- **Tendermint Faucet Support** ([@Francois], #3206) - Request test coins for Tendermint-based assets with activation guardrails +- **Optional Verbose Logging** ([@Kadan], #3332) - Configurable logging levels for development and troubleshooting +- **SDK Log Integration** ([@CharlVS], #3159) - SDK logs now route through the app logger for unified log management -## 🔒 Security & Compliance +### SDK Updates (komodo-defi-sdk-flutter) + +This release integrates [komodo-defi-sdk v1.0.0-pre.1](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter) with 82 commits bringing substantial improvements: + +- **Flutter Web WASM Support** ([SDK#176](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/176)) - WASM support with OPFS integration and unified storage +- **Enhanced RPC Coverage** ([SDK#179](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/179), [#188](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/188), [#191](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/191)) - Trading, orderbook, and Lightning RPC support +- **Streaming Infrastructure** ([SDK#178](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/178), [#232](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/232), [#269](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/269)) - Pubkey and balance watch streams with comprehensive caching +- **Multi-Provider Market Data** ([SDK#145](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/145), [#224](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/224)) - CoinPaprika fallback and refined Binance quotes +- **Custom Token Storage** ([SDK#225](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/225), [#190](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/190)) - Runtime coin updates integration +- **Platform Support** ([SDK#237](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/237), [#247](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/247)) - macOS universal binary and packaging updates + +See the [full SDK changelog](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/releases) for complete details. + +## 🎨 UI/UX Improvements + +- **Fiat Value Display** ([@Francois], #3049) - Coin detail pages now show fiat balance for individual addresses +- **Withdraw Form Enhancements** ([@Francois], #3274) - Vertical responsive layout, fiat value previews for amount and fee, and alignment improvements +- **Loading State Placeholders** ([@Francois], #3134) - Hide asset lists and show placeholders until fiat prices are available for better UX +- **Transaction History Ordering** ([@CharlVS], #9900372) - Unconfirmed transactions now appear first in the list +- **Token Parent Labelling** ([@dragonhound], #2988) - Parent coins now tagged as "native" for clearer asset hierarchy +- **Trezor Visibility Toggles** ([@smk762], #3214) - Password and PIN visibility controls for Trezor authentication +- **Market Maker Value Display** ([@smk762], #3215) - Fixed bot maker order values display +- **Activation Filter Compatibility** ([@smk762], #3249) - Only show compatible activation filter options to prevent errors +- **Buy Coin List Sorting** ([@smk762], #3328) - Market maker buy coin list now sorted with price filters and "add assets" footer +- **Keyboard Dismissal** ([@Francois], #3225) - Dismiss keyboard on scroll for fiat and swap inputs +- **Mobile Seed Backup Banner** ([@Francois], #3225) - Seed backup banner now visible in mobile view +- **Post-Login Navigation** ([@smk762], #3262) - Consistent routing to wallet page after login or logout with delayed navigation for Trezor PIN/passphrase entry +- **Custom Seed Toggle** ([@smk762], #3260) - Hide custom seed toggle unless BIP39 validation fails +- **NFT Withdraw QR Scanning** ([@smk762], #3243) - QR code scan button added to NFT withdrawal address input +- **Consistent Pill Styling** ([@dragonhound], #2974) - Applied uniform "swap address" pill style throughout the app +- **Thoughtful Scrollbar Disposal** ([@dragonhound], #3008) - Improved scrollbar lifecycle management -- **Geo Blocker Bouncer Integration** ([@CharlVS], #3150) - Streams the new trading-status API, filters blocked assets during wallet bootstrap, and exposes an override for regulated builds. -- **Password Policy Hardening** ([@CharlVS], #3149/#3141) - Expands password limits to 128 characters across forms and makes validation consistent between Flutter and SDK layers. -- **Weak Password Flag Fix** ([@smk762], #3101) - Ensures the configuration flag actually respects weak-password bypass scenarios. -- **Pubkey Hygiene on Logout** ([@CharlVS], #3144) - Clears cached pubkeys when switching wallets or signing out to prevent stale address reuse. -- **Bip39 Compatibility Storage Fix** [SDK] ([@takenagain], [SDK#216](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/216)) - Ensures bip39 compatibility is stored regardless of wallet type, fixing HD wallet seed validation errors. -- **Trezor PIN/Passphrase Security** [SDK] ([@takenagain], [SDK#126](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/126)) - Mitigates potential exposure of Trezor PIN/passphrase by adding custom converters to prevent sensitive data in logs. -- **HD Wallet Message Signing** [SDK] ([@CharlVS], [SDK#198](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/198)) - Adds support for message signing with HD wallets including derivation path specification. -- **Trezor Connection Polling** [SDK] ([@takenagain], [SDK#126](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/126)) - Automatic polling of Trezor device connection status with auto sign-out when disconnected, preventing stale sessions. - -## ⚡ Performance & Reliability - -- **Fiat Onramp Debounce** ([@takenagain], #3125) - Debounces fiat amount edits so API calls only fire after user pauses typing. -- **Custom Token Activation Guardrails** ([@takenagain], #3129) - Waits for token propagation, limits retry loops, and deactivates test imports if the dialog closes without confirmation. -- **Legacy Wallet Import Stability** ([@takenagain], #3126) - Re-applies migrated coin lists after legacy imports and filters unsupported assets before activation. -- **SDK Subscription Lifecycle Management** [SDK] ([@takenagain], [SDK#232](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/232)) - Closes balance and pubkey subscriptions on auth state changes to prevent memory leaks and resource waste. -- **Market Metrics Log Reduction** [SDK] ([@takenagain], [SDK#223](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/223)) - Reduces market metrics log verbosity and removes duplicate logging for better performance. -- **Binance Per-Coin Currency Lists** [SDK] ([@takenagain], [SDK#224](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/224)) - Uses per-coin supported quote currency lists with USD stablecoin fallback mappings. -- **Multi-Provider Market Data System** [SDK] ([@takenagain], [SDK#145](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/145)) - Adds support for multiple market data providers with fallback logic and retry mechanisms. -- **Pubkey Watch Stream** [SDK] ([@takenagain], [SDK#178](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/178)) - Adds reactive public key monitoring with automatic auth state handling. +## ⚡ Performance Enhancements + +- **RPC Spam Reduction** ([@CharlVS], #3253) - Comprehensive SDK-side caching and streaming support drastically reduces redundant RPC calls +- **Fiat On-Ramp Debouncing** ([@Francois], #3125) - Reduced API calls on user input changes for smoother fiat amount entry +- **Balance Watch Streams** (SDK, #178) - Realtime balance updates from SDK eliminate polling +- **Pubkey Caching** ([@CharlVS], #3251) - Prefer cached pubkeys before RPC across the app with post-swap fetch delays +- **Best Orders Optimization** ([@smk762], #3328) - Avoid best_orders calls unless on DEX/bridge; fail gracefully and retry +- **Activation Patience** ([@smk762], #3272) - Await initial activations and avoid duplicated activation tasks with proper parent/child coin sync ## 🐛 Bug Fixes -- **Unauthenticated Coin State Fixes** ([@CharlVS], #3138) - Repairs coin list and sparkline loading when viewing portfolios before logging in. -- **DEX Precision Regression** ([@CharlVS], #3123) - Eliminates precision loss in taker forms and adds tests for large rational conversions. -- **macOS File Picker Entitlements** ([@CharlVS], #3111) - Restores the native file picker by adding the required read-only entitlement and window focus handling. -- **NFT IPFS Fallbacks** ([@takenagain], #3020) - Introduces an IPFS gateway manager with retries, cooldowns, and unit tests so NFT media loads consistently. -- **SDK Disposal Crash** ([@DeckerSU], #3117) - Avoids crashes when mm2 shuts down mid-fetch by guarding the periodic fetch loop. -- **Price/Version Reporting** ([@DeckerSU], #3115) - Ensures the settings screen shows the actual mm2 commit hash instead of a closure string. -- **Custom Token Import & Refresh** [SDK] ([@takenagain], [SDK#220](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/220)) - Fixes custom token refresh, lowercase icon identifiers, and parent-child relationships. -- **ZHTLC Activation Resilience** [SDK] ([@takenagain], [SDK#227](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/227)) - Multiple fixes for ZHTLC activation including config transformation and orderbook v2 alignment. -- **Coingecko OHLC Parsing** [SDK] ([@takenagain], [SDK#203](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/203)) - Fixes OHLC data parsing differences between Coingecko and Binance formats. -- **External Logger Memory Leak** [SDK] ([@CharlVS], [SDK#222](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/222)) - Resolves memory leak from native callback by properly disposing \_kdfOperations. -- **BIP39 Validation for Legacy Wallets** [SDK] ([@takenagain], [SDK#216](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/216)) - Simplifies HD bip39 verification and allows custom seeds for legacy wallets. -- **Runtime Coin Updates Asset Ordering** [SDK] ([@takenagain], [SDK#217](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/217)) - Fixes Linux segfault by sorting assets before returning. -- **WASM JS Call Error Handling** [SDK] ([@takenagain], [SDK#185](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/185)) - Improves JS call error handling on startup for WASM operations. -- **Market Data Price Fetching** [SDK] ([@takenagain], [SDK#167](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/167)) - Prioritizes KomodoPriceRepository over CexRepository for current prices. - -## 🛠️ Build & Developer Experience - -- **Flutter 3.35.1 & SDK Roll** ([@CharlVS], #3108) - Upgrades Flutter, aligns SDK dependencies, and refreshes package overrides for the new mono-repo workspace. -- **SDK Git Submodule Adoption** ([@takenagain], #3110) - Vendors komodo-defi-sdk as a submodule with workspace overrides and adds tooling for deterministic rolls. -- **Matomo Build Validation** ([@DeckerSU], #3165) - Validates Matomo tracking parameters during CI builds to prevent misconfigured releases. -- **Build Script Environment Sanitization** ([@DeckerSU], #3037/#3055/#3058) - Normalises docker env defines, forces web builds off CDN resources, and removes stray .dgph artifacts from iOS outputs. -- **Linux Build Workflow Check** ([@DeckerSU], #3106) - Adds a GitHub workflow that exercises the Linux build script after SDK rolls. -- **Devcontainer Modernisation** ([@CharlVS], #3114) - Switches the devcontainer to lightweight .docker images and consolidates post-create provisioning. -- **SDK Dart Pub Workspaces Migration** [SDK] ([@CharlVS], [SDK#204](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/204)) - Migrates SDK to Dart Pub workspaces, bumps Dart SDK to >=3.9.0, unifies Flutter constraints (>=3.35.0 <3.36.0). -- **SDK Coin Updates Integration** [SDK] ([@takenagain], [SDK#190](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/190)) - Integrates komodo_coin_updates into komodo_coins with Hive CE migration and runtime update capabilities. -- **KDF API Branch Update** [SDK] ([@CharlVS], sdk/72c9de3) - Updates API branch to hotfix-remove-memorydb-size-metric with refreshed checksums for all platform binaries. -- **SDK Package Publishing** [SDK] ([@CharlVS], [SDK#204](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/204)) - Prepares SDK packages for pub.dev with updated dependencies, licenses, and repository links. -- **GitHub Token Authentication** [SDK] ([@CharlVS], [SDK#176](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/176)) - Adds GITHUB_API_PUBLIC_READONLY_TOKEN support to prevent API rate limiting (60→5,000 requests/hour). +- **Transaction History Cross-Asset Bleed** ([@CharlVS], #3289) - Isolated `TransactionHistoryBloc` per-coin to prevent history mixing +- **Balance Update State Preservation** ([@CharlVS], #3253) - Realtime balance updates now preserve coin activation state to avoid turning off the Send button +- **Transaction Sorting** ([@CharlVS], #3253) - Fixed transaction history list sorting logic +- **Dropdown Null Safety** ([@Cursor Agent], #3050) - Fixed null safety issues in `UiDropdown` widget, preventing app freeze on logout +- **Legacy Wallet Migration** ([@CharlVS], #3207) - Preserve legacy flag, sanitise wallet names, ensure uniqueness, and avoid duplicate imports during migration +- **Wallet Coin Restoration** ([@Francois], #3126) - Restore wallet coins for legacy wallet migrations and seed file imports +- **Password Length Validation** ([@CharlVS], #3141, #3149) - Consistent 128-character password handling across all flows with hardened validation +- **Custom Token Import** ([@Francois], #3129) - Check platform in deduplication and correctly update fields; refresh asset list on import +- **Precision Loss in Wallet** ([@CharlVS], #3123) - Resolved DEX precision regression with comprehensive tests +- **Withdraw Form Fixes** ([@Francois], #3274) - Fixed fiat alignment, max value detection, and use signed hex from preview for broadcast +- **ZHTLC Activation Toggle** ([@smk762], #3283) - Revert toggle on ZHTLC activation config cancel +- **Coin Variant Sum** ([@smk762], #3317) - Fixed coin variant sum display in dropdowns +- **Decimals Precision** ([@Kadan], #3297) - Added unit tests and fixed decimal handling with proper fiat amount input refactoring +- **Trading Bot Improvements** ([@Francois], #3223, #3328) - Remove price URL parameter to default to KDF URL list; add guard against swap button spamming; use `max_maker_vol` for spendable balance +- **Market Maker Dropdown** ([@Francois], #3187) - Fixed sell coin dropdown reverting to previous coin with occasional flickering +- **ARRR Reactivation** ([@Francois], #3184) - Fixed ARRR not reappearing in coins list after deactivation and reactivation +- **Pubkey Clearing** ([@CharlVS], #3144) - Clear pubkeys on wallet change or logout to prevent cross-wallet contamination +- **Unban Pubkeys Null Check** ([@smk762], #3276) - Avoid null check error on unban_pubkey button press +- **Timer Leaks** ([@Kadan], #3305) - Fixed timer leaks preventing proper cleanup +- **SSE Lifecycle** ([@Kadan], #3313, #3318) - Tie SSE to the auth state lifecycle and remove the SSE package for better stability +- **iOS/macOS KDF Reinitialization** ([@Kadan], #3286) - Proper KDF health check and reinitialisation on iOS/macOS +- **Withdrawal Form** ([@Kadan], #3288) - Fixed withdrawal regression +- **KDF Disposal Crash** ([@DeckerSU], #3117) - Fixed crash when `KomodoDefiSdk` is disposed during periodic fetch +- **Fiat On-Ramp CSP** ([@Francois], #3225) - Disable overly restrictive CSP with limited platform support; add Komodo and sandbox domains to allowlist +- **NFT IPFS Loading** ([@Francois], #3020) - Add IPFS gateway resolution, retry, and fallback to improve NFT image loading +- **macOS File Picker** ([@CharlVS], #3111) - Show file picker by adding user-selected read-only entitlement +- **Settings Version Isolation** ([@smk762], #3324) - Isolate version settings in shared_preferences.json for backwards compatibility +- **Unconfirmed Transaction Detection** ([@Francois], #3328) - Only consider empty timestamps and confirmations as unconfirmed + +## 🔒 Security & Compliance + +- **128-Character Password Support** ([@CharlVS], #3141, #3149) - Increased password length limit to 128 characters with consistent validation across all auth flows +- **Pubkey Hygiene** ([@CharlVS], #3144, #3251) - Purge cached pubkeys on wallet change, prefer cached pubkeys before RPC, and add post-swap delays +- **Geo-blocking Bouncer** ([@CharlVS], #3150) - Privacy coin restrictions with FD monitoring and regulated build overrides +- **Legacy Wallet Sanitisation** ([@CharlVS], #3207) - Sanitise names, preserve flags, and prevent duplicate imports during legacy wallet migration + +## 💻 Platform-Specific Changes + +### All Platforms + +- **Flutter 3.35.1 Upgrade** ([@CharlVS], #3108) - Updated Flutter SDK with dependency roll and improved roll script +- **SDK Submodule Integration** ([@Francois], #3110) - SDK adopted as a git submodule with path overrides and deterministic roll script + +### macOS + +- **Production Scheme & Signing** ([@DeckerSU], #3185) - Added macOS production scheme and Developer ID Application signing support for standalone distribution +- **Universal Binary Support** (SDK, #237) - macOS universal binary (Intel + Apple Silicon) support +- **Development Team Update** ([@DeckerSU], #3177, [@DeckerSU] SDK #239) - Changed development team to production identifier (WDS9WYN969→8HPBYKKKQP) +- **KDF Binary Placement** (SDK, #247) - Streamlined KDF binary placement and updated signing flow + +### Linux + +- **GLib Compatibility** ([@DeckerSU], #3105) - Guard `G_APPLICATION_DEFAULT_FLAGS` behind GLib ≥ 2.74 with fallback for older versions +- **Single-Instance Enforcement** ([@CharlVS], #3063) - Enforce single-instance; focus existing window; prevent zombie processes +- **Build Script Validation** ([@DeckerSU], #3106) - Added GitHub Actions workflow to validate Linux build script + +### Windows + +- **Single-Instance Enforcement** ([@CharlVS], #3063) - Prevent multiple instances and zombie processes + +### iOS + +- **FD Monitoring** ([@Kadan], #3259) - File descriptor monitoring for release mode +- **Health Check Integration** ([@Kadan], #3257) - Added KDF health check with reinitialization support +- **Xcode Configuration** ([@DeckerSU], #3324) - Added FdMonitor.swift to Xcode project configuration and updated DEVELOPMENT_TEAM identifier +- **Build Artifact Cleanup** ([@DeckerSU], #3058) - Removed .dgph build artifacts from iOS project +- **Ruby Installation Guide** ([@Francois], #3128) - Added Ruby installation step for iOS builds + +### Web + +- **WASM Support** (SDK, #176) - Flutter Web WASM support with OPFS integration and unified storage implementation +- **CDN Disable** ([@DeckerSU], #3055) - Add `--no-web-resources-cdn` to web build in build.sh + +### Docker & DevOps + +- **DevContainer Modernization** ([@CharlVS], #3114) - Switched to .docker images and Linux-only devcontainer +- **Environment Variable Passing** ([@DeckerSU], #3037) - Correct env vars passing to Docker and Dart via --dart-define +- **Matomo Validation** ([@DeckerSU], #3165) - Added Matomo tracking params validation in build script +- **CI Improvements** ([@CharlVS], #3167, #3336) - Fix missing secrets, remove duplicate steps; fix CI issues +- **Artifact Naming** ([@dragonhound], #3181) - Append short commit hash to upload-artifact filenames +- **Submodule Updates** ([@DeckerSU], #3139) - Clean submodules before update to fix build errors + +## 🔧 Technical Improvements + +### SDK Integration (komodo-defi-sdk v1.0.0-pre.1) + +- **RPC Coverage Expansion** ([SDK#179](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/179), [#188](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/188), [#191](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/191)) - Implemented missing RPCs, including trading-related endpoints and Lightning support +- **Message Signing** ([SDK#198](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/198), [#231](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/231)) - HD wallet support for message signing with derivation path; added AddressPath type and refactored to use Asset/PubkeyInfo +- **Multi-Provider Market Data** ([SDK#145](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/145), [#215](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/215)) - Support for multiple market data providers with CoinPaprika fallback option +- **Custom Token Integration** ([SDK#225](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/225), [#190](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/190)) - Custom token support in coin config manager; integrate komodo_coin_updates into komodo_coins +- **Balance & Pubkey Streaming** ([SDK#178](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/178), [#232](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/232), [#262](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/262), [#269](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/269)) - Add pubkey watch function similar to balance watch; comprehensive caching and streaming support +- **ETH-BASE Support** ([SDK#254](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/254)) - Add support for ETH-BASE and derived assets +- **Asset Tagging Fixes** ([SDK#244](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/244)) - Correct UTXO coins incorrectly tagged as Smart Chain +- **ZHTLC Fixes** ([SDK#227](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/227), [#264](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/264)) - ZHTLC activation fixes with optional sync params and sign-out cleanup +- **Binance Quote Fixes** ([SDK#224](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/224)) - Use per-coin supported quote currency list instead of global cache +- **Market Metrics Logging** ([SDK#223](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/223)) - Reduce market metrics log verbosity and duplication +- **Etherscan URLs** ([SDK#217](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/217)) - Fix Etherscan URL formatting +- **Sparkline Configuration** ([SDK#248](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/248)) - Add configurable sparkline baseline +- **Dragon Charts Migration** ([SDK#164](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/164)) - Migrate dragon_charts_flutter to monorepo packages +- **Trezor Polling** ([SDK#126](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/126)) - Poll Trezor connection status and sign out when disconnected +- **KDF Version Updates** ([SDK#218](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/218), [#237](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/237), [#249](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/249), [#241](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/241), [#247](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/247)) - Roll KDF to latest releases with checksum updates +- **Runtime Fetch Libraries** ([SDK#280](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/280)) - Use runtime fetch libraries with updated checksums + +### Analytics & Monitoring + +- **Persistent Event Queue** ([@CharlVS], #2932) - Analytics events persist across sessions with a standardised event structure +- **CI Analytics Toggles** ([@CharlVS], #2932, #3165) - Disable analytics in CI builds with Matomo validation +- **NFT Analytics Integration** ([@dragonhound], #3202) - Use AnalyticsRepo to enqueue NFT analytics events +- **Updated Events** ([@CharlVS], #3194) - Update completed events and remove scroll attempt tracking +- **Settings Logging** ([@Francois], #3324) - Add logging and avoid silent skipping in settings + +### Developer Experience + +- **PR Body Template** ([@CharlVS], #3207) - Add PR_BODY.md helper file for CLI editing +- **SDK Submodule Management** ([@Francois], #3110) - Deterministic SDK roll script with path overrides +- **API Commit Hash Display** ([@DeckerSU], #3115) - Fix logging of apiCommitHash to output actual value instead of closure +- **Dependency Documentation** ([@Francois], #3128) - Ruby installation guide for iOS/macOS builds +- **Optional Verbose Logging** ([@Kadan], #3332, SDK #278) - Configurable logging levels for debugging + +### Code Quality + +- **Null Safety Improvements** ([@Cursor Agent], #3050) - Fixed null safety issues in UiDropdown widget +- **Type Safety** ([@Kadan], #3279, #3280) - Bound checking, non-nullable type tweaks, explicit enum mapping, defensive array access guards, cast num to int +- **Error Propagation** ([@smk762], #3328) - Propagate best_orders failures, avoid masking as no liquidity +- **Unused Code Cleanup** ([@Francois], #3225) - Remove unused widgets and update enum docs +- **Code Formatting** ([@CharlVS], #3251) - Run dart format on pubkey cache call-sites and taker delay +- **Logging Improvements** ([@Francois], #3328) - Add logging for errors not propagated to UI layer ## 📚 Documentation -- **Matomo Analytics Guide** ([@CharlVS], #2932) - Documents how to enable Matomo alongside Firebase, including CI toggles and queue behaviour. -- **SDK Submodule Management** ([@takenagain], #3110) - Provides end-to-end instructions for updating, hotfixing, and testing the vendored SDK. -- **macOS/iOS Build Prerequisites** ([@takenagain], #3128) - Expands setup docs with Ruby installation steps and refreshed platform notes. -- **SDK Package Documentation Overhaul** [SDK] ([@CharlVS], [SDK#201](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/201)) - Major documentation improvements across all SDK packages with comprehensive guides and API references. -- **ZHTLC Technical Documentation** [SDK] ([@takenagain], [SDK#227](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/227)) - Adds activation refactoring plan, Firebase deployment setup, and tech debt report for ZHTLC implementations. +- **SDK Changelog Cross-Linking** ([@CharlVS], #3172) - Link SDK PRs with short labels and mark SDK items in wallet changelog +- **Ruby Installation Guide** ([@Francois], #3128) - Added Ruby installation step for iOS and macOS builds +- **SDK Documentation** ([SDK#201](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/pull/201)) - Document project and packages for pub.dev release -## ⚠️ Breaking Changes +## ⚠️ Known Issues -- **SDK Dart/Flutter Version Requirements** [SDK] - Minimum Dart SDK version is now `^3.9.0` and Flutter version constraint is `>=3.35.0 <3.36.0` -- **ZHTLC Activation Parameters** [SDK] - Breaking changes to activation params serialization and structure for ZHTLC and ERC20 coins -- **SDK Workspace Migration** [SDK] - Legacy pubspec_overrides.yaml files removed in favor of Dart Pub workspace resolution +- Full automated test suite remains triaged; manual QA recommended per `docs/MANUAL_TESTING_DEBUGGING.md` +- Some analytics events may require additional validation on specific platforms +- Large portfolios (>100 assets) may experience slower initial loading times during first activation -## 📦 SDK Changes +## 🙏 Contributors -**SDK Version**: Updated from `68429b23dac43eddd53434dda1bd23296523f27d` to `72c9de3b370f1b4169ebbb3150e8adedf4ed3b23` +This release includes contributions from 11 developers: + +- @CharlVS +- @takenagain +- @ca333 +- @smk762 +- @DeckerSU +- @gcharang +- @TazzyMeister +- @naezith +- Cursor Agent (automated refactoring) **Full Changelog**: [0.9.2...0.9.3](https://github.com/KomodoPlatform/komodo-wallet/compare/0.9.2...0.9.3) +--- + +_For developers building with Komodo DeFi SDK: This release includes [komodo-defi-sdk v1.0.0-pre.1](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/releases) with breaking changes related to streaming APIs and caching behaviour. Review the [SDK changelog](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/blob/main/CHANGELOG.md) for migration guidance._ + # Komodo Wallet v0.9.2 Release Notes This release brings numerous improvements to wallet functionality, enhanced user experience, and critical bug fixes. Key highlights include HD wallet private key export, improved Trezor support, enhanced UI/UX throughout the application, and platform-specific optimizations. diff --git a/assets/translations/en.json b/assets/translations/en.json index 2237c7d7d1..60ab6a2a91 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -507,6 +507,8 @@ "orderBookNoAsks": "No asks found", "orderBookNoBids": "No bids found", "orderBookEmpty": "Orderbook is empty", + "dexNoSwapOffers": "No swap offers available for the selected asset.", + "bridgeNoCrossNetworkRoutes": "No cross-network routes found for this asset.", "freshAddress": "Fresh address", "userActionRequired": "User action required", "unknown": "Unknown", @@ -748,6 +750,7 @@ "trend7d": "7d trend", "tradingDisabledTooltip": "Trading features are currently disabled", "tradingDisabled": "Trading unavailable in your location", + "nftDisabledTooltip": "NFT functionality is currently disabled. Improvements are on the way!", "includeBlockedAssets": "Include blocked assets", "unbanPubkeysResults": "Unban Pubkeys Results", "unbannedPubkeys": { @@ -766,6 +769,7 @@ "noBannedPubkeys": "No banned pubkeys found", "unbanPubkeysFailed": "Failed to unban pubkeys", "privateKeyRetrievalFailed": "Failed to retrieve private keys. Please try again.", + "privateKeysEmptyError": "No private keys found. Assets may need to be activated. Please try again later.", "fetchingPrivateKeysTitle": "Fetching Private Keys...", "fetchingPrivateKeysMessage": "Please wait while we securely fetch your private keys...", "pubkeyType": "Type", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 700b62cf9b..a195372a76 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 49484A33FCF0585DB40EBAD9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C74478EE63B90E2A48A7AB3C /* GoogleService-Info.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + F1D1A3B2C3D4E5F6A7B8C9D1 /* FdMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D1A3B2C3D4E5F6A7B8C9D0 /* FdMonitor.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -45,6 +46,7 @@ 6DB340A008F6FECB3B82619D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F1D1A3B2C3D4E5F6A7B8C9D0 /* FdMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FdMonitor.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 91B045D447C7C6266906543C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -122,6 +124,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + F1D1A3B2C3D4E5F6A7B8C9D0 /* FdMonitor.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -303,6 +306,7 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + F1D1A3B2C3D4E5F6A7B8C9D1 /* FdMonitor.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -387,7 +391,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = G3VBBBMD8T; + DEVELOPMENT_TEAM = 8HPBYKKKQP; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( @@ -530,7 +534,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = G3VBBBMD8T; + DEVELOPMENT_TEAM = 8HPBYKKKQP; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( @@ -565,7 +569,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = G3VBBBMD8T; + DEVELOPMENT_TEAM = 8HPBYKKKQP; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2f37ec4f57..e09082eda8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -31,6 +31,8 @@ NSCameraUsageDescription This app needs camera access to scan QR codes + NSPhotoLibraryUsageDescription + This app needs access to your photo library to import QR codes UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 08d98de146..bfafab3fb2 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -44,6 +44,11 @@ const Duration kPerformanceLogInterval = Duration(minutes: 1); /// - Balance and price update polling const bool kDebugElectrumLogs = true; +/// Temporary failure simulation toggles for testing UI/flows. +/// Guarded by kDebugMode in calling sites. +const bool kSimulateBestOrdersFailure = false; +const double kSimulatedBestOrdersFailureRate = 0.5; // 50% + // This information is here because it is not contextual and is branded. // Names of their own are not localized. Also, the application is initialized before // the localization package is initialized. diff --git a/lib/bloc/bridge_form/bridge_bloc.dart b/lib/bloc/bridge_form/bridge_bloc.dart index 894dcab392..354dee5825 100644 --- a/lib/bloc/bridge_form/bridge_bloc.dart +++ b/lib/bloc/bridge_form/bridge_bloc.dart @@ -238,6 +238,17 @@ class BridgeBloc extends Bloc { ), ); + // Before login, show 0.00 instead of spinner + if (!_isLoggedIn) { + emit( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.unavailable, + maxSellAmount: () => null, + ), + ); + return; + } + _autoActivateCoin(event.coin.abbr); _subscribeMaxSellAmount(); @@ -392,22 +403,27 @@ class BridgeBloc extends Bloc { return; } - if (state.availableBalanceState == AvailableBalanceState.initial || - event.setLoadingStatus) { + // If not logged in, show 0.00 (unavailable) and skip spinner + if (!_isLoggedIn) { emit( state.copyWith( - availableBalanceState: () => AvailableBalanceState.loading, + availableBalanceState: () => AvailableBalanceState.unavailable, + maxSellAmount: () => null, ), ); + return; } - if (!_isLoggedIn) { + if (state.availableBalanceState == AvailableBalanceState.initial || + event.setLoadingStatus) { emit( state.copyWith( - availableBalanceState: () => AvailableBalanceState.unavailable, + availableBalanceState: () => AvailableBalanceState.loading, ), ); - } else { + } + + { Rational? maxSellAmount = await _dexRepository.getMaxTakerVolume( state.sellCoin!.abbr, ); diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart index 8721b25cf6..9b0fc9824c 100644 --- a/lib/bloc/coins_bloc/coins_bloc.dart +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -523,10 +523,18 @@ class CoinsBloc extends Bloc { .map((assetsSet) => assetsSet.single); // Filter out blocked assets - final coinsToActivate = _tradingStatusService.filterAllowedAssets( + var coinsToActivate = _tradingStatusService.filterAllowedAssets( availableAssets.toList(), ); + // During initial login auto-activation, skip ZHTLC assets that would + // trigger configuration dialogs (i.e. no saved configuration yet). + if (_isInitialActivationInProgress) { + coinsToActivate = await _filterAssetsForInitialActivation( + coinsToActivate, + ); + } + final enableFutures = coinsToActivate .map((asset) => _coinsRepo.activateAssetsSync([asset])) .toList(); @@ -536,6 +544,41 @@ class CoinsBloc extends Bloc { await Future.wait(enableFutures); } + /// Filters assets for initial auto-activation on login. + /// + /// - Keeps all non-ZHTLC assets + /// - Keeps ZHTLC assets only if a saved configuration already exists + Future> _filterAssetsForInitialActivation( + List assets, + ) async { + final filtered = []; + for (final asset in assets) { + if (asset.id.subClass != CoinSubClass.zhtlc) { + filtered.add(asset); + continue; + } + + try { + final saved = + await _kdfSdk.activationConfigService.getSavedZhtlc(asset.id); + if (saved != null) { + filtered.add(asset); + } else { + _log.info( + 'Skipping auto-activation of ZHTLC asset ${asset.id.id} during login: no saved configuration found', + ); + } + } catch (e, s) { + _log.shout( + 'Error checking saved ZHTLC configuration for ${asset.id.id}', + e, + s, + ); + } + } + return filtered; + } + CoinsState _prePopulateListWithActivatingCoins(Iterable coins) { final knownCoins = _coinsRepo.getKnownCoinsMap(); final activatingCoins = Map.fromIterable( diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index e59b5856d1..3a464025a6 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart' show Bloc, Emitter; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart' show CoinSubClass; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:logging/logging.dart'; import 'package:web_dex/analytics/events/portfolio_events.dart'; @@ -374,6 +375,21 @@ class CoinsManagerBloc extends Bloc { final selectedCoinIds = result.map((c) => c.id.id).toSet(); for (final walletCoin in walletCoins) { + // Do not pre-select ZHTLC coins without saved configuration. + // This ensures toggles remain OFF if auto-activation was bypassed. + if (walletCoin.id.subClass == CoinSubClass.zhtlc) { + try { + final saved = + await _sdk.activationConfigService.getSavedZhtlc(walletCoin.id); + if (saved == null) { + continue; + } + } catch (_) { + // On any error, be conservative and keep toggle OFF + continue; + } + } + if (!selectedCoinIds.contains(walletCoin.id.id)) { result.add(walletCoin); } diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index 8e1384c05c..afceae9f20 100644 --- a/lib/bloc/dex_repository.dart +++ b/lib/bloc/dex_repository.dart @@ -1,3 +1,5 @@ +import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; @@ -19,6 +21,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.da import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart'; import 'package:web_dex/model/data_from_service.dart'; import 'package:web_dex/model/swap.dart'; +import 'package:web_dex/router/state/routing_state.dart'; +import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/trade_preimage.dart'; import 'package:web_dex/services/mappers/trade_preimage_mappers.dart'; @@ -54,9 +58,12 @@ class DexRepository { swapMethod: swapMethod, max: max, ); - final ApiResponse> response = - await _mm2Api.getTradePreimage(request); + final ApiResponse< + TradePreimageRequest, + TradePreimageResponseResult, + Map + > + response = await _mm2Api.getTradePreimage(request); final Map? error = response.error; final TradePreimageResponseResult? result = response.result; @@ -88,8 +95,9 @@ class DexRepository { } Future getMaxTakerVolume(String coinAbbr) async { - final MaxTakerVolResponse? response = - await _mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr)); + final MaxTakerVolResponse? response = await _mm2Api.getMaxTakerVolume( + MaxTakerVolRequest(coin: coinAbbr), + ); if (response == null) { return null; } @@ -98,8 +106,9 @@ class DexRepository { } Future getMaxMakerVolume(String coinAbbr) async { - final MaxMakerVolResponse? response = - await _mm2Api.getMaxMakerVolume(MaxMakerVolRequest(coin: coinAbbr)); + final MaxMakerVolResponse? response = await _mm2Api.getMaxMakerVolume( + MaxMakerVolRequest(coin: coinAbbr), + ); if (response == null) { return null; } @@ -108,8 +117,9 @@ class DexRepository { } Future getMinTradingVolume(String coinAbbr) async { - final MinTradingVolResponse? response = - await _mm2Api.getMinTradingVol(MinTradingVolRequest(coin: coinAbbr)); + final MinTradingVolResponse? response = await _mm2Api.getMinTradingVol( + MinTradingVolRequest(coin: coinAbbr), + ); if (response == null) { return null; } @@ -122,28 +132,82 @@ class DexRepository { } Future getBestOrders(BestOrdersRequest request) async { + // Only allow best_orders when user is on Swap (DEX) or Bridge pages + final MainMenuValue current = routingState.selectedMenu; + final bool isTradingPage = + current == MainMenuValue.dex || current == MainMenuValue.bridge; + if (!isTradingPage) { + // Not an error – we intentionally suppress best_orders away from trading pages + return BestOrders(result: >{}); + } + + // Testing aid: opt-in random failure in debug mode + if (kDebugMode && + kSimulateBestOrdersFailure && + Random().nextDouble() < kSimulatedBestOrdersFailureRate) { + return BestOrders( + error: TextError(error: 'Simulated best_orders failure (debug)'), + ); + } + Map? response; try { response = await _mm2Api.getBestOrders(request); - } catch (e) { + } catch (e, s) { + log( + 'best_orders request failed: $e', + trace: s, + path: 'api => getBestOrders', + isError: true, + ).ignore(); return BestOrders(error: TextError.fromString(e.toString())); } - final isErrorResponse = - (response?['error'] as String?)?.isNotEmpty ?? false; - final hasResult = - (response?['result'] as Map?)?.isNotEmpty ?? false; + if (response == null) { + return BestOrders( + error: TextError(error: 'best_orders returned null response'), + ); + } + + final String? errorText = response['error'] as String?; + if (errorText != null && errorText.isNotEmpty) { + // Map known "no orders" network condition to empty result so UI shows a + // graceful "Nothing found" instead of an error panel. + final String? errorType = response['error_type'] as String?; + final String? errorPath = response['error_path'] as String?; + final bool isNoOrdersNetworkCondition = + errorPath == 'best_orders' && + errorType == 'P2PError' && + errorText.contains('No response from any peer'); - if (isErrorResponse) { - return BestOrders(error: TextError(error: response!['error']!)); + // Mm2Api.getBestOrders may wrap MM2 errors in an Exception() during + // retry handling, yielding text like: "Exception: No response from any peer" + // (without error_type/error_path). Treat these as "no orders" as well. + final bool isWrappedNoOrdersText = errorText.toLowerCase().contains( + 'no response from any peer', + ); + + if (isNoOrdersNetworkCondition || isWrappedNoOrdersText) { + return BestOrders(result: >{}); + } + + log( + 'best_orders returned error: $errorText', + path: 'api => getBestOrders', + isError: true, + ).ignore(); + return BestOrders(error: TextError(error: errorText)); } - if (!hasResult) { - return BestOrders(error: TextError(error: 'Orders not found!')); + final Map? result = + response['result'] as Map?; + if (result == null || result.isEmpty) { + // No error and no result → no liquidity available + return BestOrders(result: >{}); } try { - return BestOrders.fromJson(response!); + return BestOrders.fromJson(response); } catch (e, s) { log('Error parsing best_orders response: $e', trace: s, isError: true); @@ -156,8 +220,9 @@ class DexRepository { } Future getSwapStatus(String swapUuid) async { - final response = - await _mm2Api.getSwapStatus(MySwapStatusReq(uuid: swapUuid)); + final response = await _mm2Api.getSwapStatus( + MySwapStatusReq(uuid: swapUuid), + ); if (response['error'] != null) { throw TextError(error: response['error']); diff --git a/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart b/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart index b194e0a87c..9c73dcf4b2 100644 --- a/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart +++ b/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:get_it/get_it.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:logging/logging.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; @@ -44,11 +45,16 @@ class MarketMakerTradeFormBloc required CoinsRepo coinsRepo, }) : _dexRepository = dexRepo, _coinsRepo = coinsRepo, + _log = Logger('MarketMakerTradeFormBloc'), super(MarketMakerTradeFormState.initial()) { on(_onSellCoinChanged); on(_onBuyCoinChanged); on(_onTradeVolumeChanged); - on(_onSwapCoinsRequested); + // Prevent/reduce spamming by only processing one event at a time + on( + _onSwapCoinsRequested, + transformer: droppable(), + ); on(_onTradeMarginChanged); on(_onUpdateIntervalChanged); on(_onClearForm); @@ -60,8 +66,6 @@ class MarketMakerTradeFormBloc ); } - final _sdk = GetIt.I(); - /// The dex repository is used to get the trade preimage, which is used /// to pre-emptively check if a trade will be successful final DexRepository _dexRepository; @@ -70,17 +74,38 @@ class MarketMakerTradeFormBloc /// when they are selected in the trade form final CoinsRepo _coinsRepo; + final Logger _log; + + final _sdk = GetIt.I(); + Future _onSellCoinChanged( MarketMakerTradeFormSellCoinChanged event, Emitter emit, ) async { final identicalBuyAndSellCoins = state.buyCoin.value == event.sellCoin; - final sellCoin = event.sellCoin?.id; - final sellCoinBalance = sellCoin == null - ? 0 - : (await _coinsRepo.tryGetBalanceInfo(sellCoin)).spendable.toDouble(); + + // Emit immediately with new coin selection for fast UI update + emit( + state.copyWith( + sellCoin: CoinSelectInput.dirty(event.sellCoin), + buyCoin: identicalBuyAndSellCoins + ? const CoinSelectInput.dirty(null, -1) + : state.buyCoin, + status: MarketMakerTradeFormStatus.success, + isLoadingMaxMakerVolume: true, + ), + ); + + // Fetch max maker volume with fallback to swap address balance + final maxMakerVolume = await _getMaxMakerVolumeWithFallback(event.sellCoin); + // Fetch coin-specific minimum trading volume + final minTradingVol = event.sellCoin == null + ? null + : await _dexRepository.getMinTradingVolume(event.sellCoin!.abbr); + + final maxMakerVolumeDouble = maxMakerVolume?.toDouble() ?? 0; final newSellAmount = CoinTradeAmountInput.dirty( - (state.maximumTradeVolume.value * sellCoinBalance).toString(), + (state.maximumTradeVolume.value * maxMakerVolumeDouble).toString(), ); // Calculate buy amount if applicable @@ -93,21 +118,19 @@ class MarketMakerTradeFormBloc newBuyAmount = CoinTradeAmountInput.dirty(buyAmountValue.toString()); } - // Emit immediately with new coin selection for fast UI update + // Emit with calculated amounts after fetching max maker volume emit( state.copyWith( - sellCoin: CoinSelectInput.dirty(event.sellCoin), sellAmount: newSellAmount, - buyCoin: identicalBuyAndSellCoins - ? const CoinSelectInput.dirty(null, -1) - : state.buyCoin, buyAmount: newBuyAmount, - status: MarketMakerTradeFormStatus.success, + maxMakerVolume: maxMakerVolume, + minTradingVolume: minTradingVol, + isLoadingMaxMakerVolume: false, ), ); // Activate coin before checking preimage - // TODO: consider removing this, as only enabled coins with a balance are + // TODO: consider removing this, as only enabled coins with a balance are // displayed in the sell coins dropdown await _autoActivateCoin(event.sellCoin); @@ -175,16 +198,16 @@ class MarketMakerTradeFormBloc MarketMakerTradeFormTradeVolumeChanged event, Emitter emit, ) async { - final sellCoinBalance = - await state.sellCoin.value?.getBalance(_sdk) ?? BalanceInfo.zero(); - final spendableBalance = sellCoinBalance.spendable.toDouble(); + // Use cached maxMakerVolume instead of spendable balance, as only one + // address in HD mode can be used for swaps, the "Swap address" + final maxMakerVolumeDouble = state.maxMakerVolume?.toDouble() ?? 0; final maximumTradeVolume = double.tryParse(event.maximumTradeVolume.toString()) ?? 0.0; final newSellAmount = CoinTradeAmountInput.dirty( - (maximumTradeVolume * spendableBalance).toString(), + (maximumTradeVolume * maxMakerVolumeDouble).toString(), 0, - spendableBalance, + maxMakerVolumeDouble, ); final newBuyAmount = _getBuyAmountFromSellAmount( @@ -202,7 +225,13 @@ class MarketMakerTradeFormBloc ), ); - // Check for preimage errors asynchronously + // Trade preimage requires both buy and sell coins to be set, so no use in + // calling it before both are set. _getPreimageData checks this internally, + // but emits unnecessary failure states. + if (state.buyCoin.value == null || state.sellCoin.value == null) { + return; + } + final preImage = await _getPreimageData(state); final preImageError = await _getPreImageError(preImage.error, state); final newSellAmountFromPreImage = await _getMaxSellAmountFromPreImage( @@ -228,35 +257,50 @@ class MarketMakerTradeFormBloc MarketMakerTradeFormSwapCoinsRequested event, Emitter emit, ) async { - final buyCoinBalance = - await state.buyCoin.value?.getBalance(_sdk) ?? BalanceInfo.zero(); - final spendableBalance = buyCoinBalance.spendable.toDouble(); + // Emit immediately with swapped coins for fast UI update + final sellCoin = state.buyCoin.value; + final buyCoin = state.sellCoin.value; + emit( + state.copyWith( + sellCoin: CoinSelectInput.dirty(sellCoin), + buyCoin: CoinSelectInput.dirty(buyCoin, -1, -1), + buyAmount: const CoinTradeAmountInput.dirty('0', -1), + isLoadingMaxMakerVolume: true, + ), + ); + + // Fetch max maker volume with fallback to swap address balance + final maxMakerVolume = await _getMaxMakerVolumeWithFallback(sellCoin); + // Fetch coin-specific minimum trading volume for new base coin + final minTradingVol = sellCoin == null + ? null + : await _dexRepository.getMinTradingVolume(sellCoin.abbr); + + final maxMakerVolumeDouble = maxMakerVolume?.toDouble() ?? 0; final maxVolumeValue = double.tryParse(state.maximumTradeVolume.value.toString()) ?? 0.0; - final newSellAmount = maxVolumeValue * spendableBalance; + final newSellAmount = maxVolumeValue * maxMakerVolumeDouble; + // Calculate buy amount if applicable + final newBuyAmount = state.buyCoin.value != null + ? _getBuyAmountFromSellAmount( + newSellAmount.toString(), + state.priceFromUsdWithMargin, + ) + : 0.0; + + // Emit with calculated amounts after fetching max maker volume + // Always clear loading flag, even on error emit( state.copyWith( - sellCoin: CoinSelectInput.dirty(state.buyCoin.value), sellAmount: CoinTradeAmountInput.dirty(newSellAmount.toString()), - buyCoin: CoinSelectInput.dirty(state.sellCoin.value, -1, -1), - buyAmount: const CoinTradeAmountInput.dirty('0', -1), + buyAmount: CoinTradeAmountInput.dirty(newBuyAmount.toString()), + maxMakerVolume: maxMakerVolume, + minTradingVolume: minTradingVol, + isLoadingMaxMakerVolume: false, ), ); - - if (state.buyCoin.value != null) { - final newBuyAmount = _getBuyAmountFromSellAmount( - newSellAmount.toString(), - state.priceFromUsdWithMargin, - ); - - emit( - state.copyWith( - buyAmount: CoinTradeAmountInput.dirty(newBuyAmount.toString()), - ), - ); - } } Future _onTradeMarginChanged( @@ -310,15 +354,21 @@ class MarketMakerTradeFormBloc ); final maxTradeVolume = event.tradePair.config.maxVolume?.value ?? 0.9; final minTradeVolume = event.tradePair.config.minVolume?.value ?? 0.01; - final coinBalance = - (await sellCoin.value?.getBalance(_sdk)) ?? BalanceInfo.zero(); - final sellAmountFromVolume = - maxTradeVolume * coinBalance.spendable.toDouble(); + + // Fetch max maker volume with fallback to swap address balance + final maxMakerVolume = await _getMaxMakerVolumeWithFallback(sellCoin.value); + // Fetch coin-specific minimum trading volume for base coin + final minTradingVol = sellCoin.value == null + ? null + : await _dexRepository.getMinTradingVolume(sellCoin.value!.abbr); + + final maxMakerVolumeDouble = maxMakerVolume?.toDouble() ?? 0; + final sellAmountFromVolume = maxTradeVolume * maxMakerVolumeDouble; final sellAmount = CoinTradeAmountInput.dirty( sellAmountFromVolume.toString(), 0, - coinBalance.spendable.toDouble(), + maxMakerVolumeDouble, ); final tradeMargin = TradeMarginInput.dirty( event.tradePair.config.margin.toStringAsFixed(2), @@ -337,6 +387,8 @@ class MarketMakerTradeFormBloc buyAmount: const CoinTradeAmountInput.dirty('0'), tradeMargin: tradeMargin, updateInterval: updateInterval, + maxMakerVolume: maxMakerVolume, + minTradingVolume: minTradingVol, ), ); @@ -392,10 +444,23 @@ class MarketMakerTradeFormBloc final preImage = await _getPreimageData(state); final preImageError = await _getPreImageError(preImage.error, state); + if (preImage.error is TradePreimageTransportError) { + // After retries, still transport error -> show raw error + emit( + state.copyWith( + status: MarketMakerTradeFormStatus.error, + rawErrorMessage: (preImage.error as TradePreimageTransportError) + .message, + ), + ); + return; + } + if (preImageError == MarketMakerTradeFormError.none) { return emit( state.copyWith( tradePreImage: preImage.data, + rawErrorMessage: null, status: MarketMakerTradeFormStatus.success, ), ); @@ -416,6 +481,7 @@ class MarketMakerTradeFormBloc state.copyWith( tradePreImage: preImage.data, preImageError: isInsufficientBaseBalance ? null : preImageError, + rawErrorMessage: null, sellAmount: isInsufficientBaseBalance ? CoinTradeAmountInput.dirty(newSellAmount.toString()) : state.sellAmount, @@ -466,14 +532,11 @@ class MarketMakerTradeFormBloc if (sellCoin.value?.abbr != preImageError.coin) { return sellAmountValue; } - final sellId = sellCoin.value?.assetId; - final balance = sellId != null ? await _coinsRepo.balance(sellId) : null; final requiredAmount = double.tryParse(preImageError.required) ?? 0; - final sellCoinBalance = balance ?? BalanceInfo.zero(); - final newSellAmount = - sellAmountValue - - (requiredAmount - sellCoinBalance.spendable.toDouble()); + final maxMakerVolume = state.maxMakerVolume?.toDouble() ?? 0; + final newSellAmount = sellAmountValue - (requiredAmount - maxMakerVolume); + // Clamp to minimum of 0 to prevent negative sell amounts return newSellAmount.clamp(0, double.infinity); } @@ -498,8 +561,13 @@ class MarketMakerTradeFormBloc // if Rel coin has a parent, e.g. 1INCH-AVX-20, then the error is // due to insufficient balance of the parent coin return MarketMakerTradeFormError.insufficientBalanceRelParent; - } else if (preImageError is TradePreimageTransportError) { + } else if (preImageError is TradePreimageVolumeTooLowError) { + // Explicit VolumeTooLow should map to insufficient trade amount return MarketMakerTradeFormError.insufficientTradeAmount; + } else if (preImageError is TradePreimageTransportError) { + // Transport is a generic connectivity/transport layer issue; don't + // mislabel it as a min-volume problem + return MarketMakerTradeFormError.none; } else { return MarketMakerTradeFormError.none; } @@ -522,7 +590,9 @@ class MarketMakerTradeFormBloc throw ArgumentError('Base and rel coins must be set'); } - final preimageData = await _dexRepository.getTradePreimage( + // initial attempt + DataFromService preimageData = + await _dexRepository.getTradePreimage( base, rel, price, @@ -530,8 +600,33 @@ class MarketMakerTradeFormBloc volume, ); + // If transport error, retry every second up to 10 seconds while UI + // remains in loading state. + int attemptsLeft = 10; + while (preimageData.error is TradePreimageTransportError && + attemptsLeft > 0) { + _log.warning( + 'trade_preimage transport error for $base/$rel, retrying... ' + '(${11 - attemptsLeft}/10)', + ); + await Future.delayed(const Duration(seconds: 1)); + preimageData = await _dexRepository.getTradePreimage( + base, + rel, + price, + 'setprice', + volume, + ); + attemptsLeft--; + } + return preimageData; - } catch (e) { + } catch (e, s) { + _log.shout( + 'Failed to get preimage data for ${state.sellCoin.value?.abbr}/${state.buyCoin.value?.abbr}', + e, + s, + ); return DataFromService( error: TradePreimagePriceTooLowError( price: '0', @@ -559,4 +654,62 @@ class MarketMakerTradeFormBloc } } } + + /// Fetches the max maker volume for a coin with automatic fallback. + /// + /// First attempts to fetch from the DEX API via [getMaxMakerVolume]. + /// If that fails or returns null, falls back to [_getSwapAddressBalance]. + /// + /// Returns null if the coin is null or all attempts fail. + Future _getMaxMakerVolumeWithFallback(Coin? coin) async { + if (coin == null) { + return null; + } + + try { + // Fetch max maker volume from DEX API + final maxMakerVolume = await _dexRepository.getMaxMakerVolume(coin.abbr); + + // Fallback to swap address balance if RPC fails + if (maxMakerVolume == null) { + return await _getSwapAddressBalance(coin); + } + + return maxMakerVolume; + } catch (e, s) { + _log.warning( + 'Failed to get max maker volume for ${coin.abbr}, falling back to swap address balance', + e, + s, + ); + // Fallback to swap address balance on error + return await _getSwapAddressBalance(coin); + } + } + + /// Get the swap address balance as a fallback when getMaxMakerVolume fails. + /// This method retrieves the spendable balance from the address marked as + /// active for swaps (derivationPath ending with '/0' or null). + Future _getSwapAddressBalance(Coin coin) async { + try { + final asset = _sdk.getSdkAsset(coin.abbr); + final pubkeys = _sdk.pubkeys.lastKnown(asset.id); + + if (pubkeys == null) { + return null; + } + + // Find the swap address (isActiveForSwap = true) + final swapAddress = pubkeys.keys.firstWhere( + (pubkey) => pubkey.isActiveForSwap, + orElse: () => pubkeys.keys.first, + ); + + final spendable = swapAddress.balance.spendable; + return Rational.parse(spendable.toString()); + } catch (e, s) { + _log.shout('Failed to get swap address balance for ${coin.abbr}', e, s); + return null; + } + } } diff --git a/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_state.dart b/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_state.dart index 1c8e632cc9..007fccb01c 100644 --- a/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_state.dart +++ b/lib/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_state.dart @@ -13,10 +13,7 @@ enum MarketMakerTradeFormStatus { initial, loading, success, error } // Usually this would be a dedicated tab contoller/ui flow bloc, but because // there is only two stages (initial and confirmationRequired), and for the // sake of simplicity, we are using the form state to manage the form stages. -enum MarketMakerTradeFormStage { - initial, - confirmationRequired, -} +enum MarketMakerTradeFormStage { initial, confirmationRequired } /// The state of the market maker trade form. The state is a formz mixin /// which allows the form to be validated and checked for errors. @@ -34,21 +31,29 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { required this.stage, this.tradePreImageError, this.tradePreImage, + this.maxMakerVolume, + this.minTradingVolume, + this.rawErrorMessage, + this.isLoadingMaxMakerVolume = false, }); MarketMakerTradeFormState.initial() - : sellCoin = const CoinSelectInput.pure(), - buyCoin = const CoinSelectInput.pure(), - minimumTradeVolume = const TradeVolumeInput.pure(0.1), - maximumTradeVolume = const TradeVolumeInput.pure(0.9), - sellAmount = const CoinTradeAmountInput.pure(), - buyAmount = const CoinTradeAmountInput.pure(), - tradeMargin = const TradeMarginInput.pure(), - updateInterval = const UpdateIntervalInput.pure(), - status = MarketMakerTradeFormStatus.initial, - stage = MarketMakerTradeFormStage.initial, - tradePreImageError = null, - tradePreImage = null; + : sellCoin = const CoinSelectInput.pure(), + buyCoin = const CoinSelectInput.pure(), + minimumTradeVolume = const TradeVolumeInput.pure(0.1), + maximumTradeVolume = const TradeVolumeInput.pure(0.9), + sellAmount = const CoinTradeAmountInput.pure(), + buyAmount = const CoinTradeAmountInput.pure(), + tradeMargin = const TradeMarginInput.pure(), + updateInterval = const UpdateIntervalInput.pure(), + status = MarketMakerTradeFormStatus.initial, + stage = MarketMakerTradeFormStage.initial, + tradePreImageError = null, + tradePreImage = null, + maxMakerVolume = null, + minTradingVolume = null, + rawErrorMessage = null, + isLoadingMaxMakerVolume = false; /// The coin being sold in the trade pair (base coin). final CoinSelectInput sellCoin; @@ -88,6 +93,22 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { /// The preimage of the trade pair, used to calculate the trade pair fees. final TradePreimage? tradePreImage; + /// The maximum maker volume available for swaps for the sell coin. + /// This value is fetched from the DEX API and cached in the state. + final Rational? maxMakerVolume; + + /// The minimum trading volume required by the base coin (sell coin). + /// Retrieved from the DEX API `min_trading_vol` for clearer validation + /// and error messaging. + final Rational? minTradingVolume; + + /// Raw error message for displaying unparsed backend errors to users when + /// a generic/transport failure persists after retries. + final String? rawErrorMessage; + + /// Indicates whether the max maker volume is currently being fetched. + final bool isLoadingMaxMakerVolume; + /// The price of the trade pair derived from the USD price of the coins. /// Price = baseCoinUsdPrice / relCoinUsdPrice. double? get priceFromUsd { @@ -163,6 +184,10 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { MarketMakerTradeFormError? preImageError, MarketMakerTradeFormStage? stage, TradePreimage? tradePreImage, + Rational? maxMakerVolume, + Rational? minTradingVolume, + String? rawErrorMessage, + bool? isLoadingMaxMakerVolume, }) { return MarketMakerTradeFormState( sellCoin: sellCoin ?? this.sellCoin, @@ -177,6 +202,11 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { tradePreImageError: preImageError, stage: stage ?? this.stage, tradePreImage: tradePreImage ?? this.tradePreImage, + maxMakerVolume: maxMakerVolume ?? this.maxMakerVolume, + minTradingVolume: minTradingVolume ?? this.minTradingVolume, + rawErrorMessage: rawErrorMessage ?? this.rawErrorMessage, + isLoadingMaxMakerVolume: + isLoadingMaxMakerVolume ?? this.isLoadingMaxMakerVolume, ); } @@ -201,13 +231,13 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { @override List> get inputs => [ - sellCoin, - buyCoin, - minimumTradeVolume, - maximumTradeVolume, - tradeMargin, - updateInterval, - ]; + sellCoin, + buyCoin, + minimumTradeVolume, + maximumTradeVolume, + tradeMargin, + updateInterval, + ]; @override bool get isValid { @@ -218,17 +248,21 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { @override List get props => [ - sellCoin, - buyCoin, - minimumTradeVolume, - maximumTradeVolume, - sellAmount, - buyAmount, - tradeMargin, - updateInterval, - tradePreImageError, - stage, - status, - tradePreImage, - ]; + sellCoin, + buyCoin, + minimumTradeVolume, + maximumTradeVolume, + sellAmount, + buyAmount, + tradeMargin, + updateInterval, + tradePreImageError, + stage, + status, + tradePreImage, + maxMakerVolume, + minTradingVolume, + rawErrorMessage, + isLoadingMaxMakerVolume, + ]; } diff --git a/lib/bloc/settings/settings_repository.dart b/lib/bloc/settings/settings_repository.dart index 104983d401..adae4f37c2 100644 --- a/lib/bloc/settings/settings_repository.dart +++ b/lib/bloc/settings/settings_repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:logging/logging.dart'; import 'package:web_dex/model/stored_settings.dart'; import 'package:web_dex/services/storage/base_storage.dart'; import 'package:web_dex/services/storage/get_storage.dart'; @@ -7,25 +8,49 @@ import 'package:web_dex/shared/constants.dart'; class SettingsRepository { SettingsRepository({BaseStorage? storage}) - : _storage = storage ?? getStorage(); + : _storage = storage ?? getStorage(); final BaseStorage _storage; + static final _log = Logger('SettingsRepository'); Future loadSettings() async { - final dynamic storedAppPrefs = await _storage.read(storedSettingsKey); - - return StoredSettings.fromJson(storedAppPrefs); + return loadStoredSettings(settingsStorage: _storage); } Future updateSettings(StoredSettings settings) async { - final String encodedData = jsonEncode(settings.toJson()); - await _storage.write(storedSettingsKey, encodedData); + // Write the new versioned key for current app reads + final String v2Data = jsonEncode(settings.toJson()); + await _storage.write(storedSettingsKeyV2, v2Data); + + // Also write a backward-compatible legacy shape so older app versions + // can continue to read their expected key without crashing. + final String legacyData = jsonEncode(settings.toLegacyJson()); + await _storage.write(storedSettingsKey, legacyData); } - static Future loadStoredSettings() async { - final storage = getStorage(); - final dynamic storedAppPrefs = await storage.read(storedSettingsKey); - - return StoredSettings.fromJson(storedAppPrefs); + static Future loadStoredSettings({ + BaseStorage? settingsStorage, + }) async { + final storage = settingsStorage ?? getStorage(); + try { + // Prefer V2 settings if present + final dynamic v2 = await storage.read(storedSettingsKeyV2); + if (v2 is Map) { + return StoredSettings.fromJson(v2); + } + + // Fallback to legacy key + final dynamic legacy = await storage.read(storedSettingsKey); + return StoredSettings.fromJson( + legacy is Map ? legacy : null, + ); + } catch (e, stackTrace) { + _log.warning( + 'Failed to load stored settings, returning initial settings', + e, + stackTrace, + ); + return StoredSettings.initial(); + } } } diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index dc2b982f74..44e2c3e911 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -318,6 +318,17 @@ class TakerBloc extends Bloc { add(TakerUpdateBestOrders(autoSelectOrderAbbr: event.autoSelectOrderAbbr)); + // Before login, show 0.00 instead of spinner + if (!_isLoggedIn) { + emit( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.unavailable, + maxSellAmount: () => null, + ), + ); + return; + } + await _autoActivateCoin(state.sellCoin?.abbr); _subscribeMaxSellAmount(); add(TakerGetMinSellAmount()); @@ -440,6 +451,17 @@ class TakerBloc extends Bloc { _maxSellAmountTimer?.cancel(); return; } + // If not logged in, show 0.00 (unavailable) and skip spinner + if (!_isLoggedIn) { + emitter( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.unavailable, + maxSellAmount: () => null, + ), + ); + return; + } + if (state.availableBalanceState == AvailableBalanceState.initial || event.setLoadingStatus) { emitter( @@ -469,34 +491,26 @@ class TakerBloc extends Bloc { return; } - if (!_isLoggedIn) { + Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume( + state.sellCoin!.abbr, + ); + if (maxSellAmount != null) { emitter( state.copyWith( - availableBalanceState: () => AvailableBalanceState.unavailable, + maxSellAmount: () => maxSellAmount, + availableBalanceState: () => AvailableBalanceState.success, ), ); } else { - Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume( - state.sellCoin!.abbr, + maxSellAmount = await _frequentlyGetMaxTakerVolume(); + emitter( + state.copyWith( + maxSellAmount: () => maxSellAmount, + availableBalanceState: maxSellAmount == null + ? () => AvailableBalanceState.failure + : () => AvailableBalanceState.success, + ), ); - if (maxSellAmount != null) { - emitter( - state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: () => AvailableBalanceState.success, - ), - ); - } else { - maxSellAmount = await _frequentlyGetMaxTakerVolume(); - emitter( - state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: maxSellAmount == null - ? () => AvailableBalanceState.failure - : () => AvailableBalanceState.success, - ), - ); - } } } catch (e, s) { _log.severe('Failed to update max sell amount', e, s); diff --git a/lib/bloc/transaction_history/transaction_history_bloc.dart b/lib/bloc/transaction_history/transaction_history_bloc.dart index c71f27d727..f3e2b84be9 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -249,24 +249,13 @@ class TransactionHistoryBloc } int _compareTransactions(Transaction left, Transaction right) { - // Unconfirmed (pending) transactions should appear first. - final leftIsUnconfirmed = left.confirmations == 0; - final rightIsUnconfirmed = right.confirmations == 0; - - if (leftIsUnconfirmed != rightIsUnconfirmed) { - return leftIsUnconfirmed ? -1 : 1; + final unconfirmedTimestamp = DateTime.fromMillisecondsSinceEpoch(0); + if (right.timestamp == unconfirmedTimestamp) { + return 1; + } else if (left.timestamp == unconfirmedTimestamp) { + return -1; } - - // Within each group, sort by effective time (handles zero timestamps) - final timeComparison = _sortTime(right).compareTo(_sortTime(left)); - if (timeComparison != 0) return timeComparison; - - // Prefer higher block heights first - final heightComparison = right.blockHeight.compareTo(left.blockHeight); - if (heightComparison != 0) return heightComparison; - - // Final tiebreaker to ensure deterministic ordering - return right.internalId.compareTo(left.internalId); + return right.timestamp.compareTo(left.timestamp); } } diff --git a/lib/blocs/maker_form_bloc.dart b/lib/blocs/maker_form_bloc.dart index 792f58f6d6..fe7443bd26 100644 --- a/lib/blocs/maker_form_bloc.dart +++ b/lib/blocs/maker_form_bloc.dart @@ -246,7 +246,10 @@ class MakerFormBloc implements BlocBase { Future _updateMaxSellAmountListener() async { _maxSellAmountTimer?.cancel(); maxSellAmount = null; - availableBalanceState = AvailableBalanceState.loading; + // Only show loading spinner when signed in + final bool isSignedIn = await kdfSdk.auth.isSignedIn(); + availableBalanceState = + isSignedIn ? AvailableBalanceState.loading : AvailableBalanceState.unavailable; isMaxActive = false; await _updateMaxSellAmount(); @@ -259,10 +262,6 @@ class MakerFormBloc implements BlocBase { Future _updateMaxSellAmount() async { final Coin? coin = sellCoin; - if (availableBalanceState == AvailableBalanceState.initial) { - availableBalanceState = AvailableBalanceState.loading; - } - final bool isSignedIn = await kdfSdk.auth.isSignedIn(); if (!isSignedIn) { maxSellAmount = null; @@ -270,6 +269,10 @@ class MakerFormBloc implements BlocBase { return; } + if (availableBalanceState == AvailableBalanceState.initial) { + availableBalanceState = AvailableBalanceState.loading; + } + if (coin == null) { maxSellAmount = null; availableBalanceState = AvailableBalanceState.unavailable; diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 22a4045ab6..ceb3c15a21 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -503,6 +503,8 @@ abstract class LocaleKeys { static const orderBookNoAsks = 'orderBookNoAsks'; static const orderBookNoBids = 'orderBookNoBids'; static const orderBookEmpty = 'orderBookEmpty'; + static const dexNoSwapOffers = 'dexNoSwapOffers'; + static const bridgeNoCrossNetworkRoutes = 'bridgeNoCrossNetworkRoutes'; static const freshAddress = 'freshAddress'; static const userActionRequired = 'userActionRequired'; static const unknown = 'unknown'; @@ -742,6 +744,7 @@ abstract class LocaleKeys { static const trend7d = 'trend7d'; static const tradingDisabledTooltip = 'tradingDisabledTooltip'; static const tradingDisabled = 'tradingDisabled'; + static const nftDisabledTooltip = 'nftDisabledTooltip'; static const includeBlockedAssets = 'includeBlockedAssets'; static const unbanPubkeysResults = 'unbanPubkeysResults'; static const unbannedPubkeys = 'unbannedPubkeys'; @@ -753,6 +756,7 @@ abstract class LocaleKeys { static const noBannedPubkeys = 'noBannedPubkeys'; static const unbanPubkeysFailed = 'unbanPubkeysFailed'; static const privateKeyRetrievalFailed = 'privateKeyRetrievalFailed'; + static const privateKeysEmptyError = 'privateKeysEmptyError'; static const fetchingPrivateKeysTitle = 'fetchingPrivateKeysTitle'; static const fetchingPrivateKeysMessage = 'fetchingPrivateKeysMessage'; static const pubkeyType = 'pubkeyType'; diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index c5f2114940..a514fba5c2 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -241,7 +241,27 @@ class Mm2Api { Future?> getBestOrders(BestOrdersRequest request) async { try { - return await _mm2.call(request) as Map?; + final Map? response = await retry( + () async { + final Map? resp = + await _mm2.call(request) as Map?; + if (resp == null) { + throw Exception('null response'); + } + if (resp['error'] != null) { + // Throw to allow a quick retry during transient auth/session races + throw Exception(resp['error'].toString()); + } + return resp; + }, + maxAttempts: 4, + backoffStrategy: const LinearBackoff( + initialDelay: Duration(milliseconds: 500), + increment: Duration(milliseconds: 250), + maxDelay: Duration(seconds: 3), + ), + ); + return response; } catch (e, s) { log( 'Error getting best orders ${request.coin}: $e', diff --git a/lib/model/settings/market_maker_bot_settings.dart b/lib/model/settings/market_maker_bot_settings.dart index 8aee2a1284..559cd2370c 100644 --- a/lib/model/settings/market_maker_bot_settings.dart +++ b/lib/model/settings/market_maker_bot_settings.dart @@ -1,9 +1,12 @@ import 'package:equatable/equatable.dart'; +import 'package:logging/logging.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/message_service_config/message_service_config.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config.dart'; /// Settings for the KDF Simple Market Maker Bot. class MarketMakerBotSettings extends Equatable { + static final Logger _log = Logger('MarketMakerBotSettings'); + const MarketMakerBotSettings({ required this.isMMBotEnabled, required this.botRefreshRate, @@ -28,21 +31,57 @@ class MarketMakerBotSettings extends Equatable { /// Returns the initial settings if the JSON map is null or does not contain /// the required `is_market_maker_bot_enabled` key. factory MarketMakerBotSettings.fromJson(Map? json) { - if (json == null || !json.containsKey('is_market_maker_bot_enabled')) { - return MarketMakerBotSettings.initial(); - } + if (json == null) return MarketMakerBotSettings.initial(); + + final bool? enabled = json['is_market_maker_bot_enabled'] as bool?; + final int refresh = (json['bot_refresh_rate'] is int) + ? json['bot_refresh_rate'] as int + : int.tryParse('${json['bot_refresh_rate']}') ?? 60; + + final dynamic configsRaw = json['trade_coin_pair_configs']; + final List configs = (configsRaw is List) + ? configsRaw + .map((dynamic e) { + // Log before skipping, rather than silently filtering invalid entries + if (e is! Map) { + _log.warning('Invalid trade coin pair config: $e'); + return null; + } + + try { + // Skip invalid entries rather than crashing on startup + if (!e.containsKey('name') || + !e.containsKey('base') || + !e.containsKey('rel') || + !e.containsKey('spread') || + !e.containsKey('enable')) { + _log.warning('Invalid trade coin pair config: $e'); + return null; + } + return TradeCoinPairConfig.fromJson(e); + } catch (error, stackTrace) { + _log.warning( + 'Invalid trade coin pair config', + error, + stackTrace, + ); + return null; + } + }) + .whereType() + .toList() + : const []; + + final MessageServiceConfig? messageCfg = + (json['message_service_config'] is Map) + ? MessageServiceConfig.fromJson(json['message_service_config']) + : null; return MarketMakerBotSettings( - isMMBotEnabled: json['is_market_maker_bot_enabled'] as bool, - botRefreshRate: json['bot_refresh_rate'] as int, - tradeCoinPairConfigs: (json['trade_coin_pair_configs'] as List) - .map((e) => TradeCoinPairConfig.fromJson(e as Map)) - .toList(), - messageServiceConfig: json['message_service_config'] == null - ? null - : MessageServiceConfig.fromJson( - json['message_service_config'] as Map, - ), + isMMBotEnabled: enabled ?? false, + botRefreshRate: refresh, + tradeCoinPairConfigs: configs, + messageServiceConfig: messageCfg, ); } @@ -72,6 +111,22 @@ class MarketMakerBotSettings extends Equatable { }; } + // Legacy representation kept for backward-compatible writes + Map toLegacyJson() { + return { + 'is_market_maker_bot_enabled': isMMBotEnabled, + // Old builds included a price_url; provide the previous default + 'price_url': + 'https://defi-stats.komodo.earth/api/v3/prices/tickers_v2?expire_at=60', + 'bot_refresh_rate': botRefreshRate, + 'trade_coin_pair_configs': tradeCoinPairConfigs + .map((e) => e.toJson()) + .toList(), + if (messageServiceConfig != null) + 'message_service_config': messageServiceConfig?.toJson(), + }; + } + MarketMakerBotSettings copyWith({ bool? isMMBotEnabled, int? botRefreshRate, diff --git a/lib/model/stored_settings.dart b/lib/model/stored_settings.dart index dfd4301b49..c10085ebdd 100644 --- a/lib/model/stored_settings.dart +++ b/lib/model/stored_settings.dart @@ -62,6 +62,19 @@ class StoredSettings { }; } + // Legacy representation kept for backward-compatible writes to + // shared_preferences.json so older app versions can still parse it. + Map toLegacyJson() { + return { + 'themeModeIndex': mode.index, + storedAnalyticsSettingsKey: analytics.toJson(), + storedMarketMakerSettingsKey: marketMakerBotSettings.toLegacyJson(), + 'testCoinsEnabled': testCoinsEnabled, + 'weakPasswordsAllowed': weakPasswordsAllowed, + 'hideZeroBalanceAssets': hideZeroBalanceAssets, + }; + } + StoredSettings copyWith({ ThemeMode? mode, AnalyticsSettings? analytics, diff --git a/lib/services/file_loader/mobile/file_loader_native_ios.dart b/lib/services/file_loader/mobile/file_loader_native_ios.dart index 5f6cf830a7..b5f0bdf224 100644 --- a/lib/services/file_loader/mobile/file_loader_native_ios.dart +++ b/lib/services/file_loader/mobile/file_loader_native_ios.dart @@ -1,15 +1,27 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/shared/utils/zip.dart'; class FileLoaderNativeIOS implements FileLoader { const FileLoaderNativeIOS(); + Rect? _getSharePositionOrigin() { + final context = scaffoldKey.currentContext; + if (context == null) return null; + + final box = context.findRenderObject() as RenderBox?; + if (box == null) return null; + + return box.localToGlobal(Offset.zero) & box.size; + } + @override Future save({ required String fileName, @@ -19,8 +31,10 @@ class FileLoaderNativeIOS implements FileLoader { switch (type) { case LoadFileType.text: await _saveAsTextFile(fileName: fileName, data: data); + break; case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); + break; } } @@ -33,7 +47,18 @@ class FileLoaderNativeIOS implements FileLoader { final File file = File(filePath); await file.writeAsString(data); - await Share.shareXFiles([XFile(file.path)]); + await SharePlus.instance.share( + ShareParams( + files: [ + XFile( + file.path, + name: '$fileName.txt', + mimeType: 'text/plain', + ) + ], + sharePositionOrigin: _getSharePositionOrigin(), + ), + ); } Future _saveAsCompressedFile({ @@ -49,7 +74,18 @@ class FileLoaderNativeIOS implements FileLoader { final File compressedFile = File(filePath); await compressedFile.writeAsBytes(compressedBytes); - await Share.shareXFiles([XFile(compressedFile.path)]); + await SharePlus.instance.share( + ShareParams( + files: [ + XFile( + compressedFile.path, + name: '$fileName.zip', + mimeType: 'application/zip', + ) + ], + sharePositionOrigin: _getSharePositionOrigin(), + ), + ); } @override diff --git a/lib/shared/constants.dart b/lib/shared/constants.dart index a7c7235f4e..7c349885c3 100644 --- a/lib/shared/constants.dart +++ b/lib/shared/constants.dart @@ -14,6 +14,8 @@ const Duration kActivationPollingInterval = Duration( // stored app preferences const String storedSettingsKey = '_atomicDexStoredSettings'; +// New settings key to avoid breaking older versions reading the legacy key +const String storedSettingsKeyV2 = 'komodo_wallet_settings_v2'; const String storedAnalyticsSettingsKey = 'analytics_settings'; const String storedMarketMakerSettingsKey = 'market_maker_settings'; const String lastLoggedInWalletKey = 'last_logged_in_wallet'; diff --git a/lib/views/bridge/view/table/bridge_nothing_found.dart b/lib/views/bridge/view/table/bridge_nothing_found.dart index 55b88259df..4ddab3854e 100644 --- a/lib/views/bridge/view/table/bridge_nothing_found.dart +++ b/lib/views/bridge/view/table/bridge_nothing_found.dart @@ -1,18 +1,38 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/router/state/routing_state.dart'; +import 'package:web_dex/model/main_menu_value.dart'; class BridgeNothingFound extends StatelessWidget { @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(0, 30, 0, 20), - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - LocaleKeys.nothingFound.tr(), - style: Theme.of(context).textTheme.bodySmall, + Text( + LocaleKeys.bridgeNoCrossNetworkRoutes.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 8), + UiSimpleButton( + onPressed: () { + routingState.selectedMenu = MainMenuValue.dex; + routingState.dexState.orderType = 'maker'; + }, + child: Text( + LocaleKeys.makerOrder.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), ), ], ), diff --git a/lib/views/common/main_menu/main_menu_bar_mobile.dart b/lib/views/common/main_menu/main_menu_bar_mobile.dart index 1cb66a7110..71bbaf4759 100644 --- a/lib/views/common/main_menu/main_menu_bar_mobile.dart +++ b/lib/views/common/main_menu/main_menu_bar_mobile.dart @@ -94,10 +94,13 @@ class MainMenuBarMobile extends StatelessWidget { ), ), Expanded( - child: MainMenuBarMobileItem( - value: MainMenuValue.nft, - enabled: currentWallet?.isHW != true, - isActive: selected == MainMenuValue.nft, + child: Tooltip( + message: LocaleKeys.nftDisabledTooltip.tr(), + child: MainMenuBarMobileItem( + value: MainMenuValue.nft, + enabled: false, + isActive: selected == MainMenuValue.nft, + ), ), ), Expanded( diff --git a/lib/views/common/main_menu/main_menu_desktop.dart b/lib/views/common/main_menu/main_menu_desktop.dart index 4ae6450582..364ef7956e 100644 --- a/lib/views/common/main_menu/main_menu_desktop.dart +++ b/lib/views/common/main_menu/main_menu_desktop.dart @@ -135,12 +135,16 @@ class _MainMenuDesktopState extends State { ), ), ), - DesktopMenuDesktopItem( - key: const Key('main-menu-nft'), - enabled: currentWallet?.isHW != true, - menu: MainMenuValue.nft, - onTap: onTapItem, - isSelected: _checkSelectedItem(MainMenuValue.nft), + Tooltip( + message: LocaleKeys.nftDisabledTooltip.tr(), + child: DesktopMenuDesktopItem( + key: const Key('main-menu-nft'), + enabled: false, + menu: MainMenuValue.nft, + onTap: onTapItem, + isSelected: + _checkSelectedItem(MainMenuValue.nft), + ), ), const Spacer(), Divider(thickness: 1), diff --git a/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart b/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart index f8a9b812ea..cce1dcc260 100644 --- a/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart +++ b/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart @@ -11,10 +11,11 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/views/dex/simple/form/tables/nothing_found.dart'; import 'package:web_dex/views/dex/simple/form/tables/orders_table/grouped_list_view.dart'; import 'package:web_dex/views/dex/simple/form/tables/table_utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/router/state/routing_state.dart'; +import 'package:web_dex/model/main_menu_value.dart'; class OrdersTableContent extends StatelessWidget { const OrdersTableContent({ @@ -61,7 +62,9 @@ class OrdersTableContent extends StatelessWidget { .testCoinsEnabled, ); - if (orders.isEmpty) return const NothingFound(); + if (orders.isEmpty) { + return const _NoOrdersCta(); + } return GroupedListView( items: orders, @@ -116,3 +119,37 @@ class _ErrorMessage extends StatelessWidget { ); } } + +class _NoOrdersCta extends StatelessWidget { + const _NoOrdersCta(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(12, 30, 12, 20), + alignment: const Alignment(0, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + LocaleKeys.dexNoSwapOffers.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 8), + UiSimpleButton( + child: Text( + // Reuse existing localization for Maker order label + LocaleKeys.makerOrder.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + onPressed: () { + routingState.selectedMenu = MainMenuValue.dex; + routingState.dexState.orderType = 'maker'; + }, + ), + ], + ), + ); + } +} diff --git a/lib/views/dex/simple/form/taker/taker_form.dart b/lib/views/dex/simple/form/taker/taker_form.dart index 9beb06e259..44c50f88b0 100644 --- a/lib/views/dex/simple/form/taker/taker_form.dart +++ b/lib/views/dex/simple/form/taker/taker_form.dart @@ -29,6 +29,16 @@ class _TakerFormState extends State { takerBloc.add(TakerSetDefaults()); takerBloc.add(TakerSetWalletIsReady(authBlocState.isSignedIn)); routingState.dexState.addListener(_consumeRouteParameters); + // If entering the swap page while already authenticated, ensure the + // available balance initializes without waiting for further user action. + if (authBlocState.isSignedIn) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentSellCoin = takerBloc.state.sellCoin; + if (currentSellCoin != null) { + takerBloc.add(TakerSetSellCoin(currentSellCoin)); + } + }); + } super.initState(); } @@ -87,6 +97,21 @@ class _TakerFormState extends State { listener: (context, state) { final takerBloc = context.read(); takerBloc.add(TakerSetWalletIsReady(state.isSignedIn)); + + // When the user becomes authenticated while on the swap page, + // refresh the available balance/max sell amount immediately so it + // doesn't remain at 0.00 until the user re-selects the sell coin. + if (state.isSignedIn) { + final currentSellCoin = takerBloc.state.sellCoin; + if (currentSellCoin != null) { + // Re-dispatching the same sell coin sets up periodic polling + // and triggers max-sell/min-sell refresh without user action. + takerBloc.add(TakerSetSellCoin(currentSellCoin)); + } else { + // Ensure defaults are set so the form initializes properly. + takerBloc.add(TakerSetDefaults()); + } + } }, child: const TakerFormLayout(), ); diff --git a/lib/views/market_maker_bot/buy_coin_select_dropdown.dart b/lib/views/market_maker_bot/buy_coin_select_dropdown.dart index b02554f7bd..26919d3267 100644 --- a/lib/views/market_maker_bot/buy_coin_select_dropdown.dart +++ b/lib/views/market_maker_bot/buy_coin_select_dropdown.dart @@ -6,7 +6,7 @@ import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/forms/coin_select_input.dart'; import 'package:web_dex/model/forms/coin_trade_amount_input.dart'; import 'package:web_dex/views/market_maker_bot/coin_selection_and_amount_input.dart'; -import 'package:web_dex/views/market_maker_bot/coin_trade_amount_form_field.dart'; +import 'package:web_dex/views/market_maker_bot/coin_trade_amount_label.dart' show CoinTradeAmountLabel; import 'package:web_dex/views/market_maker_bot/market_maker_form_error_message_extensions.dart'; class BuyCoinSelectDropdown extends StatelessWidget { @@ -32,10 +32,9 @@ class BuyCoinSelectDropdown extends StatelessWidget { trailing: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - CoinTradeAmountFormField( + CoinTradeAmountLabel( coin: buyCoin.value, - initialValue: buyAmount.value, - isEnabled: false, + value: buyAmount.valueAsRational, errorText: buyCoin.displayError?.text(buyCoin.value), ), Padding( diff --git a/lib/views/market_maker_bot/coin_trade_amount_label.dart b/lib/views/market_maker_bot/coin_trade_amount_label.dart new file mode 100644 index 0000000000..cd7c769e9b --- /dev/null +++ b/lib/views/market_maker_bot/coin_trade_amount_label.dart @@ -0,0 +1,118 @@ +import 'package:app_theme/app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:rational/rational.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/formatters.dart' show formatAmt; +import 'package:web_dex/views/dex/dex_helpers.dart' show getFormattedFiatAmount; + +class CoinTradeAmountLabel extends StatelessWidget { + const CoinTradeAmountLabel({ + required this.value, + super.key, + this.coin, + this.errorText, + }); + + final Coin? coin; + final Rational value; + final String? errorText; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 250), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(right: 18, top: 1), + child: TradeAmountDisplayText( + key: const Key('maker-amount-display'), + value: value, + coin: coin, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 18), + child: TradeAmountFiatPriceText( + key: const Key('maker-amount-fiat'), + coin: coin, + amount: value, + ), + ), + if (errorText != null) + Padding( + padding: const EdgeInsets.only(right: 18), + child: AutoScrollText( + text: errorText!, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ) + else + const SizedBox(height: 16), + ], + ), + ); + } +} + +class TradeAmountFiatPriceText extends StatelessWidget { + const TradeAmountFiatPriceText({super.key, this.coin, this.amount}); + + final Rational? amount; + final Coin? coin; + + @override + Widget build(BuildContext context) { + return Text( + coin == null + ? r'≈$0' + : getFormattedFiatAmount( + context, + coin!.abbr, + amount ?? Rational.zero, + ), + style: Theme.of(context).textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ); + } +} + +class TradeAmountDisplayText extends StatelessWidget { + const TradeAmountDisplayText({super.key, required this.value, this.coin}); + + final Rational value; + final Coin? coin; + + @override + Widget build(BuildContext context) { + final formattedValue = formatAmt(value.toDouble()); + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: Text( + formattedValue, + textAlign: TextAlign.end, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: dexPageColors.activeText, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '*', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontFeatures: [const FontFeature.superscripts()], + ), + ), + ], + ); + } +} diff --git a/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart b/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart index 51d14a2f66..05eacd2301 100644 --- a/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart +++ b/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart @@ -133,10 +133,11 @@ class _MarketMakerBotConfirmationFormState TotalFees(preimage: state.tradePreImage), const SizedBox(height: 24), SwapErrorMessage( - errorMessage: state.tradePreImageError?.text( - state.sellCoin.value, - state.buyCoin.value, - ), + errorMessage: state.rawErrorMessage ?? + state.tradePreImageError?.text( + state.sellCoin.value, + state.buyCoin.value, + ), context: context, ), Flexible( diff --git a/lib/views/market_maker_bot/market_maker_bot_form_content.dart b/lib/views/market_maker_bot/market_maker_bot_form_content.dart index d3abe49c99..cfe76669ad 100644 --- a/lib/views/market_maker_bot/market_maker_bot_form_content.dart +++ b/lib/views/market_maker_bot/market_maker_bot_form_content.dart @@ -82,6 +82,7 @@ class _MarketMakerBotFormContentState extends State { maximumTradeVolume: state.maximumTradeVolume, onItemSelected: _onSelectSellCoin, onTradeVolumeChanged: _onVolumeRangeChanged, + showSellAmountSpinner: state.isLoadingMaxMakerVolume, ), bottomWidget: BuyCoinSelectDropdown( key: const Key('$keyPrefix-buy-select'), @@ -130,10 +131,10 @@ class _MarketMakerBotFormContentState extends State { const SizedBox(height: 12), if (state.tradePreImageError != null) ImportantNote( - text: - state.tradePreImageError?.text( + text: state.tradePreImageError?.textWithMin( state.sellCoin.value, state.buyCoin.value, + state.minTradingVolume?.toString(), ) ?? '', ), diff --git a/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart b/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart index 7794c91825..df5ae431e9 100644 --- a/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart +++ b/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart @@ -68,7 +68,8 @@ extension AmountValidationErrorText on AmountValidationError { return LocaleKeys.dexInsufficientFundsError .tr(args: [balance.toString(), coin?.abbr ?? '']); case AmountValidationError.lessThanMinimum: - return LocaleKeys.mmBotMinimumTradeVolume.tr(args: ["0.00000001"]); + return LocaleKeys.mmBotMinimumTradeVolume + .tr(args: ["0.00000001 ${coin?.abbr ?? ''}"]); } } } @@ -93,9 +94,27 @@ extension MarketMakerTradeFormErrorText on MarketMakerTradeFormError { return LocaleKeys.withdrawNotEnoughBalanceForGasError .tr(args: [relCoin?.parentCoin?.abbr ?? relCoin?.abbr ?? '']); case MarketMakerTradeFormError.insufficientTradeAmount: - return LocaleKeys.mmBotMinimumTradeVolume.tr(args: ["0.00000001"]); + return LocaleKeys.mmBotMinimumTradeVolume + .tr(args: ["too low for ${baseCoin?.abbr ?? ''}"]); default: return LocaleKeys.dexErrorMessage.tr(); } } } + +extension MarketMakerTradeFormErrorTextWithMin on MarketMakerTradeFormError { + String textWithMin( + Coin? baseCoin, + Coin? relCoin, + String? minTradingVolume, + ) { + if (this == MarketMakerTradeFormError.insufficientTradeAmount) { + final ticker = baseCoin?.abbr ?? ''; + final minText = (minTradingVolume?.isNotEmpty ?? false) + ? minTradingVolume + : 'too low for'; + return LocaleKeys.mmBotMinimumTradeVolume.tr(args: ['$minText $ticker']); + } + return text(baseCoin, relCoin); + } +} diff --git a/lib/views/market_maker_bot/sell_coin_select_dropdown.dart b/lib/views/market_maker_bot/sell_coin_select_dropdown.dart index 6a631f7b3b..3a29861872 100644 --- a/lib/views/market_maker_bot/sell_coin_select_dropdown.dart +++ b/lib/views/market_maker_bot/sell_coin_select_dropdown.dart @@ -9,7 +9,7 @@ import 'package:web_dex/model/forms/coin_trade_amount_input.dart'; import 'package:web_dex/model/forms/trade_volume_input.dart'; import 'package:web_dex/views/dex/common/front_plate.dart'; import 'package:web_dex/views/market_maker_bot/coin_selection_and_amount_input.dart'; -import 'package:web_dex/views/market_maker_bot/coin_trade_amount_form_field.dart'; +import 'package:web_dex/views/market_maker_bot/coin_trade_amount_label.dart'; import 'package:web_dex/views/market_maker_bot/market_maker_form_error_message_extensions.dart'; class SellCoinSelectDropdown extends StatelessWidget { @@ -23,6 +23,7 @@ class SellCoinSelectDropdown extends StatelessWidget { this.onTradeVolumeChanged, super.key, this.padding = EdgeInsets.zero, + this.showSellAmountSpinner = false, }); final CoinSelectInput sellCoin; final CoinTradeAmountInput sellAmount; @@ -32,6 +33,7 @@ class SellCoinSelectDropdown extends StatelessWidget { final EdgeInsets padding; final Function(Coin?)? onItemSelected; final Function(RangeValues)? onTradeVolumeChanged; + final bool showSellAmountSpinner; @override Widget build(BuildContext context) { @@ -44,30 +46,41 @@ class SellCoinSelectDropdown extends StatelessWidget { coins: coins, onItemSelected: onItemSelected, useFrontPlate: false, - trailing: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - CoinTradeAmountFormField( - coin: sellCoin.value, - initialValue: sellAmount.value, - isEnabled: false, - errorText: sellCoin.displayError?.text(sellCoin.value), - ), - Padding( - padding: const EdgeInsets.only(right: 18), - child: Text( - '* ${LocaleKeys.mmBotFirstTradeEstimate.tr()}', - style: TextStyle( - color: dexPageColors.inactiveText, - fontSize: 12, - fontWeight: FontWeight.w400, + trailing: showSellAmountSpinner + ? const Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: 18, top: 27, bottom: 27), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), - textAlign: TextAlign.end, - overflow: TextOverflow.ellipsis, + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CoinTradeAmountLabel( + coin: sellCoin.value, + value: sellAmount.valueAsRational, + errorText: sellCoin.displayError?.text(sellCoin.value), + ), + Padding( + padding: const EdgeInsets.only(right: 18), + child: Text( + '* ${LocaleKeys.mmBotFirstTradeEstimate.tr()}', + style: TextStyle( + color: dexPageColors.inactiveText, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.end, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - ), - ], - ), ), PercentageRangeSlider( title: AutoScrollText( diff --git a/lib/views/settings/widgets/security_settings/security_settings_page.dart b/lib/views/settings/widgets/security_settings/security_settings_page.dart index 0cf6016229..14f8b06599 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -245,29 +245,44 @@ class _SecuritySettingsPageState extends State { /// /// This approach provides better UX by showing loading state during the entire operation. Future onViewPrivateKeysPressed(BuildContext context) async { + // IMPORTANT: Store keys in a local variable first to avoid state loss during async operations. + // The onPasswordValidated callback executes asynchronously within WidgetsBinding.instance.addPostFrameCallback, + // and setting _sdkPrivateKeys directly inside the callback may cause the state to be lost when the widget + // rebuilds or when the dialog closes. By storing data in a local variable first and then assigning it to + // _sdkPrivateKeys AFTER the dialog closes, we ensure the state is preserved when the widget rebuilds. + Map>? fetchedKeys; + bool isEmptyKeys = false; + + // Store SDK reference before async operations to avoid BuildContext usage across async gaps + final sdk = context.sdk; + final bool success = await walletPasswordDialogWithLoading( context, onPasswordValidated: (String password) async { try { // Fetch private keys directly into local UI state // This keeps sensitive data in minimal scope - final privateKeys = await context.sdk.security.getPrivateKeys(); + final privateKeys = await sdk.security.getPrivateKeys(); + + // Check if private keys are empty (e.g., when coins haven't been activated yet) + if (privateKeys.isEmpty) { + isEmptyKeys = true; + return false; // Failure - empty private keys + } // Filter out excluded assets (NFTs only) // Geo-blocked assets are handled by the UI toggle final filteredPrivateKeyEntries = privateKeys.entries.where( (entry) => !excludedAssetList.contains(entry.key.id), ); - _sdkPrivateKeys = Map.fromEntries(filteredPrivateKeyEntries); + fetchedKeys = Map.fromEntries(filteredPrivateKeyEntries); return true; // Success } catch (e) { - // Clear sensitive data on any error + isEmptyKeys = false; // Exception occurred, not empty keys + // Clear any previously stored private keys to prevent stale data from persisting _clearPrivateKeyData(); - // Log error for debugging - debugPrint('Failed to retrieve private keys: ${e.toString()}'); - return false; // Failure } }, @@ -277,16 +292,31 @@ class _SecuritySettingsPageState extends State { passwordFieldKey: 'confirmation-showing-private-keys', ); - if (!mounted) return; + if (!mounted) { + return; + } - if (success) { + if (success && fetchedKeys != null) { + // Set the keys AFTER dialog closes to ensure state is preserved + setState(() { + _sdkPrivateKeys = fetchedKeys; + }); + // Clear the local reference to minimize the number of places holding sensitive data + // Note: fetchedKeys is a reference to the same Map object, so we only set it to null + // to remove the extra reference, not clear() which would clear the data used by _sdkPrivateKeys + fetchedKeys = null; + // Private keys are ready, show the private keys screen // ignore: use_build_context_synchronously context.read().add(const ShowPrivateKeysEvent()); } else { // Show error to user + // Check if failure was due to empty private keys + final errorMessage = isEmptyKeys + ? LocaleKeys.privateKeysEmptyError.tr() + : LocaleKeys.privateKeyRetrievalFailed.tr(); // ignore: use_build_context_synchronously - _showPrivateKeyError(context, LocaleKeys.privateKeyRetrievalFailed.tr()); + _showPrivateKeyError(context, errorMessage); } } diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_.png new file mode 100644 index 0000000000..516ee96418 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ac3c91ada6..d8259aedf6 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index c20605bcda..efb3d3ae94 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index e261c21277..9105176fa6 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 2d111e212f..27d794970b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index d42984244f..146c18ce2b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 928b301e85..714ff6dbf1 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/sdk b/sdk index 0cdff3c3d8..79216f0732 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 0cdff3c3d83c3b8f523297cc0283627cca7c4445 +Subproject commit 79216f0732abe7b08d2ef8390af51049a48a439c diff --git a/test_integration/runners/app_data.dart b/test_integration/runners/app_data.dart index 8b5cae157f..2603dd4bde 100644 --- a/test_integration/runners/app_data.dart +++ b/test_integration/runners/app_data.dart @@ -3,7 +3,7 @@ import 'dart:io'; //app data path for mac and linux -const String macAppData = '/Library/Containers/com.komodo.komodowallet'; +const String macAppData = '/Library/Containers/com.komodo.wallet'; const String linuxAppData = '/.local/share/com.komodo.KomodoWallet'; const String windowsAppData = r'\AppData\Roaming\com.komodo';