From 337efc0f40862ae02382399ae6af74898d80a448 Mon Sep 17 00:00:00 2001 From: smk762 Date: Fri, 8 Nov 2024 23:22:58 +0800 Subject: [PATCH 01/34] use gha runners --- .github/workflows/ui-tests-on-pr.yml | 4 ++-- .github/workflows/unit-tests-on-pr.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ui-tests-on-pr.yml b/.github/workflows/ui-tests-on-pr.yml index f5f4536da4..b099d8b9fb 100644 --- a/.github/workflows/ui-tests-on-pr.yml +++ b/.github/workflows/ui-tests-on-pr.yml @@ -18,10 +18,10 @@ jobs: ] include: - name: web-app-linux - os: [self-hosted, Linux] + os: ubuntu-latest - name: web-app-macos - os: [self-hosted, macos] + os: macos-14 steps: diff --git a/.github/workflows/unit-tests-on-pr.yml b/.github/workflows/unit-tests-on-pr.yml index b31ae0a9ac..dcbe8b99fe 100644 --- a/.github/workflows/unit-tests-on-pr.yml +++ b/.github/workflows/unit-tests-on-pr.yml @@ -6,7 +6,7 @@ on: jobs: build_and_preview: - runs-on: [self-hosted, Linux] + runs-on: ubuntu-latest timeout-minutes: 15 steps: From cf5849c37459e33a577525e3adb05bd0877c38ee Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 1 Oct 2024 14:29:25 +0200 Subject: [PATCH 02/34] fix(tests): failing unit and integration tests (#167) * CI self-hosted config test #1 * Fix incorrect duplicated job ID * CI test #2 * CI Test #3 Results of test #2: integration tests work when using GitHub-hosted Actions latest ubuntu * CI Test #4 * Test unit/integration tests on hosted runner Test unit/integration tests on the hosted runner to determine if the cause of the failing tests is related to differences between self-hosted and hosted environment. * Update CI conditional statements of self-hosted runners Update CI conditional statements that referenced variables only applicable to self-hosted runners. Change to the equivalent for hosted runners. * Remove very_good_analysis linting dependency Flutter is not fetching dependencies for sub-packages in the `packages` directory, so the very_good_analysis files are missing until `flutter pub get` is ran for each package * Add composite actions for asset generation and build validation * Remove chromedriver from unit tests workflow * Add missing `shell` property to the `generate-assets` action Also remove the default value, and unset the environment variable if it does not exist * Delete the `build/` directory between builds to clear AssetManifest.bin Rebuilds do not appear to consistently update AssetManifest.bin, even if there are new assets generated between builds (icons, coin configs) * Add code coverage report as artefact upload Temporary until link with 3rd-party sources like CodeCov is setup * Only fail the coverage step if the report does not exist The flutter test command failing or passing should not affect the test coverage step success/failure * Fix profit/loss unit tests Binance closing price introduces some variability in the calculated profit/loss that has to be accounted for. The margin of error can be reduced by improved price and timestamp matching, but this should suffice for now * Fix code coverage generation action I forgot the spaces in the bash condition :(. It should be "if [ ... ]; then" * Revert to using `strings` before `grep` to be platform independent macOS handles binary files differently to Linux, meaning that `grep -i ...` works with binary files on Linux systems, but not macOS * Remove trailing spaces in zip generation command Trailing spaces after the backslash in a multi-line command causes it to fail with "command not found" * Prefix artefact with runner name & move coverage step to end of ui tests * Update job names and add descriptions * Switch build_transformer over to the new sdk repo * Remove old build_transformer package * Revert changes to web/src/index.html * Add index.html to .gitignore * Move webpack JS files out of web/src * Revert changes to index.html once more :( * Untrack index.html & update build transformer package * Use default GITHUB_TOKEN to make authenticated requests * Add test case for failing binance requests * demo data generator: check if coin is supported before fetching * Add validation step for build_config.json * Fix silently failing asset manifest validation check * Add json validity checks for coin configs * Update commit sha of build transformer package * Use abstract repository class in generate demo data * Install Chrome alongside chromedriver on linux for UI tests * Skip generate demo test data unit tests * Bump build transformer to dev branch * Switch UI and unit test workflows back to self-hosted runners * Restrict coverage report to linux-based runners * Improve npm build cross-platform compatibility '&&' is invalid in Windows PowerShell. ';' works on Windows, macOS, and Linux * Move Flutter and NodeJS installation to composite action * Add github token to UI and unit test steps Seeing 403 responses on self-hosted runners * Fix UI integration tests on self-hosted macos * Export logs for safaridriver in workflows Use the --diagnose flag when starting safaridriver * Fix failing restore wallet integration test * Add verbose flag to integration test runner * Fix issues with pumpAndSettle timeout Add pumpNFrames and use that instead. This is based on the recommendation in the docs: https://api.flutter.dev/flutter/flutter_test/WidgetTester/pumpAndSettle.html * Fix missing balance overview and back button * Fix wallet & wallet manager integration tests * Fix maker & taker integration tests * Fix misc integration tests * Fix analyse warnings and remove support page test * Pin chromedriver to the self-hosted runner version 116.0.5845.96 with path /opt/google/chrome/chrome * Use mock binance repository for unit testing Using the live Binance endpoints resulted in some variance of the closing price & final calculation that caused the unit tests to fail on occasion. * Add note about the possibility of 403 error --------- Co-authored-by: CharlVS <77973576+CharlVS@users.noreply.github.com> Co-authored-by: Francois <11577022+takenagain@users.noreply.github.com> --- .github/actions/code-coverage/action.yaml | 63 +++ .github/actions/flutter-deps/action.yml | 25 + .github/actions/generate-assets/action.yml | 40 ++ .github/actions/validate-build/action.yml | 84 +++ .github/workflows/firebase-hosting-merge.yml | 83 +-- .../firebase-hosting-pull-request.yml | 76 +-- .github/workflows/ui-tests-on-pr.yml | 152 +++-- .github/workflows/unit-tests-on-pr.yml | 79 +-- .../workflows/validate-code-guidelines.yml | 25 +- .gitignore | 3 +- app_build/build_config.json | 6 +- docs/INTEGRATION_TESTING.md | 6 +- docs/PROJECT_SETUP.md | 38 ++ lib/app_config/app_config.dart | 8 +- .../mockup/generate_demo_data.dart | 10 +- .../profit_loss/profit_loss_calculator.dart | 25 +- .../simple/form/maker/maker_form_layout.dart | 2 +- .../wallet_main/wallet_overview.dart | 4 +- package.json | 8 +- .../lib/src/binance/binance.dart | 2 + .../src/binance/data/binance_provider.dart | 52 +- .../data/binance_provider_interface.dart | 65 +++ .../src/binance/data/binance_repository.dart | 7 +- packages/komodo_cex_market_data/pubspec.yaml | 2 +- packages/komodo_coin_updates/pubspec.yaml | 2 +- .../.gitignore | 3 - .../CHANGELOG.md | 3 - .../komodo_wallet_build_transformer/README.md | 1 - .../analysis_options.yaml | 29 - .../bin/komodo_wallet_build_transformer.dart | 186 ------ .../lib/src/build_step.dart | 41 -- .../coin_assets/build_progress_message.dart | 27 - .../src/steps/coin_assets/coin_ci_config.dart | 163 ------ .../coin_assets/github_download_event.dart | 11 - .../src/steps/coin_assets/github_file.dart | 78 --- .../github_file_download_event.dart | 19 - .../coin_assets/github_file_downloader.dart | 410 -------------- .../lib/src/steps/coin_assets/links.dart | 29 - .../lib/src/steps/coin_assets/result.dart | 20 - .../copy_platform_assets_build_step.dart | 114 ---- .../steps/fetch_coin_assets_build_step.dart | 294 ---------- .../src/steps/fetch_defi_api_build_step.dart | 533 ------------------ .../pubspec.lock | 426 -------------- .../pubspec.yaml | 21 - pubspec.lock | 10 +- pubspec.yaml | 18 +- run_integration_tests.dart | 250 +++++--- test_integration/common/pump_and_settle.dart | 10 +- test_integration/common/tester_utils.dart | 3 +- test_integration/helpers/restore_wallet.dart | 26 +- .../helpers/switch_coins_active_state.dart | 2 +- .../tests/dex_tests/dex_tests.dart | 32 +- .../tests/dex_tests/maker_orders_test.dart | 6 +- .../tests/dex_tests/taker_orders_test.dart | 95 ++-- .../tests/misc_tests/menu_tests.dart | 31 +- .../tests/misc_tests/misc_tests.dart | 36 +- .../tests/misc_tests/theme_test.dart | 64 ++- .../wallets_manager_create_test.dart | 30 +- .../wallets_manager_import_test.dart | 42 +- .../tests/wallets_tests/test_cex_prices.dart | 47 +- .../tests/wallets_tests/test_withdraw.dart | 37 +- .../tests/wallets_tests/wallet_tools.dart | 35 +- .../tests/wallets_tests/wallets_tests.dart | 42 +- test_units/main.dart | 2 + .../binance_repository_test.dart | 59 ++ .../generate_demo_data_test.dart | 165 +++--- .../mocks/mock_binance_provider.dart | 161 ++++++ .../mocks/mock_failing_binance_provider.dart | 33 ++ .../profit_loss_repository_test.dart | 51 +- .../transaction_generation.dart | 19 +- web/{src => }/index.js | 4 +- .../services/theme_checker/theme_checker.js | 0 web/services/zip/zip.js | 27 + web/services/zip/zip_worker.js | 19 + web/src/services/zip/zip.js | 27 - web/src/services/zip/zip_worker.js | 19 - webpack.config.js | 2 +- 77 files changed, 1432 insertions(+), 3247 deletions(-) create mode 100644 .github/actions/code-coverage/action.yaml create mode 100644 .github/actions/flutter-deps/action.yml create mode 100644 .github/actions/generate-assets/action.yml create mode 100644 .github/actions/validate-build/action.yml create mode 100644 packages/komodo_cex_market_data/lib/src/binance/data/binance_provider_interface.dart delete mode 100644 packages/komodo_wallet_build_transformer/.gitignore delete mode 100644 packages/komodo_wallet_build_transformer/CHANGELOG.md delete mode 100644 packages/komodo_wallet_build_transformer/README.md delete mode 100644 packages/komodo_wallet_build_transformer/analysis_options.yaml delete mode 100644 packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/build_step.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/build_progress_message.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/coin_ci_config.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_download_event.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_download_event.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_downloader.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/links.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/result.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/copy_platform_assets_build_step.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart delete mode 100644 packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart delete mode 100644 packages/komodo_wallet_build_transformer/pubspec.lock delete mode 100644 packages/komodo_wallet_build_transformer/pubspec.yaml create mode 100644 test_units/tests/cex_market_data/binance_repository_test.dart create mode 100644 test_units/tests/cex_market_data/mocks/mock_binance_provider.dart create mode 100644 test_units/tests/cex_market_data/mocks/mock_failing_binance_provider.dart rename web/{src => }/index.js (96%) rename web/{src => }/services/theme_checker/theme_checker.js (100%) create mode 100644 web/services/zip/zip.js create mode 100644 web/services/zip/zip_worker.js delete mode 100644 web/src/services/zip/zip.js delete mode 100644 web/src/services/zip/zip_worker.js diff --git a/.github/actions/code-coverage/action.yaml b/.github/actions/code-coverage/action.yaml new file mode 100644 index 0000000000..88ed8767a1 --- /dev/null +++ b/.github/actions/code-coverage/action.yaml @@ -0,0 +1,63 @@ +name: "Code Coverage" +description: "Generates and uploads code coverage report" +inputs: + test_file: + description: "The test file to run" + required: false + default: "test_units/main.dart" +runs: + using: "composite" + steps: + - name: Code Coverage + continue-on-error: false + shell: bash + run: | + echo "Running code coverage" + if [ "$RUNNER_OS" == "Linux" ]; then + echo "Installing lcov..." + sudo apt-get update -qq -y 2>&1 > /dev/null + sudo apt-get install lcov -y 2>&1 > /dev/null + echo "lcov has been successfully installed." + elif [ "$RUNNER_OS" == "macOS" ]; then + if ! command -v brew &> /dev/null; then + echo "Homebrew is not installed. Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + # Check if installation was successful + if [ $? -eq 0 ]; then + echo "Homebrew has been successfully installed." + else + echo "Failed to install Homebrew. Please check the error messages above and try again." + exit 1 + fi + fi + + brew install lcov + else + echo "Unsupported operating system" + exit 1 + fi + flutter test --coverage ${{ inputs.test_file }} 2>&1 > /dev/null && \ + echo "Generated code coverage report" || \ + echo "ERROR: Failed to generate code coverage report" + + echo "Generating HTML report from lcov.info..." + genhtml -q coverage/lcov.info -o coverage/html && \ + echo "Generated code coverage report" || \ + echo "ERROR: Failed to generate code coverage report" + + zip -q -r coverage-html.zip coverage/html && \ + echo "Created coverage-html.zip" || \ + echo "ERROR: Failed to compress coverage/html into a ZIP archive" + echo "Done running code coverage" + + - name: Upload Code Coverage Report + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-lcov.info + path: ./coverage/lcov.info + + - name: Upload Code Coverage HTML Report + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-coverage-html + path: ./coverage-html.zip diff --git a/.github/actions/flutter-deps/action.yml b/.github/actions/flutter-deps/action.yml new file mode 100644 index 0000000000..bc075a3fee --- /dev/null +++ b/.github/actions/flutter-deps/action.yml @@ -0,0 +1,25 @@ +name: "Flutter Dependencies" +description: "Installs Flutter and any other dependencies required for the build" +runs: + using: "composite" + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Get stable flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.22.x" + channel: "stable" + + - name: Prepare build directory + shell: bash + run: | + flutter clean + rm -rf build/* + rm -rf web/src/mm2/* + rm -rf web/src/kdfi/* + rm -rf web/dist/* + diff --git a/.github/actions/generate-assets/action.yml b/.github/actions/generate-assets/action.yml new file mode 100644 index 0000000000..fa83013f8e --- /dev/null +++ b/.github/actions/generate-assets/action.yml @@ -0,0 +1,40 @@ +name: "Generates assets" +description: "Runs the flutter build command to transform and generate assets for the deployment build" + +inputs: + GITHUB_TOKEN: + description: "The GitHub API public readonly token" + required: true +runs: + using: "composite" + steps: + - name: Fetch packages and generate assets + shell: bash + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ inputs.GITHUB_TOKEN }} + run: | + echo "Running \`flutter build\` to generate assets for the deployment build" + + if [ -n "$GITHUB_API_PUBLIC_READONLY_TOKEN" ]; then + echo "GITHUB_TOKEN provided, running flutter build with token" + else + echo "GITHUB_TOKEN not provided or empty, running flutter build without token" + unset GITHUB_API_PUBLIC_READONLY_TOKEN + fi + + flutter pub get > /dev/null 2>&1 + flutter build web --release > /dev/null 2>&1 || true + rm -rf build/* + + # Run flutter build and capture its output and exit status + flutter pub get > /dev/null 2>&1 + output=$(flutter build web --release) + exit_status=$? + + # Check if the exit status is non-zero (indicating an error) + if [ $exit_status -ne 0 ]; then + echo "Flutter build exited with status $exit_status. Output:" + echo "$output" + exit $exit_status + fi + echo "Done fetching packages and generating assets" diff --git a/.github/actions/validate-build/action.yml b/.github/actions/validate-build/action.yml new file mode 100644 index 0000000000..ebf5b8b49a --- /dev/null +++ b/.github/actions/validate-build/action.yml @@ -0,0 +1,84 @@ +name: "Validate build" +description: "Checks that all the necessary files are present in the build directory" +runs: + using: "composite" + steps: + - name: Validate build + continue-on-error: false + shell: bash + run: | + # Check that the web build folder contains a wasm file in the format build/web/dist/*.wasm + if [ ! -f build/web/dist/*.wasm ]; then + echo "Error: Web build failed. No wasm file found in build/web/dist/" + # List files for debugging + echo "Listing files in build/web recursively" + ls -R build/web + + echo "Listing files in web recursively" + ls -R web + + exit 1 + fi + + # 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 + echo "Error: Web build failed. index.html is not equal to the source index.html" + exit 1 + fi + + # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content + git update-index --no-assume-unchanged web/index.html + if git diff --quiet web/index.html; then + echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" + exit 1 + fi + + # Decode the AssetManifest.bin and check for the coin icon presence + if [ ! -f build/web/assets/AssetManifest.bin ]; then + echo "Error: AssetManifest.bin file not found." + exit 1 + fi + if ! strings build/web/assets/AssetManifest.bin | grep -qi "assets/coin_icons/png/kmd.png"; then + echo "Error: Coin icon not found in AssetManifest.bin" + echo "Output of case-invariant grep on build/web/assets/AssetManifest.bin" + strings build/web/assets/AssetManifest.bin | grep -i "assets/coin_icons/png/kmd.png" + echo "Listing kmd png files in assets/coin_icons/png" + ls -R assets/coin_icons/png | grep kmd.png + if ! strings build/web/assets/AssetManifest.bin | grep -qi "assets/coin_icons/png/kmd.png"; then + echo "Error: Coin icon not found in AssetManifest.bin" + exit 1 + fi + fi + + # Check that build/web/assets/app_build/build_config.json is present, is valid json + # and does not contain "LAST_RUN" in the first line (invalid json format) + if [ ! -f build/web/assets/app_build/build_config.json ]; then + echo "Error: build_config.json file not found." + exit 1 + fi + if ! jq . build/web/assets/app_build/build_config.json > /dev/null; then + echo "Error: build_config.json is not valid json" + exit 1 + fi + + # Check that build/web/assets/assets/config/coins.json is present, is valid json + # and does not contain "LAST_RUN" in the first line (invalid json format) + if [ ! -f build/web/assets/assets/config/coins.json ]; then + echo "Error: coins.json file not found." + exit 1 + fi + if ! jq . build/web/assets/assets/config/coins.json > /dev/null; then + echo "Error: coins.json is not valid json" + exit 1 + fi + + # Check that build/web/assets/assets/config/coins_config.json is present, is valid json + # and does not contain "LAST_RUN" in the first line (invalid json format) + if [ ! -f build/web/assets/assets/config/coins_config.json ]; then + echo "Error: coins_config.json file not found." + exit 1 + fi + if ! jq . build/web/assets/assets/config/coins_config.json > /dev/null; then + echo "Error: coins_config.json is not valid json" + exit 1 + fi diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index 846a5ca925..e09b6a40a7 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -1,8 +1,12 @@ -name: Deploy to Firebase Hosting on merge +# Deploys a Release Candidate build to Firebase Hosting (https://walletrc.web.app) when pushing/merging commits into the `dev` branch. +name: Deploy RC to Firebase Hosting on merge +run-name: ${{ github.actor }} is deploying RC build to Firebase 🚀 + on: push: branches: - dev + jobs: build_and_deploy: runs-on: ubuntu-latest @@ -21,74 +25,16 @@ jobs: - name: Setup GH Actions uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Get stable flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.22.x' - channel: 'stable' - - - name: Prepare build directory - run: | - flutter clean - rm -rf build/* - rm -rf web/src/mm2/* - rm -rf web/src/kdfi/* - rm -rf web/dist/* - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - name: Fetch packages and generate assets - run: | - echo "Running \`flutter build\` to generate assets for the deployment build" - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - flutter pub get > /dev/null 2>&1 - echo "Done fetching packages and generating assets" - - - name: Build Komodo Wallet web - run: | - flutter doctor -v - flutter build web --csp --profile --no-web-resources-cdn - + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Validate build - run: | - # Check that the web build folder contains a file with format build/web/dist/*.wasm - if [ ! -f build/web/dist/*.wasm ]; then - echo "Error: Web build failed. No wasm file found in build/web/dist/" - - echo "Listing files in build/web recursively" - ls -R build/web - - echo "Listing files in web recursively" - ls -R web - - exit 1 - fi - # 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 - echo "Error: Web build failed. index.html is not equal to the source index.html" - exit 1 - fi - # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content - if git diff --exit-code web/index.html; then - echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" - exit 1 - fi - # Decode the AssetManifest.bin and check for the coin icon presence - if [ ! -f build/web/assets/AssetManifest.bin ]; then - echo "Error: AssetManifest.bin file not found." - exit 1 - fi - if ! strings build/web/assets/AssetManifest.bin | grep -q "assets/coin_icons/png/kmd.png"; then - echo "Error: Coin icon not found in AssetManifest.bin" - exit 1 - fi + uses: ./.github/actions/validate-build - name: Deploy Komodo Wallet Web dev preview (`dev` branch) if: github.ref == 'refs/heads/dev' @@ -100,8 +46,8 @@ jobs: target: walletrc projectId: komodo-wallet-official - - name: Deploy Komodo Wallet Web RC (`master` branch) - if: github.ref == 'refs/heads/master' + - name: Deploy Komodo Wallet Web RC (`main` branch) + if: github.ref == 'refs/heads/main' uses: FirebaseExtended/action-hosting-deploy@v0.7.1 with: repoToken: '${{ secrets.GITHUB_TOKEN }}' @@ -109,3 +55,4 @@ jobs: channelId: live target: prodrc projectId: komodo-wallet-official + diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index f2b622fe8e..9c9e81b350 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -1,7 +1,7 @@ -# This file was auto-generated by the Firebase CLI -# https://github.com/firebase/firebase-tools - +# Deploys a preview build to Firebase Hosting when a pull request is opened, synchronized, or reopened name: Deploy to Firebase Hosting on PR +run-name: ${{ github.actor }} is deploying a preview build to Firebase Hosting 🚀 + on: pull_request: branches: @@ -32,72 +32,16 @@ jobs: - name: Setup GH Actions uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Get stable flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: "3.22.x" - channel: "stable" - - - name: Prepare build directory - run: | - flutter clean - rm -rf build/* - rm -rf web/src/mm2/* - rm -rf web/src/kdfi/* - rm -rf web/dist/* + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - name: Fetch packages and generate assets - run: | - echo "Running \`flutter build\` to generate assets for the deployment build" - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - flutter pub get > /dev/null 2>&1 - echo "Done fetching packages and generating assets" - - - name: Build Komodo Wallet web - run: | - flutter doctor - # https://github.com/flutter/flutter/issues/60069#issuecomment-1913588937 - flutter build web --csp --no-web-resources-cdn - + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Validate build - run: | - # Check that the web build folder contains a wasm file in the format build/web/dist/*.wasm - if [ ! -f build/web/dist/*.wasm ]; then - echo "Error: Web build failed. No wasm file found in build/web/dist/" - # List files for debugging - echo "Listing files in build/web recursively" - ls -R build/web - - echo "Listing files in web recursively" - ls -R web - - exit 1 - fi - # 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 - echo "Error: Web build failed. index.html is not equal to the source index.html" - exit 1 - fi - # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content - if git diff --exit-code web/index.html; then - echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" - exit 1 - fi - # Decode the AssetManifest.bin and check for the coin icon presence - if [ ! -f build/web/assets/AssetManifest.bin ]; then - echo "Error: AssetManifest.bin file not found." - exit 1 - fi - if ! strings build/web/assets/AssetManifest.bin | grep -q "assets/coin_icons/png/kmd.png"; then - echo "Error: Coin icon not found in AssetManifest.bin" - exit 1 - fi + uses: ./.github/actions/validate-build - name: Deploy Komodo Wallet web feature preview (Expires in 7 days) uses: FirebaseExtended/action-hosting-deploy@v0.7.1 diff --git a/.github/workflows/ui-tests-on-pr.yml b/.github/workflows/ui-tests-on-pr.yml index b099d8b9fb..880e3e5439 100644 --- a/.github/workflows/ui-tests-on-pr.yml +++ b/.github/workflows/ui-tests-on-pr.yml @@ -1,141 +1,125 @@ -name: UI flutter tests on PR +# Runs UI tests on PRs to ensure the app is working as expected +name: UI Integration tests on PR +run-name: ${{ github.actor }} is running UI tests on PR 🚀 on: pull_request: types: [opened, synchronize, reopened] jobs: - tests: + ui_tests: name: Test ${{ matrix.name }} runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 45 strategy: fail-fast: false matrix: - name: [ - web-app-linux, - web-app-macos, - ] - include: - - name: web-app-linux - os: ubuntu-latest + name: [ + web-app-linux, + web-app-macos, + ] + include: + - name: web-app-linux + os: ubuntu-latest - - name: web-app-macos - os: macos-14 + - name: web-app-macos + os: macos-14 steps: - - name: Setup GH Actions uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Install Chrome and chromedriver + if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} + uses: browser-actions/setup-chrome@v1 + id: setup-chrome with: - node-version: '18' - - - run: | - npx @puppeteer/browsers install chromedriver@stable + chrome-version: 116.0.5845.96 + install-chromedriver: true + install-dependencies: true - - name: Get stable flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.22.x' - channel: 'stable' + - name: Add chromedriver to PATH + if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} + run: | + export PATH="${{ steps.setup-chrome.outputs.chrome-path }}:${PATH}" + export PATH="${{ steps.setup-chrome.outputs.chromedriver-path }}:${PATH}" - - name: Prepare build directory + echo "Chrome version: ${{ steps.setup-chrome.outputs.chrome-version }}" + echo "chromedriver version: $(chromedriver --version)" + + - name: Enable safaridriver (sudo) (MacOS) + if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} + timeout-minutes: 1 run: | - flutter clean - rm -rf build/* - rm -rf web/src/mm2/* - rm -rf web/src/kdfi/* - rm -rf web/dist/* + defaults write com.apple.Safari IncludeDevelopMenu YES + defaults write com.apple.Safari AllowRemoteAutomation 1 + sudo /usr/bin/safaridriver --enable || echo "Failed to enable safaridriver!" + + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - name: Fetch packages and generate assets - run: | - echo "Running \`flutter build\` to generate assets for the deployment build" - flutter pub get > /dev/null 2>&1 - flutter build web --profile > /dev/null 2>&1 || true - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - echo "Done fetching packages and generating assets" + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Validate build - run: | - # Check that the web build folder contains a wasm file in the format build/web/dist/*.wasm - if [ ! -f build/web/dist/*.wasm ]; then - echo "Error: Web build failed. No wasm file found in build/web/dist/" - # List files for debugging - echo "Listing files in build/web recursively" - ls -R build/web - - echo "Listing files in web recursively" - ls -R web - - exit 1 - fi - # 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 - echo "Error: Web build failed. index.html is not equal to the source index.html" - exit 1 - fi - # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content - if git diff --exit-code web/index.html; then - echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" - exit 1 - fi - # Decode the AssetManifest.bin and check for the coin icon presence - if [ ! -f build/web/assets/AssetManifest.bin ]; then - echo "Error: AssetManifest.bin file not found." - exit 1 - fi - if ! strings build/web/assets/AssetManifest.bin | grep -q "assets/coin_icons/png/kmd.png"; then - echo "Error: Coin icon not found in AssetManifest.bin" - exit 1 - fi + uses: ./.github/actions/validate-build - name: Test air_dex chrome (unix) (Linux) - if: runner.name == 'ci-builder-radon' + if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} id: tests-chrome continue-on-error: true timeout-minutes: 35 + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chromedriver --port=4444 --silent --enable-chrome-logs --log-path=chrome_console.log & dart run_integration_tests.dart -d 'headless' -b '1600,1024' --browser-name=chrome - - name: Enable safaridriver (sudo) (MacOS) - if: runner.name == 'ci-builder-astatine' - timeout-minutes: 1 - run: | - defaults write com.apple.Safari IncludeDevelopMenu YES - defaults write com.apple.Safari AllowRemoteAutomation 1 - sudo /usr/bin/safaridriver --enable || echo "Failed to enable safaridriver!" - - name: Run safaridriver in background (MacOS) - if: runner.name == 'ci-builder-astatine' + if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} continue-on-error: true run: | - safaridriver -p 4444 & + safaridriver -p 4444 --diagnose & - name: Test air_dex safari (MacOS) - if: runner.name == 'ci-builder-astatine' + if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} id: tests-safari continue-on-error: true timeout-minutes: 35 + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | flutter pub get dart run_integration_tests.dart --browser-name=safari - name: Upload logs (Linux) - if: runner.name == 'ci-builder-radon' + if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} uses: actions/upload-artifact@v4 with: name: ${{ runner.os }}-chromedriver-logs.zip path: ./chrome_console.log + - name: Upload logs (macOS) + if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-safaridriver-logs.zip + path: ~/Library/Logs/com.apple.WebDriver/ + + - name: Generate coverage report + if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} + id: test-coverage-report + timeout-minutes: 35 + uses: ./.github/actions/code-coverage + with: + test_file: 'test_integration' + - name: Fail workflow if tests failed (Linux) - if: runner.name == 'ci-builder-radon' && steps.tests-chrome.outcome == 'failure' + if: ${{ (matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon') && steps.tests-chrome.outcome == 'failure' }} run: exit 1 - name: Fail workflow if tests failed (MacOS) - if: runner.name == 'ci-builder-astatine' && steps.tests-safari.outcome == 'failure' + if: ${{ (matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine') && steps.tests-safari.outcome == 'failure' }} run: exit 1 diff --git a/.github/workflows/unit-tests-on-pr.yml b/.github/workflows/unit-tests-on-pr.yml index dcbe8b99fe..159034ca51 100644 --- a/.github/workflows/unit-tests-on-pr.yml +++ b/.github/workflows/unit-tests-on-pr.yml @@ -1,11 +1,13 @@ +# Runs unit tests on PRs to ensure the app is working as expected name: Run unit test on PR +run-name: ${{ github.actor }} is running unit tests on PR 🚀 on: pull_request: types: [opened, synchronize, reopened] jobs: - build_and_preview: + unit_tests_: runs-on: ubuntu-latest timeout-minutes: 15 @@ -13,74 +15,29 @@ jobs: - name: Setup GH Actions uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - run: | - npx @puppeteer/browsers install chromedriver@stable - - - name: Get stable flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.22.x' - channel: 'stable' - - - name: Prepare build directory - run: | - flutter clean - rm -rf build/* - rm -rf web/src/mm2/* - rm -rf web/src/kdfi/* - rm -rf web/dist/* + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - name: Fetch packages and generate assets - run: | - echo "Running \`flutter build\` to generate assets for the deployment build" - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - echo "Done fetching packages and generating assets" + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Validate build - run: | - # Check that the web build folder contains a wasm file in the format build/web/dist/*.wasm - if [ ! -f build/web/dist/*.wasm ]; then - echo "Error: Web build failed. No wasm file found in build/web/dist/" - # List files for debugging - echo "Listing files in build/web recursively" - ls -R build/web - - echo "Listing files in web recursively" - ls -R web - - exit 1 - fi - # 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 - echo "Error: Web build failed. index.html is not equal to the source index.html" - exit 1 - fi - # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content - if git diff --exit-code web/index.html; then - echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" - exit 1 - fi - # Decode the AssetManifest.bin and check for the coin icon presence - if [ ! -f build/web/assets/AssetManifest.bin ]; then - echo "Error: AssetManifest.bin file not found." - exit 1 - fi - if ! strings build/web/assets/AssetManifest.bin | grep -q "assets/coin_icons/png/kmd.png"; then - echo "Error: Coin icon not found in AssetManifest.bin" - exit 1 - fi + uses: ./.github/actions/validate-build - name: Test unit_test (unix) id: unit_tests continue-on-error: false timeout-minutes: 15 + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | flutter test test_units/main.dart + + - name: Generate unit test coverage report + id: unit_test_coverage + timeout-minutes: 15 + uses: ./.github/actions/code-coverage + with: + test_file: 'test_units/main.dart' diff --git a/.github/workflows/validate-code-guidelines.yml b/.github/workflows/validate-code-guidelines.yml index eed0250384..d1c3899334 100644 --- a/.github/workflows/validate-code-guidelines.yml +++ b/.github/workflows/validate-code-guidelines.yml @@ -1,32 +1,29 @@ # Rule for running static analysis and code formatting checks on all PRs +# Runs static analysis and code formatting checks on all PRs to ensure the codebase is clean and consistent name: Validate Code Guidelines +run-name: ${{ github.actor }} is validating code guidelines 🚀 + on: pull_request: branches: - '*' + jobs: - build_and_deploy: + validate_code_guidelines: runs-on: ubuntu-latest steps: - - name: Setup GH Actions uses: actions/checkout@v4 - - name: Get stable flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.22.x' - channel: 'stable' + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - name: Fetch packages and generate assets - run: | - echo "Running \`flutter build\` to generate assets for the deployment build" - flutter pub get > /dev/null 2>&1 - flutter build web --release > /dev/null 2>&1 || true - flutter pub get > /dev/null 2>&1 - echo "Done fetching packages and generating assets" - + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Validate dart code run: | flutter analyze diff --git a/.gitignore b/.gitignore index 775a362c6b..5f1d07731d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ contrib/coins_config.json # Web related +web/index.html +web/src/* web/dist/*.js web/dist/*.wasm web/dist/*LICENSE.txt @@ -63,7 +65,6 @@ airdex-build.tar.gz # js node_modules - assets/config/test_wallet.json assets/**/debug_data.json contrib/coins_config.json diff --git a/app_build/build_config.json b/app_build/build_config.json index 32890506c7..54d64808bf 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -61,9 +61,9 @@ }, "coins": { "update_commit_on_build": true, - "bundled_coins_repo_commit": "bc7e8a7dcb4e3616331b4d6224b2cf46b26e0105", + "bundled_coins_repo_commit": "83794c99944ae451d7898a8db78898be7a63de8a", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", - "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", + "coins_repo_content_url": "https://komodoplatform.github.io/coins", "coins_repo_branch": "master", "runtime_updates_enabled": true, "mapped_files": { @@ -74,4 +74,4 @@ "assets/coin_icons/png/": "icons" } } -} +} \ No newline at end of file diff --git a/docs/INTEGRATION_TESTING.md b/docs/INTEGRATION_TESTING.md index 8760e8dfd0..bd94c8be66 100644 --- a/docs/INTEGRATION_TESTING.md +++ b/docs/INTEGRATION_TESTING.md @@ -37,15 +37,15 @@ #### 2.1.2. Launch the WebDriver - for Google Chrome ```bash - ./chromedriver --port=4444 --silent --enable-chrome-logs --log-path=console.log + chromedriver --port=4444 --silent --enable-chrome-logs --log-path=console.log ``` - or Firefox ```bash - ./geckodriver --port=4444 + geckodriver --port=4444 ``` - or Safari ```bash - /usr/bin/safaridriver --port=4444 + /usr/bin/safaridriver --port=4444 --diagnose ``` #### 2.1.3. Run test. From the root of the project, run the following command: diff --git a/docs/PROJECT_SETUP.md b/docs/PROJECT_SETUP.md index 79e2a2295b..cf3888cdef 100644 --- a/docs/PROJECT_SETUP.md +++ b/docs/PROJECT_SETUP.md @@ -51,3 +51,41 @@ Komodo Wallet is a cross-platform application, meaning it can be built for multi - enable `Dart: Use recommended settings` via the Command Pallette 3. Install the VSCode [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 4. Open the command palette (Ctrl+Shift+P) and run `Remote-Containers: Reopen in Container` + +## Possible Issues + +### GitHub API 403 rate limit exceeded + +If you get a 403 error when trying to build or run your app, it is likely that you have hit the [GitHub API rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). You can either wait for some time or create and add a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) to your environment variables. + +NOTE: The name of the environment variable should be `GITHUB_API_PUBLIC_READONLY_TOKEN`. + +```bash +export GITHUB_API_PUBLIC_READONLY_TOKEN= +``` + +Example of the 403 error message (more likely after multiple repeated builds): + +```bash +test@test komodo-wallet % flutter build web + +Expected to find fonts for (MaterialIcons, packages/komodo_ui_kit/Custom, packages/cupertino_icons/CupertinoIcons), but found (MaterialIcons, packages/komodo_ui_kit/Custom). This usually means you are referring to font families in an IconData class but not including them in the assets section of your pubspec.yaml, are missing the package that would include +them, or are missing "uses-material-design: true". +Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 13640 bytes (99.2% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app. +Target web_release_bundle failed: Error: User-defined transformation of asset "/Users/test/Repos/komodo/komodo-wallet/app_build/build_config.json" failed. +Transformer process terminated with non-zero exit code: 1 +Transformer package: komodo_wallet_build_transformer +Full command: /Users/test/fvm/versions/3.22.3/bin/cache/dart-sdk/bin/dart run komodo_wallet_build_transformer --input=/var/folders/p7/4z261zj174l1hw7q7q7pnc200000gn/T/flutter_tools.2WE4fK/build_config.json-transformOutput0.json --output=/var/folders/p7/4z261zj174l1hw7q7q7pnc200000gn/T/flutter_tools.2WE4fK/build_config.json-transformOutput1.json +--fetch_defi_api --fetch_coin_assets --copy_platform_assets --artifact_output_package=web_dex --config_output_path=app_build/build_config.json +stdout: +SHOUT: 2024-09-30 13:18:58.286118: Error running build steps +Exception: Failed to retrieve latest commit hash: master[403]: rate limit exceeded +#0 GithubApiProvider.getLatestCommitHash (package:komodo_wallet_build_transformer/src/steps/github/github_api_provider.dart:92:7) + +#1 FetchCoinAssetsBuildStep.canSkip (package:komodo_wallet_build_transformer/src/steps/fetch_coin_assets_build_step.dart:139:30) + +#2 _runStep (file:///Users/test/.pub-cache/git/komodo-defi-sdk-flutter-388f04296a5531c3cdad766269a3040d2b4ee9ac/packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart:224:7) + +#3 main (file:///Users/test/.pub-cache/git/komodo-defi-sdk-flutter-388f04296a5531c3cdad766269a3040d2b4ee9ac/packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart:189:9) + +``` diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 9b00f2e1d9..36657c64eb 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -22,8 +22,8 @@ const Duration kPerformanceLogInterval = Duration(minutes: 1); // 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. -String get appTitle => "Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX"; -String get appShortTitle => "Komodo Wallet"; +String get appTitle => 'Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX'; +String get appShortTitle => 'Komodo Wallet'; // We're using a hardcoded seed for the hidden login instead // of generating it on the fly. This will allow us to access @@ -113,8 +113,8 @@ List get enabledByDefaultCoins => [ 'BNB', 'AVAX', 'FTM', - if (kDebugMode || kProfileMode) 'DOC', - if (kDebugMode || kProfileMode) 'MARTY', + if (kDebugMode) 'DOC', + if (kDebugMode) 'MARTY', ]; List get enabledByDefaultTrezorCoins => [ diff --git a/lib/bloc/cex_market_data/mockup/generate_demo_data.dart b/lib/bloc/cex_market_data/mockup/generate_demo_data.dart index a50c7015fa..e715a96d60 100644 --- a/lib/bloc/cex_market_data/mockup/generate_demo_data.dart +++ b/lib/bloc/cex_market_data/mockup/generate_demo_data.dart @@ -14,7 +14,7 @@ final _ohlcvCache = >{}; /// transactions are generated in a way that the overall balance of the user /// will increase or decrease based on the given performance mode. class DemoDataGenerator { - final BinanceRepository _ohlcRepo; + final CexRepository _ohlcRepo; final int randomSeed; final List coinPairs; final Map transactionsPerMode; @@ -155,7 +155,15 @@ class DemoDataGenerator { Future>> fetchOhlcData() async { final ohlcvData = >{}; + final supportedCoins = await _ohlcRepo.getCoinList(); for (final CexCoinPair coin in coinPairs) { + final supportedCoin = supportedCoins.where( + (element) => element.id == coin.baseCoinTicker, + ); + if (supportedCoin.isEmpty) { + continue; + } + const interval = GraphInterval.oneDay; final startAt = DateTime.now().subtract(const Duration(days: 365)); diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart index 5b7b3d0eb9..ee23097411 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart @@ -5,9 +5,8 @@ import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss.dart import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; class ProfitLossCalculator { - final CexRepository _cexRepository; - ProfitLossCalculator(this._cexRepository); + final CexRepository _cexRepository; /// Get the running profit/loss for a coin based on the transactions. /// ProfitLoss = Proceeds - CostBasis @@ -19,7 +18,6 @@ class ProfitLossCalculator { /// [fiatCoinId] is id of the fiat currency tether to convert the [coinId] to. /// E.g. 'USDT'. This can be any supported coin id, but the idea is to convert /// the coin to a fiat currency to calculate the profit/loss in fiat. - /// [cexRepository] is the repository to fetch the fiat price of the coin. /// /// Returns the list of [ProfitLoss] for the coin. Future> getProfitFromTransactions( @@ -69,7 +67,7 @@ class ProfitLossCalculator { List dates, ) async { final cleanCoinId = coinId.split('-').firstOrNull?.toUpperCase() ?? ''; - return await _cexRepository.getCoinFiatPrices(cleanCoinId, dates); + return _cexRepository.getCoinFiatPrices(cleanCoinId, dates); } List _calculateProfitLosses( @@ -79,7 +77,7 @@ class ProfitLossCalculator { var state = _ProfitLossState(); final profitLosses = []; - for (var transaction in transactions) { + for (final transaction in transactions) { if (transaction.totalAmountAsDouble == 0) continue; if (transaction.isReceived) { @@ -127,7 +125,7 @@ class ProfitLossCalculator { // calculate the cost basis (formula assumes positive "total" value). var remainingToSell = transaction.balanceChange.abs(); var costBasis = 0.0; - var newHoldings = + final newHoldings = List<({double holdings, double price})>.from(state.holdings); while (remainingToSell > 0) { @@ -175,8 +173,7 @@ class ProfitLossCalculator { } class RealisedProfitLossCalculator extends ProfitLossCalculator { - RealisedProfitLossCalculator(CexRepository cexRepository) - : super(cexRepository); + RealisedProfitLossCalculator(super.cexRepository); @override double _calculateProfitLoss( @@ -188,8 +185,7 @@ class RealisedProfitLossCalculator extends ProfitLossCalculator { } class UnRealisedProfitLossCalculator extends ProfitLossCalculator { - UnRealisedProfitLossCalculator(CexRepository cexRepository) - : super(cexRepository); + UnRealisedProfitLossCalculator(super.cexRepository); @override double _calculateProfitLoss( @@ -203,15 +199,14 @@ class UnRealisedProfitLossCalculator extends ProfitLossCalculator { } class _ProfitLossState { - final List<({double holdings, double price})> holdings; - final double realizedProfitLoss; - final double totalInvestment; - final double currentHoldings; - _ProfitLossState({ List<({double holdings, double price})>? holdings, this.realizedProfitLoss = 0.0, this.totalInvestment = 0.0, this.currentHoldings = 0.0, }) : holdings = holdings ?? []; + final List<({double holdings, double price})> holdings; + final double realizedProfitLoss; + final double totalInvestment; + final double currentHoldings; } diff --git a/lib/views/dex/simple/form/maker/maker_form_layout.dart b/lib/views/dex/simple/form/maker/maker_form_layout.dart index ecb292d64e..0dcb78e24a 100644 --- a/lib/views/dex/simple/form/maker/maker_form_layout.dart +++ b/lib/views/dex/simple/form/maker/maker_form_layout.dart @@ -43,7 +43,7 @@ class _MakerFormLayoutState extends State { builder: (context, snapshot) { if (snapshot.data == true) { return MakerOrderConfirmation( - onCreateOrder: () => bloc.add(const TabChanged(1)), + onCreateOrder: () => bloc.add(const TabChanged(2)), onCancel: () { makerFormBloc.showConfirmation = false; }, diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart index 837cc09314..19c64ebe76 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -10,6 +10,7 @@ import 'package:web_dex/model/coin.dart'; class WalletOverview extends StatelessWidget { const WalletOverview({ + super.key, this.onPortfolioGrowthPressed, this.onPortfolioProfitLossPressed, }); @@ -28,7 +29,7 @@ class WalletOverview extends StatelessWidget { final portfolioAssetsOverviewBloc = context.watch(); - int assetCount = coins.length; + final int assetCount = coins.length; final stateWithData = portfolioAssetsOverviewBloc.state is PortfolioAssetsOverviewLoadSuccess @@ -42,6 +43,7 @@ class WalletOverview extends StatelessWidget { FractionallySizedBox( widthFactor: isMobile ? 1 : 0.5, child: StatisticCard( + key: const Key('overview-total-balance'), caption: Text(LocaleKeys.allTimeInvestment.tr()), value: stateWithData?.totalInvestment.value ?? 0, actionIcon: const Icon(CustomIcons.fiatIconCircle), diff --git a/package.json b/package.json index ae583c6f10..020e5aa553 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "test": "test" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "npx webpack --config=webpack.config.js --mode=production", - "build:dev": "npx webpack --config=webpack.config.js --mode=development" + "test": "echo \"Error: no test specified\" ; exit 1", + "build": "git update-index --assume-unchanged web/index.html ; npx webpack --config=webpack.config.js --mode=production", + "build:dev": "git update-index --assume-unchanged web/index.html ; npx webpack --config=webpack.config.js --mode=development" }, "repository": { "type": "git", @@ -27,4 +27,4 @@ "dependencies": { "jszip": "^3.10.1" } -} +} \ No newline at end of file diff --git a/packages/komodo_cex_market_data/lib/src/binance/binance.dart b/packages/komodo_cex_market_data/lib/src/binance/binance.dart index 8b1c2bc553..a7aea8fd6c 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/binance.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/binance.dart @@ -1,6 +1,8 @@ export 'data/binance_provider.dart'; +export 'data/binance_provider_interface.dart'; export 'data/binance_repository.dart'; export 'models/binance_exchange_info.dart'; export 'models/filter.dart'; export 'models/rate_limit.dart'; export 'models/symbol.dart'; +export 'models/symbol_reduced.dart'; diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart index 946769c3c9..2354802397 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider.dart @@ -1,12 +1,13 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:komodo_cex_market_data/src/binance/data/binance_provider_interface.dart'; import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info.dart'; import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart'; import 'package:komodo_cex_market_data/src/models/coin_ohlc.dart'; /// A provider class for fetching data from the Binance API. -class BinanceProvider { +class BinanceProvider implements IBinanceProvider { /// Creates a new BinanceProvider instance. const BinanceProvider({this.apiUrl = 'https://api.binance.com/api/v3'}); @@ -14,40 +15,7 @@ class BinanceProvider { /// Defaults to 'https://api.binance.com/api/v3'. final String apiUrl; - /// Fetches candlestick chart data from Binance API. - /// - /// Retrieves the candlestick chart data for a specific symbol and interval - /// from the Binance API. - /// Optionally, you can specify the start time, end time, and limit of the - /// data to fetch. - /// - /// Parameters: - /// - [symbol]: The trading symbol for which to fetch the candlestick - /// chart data. - /// - [interval]: The time interval for the candlestick chart data - /// (e.g., '1m', '1h', '1d'). - /// - [startTime]: The start time (in milliseconds since epoch, Unix time) of - /// the data range to fetch (optional). - /// - [endTime]: The end time (in milliseconds since epoch, Unix time) of the - /// data range to fetch (optional). - /// - [limit]: The maximum number of data points to fetch (optional). Defaults - /// to 500, maximum is 1000. - /// - /// Returns: - /// A [Future] that resolves to a [CoinOhlc] object containing the fetched - /// candlestick chart data. - /// - /// Example usage: - /// ```dart - /// final BinanceKlinesResponse klines = await fetchKlines( - /// 'BTCUSDT', - /// '1h', - /// limit: 100, - /// ); - /// ``` - /// - /// Throws: - /// - [Exception] if the API request fails. + @override Future fetchKlines( String symbol, String interval, { @@ -77,15 +45,13 @@ class BinanceProvider { ); } else { throw Exception( - 'Failed to load klines: ${response.statusCode} ${response.body}', + 'Failed to load klines for \'$symbol\': ' + '${response.statusCode} ${response.body}', ); } } - /// Fetches the exchange information from Binance. - /// - /// Returns a [Future] that resolves to a [BinanceExchangeInfoResponse] object - /// Throws an [Exception] if the request fails. + @override Future fetchExchangeInfo({ String? baseUrl, }) async { @@ -103,11 +69,7 @@ class BinanceProvider { } } - /// Fetches the exchange information from Binance. - /// - /// Returns a [Future] that resolves to a [BinanceExchangeInfoResponseReduced] - /// object. - /// Throws an [Exception] if the request fails. + @override Future fetchExchangeInfoReduced({ String? baseUrl, }) async { diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider_interface.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider_interface.dart new file mode 100644 index 0000000000..2238abfd5b --- /dev/null +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_provider_interface.dart @@ -0,0 +1,65 @@ +import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info.dart'; +import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart'; +import 'package:komodo_cex_market_data/src/models/coin_ohlc.dart'; + +abstract class IBinanceProvider { + /// Fetches candlestick chart data from Binance API. + /// + /// Retrieves the candlestick chart data for a specific symbol and interval + /// from the Binance API. + /// Optionally, you can specify the start time, end time, and limit of the + /// data to fetch. + /// + /// Parameters: + /// - [symbol]: The trading symbol for which to fetch the candlestick + /// chart data. + /// - [interval]: The time interval for the candlestick chart data + /// (e.g., '1m', '1h', '1d'). + /// - [startTime]: The start time (in milliseconds since epoch, Unix time) of + /// the data range to fetch (optional). + /// - [endTime]: The end time (in milliseconds since epoch, Unix time) of the + /// data range to fetch (optional). + /// - [limit]: The maximum number of data points to fetch (optional). Defaults + /// to 500, maximum is 1000. + /// + /// Returns: + /// A [Future] that resolves to a [CoinOhlc] object containing the fetched + /// candlestick chart data. + /// + /// Example usage: + /// ```dart + /// final BinanceKlinesResponse klines = await fetchKlines( + /// 'BTCUSDT', + /// '1h', + /// limit: 100, + /// ); + /// ``` + /// + /// Throws: + /// - [Exception] if the API request fails. + Future fetchKlines( + String symbol, + String interval, { + int? startUnixTimestampMilliseconds, + int? endUnixTimestampMilliseconds, + int? limit, + String? baseUrl, + }); + + /// Fetches the exchange information from Binance. + /// + /// Returns a [Future] that resolves to a [BinanceExchangeInfoResponse] object + /// Throws an [Exception] if the request fails. + Future fetchExchangeInfo({ + String? baseUrl, + }); + + /// Fetches the exchange information from Binance. + /// + /// Returns a [Future] that resolves to a [BinanceExchangeInfoResponseReduced] + /// object. + /// Throws an [Exception] if the request fails. + Future fetchExchangeInfoReduced({ + String? baseUrl, + }); +} diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart index 5e302316ca..1bfaf2d8b5 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart @@ -1,6 +1,7 @@ // Using relative imports in this "package" to make it easier to track external // dependencies when moving or copying this "package" to another project. import 'package:komodo_cex_market_data/src/binance/data/binance_provider.dart'; +import 'package:komodo_cex_market_data/src/binance/data/binance_provider_interface.dart'; import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart'; import 'package:komodo_cex_market_data/src/cex_repository.dart'; import 'package:komodo_cex_market_data/src/models/models.dart'; @@ -18,10 +19,10 @@ BinanceRepository binanceRepository = BinanceRepository( /// This class provides methods to fetch legacy tickers and OHLC candle data. class BinanceRepository implements CexRepository { /// Creates a new [BinanceRepository] instance. - BinanceRepository({required BinanceProvider binanceProvider}) + BinanceRepository({required IBinanceProvider binanceProvider}) : _binanceProvider = binanceProvider; - final BinanceProvider _binanceProvider; + final IBinanceProvider _binanceProvider; List? _cachedCoinsList; @@ -50,7 +51,7 @@ class BinanceRepository implements CexRepository { try { return await callback(binanceApiEndpoint.elementAt(i)); } catch (e) { - if (i >= binanceApiEndpoint.length) { + if (i >= (binanceApiEndpoint.length - 1)) { rethrow; } } diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index 8f73e78c17..f61bfb7985 100644 --- a/packages/komodo_cex_market_data/pubspec.yaml +++ b/packages/komodo_cex_market_data/pubspec.yaml @@ -8,7 +8,7 @@ environment: # Add regular dependencies here. dependencies: - http: 0.13.6 # dart.dev + http: 1.2.2 # dart.dev equatable: git: diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index eda1a1fc7d..c6ed90e2a2 100644 --- a/packages/komodo_coin_updates/pubspec.yaml +++ b/packages/komodo_coin_updates/pubspec.yaml @@ -8,7 +8,7 @@ environment: # Add regular dependencies here. dependencies: - http: 0.13.6 # dart.dev + http: 1.2.2 # dart.dev komodo_persistence_layer: path: ../komodo_persistence_layer/ diff --git a/packages/komodo_wallet_build_transformer/.gitignore b/packages/komodo_wallet_build_transformer/.gitignore deleted file mode 100644 index 3a85790408..0000000000 --- a/packages/komodo_wallet_build_transformer/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ diff --git a/packages/komodo_wallet_build_transformer/CHANGELOG.md b/packages/komodo_wallet_build_transformer/CHANGELOG.md deleted file mode 100644 index effe43c82c..0000000000 --- a/packages/komodo_wallet_build_transformer/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 1.0.0 - -- Initial version. diff --git a/packages/komodo_wallet_build_transformer/README.md b/packages/komodo_wallet_build_transformer/README.md deleted file mode 100644 index b7639b54b7..0000000000 --- a/packages/komodo_wallet_build_transformer/README.md +++ /dev/null @@ -1 +0,0 @@ -A sample command-line application providing basic argument parsing with an entrypoint in `bin/`. diff --git a/packages/komodo_wallet_build_transformer/analysis_options.yaml b/packages/komodo_wallet_build_transformer/analysis_options.yaml deleted file mode 100644 index 204f8fb329..0000000000 --- a/packages/komodo_wallet_build_transformer/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:flutter_lints/flutter.yaml - -# Uncomment the following section to specify additional rules. - -linter: - rules: - - require_trailing_commas: true -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart b/packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart deleted file mode 100644 index 8cf12d79c1..0000000000 --- a/packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart +++ /dev/null @@ -1,186 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; -import 'dart:convert'; -import 'package:args/args.dart'; -import 'package:komodo_wallet_build_transformer/src/build_step.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/copy_platform_assets_build_step.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/fetch_coin_assets_build_step.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/fetch_defi_api_build_step.dart'; - -const String version = '0.0.1'; -const inputOptionName = 'input'; -const outputOptionName = 'output'; - -late final ArgResults _argResults; -final String _projectRoot = Directory.current.path; - -/// Defines the build steps that should be executed. Only the build steps that -/// pass the command line flags will be executed. For Flutter transformers, -/// this is configured in the root project's `pubspec.yaml` file. -/// The steps are executed in the order they are defined in this list. -List _buildStepBootstrapper(Map buildConfig) => [ - // TODO: Refactor to use data model classes instead of Map - - FetchDefiApiStep.withBuildConfig(buildConfig), - FetchCoinAssetsBuildStep.withBuildConfig(buildConfig), - CopyPlatformAssetsBuildStep(projectRoot: _projectRoot), - ]; - -const List _knownBuildStepIds = [ - FetchDefiApiStep.idStatic, - FetchCoinAssetsBuildStep.idStatic, - CopyPlatformAssetsBuildStep.idStatic, -]; - -ArgParser buildParser() { - final parser = ArgParser() - ..addOption(inputOptionName, mandatory: true, abbr: 'i') - ..addOption(outputOptionName, mandatory: true, abbr: 'o') - ..addFlag('concurrent', - abbr: 'c', - negatable: false, - help: 'Run build steps concurrently if possible.') - ..addFlag('help', - abbr: 'h', negatable: false, help: 'Print this usage information.') - ..addFlag('verbose', - abbr: 'v', negatable: false, help: 'Show additional command output.') - ..addFlag('version', negatable: false, help: 'Print the tool version.') - ..addFlag('all', abbr: 'a', negatable: false, help: 'Run all build steps.'); - - for (final id in _knownBuildStepIds) { - parser.addFlag( - id, - negatable: false, - help: - 'Run the $id build step. Must provide at least one build step flag or specify -all.', - ); - } - - return parser; -} - -void printUsage(ArgParser argParser) { - print('Usage: dart komodo_wallet_build_transformer.dart [arguments]'); - print(argParser.usage); -} - -Map loadJsonFile(String path) { - final file = File(path); - if (!file.existsSync()) { - _logMessage('Json file not found: $path', error: true); - throw Exception('Json file not found: $path'); - } - final content = file.readAsStringSync(); - return jsonDecode(content); -} - -void main(List arguments) async { - final ArgParser argParser = buildParser(); - try { - _argResults = argParser.parse(arguments); - - if (_argResults.flag('help')) { - printUsage(argParser); - return; - } - if (_argResults.flag('version')) { - _logMessage('komodo_wallet_build_transformer version: $version'); - return; - } - - final canRunConcurrent = _argResults.flag('concurrent'); - // final configFile = File('$_projectRoot/app_build/build_config.json'); - final configFile = File(_argResults.option('input')!); - if (!configFile.existsSync()) { - throw Exception( - 'Config file not found: ${configFile.path}. Trying project asset folder...'); - } - final config = json.decode(configFile.readAsStringSync()); - - final steps = _buildStepBootstrapper(config); - - if (steps.length != _knownBuildStepIds.length) { - throw Exception('Mismatch between build steps and known build step ids'); - } - - final buildStepFutures = steps - .where((step) => _argResults.flag('all') || _argResults.flag(step.id)) - .map((step) => _runStep(step, config)); - - _logMessage('${buildStepFutures.length} build steps to run'); - - if (canRunConcurrent) { - await Future.wait(buildStepFutures); - } else { - for (final future in buildStepFutures) { - await future; - } - } - - _writeSuccessStatus(); - _logMessage('Build steps completed'); - exit(0); - } on FormatException catch (e) { - _logMessage(e.message, error: true); - _logMessage(''); - printUsage(argParser); - exit(64); - } catch (e) { - _logMessage('Error running build steps: ${e.toString()}', error: true); - exit(1); - } -} - -Future _runStep(BuildStep step, Map config) async { - final stepName = step.runtimeType.toString(); - - if (await step.canSkip()) { - _logMessage('$stepName: Skipping build step'); - return; - } - - try { - _logMessage('$stepName: Running build step'); - final timer = Stopwatch()..start(); - - await step.build(); - - _logMessage( - '$stepName: Build step completed in ${timer.elapsedMilliseconds}ms'); - } catch (e) { - _logMessage('$stepName: Error running build step: ${e.toString()}', - error: true); - - await step - .revert((e is Exception) ? e : null) - .catchError((revertError) => _logMessage( - '$stepName: Error reverting build step: $revertError', - )); - - rethrow; - } -} - -/// A function that signals the Flutter asset transformer completed -/// successfully by copying the input file to the output file. -/// -/// This is used because Flutter's asset transformers require an output file -/// to be created in order for the step to be considered successful. -/// -/// NB! The input and output file paths do not refer to the file in our -/// project's assets directory, but rather the a copy that is created by -/// Flutter's asset transformer. -/// -void _writeSuccessStatus() { - final input = File(_argResults.option(inputOptionName)!); - final output = File(_argResults.option(outputOptionName)!); - input.copySync(output.path); -} - -// TODO: Consider how the verbose flag should influence logging -void _logMessage(String message, {bool error = false}) { - final prefix = error ? 'ERROR' : 'INFO'; - final output = error ? stderr : stdout; - output.writeln('[$prefix] $message'); -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/build_step.dart deleted file mode 100644 index 98b875f51f..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/build_step.dart +++ /dev/null @@ -1,41 +0,0 @@ -/// Example usage: -/// -/// class ExampleBuildStep extends BuildStep { -/// @override -/// Future build() async { -/// final File tempFile = File('${tempWorkingDir.path}/temp.txt'); -/// tempFile.createSync(recursive: true); -/// -/// /// Create a demo empty text file in the assets directory. -/// final File newAssetFile = File('${assetsDir.path}/empty.txt'); -/// newAssetFile.createSync(recursive: true); -/// } -/// -/// @override -/// bool canSkip() { -/// return false; -/// } -/// -/// @override -/// Future revert() async { -/// await Future.delayed(Duration.zero); -/// } -/// } -abstract class BuildStep { - /// A unique identifier for this build step. - String get id; - - /// Execute the build step. This should return a future that completes when - /// the build step is done. - Future build(); - - /// Whether this build step can be skipped if the output artifact already - /// exists. E.g. We don't want to re-download a file if we already have the - /// correct version. - Future canSkip(); - - /// Revert the environment to the state it was in before the build step was - /// executed. This will be called internally by the build system if a build - /// step fails. - Future revert([Exception? e]); -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/build_progress_message.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/build_progress_message.dart deleted file mode 100644 index c512982cef..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/build_progress_message.dart +++ /dev/null @@ -1,27 +0,0 @@ -/// Represents a build progress message. -class BuildProgressMessage { - /// Creates a new instance of [BuildProgressMessage]. - /// - /// The [message] parameter represents the message of the progress. - /// The [progress] parameter represents the progress value. - /// The [success] parameter indicates whether the progress was successful or not. - /// The [finished] parameter indicates whether the progress is finished. - const BuildProgressMessage({ - required this.message, - required this.progress, - required this.success, - this.finished = false, - }); - - /// The message of the progress. - final String message; - - /// Indicates whether the progress was successful or not. - final bool success; - - /// The progress value (percentage). - final double progress; - - /// Indicates whether the progress is finished. - final bool finished; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/coin_ci_config.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/coin_ci_config.dart deleted file mode 100644 index 10f1850363..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/coin_ci_config.dart +++ /dev/null @@ -1,163 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_file_downloader.dart'; -import 'package:path/path.dart' as path; - -/// Represents the build configuration for fetching coin assets. -class CoinCIConfig { - /// Creates a new instance of [CoinCIConfig]. - CoinCIConfig({ - required this.bundledCoinsRepoCommit, - required this.updateCommitOnBuild, - required this.coinsRepoApiUrl, - required this.coinsRepoContentUrl, - required this.coinsRepoBranch, - required this.runtimeUpdatesEnabled, - required this.mappedFiles, - required this.mappedFolders, - }); - - /// Creates a new instance of [CoinCIConfig] from a JSON object. - factory CoinCIConfig.fromJson(Map json) { - return CoinCIConfig( - updateCommitOnBuild: json['update_commit_on_build'] as bool, - bundledCoinsRepoCommit: json['bundled_coins_repo_commit'].toString(), - coinsRepoApiUrl: json['coins_repo_api_url'].toString(), - coinsRepoContentUrl: json['coins_repo_content_url'].toString(), - coinsRepoBranch: json['coins_repo_branch'].toString(), - runtimeUpdatesEnabled: json['runtime_updates_enabled'] as bool, - mappedFiles: Map.from( - json['mapped_files'] as Map, - ), - mappedFolders: Map.from( - json['mapped_folders'] as Map, - ), - ); - } - - /// The commit hash or branch coins repository to use when fetching coin - /// assets. - final String bundledCoinsRepoCommit; - - /// Indicates whether the commit hash should be updated on build. If `true`, - /// the commit hash will be updated and saved to the build configuration file. - /// If `false`, the commit hash will not be updated and the configured commit - /// hash will be used. - final bool updateCommitOnBuild; - - /// The GitHub API of the coins repository used to fetch directory contents - /// with SHA hashes from the GitHub API. - final String coinsRepoApiUrl; - - /// The raw content GitHub URL of the coins repository used to fetch assets. - final String coinsRepoContentUrl; - - /// The branch of the coins repository to use for fetching assets. - final String coinsRepoBranch; - - /// Indicates whether runtime updates of the coins assets are enabled. - /// - /// NB: This does not affect the build process. - final bool runtimeUpdatesEnabled; - - /// A map of mapped files to download. - /// The keys represent the local paths where the files will be saved, - /// and the values represent the relative paths of the files in the repository - final Map mappedFiles; - - /// A map of mapped folders to download. The keys represent the local paths - /// where the folders will be saved, and the values represent the - /// corresponding paths in the GitHub repository. - final Map mappedFolders; - - CoinCIConfig copyWith({ - String? bundledCoinsRepoCommit, - bool? updateCommitOnBuild, - String? coinsRepoApiUrl, - String? coinsRepoContentUrl, - String? coinsRepoBranch, - bool? runtimeUpdatesEnabled, - Map? mappedFiles, - Map? mappedFolders, - }) { - return CoinCIConfig( - updateCommitOnBuild: updateCommitOnBuild ?? this.updateCommitOnBuild, - bundledCoinsRepoCommit: - bundledCoinsRepoCommit ?? this.bundledCoinsRepoCommit, - coinsRepoApiUrl: coinsRepoApiUrl ?? this.coinsRepoApiUrl, - coinsRepoContentUrl: coinsRepoContentUrl ?? this.coinsRepoContentUrl, - coinsRepoBranch: coinsRepoBranch ?? this.coinsRepoBranch, - runtimeUpdatesEnabled: - runtimeUpdatesEnabled ?? this.runtimeUpdatesEnabled, - mappedFiles: mappedFiles ?? this.mappedFiles, - mappedFolders: mappedFolders ?? this.mappedFolders, - ); - } - - /// Converts the [CoinCIConfig] instance to a JSON object. - Map toJson() => { - 'update_commit_on_build': updateCommitOnBuild, - 'bundled_coins_repo_commit': bundledCoinsRepoCommit, - 'coins_repo_api_url': coinsRepoApiUrl, - 'coins_repo_content_url': coinsRepoContentUrl, - 'coins_repo_branch': coinsRepoBranch, - 'runtime_updates_enabled': runtimeUpdatesEnabled, - 'mapped_files': mappedFiles, - 'mapped_folders': mappedFolders, - }; - - /// Loads the coins runtime update configuration synchronously from the specified [path]. - /// - /// Prints the path from which the configuration is being loaded. - /// Reads the contents of the file at the specified path and decodes it as JSON. - /// If the 'coins' key is not present in the decoded data, prints an error message and exits with code 1. - /// Returns a [CoinCIConfig] object created from the decoded 'coins' data. - static CoinCIConfig loadSync(String path) { - print('Loading coins updates config from $path'); - - try { - final File file = File(path); - final String contents = file.readAsStringSync(); - final Map data = - jsonDecode(contents) as Map; - - return CoinCIConfig.fromJson(data['coins']); - } catch (e) { - print('Error loading coins updates config: $e'); - throw Exception('Error loading coins update config'); - } - } - - /// Saves the coins configuration to the specified asset path and optionally updates the build configuration file. - /// - /// The [assetPath] parameter specifies the path where the coins configuration will be saved. - /// The [updateBuildConfig] parameter indicates whether to update the build configuration file or not. - /// - /// If [updateBuildConfig] is `true`, the coins configuration will also be saved to the build configuration file specified by [buildConfigPath]. - /// - /// If [originalBuildConfig] is provided, the coins configuration will be merged with the original build configuration before saving. - /// - /// Throws an exception if any error occurs during the saving process. - Future save({ - required String assetPath, - Map? originalBuildConfig, - }) async { - final List foldersToCreate = [ - path.dirname(assetPath), - ]; - createFolders(foldersToCreate); - - final mergedConfig = (originalBuildConfig ?? {}) - ..addAll({'coins': toJson()}); - - print('Saving coin assets config to $assetPath'); - const encoder = JsonEncoder.withIndent(" "); - - final String data = encoder.convert(mergedConfig); - await File(assetPath).writeAsString(data, flush: true); - } -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_download_event.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_download_event.dart deleted file mode 100644 index 7c1ab31e39..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_download_event.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// Enum representing the events that can occur during a GitHub download. -enum GitHubDownloadEvent { - /// The download was successful. - downloaded, - - /// The download was skipped. - skipped, - - /// The download failed. - failed, -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file.dart deleted file mode 100644 index cf3084b057..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/links.dart'; - -/// Represents a file on GitHub. -class GitHubFile { - /// Creates a new instance of [GitHubFile]. - const GitHubFile({ - required this.name, - required this.path, - required this.sha, - required this.size, - this.url, - this.htmlUrl, - this.gitUrl, - required this.downloadUrl, - required this.type, - this.links, - }); - - /// Creates a new instance of [GitHubFile] from a JSON map. - factory GitHubFile.fromJson(Map data) => GitHubFile( - name: data['name'] as String, - path: data['path'] as String, - sha: data['sha'] as String, - size: data['size'] as int, - url: data['url'] as String?, - htmlUrl: data['html_url'] as String?, - gitUrl: data['git_url'] as String?, - downloadUrl: data['download_url'] as String, - type: data['type'] as String, - links: data['_links'] == null - ? null - : Links.fromJson(data['_links'] as Map), - ); - - /// Converts the [GitHubFile] instance to a JSON map. - Map toJson() => { - 'name': name, - 'path': path, - 'sha': sha, - 'size': size, - 'url': url, - 'html_url': htmlUrl, - 'git_url': gitUrl, - 'download_url': downloadUrl, - 'type': type, - '_links': links?.toJson(), - }; - - /// The name of the file. - final String name; - - /// The path of the file. - final String path; - - /// The SHA value of the file. - final String sha; - - /// The size of the file in bytes. - final int size; - - /// The URL of the file. - final String? url; - - /// The HTML URL of the file. - final String? htmlUrl; - - /// The Git URL of the file. - final String? gitUrl; - - /// The download URL of the file. - final String downloadUrl; - - /// The type of the file. - final String type; - - /// The links associated with the file. - final Links? links; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_download_event.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_download_event.dart deleted file mode 100644 index 10c8d57c7a..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_download_event.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_download_event.dart'; - -/// Represents an event for downloading a GitHub file. -/// -/// This event contains information about the download event and the local path where the file will be saved. -/// Represents an event for downloading a GitHub file. -class GitHubFileDownloadEvent { - /// Creates a new [GitHubFileDownloadEvent] with the specified [event] and [localPath]. - GitHubFileDownloadEvent({ - required this.event, - required this.localPath, - }); - - /// The download event. - final GitHubDownloadEvent event; - - /// The local path where the file will be saved. - final String localPath; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_downloader.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_downloader.dart deleted file mode 100644 index 160e01dbed..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/github_file_downloader.dart +++ /dev/null @@ -1,410 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:crypto/crypto.dart'; -import 'package:http/http.dart' as http; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/build_progress_message.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_download_event.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_file.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_file_download_event.dart'; -import 'package:path/path.dart' as path; - -/// A class that handles downloading files from a GitHub repository. -class GitHubFileDownloader { - /// The [GitHubFileDownloader] class requires the [repoApiUrl] and [repoContentUrl] - /// parameters to be provided during initialization. These parameters specify the - /// API URL and content URL of the GitHub repository from which files will be downloaded. - GitHubFileDownloader({ - required this.repoApiUrl, - required this.repoContentUrl, - this.sendPort, - }); - - final String repoApiUrl; - final String repoContentUrl; - final SendPort? sendPort; - - int _totalFiles = 0; - int _downloadedFiles = 0; - int _skippedFiles = 0; - - double get progress => - ((_downloadedFiles + _skippedFiles) / _totalFiles) * 100; - String get progressMessage => 'Progress: ${progress.toStringAsFixed(2)}%'; - String get downloadStats => - 'Downloaded $_downloadedFiles files, skipped $_skippedFiles files'; - - Future download( - String repoCommit, - Map mappedFiles, - Map mappedFolders, - ) async { - await downloadMappedFiles(repoCommit, mappedFiles); - await downloadMappedFolders(repoCommit, mappedFolders); - } - - /// Retrieves the latest commit hash for a given branch from the repository API. - /// - /// The [branch] parameter specifies the branch name for which to retrieve the latest commit hash. - /// By default, it is set to 'master'. - /// - /// Returns a [Future] that completes with a [String] representing the latest commit hash. - Future getLatestCommitHash({ - String branch = 'master', - }) async { - final String apiUrl = '$repoApiUrl/commits/$branch'; - final http.Response response = await http.get(Uri.parse(apiUrl)); - final Map data = - jsonDecode(response.body) as Map; - return data['sha'] as String; - } - - /// Downloads and saves multiple files from a remote repository. - /// - /// The [repoCommit] parameter specifies the commit hash of the repository. - /// The [mappedFiles] parameter is a map where the keys represent the local paths - /// where the files will be saved, and the values represent the relative paths - /// of the files in the repository. - /// - /// This method creates the necessary folders for the local paths and then - /// iterates over each entry in the [mappedFiles] map. For each entry, it - /// retrieves the file content from the remote repository using the provided - /// commit hash and relative path, and saves it to the corresponding local path. - /// - /// Throws an exception if any error occurs during the download or file saving process. - Future downloadMappedFiles( - String repoCommit, - Map mappedFiles, - ) async { - _totalFiles += mappedFiles.length; - - createFolders(mappedFiles.keys.toList()); - for (final MapEntry entry in mappedFiles.entries) { - final String localPath = entry.key; - final Uri fileContentUrl = - Uri.parse('$repoContentUrl/$repoCommit/${entry.value}'); - final http.Response fileContent = await http.get(fileContentUrl); - await File(localPath).writeAsString(fileContent.body); - - _downloadedFiles++; - sendPort?.send( - BuildProgressMessage( - message: 'Downloading file: $localPath', - progress: progress, - success: true, - ), - ); - } - } - - /// Downloads the mapped folders from a GitHub repository at a specific commit. - /// - /// The [repoCommit] parameter specifies the commit hash of the repository. - /// The [mappedFolders] parameter is a map where the keys represent the local paths - /// where the files will be downloaded, and the values represent the corresponding - /// paths in the GitHub repository. - /// The [timeout] parameter specifies the maximum duration for the download operation. - /// - /// This method iterates over each entry in the [mappedFolders] map and creates the - /// necessary local folders. Then, it retrieves the list of files in the GitHub - /// repository at the specified [repoPath] and [repoCommit]. For each file, it - /// initiates a download using the [downloadFile] method. The downloads are executed - /// concurrently using [Future.wait]. - /// - /// Throws an exception if any of the download operations fail. - Future downloadMappedFolders( - String repoCommit, - Map mappedFolders, { - Duration timeout = const Duration(seconds: 60), - }) async { - final Map> folderContents = - await _getMappedFolderContents(mappedFolders, repoCommit); - - for (final MapEntry> entry - in folderContents.entries) { - await _downloadFolderContents(entry.key, entry.value); - } - - sendPort?.send( - const BuildProgressMessage( - message: '\nDownloaded all files', - progress: 100, - success: true, - finished: true, - ), - ); - } - - Future _downloadFolderContents( - String key, - List value, - ) async { - await for (final GitHubFileDownloadEvent event - in downloadFiles(value, key)) { - switch (event.event) { - case GitHubDownloadEvent.downloaded: - _downloadedFiles++; - sendProgressMessage( - 'Downloading file: ${event.localPath}', - success: true, - ); - case GitHubDownloadEvent.skipped: - _skippedFiles++; - sendProgressMessage( - 'Skipped file: ${event.localPath}', - success: true, - ); - case GitHubDownloadEvent.failed: - sendProgressMessage( - 'Failed to download file: ${event.localPath}', - ); - } - } - } - - Future>> _getMappedFolderContents( - Map mappedFolders, - String repoCommit, - ) async { - final Map> folderContents = {}; - - for (final MapEntry entry in mappedFolders.entries) { - createFolders(mappedFolders.keys.toList()); - final String localPath = entry.key; - final String repoPath = entry.value; - final List coins = - await getGitHubDirectoryContents(repoPath, repoCommit); - - _totalFiles += coins.length; - folderContents[localPath] = coins; - } - return folderContents; - } - - /// Retrieves the contents of a GitHub directory for a given repository and commit. - /// - /// The [repoPath] parameter specifies the path of the directory within the repository. - /// The [repoCommit] parameter specifies the commit hash or branch name. - /// - /// Returns a [Future] that completes with a list of [GitHubFile] objects representing the files in the directory. - Future> getGitHubDirectoryContents( - String repoPath, - String repoCommit, - ) async { - final Map headers = { - 'Accept': 'application/vnd.github.v3+json', - }; - final String apiUrl = '$repoApiUrl/contents/$repoPath?ref=$repoCommit'; - - final http.Request req = http.Request('GET', Uri.parse(apiUrl)); - req.headers.addAll(headers); - final http.StreamedResponse response = await http.Client().send(req); - final String respString = await response.stream.bytesToString(); - final List data = jsonDecode(respString) as List; - - return data - .where( - (dynamic item) => (item as Map)['type'] == 'file', - ) - .map( - (dynamic file) => GitHubFile.fromJson(file as Map), - ) - .toList(); - } - - /// Sends a progress message to the specified [sendPort]. - /// - /// The [message] parameter is the content of the progress message. - /// The [success] parameter indicates whether the progress was successful or not. - void sendProgressMessage(String message, {bool success = false}) { - sendPort?.send( - BuildProgressMessage( - message: message, - progress: progress, - success: success, - ), - ); - } - - /// Downloads a file from GitHub. - /// - /// This method takes a [GitHubFile] object and a [localDir] path as input, - /// and downloads the file to the specified local directory. - /// - /// If the file already exists locally and has the same SHA as the GitHub file, - /// the download is skipped and a [GitHubFileDownloadEvent] with the event type - /// [GitHubDownloadEvent.skipped] is returned. - /// - /// If the file does not exist locally or has a different SHA, the file is downloaded - /// from the GitHub URL specified in the [GitHubFile] object. The downloaded file - /// is saved to the local directory and a [GitHubFileDownloadEvent] with the event type - /// [GitHubDownloadEvent.downloaded] is returned. - /// - /// If an error occurs during the download process, an exception is thrown. - /// - /// Returns a [GitHubFileDownloadEvent] object containing the event type and the - /// local path of the downloaded file. - static Future downloadFile( - GitHubFile item, - String localDir, - ) async { - final String coinName = path.basenameWithoutExtension(item.name); - final String outputPath = path.join(localDir, item.name); - - final File localFile = File(outputPath); - if (localFile.existsSync()) { - final String localFileSha = calculateGithubSha1(outputPath); - if (localFileSha == item.sha) { - return GitHubFileDownloadEvent( - event: GitHubDownloadEvent.skipped, - localPath: outputPath, - ); - } - } - - try { - final String fileResponse = await http.read(Uri.parse(item.downloadUrl)); - await File(outputPath).writeAsBytes(fileResponse.codeUnits); - return GitHubFileDownloadEvent( - event: GitHubDownloadEvent.downloaded, - localPath: outputPath, - ); - } catch (e) { - stderr.writeln('Failed to download icon for $coinName: $e'); - rethrow; - } - } - - /// Downloads multiple files from GitHub and yields download events. - /// - /// Given a list of [files] and a [localDir], this method downloads each file - /// and yields a [GitHubFileDownloadEvent] for each file. The [GitHubFileDownloadEvent] - /// contains information about the download event, such as whether the file was - /// successfully downloaded or skipped, and the [localPath] where the file was saved. - /// - /// Example usage: - /// ```dart - /// List files = [...]; - /// String localDir = '/path/to/local/directory'; - /// Stream downloadStream = downloadFiles(files, localDir); - /// await for (GitHubFileDownloadEvent event in downloadStream) { - /// } - /// ``` - static Stream downloadFiles( - List files, - String localDir, - ) async* { - for (final GitHubFile file in files) { - yield await downloadFile(file, localDir); - } - } - - /// Reverts the changes made to a Git file at the specified [filePath]. - /// Returns `true` if the changes were successfully reverted, `false` otherwise. - static Future revertChangesToGitFile(String filePath) async { - final ProcessResult result = - await Process.run('git', ['checkout', filePath]); - - if (result.exitCode != 0) { - stderr.writeln('Failed to revert changes to $filePath'); - return false; - } else { - stdout.writeln('Reverted changes to $filePath'); - return true; - } - } - - /// Reverts changes made to a Git file or deletes it if it exists. - /// - /// This method takes a [filePath] as input and reverts any changes made to the Git file located at that path. - /// If the file does not exist or the revert operation fails, the file is deleted. - /// - /// Example usage: - /// ```dart - /// await revertOrDeleteGitFile('/Users/francois/Repos/komodo/komodo-wallet-archive/app_build/fetch_coin_assets.dart'); - /// ``` - static Future revertOrDeleteGitFile(String filePath) async { - final bool result = await revertChangesToGitFile(filePath); - if (!result && File(filePath).existsSync()) { - stdout.writeln('Deleting $filePath'); - await File(filePath).delete(); - } - } - - /// Reverts or deletes the specified git files. - /// - /// This method takes a list of file paths and iterates over each path, - /// calling the [revertOrDeleteGitFile] method to revert or delete the file. - /// - /// Example usage: - /// ```dart - /// List filePaths = ['/path/to/file1', '/path/to/file2']; - /// await revertOrDeleteGitFiles(filePaths); - /// ``` - static Future revertOrDeleteGitFiles(List filePaths) async { - for (final String filePath in filePaths) { - await revertOrDeleteGitFile(filePath); - } - } -} - -/// Creates folders based on the provided list of folder paths. -/// -/// If a folder path includes a file extension, the parent directory of the file -/// will be used instead. The function creates the folders if they don't already exist. -/// -/// Example: -/// ```dart -/// List folders = ['/path/to/folder1', '/path/to/folder2/file.txt']; -/// createFolders(folders); -/// ``` -void createFolders(List folders) { - for (String folder in folders) { - if (path.extension(folder).isNotEmpty) { - folder = path.dirname(folder); - } - - final Directory dir = Directory(folder); - if (!dir.existsSync()) { - dir.createSync(recursive: true); - } - } -} - -/// Calculates the SHA-1 hash value of a file. -/// -/// Reads the contents of the file at the given [filePath] and calculates -/// the SHA-1 hash value using the `sha1` algorithm. Returns the hash value -/// as a string. -/// -/// Throws an exception if the file cannot be read or if an error occurs -/// during the hashing process. -Future calculateFileSha1(String filePath) async { - final Uint8List bytes = await File(filePath).readAsBytes(); - final Digest digest = sha1.convert(bytes); - return digest.toString(); -} - -/// Calculates the SHA-1 hash of a list of bytes. -/// -/// Takes a [bytes] parameter, which is a list of integers representing the bytes. -/// Returns the SHA-1 hash as a string. -String calculateBlobSha1(List bytes) { - final Digest digest = sha1.convert(bytes); - return digest.toString(); -} - -/// Calculates the SHA1 hash of a file located at the given [filePath]. -/// -/// The function reads the file as bytes, encodes it as a blob, and then calculates -/// the SHA1 hash of the blob. The resulting hash is returned as a string. -String calculateGithubSha1(String filePath) { - final Uint8List bytes = File(filePath).readAsBytesSync(); - final List blob = - utf8.encode('blob ${bytes.length}${String.fromCharCode(0)}') + bytes; - final String digest = calculateBlobSha1(blob); - return digest; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/links.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/links.dart deleted file mode 100644 index 4c2a552800..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/links.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: avoid_print, unreachable_from_main -/// Represents the links associated with a GitHub file resource. -class Links { - /// Creates a new instance of the [Links] class. - const Links({this.self, this.git, this.html}); - - /// Creates a new instance of the [Links] class from a JSON map. - factory Links.fromJson(Map data) => Links( - self: data['self'] as String?, - git: data['git'] as String?, - html: data['html'] as String?, - ); - - /// Converts the [Links] instance to a JSON map. - Map toJson() => { - 'self': self, - 'git': git, - 'html': html, - }; - - /// The self link. - final String? self; - - /// The git link. - final String? git; - - /// The HTML link. - final String? html; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/result.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/result.dart deleted file mode 100644 index 8b622ba71b..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/coin_assets/result.dart +++ /dev/null @@ -1,20 +0,0 @@ -/// Represents the result of an operation. -class Result { - /// Creates a [Result] object with the specified success status and optional error message. - const Result({ - required this.success, - this.error, - }); - - /// Creates a [Result] object indicating a successful operation. - factory Result.success() => const Result(success: true); - - /// Creates a [Result] object indicating a failed operation with the specified error message. - factory Result.error(String error) => Result(success: false, error: error); - - /// Indicates whether the operation was successful. - final bool success; - - /// The error message associated with a failed operation, or null if the operation was successful. - final String? error; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/copy_platform_assets_build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/copy_platform_assets_build_step.dart deleted file mode 100644 index d6bf36c66b..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/copy_platform_assets_build_step.dart +++ /dev/null @@ -1,114 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; -import 'package:komodo_wallet_build_transformer/src/build_step.dart'; -import 'package:path/path.dart' as path; - -/// A build step that copies platform-specific assets to the build directory -/// which aren't copied as part of the native build configuration and Flutter's -/// asset configuration. -/// -/// Prefer using the native build configurations over this build step -/// when possible. -class CopyPlatformAssetsBuildStep extends BuildStep { - CopyPlatformAssetsBuildStep({ - required this.projectRoot, - // required this.buildPlatform, - }); - - final String projectRoot; - // final String buildPlatform; - - @override - final String id = idStatic; - - static const idStatic = 'copy_platform_assets'; - - @override - Future build() async { - // TODO: add conditional logic for copying assets based on the target - // platform if this info is made available to the Dart VM. - - // if (buildPlatform == "linux") { - await _copyLinuxAssets(); - // } - } - - @override - Future canSkip() { - return Future.value(_canSkipLinuxAssets()); - } - - @override - Future revert([Exception? e]) async { - _revertLinuxAssets(); - } - - Future _copyLinuxAssets() async { - try { - await Future.wait([_destDesktop, _destIcon].map((file) async { - if (!file.parent.existsSync()) { - file.parent.createSync(recursive: true); - } - })); - - _sourceIcon.copySync(_destIcon.path); - _sourceDesktop.copySync(_destDesktop.path); - - print("Copying Linux assets completed"); - } catch (e) { - print("Failed to copy files with error: $e"); - - rethrow; - } - } - - void _revertLinuxAssets() async { - try { - // Done in parallel so that if one fails, the other can still be deleted - await Future.wait([_destIcon, _destDesktop].map((file) => file.delete())); - - print("Copying Linux assets completed"); - } catch (e) { - print("Failed to copy files with error: $e"); - - rethrow; - } - } - - bool _canSkipLinuxAssets() { - return !(_sourceIcon.existsSync() || _sourceDesktop.existsSync()) && - _destIcon.existsSync() && - _destDesktop.existsSync() && - _sourceIcon.lastModifiedSync().isBefore(_destIcon.lastModifiedSync()) && - _sourceDesktop - .lastModifiedSync() - .isBefore(_destDesktop.lastModifiedSync()); - } - - late final File _sourceIcon = - File(path.joinAll([projectRoot, "linux", "KomodoWallet.svg"])); - - late final File _destIcon = File(path.joinAll([ - projectRoot, - "build", - "linux", - "x64", - "release", - "bundle", - "KomodoWallet.svg" - ])); - - late final File _sourceDesktop = - File(path.joinAll([projectRoot, "linux", "KomodoWallet.desktop"])); - - late final File _destDesktop = File(path.joinAll([ - projectRoot, - "build", - "linux", - "x64", - "release", - "bundle", - "KomodoWallet.desktop" - ])); -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart deleted file mode 100644 index b9de38ec8f..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart +++ /dev/null @@ -1,294 +0,0 @@ -// ignore_for_file: avoid_print -// TODO(Francois): Change print statements to Log statements - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:http/http.dart' as http; -import 'package:komodo_wallet_build_transformer/src/build_step.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/build_progress_message.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/coin_ci_config.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_file.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/github_file_downloader.dart'; -import 'package:komodo_wallet_build_transformer/src/steps/coin_assets/result.dart'; -import 'package:path/path.dart' as path; - -/// Entry point used if invoked as a CLI script. -const String defaultBuildConfigPath = 'app_build/build_config.json'; - -class FetchCoinAssetsBuildStep extends BuildStep { - FetchCoinAssetsBuildStep({ - required this.projectDir, - required this.config, - required this.downloader, - required this.originalBuildConfig, - this.receivePort, - }) { - receivePort?.listen( - (dynamic message) => onProgressData(message, receivePort), - onError: onProgressError, - ); - } - - factory FetchCoinAssetsBuildStep.withBuildConfig( - Map buildConfig, - [ReceivePort? receivePort]) { - final CoinCIConfig config = CoinCIConfig.fromJson(buildConfig['coins']); - final GitHubFileDownloader downloader = GitHubFileDownloader( - repoApiUrl: config.coinsRepoApiUrl, - repoContentUrl: config.coinsRepoContentUrl, - ); - - return FetchCoinAssetsBuildStep( - projectDir: Directory.current.path, - config: config, - downloader: downloader, - originalBuildConfig: buildConfig, - ); - } - - @override - final String id = idStatic; - static const idStatic = 'fetch_coin_assets'; - final Map? originalBuildConfig; - final String projectDir; - final CoinCIConfig config; - final GitHubFileDownloader downloader; - final ReceivePort? receivePort; - - @override - Future build() async { - final alreadyHadCoinAssets = File('assets/config/coins.json').existsSync(); - final isDebugBuild = - (Platform.environment['FLUTTER_BUILD_MODE'] ?? '').toLowerCase() == - 'debug'; - final latestCommitHash = await downloader.getLatestCommitHash( - branch: config.coinsRepoBranch, - ); - CoinCIConfig configWithUpdatedCommit = config; - - if (config.updateCommitOnBuild) { - configWithUpdatedCommit = - config.copyWith(bundledCoinsRepoCommit: latestCommitHash); - await configWithUpdatedCommit.save( - assetPath: defaultBuildConfigPath, - originalBuildConfig: originalBuildConfig, - ); - } - - await downloader.download( - configWithUpdatedCommit.bundledCoinsRepoCommit, - configWithUpdatedCommit.mappedFiles, - configWithUpdatedCommit.mappedFolders, - ); - - final bool wasCommitHashUpdated = config.bundledCoinsRepoCommit != - configWithUpdatedCommit.bundledCoinsRepoCommit; - - if (wasCommitHashUpdated || !alreadyHadCoinAssets) { - const errorMessage = 'Coin assets have been updated. ' - 'Please re-run the build process for the changes to take effect.'; - - // If it's not a debug build and the commit hash was updated, throw an - // exception to indicate that the build process should be re-run. We can - // skip this check for debug builds if we already had coin assets. - if (!isDebugBuild || !alreadyHadCoinAssets) { - stderr.writeln(errorMessage); - receivePort?.close(); - throw StepCompletedWithChangesException(errorMessage); - } - - stdout.writeln('\n[WARN] $errorMessage\n'); - } - - receivePort?.close(); - stdout.writeln('\nCoin assets fetched successfully'); - } - - @override - Future canSkip() async { - final String latestCommitHash = await downloader.getLatestCommitHash( - branch: config.coinsRepoBranch, - ); - - if (latestCommitHash != config.bundledCoinsRepoCommit) { - return false; - } - - if (!await _canSkipMappedFiles(config.mappedFiles)) { - return false; - } - - if (!await _canSkipMappedFolders(config.mappedFolders)) { - return false; - } - - return true; - } - - @override - Future revert([Exception? e]) async { - if (e is StepCompletedWithChangesException) { - print( - 'Step not reverted because the build process was completed with changes', - ); - - return; - } - - // Try `git checkout` to revert changes instead of deleting all files - // because there may be mapped files/folders that are tracked by git - final List mappedFilePaths = config.mappedFiles.keys.toList(); - final List mappedFolderPaths = config.mappedFolders.keys.toList(); - - final Iterable> mappedFolderFilePaths = - mappedFolderPaths.map(_getFilesInFolder); - - final List allFiles = mappedFilePaths + - mappedFolderFilePaths.expand((List x) => x).toList(); - - await GitHubFileDownloader.revertOrDeleteGitFiles(allFiles); - } - - Future _canSkipMappedFiles(Map files) async { - for (final MapEntry mappedFile in files.entries) { - final GitHubFile remoteFile = await _fetchRemoteFileContent(mappedFile); - final Result canSkipFile = await _canSkipFile( - mappedFile.key, - remoteFile, - ); - if (!canSkipFile.success) { - print('Cannot skip build step: ${canSkipFile.error}'); - return false; - } - } - - return true; - } - - Future _canSkipMappedFolders(Map folders) async { - for (final MapEntry mappedFolder in folders.entries) { - final List remoteFolderContents = - await downloader.getGitHubDirectoryContents( - mappedFolder.value, - config.bundledCoinsRepoCommit, - ); - final Result canSkipFolder = await _canSkipDirectory( - mappedFolder.key, - remoteFolderContents, - ); - - if (!canSkipFolder.success) { - print('Cannot skip build step: ${canSkipFolder.error}'); - return false; - } - } - return true; - } - - Future _fetchRemoteFileContent( - MapEntry mappedFile, - ) async { - final Uri fileContentUrl = Uri.parse( - '${config.coinsRepoApiUrl}/contents/${mappedFile.value}?ref=${config.bundledCoinsRepoCommit}', - ); - final http.Response fileContentResponse = await http.get(fileContentUrl); - final GitHubFile fileContent = GitHubFile.fromJson( - jsonDecode(fileContentResponse.body) as Map, - ); - return fileContent; - } - - Future _canSkipFile( - String localFilePath, - GitHubFile remoteFile, - ) async { - final File localFile = File(localFilePath); - - if (!localFile.existsSync()) { - return Result.error( - '$localFilePath does not exist', - ); - } - - final int localFileSize = await localFile.length(); - if (remoteFile.size != localFileSize) { - return Result.error( - '$localFilePath size mismatch: ' - 'remote: ${remoteFile.size}, local: $localFileSize', - ); - } - - final String localFileSha = calculateGithubSha1(localFilePath); - if (localFileSha != remoteFile.sha) { - return Result.error( - '$localFilePath sha mismatch: ' - 'remote: ${remoteFile.sha}, local: $localFileSha', - ); - } - - return Result.success(); - } - - Future _canSkipDirectory( - String directory, - List remoteDirectoryContents, - ) async { - final Directory localFolder = Directory(directory); - - if (!localFolder.existsSync()) { - return Result.error('$directory does not exist'); - } - - for (final GitHubFile remoteFile in remoteDirectoryContents) { - final String localFilePath = path.join(directory, remoteFile.name); - final Result canSkipFile = await _canSkipFile( - localFilePath, - remoteFile, - ); - if (!canSkipFile.success) { - return Result.error('Cannot skip build step: ${canSkipFile.error}'); - } - } - - return Result.success(); - } - - List _getFilesInFolder(String folderPath) { - final Directory localFolder = Directory(folderPath); - final List localFolderContents = - localFolder.listSync(recursive: true); - return localFolderContents - .map((FileSystemEntity file) => file.path) - .toList(); - } -} - -void onProgressError(dynamic error) { - print('\nError: $error'); - - // throw Exception('An error occurred during the coin fetch build step'); -} - -void onProgressData(dynamic message, ReceivePort? recevePort) { - if (message is BuildProgressMessage) { - stdout.write( - '\r${message.message} - Progress: ${message.progress.toStringAsFixed(2)}% \x1b[K', - ); - - if (message.progress == 100 && message.finished) { - recevePort?.close(); - } - } -} - -class StepCompletedWithChangesException implements Exception { - StepCompletedWithChangesException(this.message); - - final String message; - - @override - String toString() => message; -} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart deleted file mode 100644 index 5db7bf31e7..0000000000 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart +++ /dev/null @@ -1,533 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:crypto/crypto.dart'; -import 'package:html/parser.dart' as parser; -import 'package:http/http.dart' as http; -import 'package:komodo_wallet_build_transformer/src/build_step.dart'; -import 'package:path/path.dart' as path; - -class FetchDefiApiStep extends BuildStep { - factory FetchDefiApiStep.withBuildConfig(Map buildConfig) { - final apiConfig = buildConfig['api'] as Map; - return FetchDefiApiStep( - projectRoot: Directory.current.path, - apiCommitHash: apiConfig['api_commit_hash'], - platformsConfig: apiConfig['platforms'], - sourceUrls: List.from(apiConfig['source_urls']), - apiBranch: apiConfig['branch'], - enabled: apiConfig['fetch_at_build_enabled'], - ); - } - - FetchDefiApiStep({ - required this.projectRoot, - required this.apiCommitHash, - required this.platformsConfig, - required this.sourceUrls, - required this.apiBranch, - this.selectedPlatform, - this.forceUpdate = false, - this.enabled = true, - }); - - @override - final String id = idStatic; - - static const idStatic = 'fetch_defi_api'; - - final String projectRoot; - final String apiCommitHash; - final Map platformsConfig; - final List sourceUrls; - final String apiBranch; - String? selectedPlatform; - bool forceUpdate; - bool enabled; - - @override - Future build() async { - if (!enabled) { - _logMessage('API update is not enabled in the configuration.'); - return; - } - try { - await updateAPI(); - } catch (e) { - stderr.writeln('Error updating API: $e'); - rethrow; - } - } - - @override - Future canSkip() => Future.value(!enabled); - - @override - Future revert([Exception? e]) async { - _logMessage('Reverting changes made by UpdateAPIStep...'); - } - - Future updateAPI() async { - if (!enabled) { - _logMessage('API update is not enabled in the configuration.'); - return; - } - - final platformsToUpdate = selectedPlatform != null && - platformsConfig.containsKey(selectedPlatform) - ? [selectedPlatform!] - : platformsConfig.keys.toList(); - - for (final platform in platformsToUpdate) { - final progressString = - '${(platformsToUpdate.indexOf(platform) + 1)}/${platformsToUpdate.length}'; - stdout.writeln('====================='); - stdout.writeln('[$progressString] Updating $platform platform...'); - await _updatePlatform(platform, platformsConfig); - stdout.writeln('====================='); - } - _updateDocumentation(); - } - - static const String _overrideEnvName = 'OVERRIDE_DEFI_API_DOWNLOAD'; - - /// If set, the OVERRIDE_DEFI_API_DOWNLOAD environment variable will override - /// any default behavior/configuration. e.g. - /// `flutter build web --release --dart-define=OVERRIDE_DEFI_API_DOWNLOAD=true` - /// or `OVERRIDE_DEFI_API_DOWNLOAD=true && flutter build web --release` - /// - /// If set to true/TRUE/True, the API will be fetched and downloaded on every - /// build, even if it is already up-to-date with the configuration. - /// - /// If set to false/FALSE/False, the API fetching will be skipped, even if - /// the existing API is not up-to-date with the coniguration. - /// - /// If unset, the default behavior will be used. - /// - /// If both the system environment variable and the dart-defined environment - /// variable are set, the dart-defined variable will take precedence. - /// - /// NB! Setting the value to false is not the same as it being unset. - /// If the value is unset, the default behavior will be used. - /// Bear this in mind when setting the value as a system environment variable. - /// - /// See `BUILD_CONFIG_README.md` in `app_build/BUILD_CONFIG_README.md`. - bool? get overrideDefiApiDownload => - const bool.hasEnvironment(_overrideEnvName) - ? const bool.fromEnvironment(_overrideEnvName) - : Platform.environment[_overrideEnvName] != null - ? bool.tryParse(Platform.environment[_overrideEnvName]!, - caseSensitive: false) - : null; - - Future _updatePlatform( - String platform, Map config) async { - final updateMessage = overrideDefiApiDownload != null - ? '${overrideDefiApiDownload! ? 'FORCING' : 'SKIPPING'} update of $platform platform because OVERRIDE_DEFI_API_DOWNLOAD is set to $overrideDefiApiDownload' - : null; - - if (updateMessage != null) { - stdout.writeln(updateMessage); - } - - final destinationFolder = _getPlatformDestinationFolder(platform); - final isOutdated = - await _checkIfOutdated(platform, destinationFolder, config); - - if (!_shouldUpdate(isOutdated)) { - _logMessage('$platform platform is up to date.'); - await _postUpdateActions(platform, destinationFolder); - return; - } - - String? zipFilePath; - for (final sourceUrl in sourceUrls) { - try { - final zipFileUrl = await _findZipFileUrl(platform, config, sourceUrl); - zipFilePath = await _downloadFile(zipFileUrl, destinationFolder); - - if (await _verifyChecksum(zipFilePath, platform)) { - await _extractZipFile(zipFilePath, destinationFolder); - _updateLastUpdatedFile(platform, destinationFolder, zipFilePath); - _logMessage('$platform platform update completed.'); - break; // Exit loop if update is successful - } else { - stdout - .writeln('SHA256 Checksum verification failed for $zipFilePath'); - if (sourceUrl == sourceUrls.last) { - throw Exception( - 'API fetch failed for all source URLs: $sourceUrls', - ); - } - } - } catch (e) { - stdout.writeln('Error updating from source $sourceUrl: $e'); - if (sourceUrl == sourceUrls.last) { - rethrow; - } - } finally { - if (zipFilePath != null) { - try { - File(zipFilePath).deleteSync(); - _logMessage('Deleted zip file $zipFilePath'); - } catch (e) { - _logMessage('Error deleting zip file: $e', error: true); - } - } - } - } - - await _postUpdateActions(platform, destinationFolder); - } - - bool _shouldUpdate(bool isOutdated) { - return overrideDefiApiDownload == true || - (overrideDefiApiDownload != false && (forceUpdate || isOutdated)); - } - - Future _downloadFile(String url, String destinationFolder) async { - _logMessage('Downloading $url...'); - final response = await http.get(Uri.parse(url)); - _checkResponseSuccess(response); - - final zipFileName = path.basename(url); - final zipFilePath = path.join(destinationFolder, zipFileName); - - final directory = Directory(destinationFolder); - if (!await directory.exists()) { - await directory.create(recursive: true); - } - - final zipFile = File(zipFilePath); - try { - await zipFile.writeAsBytes(response.bodyBytes); - } catch (e) { - _logMessage('Error writing file: $e', error: true); - rethrow; - } - - _logMessage('Downloaded $zipFileName'); - return zipFilePath; - } - - Future _verifyChecksum(String filePath, String platform) async { - final validChecksums = List.from( - platformsConfig[platform]['valid_zip_sha256_checksums'], - ); - - _logMessage('validChecksums: $validChecksums'); - - final fileBytes = await File(filePath).readAsBytes(); - final fileSha256Checksum = sha256.convert(fileBytes).toString(); - - if (validChecksums.contains(fileSha256Checksum)) { - stdout.writeln('Checksum validated for $filePath'); - return true; - } else { - stderr.writeln( - 'SHA256 Checksum mismatch for $filePath: expected any of ' - '$validChecksums, got $fileSha256Checksum', - ); - return false; - } - } - - void _updateLastUpdatedFile( - String platform, String destinationFolder, String zipFilePath) { - final lastUpdatedFile = - File(path.join(destinationFolder, '.api_last_updated_$platform')); - final currentTimestamp = DateTime.now().toIso8601String(); - final fileChecksum = - sha256.convert(File(zipFilePath).readAsBytesSync()).toString(); - lastUpdatedFile.writeAsStringSync(json.encode({ - 'api_commit_hash': apiCommitHash, - 'timestamp': currentTimestamp, - 'checksums': [fileChecksum] - })); - stdout.writeln('Updated last updated file for $platform.'); - } - - Future _checkIfOutdated(String platform, String destinationFolder, - Map config) async { - final lastUpdatedFilePath = - path.join(destinationFolder, '.api_last_updated_$platform'); - final lastUpdatedFile = File(lastUpdatedFilePath); - - if (!lastUpdatedFile.existsSync()) { - return true; - } - - try { - final lastUpdatedData = json.decode(lastUpdatedFile.readAsStringSync()); - if (lastUpdatedData['api_commit_hash'] == apiCommitHash) { - final storedChecksums = - List.from(lastUpdatedData['checksums'] ?? []); - final targetChecksums = - List.from(config[platform]['valid_zip_sha256_checksums']); - - if (storedChecksums.toSet().containsAll(targetChecksums)) { - _logMessage("version: $apiCommitHash and SHA256 checksum match."); - return false; - } - } - } catch (e) { - _logMessage( - 'Error reading or parsing .api_last_updated_$platform: $e', - error: true, - ); - lastUpdatedFile.deleteSync(); - rethrow; - } - - return true; - } - - Future _updateWebPackages() async { - _logMessage('Updating Web platform...'); - String npmPath = 'npm'; - if (Platform.isWindows) { - npmPath = path.join('C:', 'Program Files', 'nodejs', 'npm.cmd'); - _logMessage('Using npm path: $npmPath'); - } - final installResult = - await Process.run(npmPath, ['install'], workingDirectory: projectRoot); - if (installResult.exitCode != 0) { - throw Exception('npm install failed: ${installResult.stderr}'); - } - - final buildResult = await Process.run(npmPath, ['run', 'build'], - workingDirectory: projectRoot); - if (buildResult.exitCode != 0) { - throw Exception('npm run build failed: ${buildResult.stderr}'); - } - - _logMessage('Web platform updated successfully.'); - } - - Future _updateLinuxPlatform(String destinationFolder) async { - _logMessage('Updating Linux platform...'); - // Update the file permissions to make it executable. As part of the - // transition from mm2 naming to kdfi, update whichever file is present. - // ignore: unused_local_variable - final binaryNames = ['mm2', 'kdfi'] - .map((e) => path.join(destinationFolder, e)) - .where((filePath) => File(filePath).existsSync()); - if (!Platform.isWindows) { - for (var filePath in binaryNames) { - Process.run('chmod', ['+x', filePath]); - } - } - - _logMessage('Linux platform updated successfully.'); - } - - String _getPlatformDestinationFolder(String platform) { - if (platformsConfig.containsKey(platform)) { - return path.join(projectRoot, platformsConfig[platform]['path']); - } else { - throw ArgumentError('Invalid platform: $platform'); - } - } - - Future _findZipFileUrl( - String platform, Map config, String sourceUrl) async { - if (sourceUrl.startsWith('https://api.github.com/repos/')) { - return await _fetchFromGitHub(platform, config, sourceUrl); - } else { - return await _fetchFromBaseUrl(platform, config, sourceUrl); - } - } - - Future _fetchFromGitHub( - String platform, Map config, String sourceUrl) async { - final repoMatch = RegExp(r'^https://api\.github\.com/repos/([^/]+)/([^/]+)') - .firstMatch(sourceUrl); - if (repoMatch == null) { - throw ArgumentError('Invalid GitHub repository URL: $sourceUrl'); - } - - final owner = repoMatch.group(1)!; - final repo = repoMatch.group(2)!; - final releasesUrl = 'https://api.github.com/repos/$owner/$repo/releases'; - final response = await http.get(Uri.parse(releasesUrl)); - _checkResponseSuccess(response); - - final releases = json.decode(response.body) as List; - final apiVersionShortHash = apiCommitHash.substring(0, 7); - final matchingKeyword = config[platform]['matching_keyword']; - - for (final release in releases) { - final assets = release['assets'] as List; - for (final asset in assets) { - final url = asset['browser_download_url'] as String; - - if (url.contains(matchingKeyword) && - url.contains(apiVersionShortHash)) { - final commitHash = - await _getCommitHashForRelease(release['tag_name'], owner, repo); - if (commitHash == apiCommitHash) { - return url; - } - } - } - } - - throw Exception( - 'Zip file not found for platform $platform in GitHub releases'); - } - - Future _getCommitHashForRelease( - String tag, String owner, String repo) async { - final commitsUrl = 'https://api.github.com/repos/$owner/$repo/commits/$tag'; - final response = await http.get(Uri.parse(commitsUrl)); - _checkResponseSuccess(response); - - final commit = json.decode(response.body); - return commit['sha']; - } - - Future _fetchFromBaseUrl( - String platform, Map config, String sourceUrl) async { - final url = '$sourceUrl/$apiBranch/'; - final response = await http.get(Uri.parse(url)); - _checkResponseSuccess(response); - - final document = parser.parse(response.body); - final matchingKeyword = config[platform]['matching_keyword']; - final extensions = ['.zip']; - final apiVersionShortHash = apiCommitHash.substring(0, 7); - - for (final element in document.querySelectorAll('a')) { - final href = element.attributes['href']; - if (href != null && - href.contains(matchingKeyword) && - extensions.any((extension) => href.endsWith(extension)) && - href.contains(apiVersionShortHash)) { - return '$sourceUrl/$apiBranch/$href'; - } - } - - throw Exception('Zip file not found for platform $platform'); - } - - void _checkResponseSuccess(http.Response response) { - if (response.statusCode != 200) { - throw HttpException( - 'Failed to fetch data: ${response.statusCode} ${response.reasonPhrase}'); - } - } - - Future _postUpdateActions(String platform, String destinationFolder) { - if (platform == 'web') { - return _updateWebPackages(); - } else if (platform == 'linux') { - return _updateLinuxPlatform(destinationFolder); - } - return Future.value(); - } - - Future _extractZipFile( - String zipFilePath, String destinationFolder) async { - try { - // Determine the platform to use the appropriate extraction command - if (Platform.isMacOS || Platform.isLinux) { - // For macOS and Linux, use the `unzip` command - final result = - await Process.run('unzip', [zipFilePath, '-d', destinationFolder]); - if (result.exitCode != 0) { - throw Exception('Error extracting zip file: ${result.stderr}'); - } - } else if (Platform.isWindows) { - // For Windows, use PowerShell's Expand-Archive command - final result = await Process.run('powershell', [ - 'Expand-Archive', - '-Path', - zipFilePath, - '-DestinationPath', - destinationFolder - ]); - if (result.exitCode != 0) { - throw Exception('Error extracting zip file: ${result.stderr}'); - } - } else { - _logMessage( - 'Unsupported platform: ${Platform.operatingSystem}', - error: true, - ); - throw UnsupportedError('Unsupported platform'); - } - _logMessage('Extraction completed.'); - } catch (e) { - _logMessage('Failed to extract zip file: $e'); - } - } - - void _updateDocumentation() { - final documentationFile = File('$projectRoot/docs/UPDATE_API_MODULE.md'); - final content = documentationFile.readAsStringSync().replaceAllMapped( - RegExp(r'(Current api module version is) `([^`]+)`'), - (match) => '${match[1]} `$apiCommitHash`', - ); - documentationFile.writeAsStringSync(content); - _logMessage('Updated API version in documentation.'); - } -} - -late final ArgResults _argResults; - -void main(List arguments) async { - final parser = ArgParser() - ..addOption('platform', abbr: 'p', help: 'Specify the platform to update') - ..addOption('api-version', - abbr: 'a', help: 'Specify the API version to update to') - ..addFlag('force', - abbr: 'f', negatable: false, help: 'Force update the API module') - ..addFlag('help', - abbr: 'h', negatable: false, help: 'Display usage information'); - - _argResults = parser.parse(arguments); - - if (_argResults['help']) { - _logMessage('Usage: dart app_build/build_steps.dart [options]'); - _logMessage(parser.usage); - return; - } - - final projectRoot = Directory.current.path; - final configFile = File('$projectRoot/app_build/build_config.json'); - final config = json.decode(configFile.readAsStringSync()); - - final platform = _argResults.option('platform'); - final apiVersion = - _argResults.option('api-version') ?? config['api']['api_commit_hash']; - final forceUpdate = _argResults.flag('force'); - - final fetchDefiApiStep = FetchDefiApiStep( - projectRoot: projectRoot, - apiCommitHash: apiVersion, - platformsConfig: config['api']['platforms'], - sourceUrls: List.from(config['api']['source_urls']), - apiBranch: config['api']['branch'], - selectedPlatform: platform, - forceUpdate: forceUpdate, - enabled: true, - ); - - await fetchDefiApiStep.build(); - - if (_argResults.wasParsed('api-version')) { - config['api']['api_commit_hash'] = apiVersion; - configFile.writeAsStringSync(json.encode(config)); - } -} - -void _logMessage(String message, {bool error = false}) { - final prefix = error ? 'ERROR' : 'INFO'; - final output = '[$prefix]: $message'; - if (error) { - stderr.writeln(output); - } else { - stdout.writeln(output); - } -} diff --git a/packages/komodo_wallet_build_transformer/pubspec.lock b/packages/komodo_wallet_build_transformer/pubspec.lock deleted file mode 100644 index 6a2de45493..0000000000 --- a/packages/komodo_wallet_build_transformer/pubspec.lock +++ /dev/null @@ -1,426 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" - url: "https://pub.dev" - source: hosted - version: "68.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.1.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - args: - dependency: "direct main" - description: - name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - file: - dependency: transitive - description: - name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - html: - dependency: "direct main" - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" - http: - dependency: "direct main" - description: - name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" - source: hosted - version: "0.13.6" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" - lints: - dependency: "direct dev" - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - macros: - dependency: transitive - description: - name: macros - sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" - url: "https://pub.dev" - source: hosted - version: "0.1.0-main.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" - source: hosted - version: "0.12.16+1" - meta: - dependency: transitive - description: - name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" - source: hosted - version: "1.15.0" - mime: - dependency: transitive - description: - name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" - url: "https://pub.dev" - source: hosted - version: "1.0.5" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" - source: hosted - version: "1.9.0" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: d11b55850c68c1f6c0cf00eabded4e66c4043feaf6c0d7ce4a36785137df6331 - url: "https://pub.dev" - source: hosted - version: "1.25.5" - test_api: - dependency: transitive - description: - name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" - url: "https://pub.dev" - source: hosted - version: "0.7.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" - url: "https://pub.dev" - source: hosted - version: "0.6.2" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" - url: "https://pub.dev" - source: hosted - version: "14.2.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" - url: "https://pub.dev" - source: hosted - version: "0.1.5" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.4.0 <4.0.0" diff --git a/packages/komodo_wallet_build_transformer/pubspec.yaml b/packages/komodo_wallet_build_transformer/pubspec.yaml deleted file mode 100644 index 73d46c37bd..0000000000 --- a/packages/komodo_wallet_build_transformer/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: komodo_wallet_build_transformer -description: A build transformer for Komodo Wallet used for managing all build-time dependencies. -version: 0.0.1 -# repository: https://github.com/my_org/my_repo -publish_to: "none" - -environment: - sdk: ^3.4.0 - -# Add regular dependencies here. -dependencies: - args: ^2.5.0 # dart.dev - http: 0.13.6 # dart.dev - crypto: 3.0.3 # dart.dev - path: ^1.9.0 - - html: ^0.15.4 - -dev_dependencies: - lints: ^3.0.0 - test: ^1.24.0 diff --git a/pubspec.lock b/pubspec.lock index 250e705684..fbee4470a9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -515,10 +515,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -596,8 +596,10 @@ packages: dependency: "direct dev" description: path: "packages/komodo_wallet_build_transformer" - relative: true - source: path + ref: dev + resolved-ref: "388f04296a5531c3cdad766269a3040d2b4ee9ac" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git version: "0.0.1" leak_tracker: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index bb06fecda8..f0368a553d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,7 +54,7 @@ dependencies: args: 2.5.0 # dart.dev flutter_markdown: 0.6.14 # flutter.dev - http: 0.13.6 # dart.dev + http: 1.2.2 # dart.dev intl: 0.19.0 # dart.dev js: 0.6.7 # dart.dev shared_preferences: 2.1.1 # flutter.dev @@ -216,7 +216,10 @@ dev_dependencies: test: ^1.24.1 # dart.dev komodo_wallet_build_transformer: - path: packages/komodo_wallet_build_transformer + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_wallet_build_transformer + ref: dev # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -279,6 +282,17 @@ flutter: # execution. This is useful for reducing build time in development, # but is not recommended for production builds. # - --concurrent, + + #! NB: There may be complications if we want to publish the + # sub-packages as separate packages. However, Flutter's upcoming + # support for native/web build hooks may simplify this process. + # This package must be listed in the `package_config.json` of the + # root Flutter app. + --artifact_output_package=web_dex, + + # The path to the build config file relative to the root of the + # artifact output package. + --config_output_path=app_build/build_config.json, ] # An image asset can refer to one or more resolution-specific "variants", see diff --git a/run_integration_tests.dart b/run_integration_tests.dart index ad676310d1..b38f089d6c 100644 --- a/run_integration_tests.dart +++ b/run_integration_tests.dart @@ -11,61 +11,31 @@ final List testsList = [ 'wallets_tests/wallets_tests.dart', 'wallets_manager_tests/wallets_manager_tests.dart', 'dex_tests/dex_tests.dart', - 'misc_tests/misc_tests.dart' + 'misc_tests/misc_tests.dart', ]; //app data path for mac and linux const String macAppData = '/Library/Containers/com.komodo.komodowallet'; const String linuxAppData = '/.local/share/com.komodo.KomodoWallet'; -const String windowsAppData = '\\AppData\\Roaming\\com.komodo'; +const String windowsAppData = r'\AppData\Roaming\com.komodo'; +// TODO: convert to class & include args as class members const String suspendedCoin = 'KMD'; File? _configFile; +bool verbose = false; Future main(List args) async { - // Configure CLI - final parser = ArgParser(); - parser.addFlag('help', - abbr: 'h', defaultsTo: false, help: 'Show help message and exit'); - parser.addOption('testToRun', - abbr: 't', - defaultsTo: '', - help: - 'Specify a single testfile to run, if option is not used, all available tests will be run instead; option usage example: -t "design_tests/theme_test.dart"'); - parser.addOption('browserDimension', - abbr: 'b', - defaultsTo: '1024,1400', - help: 'Set device window(screen) dimensions: height, width'); - parser.addOption('displayMode', - abbr: 'd', - defaultsTo: 'no-headless', - help: - 'Set to "headless" for headless mode usage, defaults to no-headless'); - parser.addOption('device', - abbr: 'D', defaultsTo: 'web-server', help: 'Set device to run tests on'); - parser.addOption('runMode', - abbr: 'm', - defaultsTo: 'profile', - help: 'App build mode selectrion', - allowed: ['release', 'debug', 'profile']); - parser.addOption('browser-name', - abbr: 'n', - defaultsTo: 'chrome', - help: 'Set browser to run tests on', - allowed: ['chrome', 'safari', 'firefox', 'edge']); + final ArgParser parser = _configureArgParser(); final ArgResults runArguments = parser.parse(args); - final String testToRunArg = runArguments['testToRun']; - final String browserDimensionArg = runArguments['browserDimension']; - final String displayArg = runArguments['displayMode']; - final String deviceArg = runArguments['device']; - final String runModeArg = runArguments['runMode']; - final bool runHelp = runArguments['help']; - final String browserNameArg = runArguments['browser-name']; - // Coins config setup for suspended_assets_test - final Map originalConfig; - _configFile = await _findCoinsConfigFile(); - originalConfig = _readConfig(); + final bool runHelp = runArguments['help'] as bool; + verbose = runArguments['verbose'] as bool; + final String testToRunArg = runArguments['testToRun'] as String; + final String browserDimensionArg = runArguments['browserDimension'] as String; + final String displayArg = runArguments['displayMode'] as String; + final String deviceArg = runArguments['device'] as String; + final String runModeArg = runArguments['runMode'] as String; + final String browserNameArg = runArguments['browser-name'] as String; // Show help message and exit if (runHelp) { @@ -73,22 +43,108 @@ Future main(List args) async { exit(0); } + // Coins config setup for suspended_assets_test + final Map originalConfig; + _configFile = await _findCoinsConfigFile(); + originalConfig = _readConfig(); + // Run tests if (testToRunArg.isNotEmpty) { - await _runTest(testToRunArg, browserDimensionArg, displayArg, deviceArg, - runModeArg, browserNameArg, originalConfig); + await _runTest( + testToRunArg, + browserDimensionArg, + displayArg, + deviceArg, + runModeArg, + browserNameArg, + originalConfig, + ); } else { for (final String test in testsList) { try { - await _runTest(test, browserDimensionArg, displayArg, deviceArg, - runModeArg, browserNameArg, originalConfig); - } catch (e) { - throw 'Caught error executing _runTest: ' + e.toString(); + await _runTest( + test, + browserDimensionArg, + displayArg, + deviceArg, + runModeArg, + browserNameArg, + originalConfig, + ); + } catch (e, s) { + print(s); + throw Exception('Caught error executing _runTest: ' + e.toString()); } } } } +ArgParser _configureArgParser() { + final parser = ArgParser() + ..addFlag( + 'help', + abbr: 'h', + help: 'Show help message and exit', + ) + ..addFlag( + 'verbose', + abbr: 'v', + help: 'Print verbose output', + ) + ..addOption( + 'testToRun', + abbr: 't', + defaultsTo: '', + help: 'Specify a single testfile to run, if option is not used, ' + 'all available tests will be run instead; option usage ' + 'example: -t "design_tests/theme_test.dart"', + ) + ..addOption( + 'browserDimension', + abbr: 'b', + defaultsTo: '1024,1400', + help: 'Set device window(screen) dimensions: height, width', + ) + ..addOption( + 'displayMode', + abbr: 'd', + defaultsTo: 'no-headless', + help: + 'Set to "headless" for headless mode usage, defaults to no-headless', + allowed: ['headless', 'no-headless'], + ) + ..addOption( + 'device', + abbr: 'D', + defaultsTo: 'web-server', + help: 'Set device to run tests on', + allowedHelp: { + 'web-server': 'Web server (default)', + 'chrome': 'Test Chrome', + 'linux': 'Test native Linux application', + 'macos': 'Test native macOS application', + 'windows': 'Test native Windows application', + 'ios': 'iOS', + 'android': 'Android', + }, + ) + ..addOption( + 'runMode', + abbr: 'm', + defaultsTo: 'profile', + help: 'App build mode selectrion', + allowed: ['release', 'debug', 'profile'], + ) + ..addOption( + 'browser-name', + abbr: 'n', + defaultsTo: 'chrome', + help: 'Set browser to run tests on', + allowed: ['chrome', 'safari', 'firefox', 'edge'], + ); + return parser; +} + Future _runTest( String test, String browserDimentionFromArg, @@ -101,12 +157,6 @@ Future _runTest( print('Running test ' + test); if (test == 'suspended_assets_test/suspended_assets_test.dart') { - if (_configFile == null) { - throw 'Coins config file not found'; - } else { - print('Temporarily breaking $suspendedCoin electrum config' - ' in \'${_configFile!.path}\' to test suspended state.'); - } _breakConfig(originalConfigPassed); } @@ -115,7 +165,17 @@ Future _runTest( ProcessResult result; try { if (deviceFromArg == 'web-server') { - //Run integration tests for web app + if (verbose) { + print( + "RUNNING: 'flutter drive --dart-define=testing_mode=true " + '--driver=test_driver/integration_test.dart ' + '--target=test_integration/tests/$test -d $deviceFromArg ' + '--browser-dimension $browserDimentionFromArg ' + '--$displayStateFromArg ' + '--$runModeFromArg ' + "--browser-name $browserNameArg'", + ); + } result = await Process.run( 'flutter', [ @@ -123,6 +183,7 @@ Future _runTest( '--dart-define=testing_mode=true', '--driver=test_driver/integration_test.dart', '--target=test_integration/tests/' + test, + if (verbose) '-v', '-d', deviceFromArg, '--browser-dimension', @@ -130,15 +191,18 @@ Future _runTest( '--' + displayStateFromArg, '--' + runModeFromArg, '--browser-name', - browserNameArg + browserNameArg, + '--web-renderer', + 'canvaskit', ], runInShell: true, ); } else { //Clear app data before tests for Desktop native app - _clearNativeAppsData(); + await _clearNativeAppsData(); - //Run integration tests for native apps (Linux, MacOS, Windows, iOS, Android) + // Run integration tests for native apps + // E.g. Linux, MacOS, Windows, iOS, Android result = await Process.run( 'flutter', [ @@ -146,19 +210,21 @@ Future _runTest( '--dart-define=testing_mode=true', '--driver=test_driver/integration_test.dart', '--target=test_integration/tests/' + test, + if (verbose) '-v', '-d', deviceFromArg, - '--' + runModeFromArg + '--' + runModeFromArg, ], runInShell: true, ); } - } catch (e) { + } catch (e, s) { if (test == 'suspended_assets_test/suspended_assets_test.dart') { _restoreConfig(originalConfigPassed); print('Restored original coins configuration file.'); } - throw 'Error running flutter drive Process: ' + e.toString(); + print(s); + throw Exception('Error running flutter drive Process: ' + e.toString()); } stdout.write(result.stdout); @@ -168,19 +234,32 @@ Future _runTest( } // Flutter drive can return failed test results just as stdout message, // we need to parse this message and detect test failure manually - if (result.stdout.toString().contains('failure')) { - throw ProcessException('flutter', ['test ' + test], - 'Failure details are in chromedriver output.\n', -1); + if (_didAnyTestFail(result)) { + throw ProcessException( + 'flutter', + ['test ' + test], + 'Failure details are in $browserNameArg driver output.\n', + -1, + ); } print('\n---\n'); } +bool _didAnyTestFail(ProcessResult result) { + final caseInvariantConsoleOutput = result.stdout.toString().toLowerCase() + + result.stderr.toString().toLowerCase(); + + return caseInvariantConsoleOutput.contains('failure details') || + caseInvariantConsoleOutput.contains('test failed') || + !caseInvariantConsoleOutput.contains('all tests passed'); +} + Map _readConfig() { Map json; try { final String jsonStr = _configFile!.readAsStringSync(); - json = jsonDecode(jsonStr); + json = jsonDecode(jsonStr) as Map; } catch (e) { print('Unable to load json from ${_configFile!.path}:\n$e'); rethrow; @@ -189,15 +268,16 @@ Map _readConfig() { return json; } -void _writeConfig(Map config) { - final String spaces = ' ' * 4; - final JsonEncoder encoder = JsonEncoder.withIndent(spaces); - - _configFile!.writeAsStringSync(encoder.convert(config)); -} - void _breakConfig(Map config) { - final Map broken = jsonDecode(jsonEncode(config)); + if (_configFile == null) { + throw Exception('Coins config file not found'); + } else { + print('Temporarily breaking $suspendedCoin electrum config' + " in '${_configFile!.path}' to test suspended state."); + } + + final broken = Map.from(config); + // ignore: avoid_dynamic_calls broken[suspendedCoin]['electrum'] = [ { 'url': 'broken.e1ectrum.net:10063', @@ -212,6 +292,13 @@ void _restoreConfig(Map originalConfig) { _writeConfig(originalConfig); } +void _writeConfig(Map config) { + final String spaces = ' ' * 4; + final JsonEncoder encoder = JsonEncoder.withIndent(spaces); + + _configFile!.writeAsStringSync(encoder.convert(config)); +} + Future _findCoinsConfigFile() async { final config = File('assets/config/coins_config.json'); @@ -222,11 +309,11 @@ Future _findCoinsConfigFile() async { return config; } -void _clearNativeAppsData() async { +Future _clearNativeAppsData() async { ProcessResult deleteResult; if (Platform.isWindows) { - var homeDir = Platform.environment['UserProfile']; - if (await Directory('$homeDir$windowsAppData').exists()) { + final homeDir = Platform.environment['UserProfile']; + if (Directory('$homeDir$windowsAppData').existsSync()) { deleteResult = await Process.run( 'rmdir', ['/s', '/q', '$homeDir$windowsAppData'], @@ -236,13 +323,14 @@ void _clearNativeAppsData() async { print('Windows App data removed successfully.'); } else { print( - 'Failed to remove Windows app data. Error: ${deleteResult.stderr}'); + 'Failed to remove Windows app data. Error: ${deleteResult.stderr}', + ); } } else { - print("No need clean windows app data"); + print('No need clean windows app data'); } } else if (Platform.isLinux) { - var homeDir = Platform.environment['HOME']; + final homeDir = Platform.environment['HOME']; deleteResult = await Process.run( 'rm', ['-rf', '$homeDir$linuxAppData'], @@ -254,7 +342,7 @@ void _clearNativeAppsData() async { print('Failed to remove Linux app data. Error: ${deleteResult.stderr}'); } } else if (Platform.isMacOS) { - var homeDir = Platform.environment['HOME']; + final homeDir = Platform.environment['HOME']; deleteResult = await Process.run( 'rm', ['-rf', '$homeDir$macAppData'], diff --git a/test_integration/common/pump_and_settle.dart b/test_integration/common/pump_and_settle.dart index b89fcc6ffa..9c63a2c307 100644 --- a/test_integration/common/pump_and_settle.dart +++ b/test_integration/common/pump_and_settle.dart @@ -9,7 +9,7 @@ Future pumpUntilDisappear( }) async { bool timerDone = false; final timer = - Timer(timeout, () => throw TimeoutException("Pump until has timed out")); + Timer(timeout, () => throw TimeoutException('Pump until has timed out')); while (timerDone != true) { await tester.pumpAndSettle(); @@ -20,3 +20,11 @@ Future pumpUntilDisappear( } timer.cancel(); } + +extension WidgetTesterPumpExtension on WidgetTester { + Future pumpNFrames(int frames) async { + for (int i = 0; i < frames; i++) { + await pump(); + } + } +} diff --git a/test_integration/common/tester_utils.dart b/test_integration/common/tester_utils.dart index a58cd9f181..129754cb93 100644 --- a/test_integration/common/tester_utils.dart +++ b/test_integration/common/tester_utils.dart @@ -1,10 +1,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'pause.dart'; +import 'pump_and_settle.dart'; Future testerTap(WidgetTester tester, Finder finder) async { await tester.tap(finder); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await pause(); } diff --git a/test_integration/helpers/restore_wallet.dart b/test_integration/helpers/restore_wallet.dart index 5e756438a6..94c037b87f 100644 --- a/test_integration/helpers/restore_wallet.dart +++ b/test_integration/helpers/restore_wallet.dart @@ -32,7 +32,7 @@ Future restoreWalletToTest(WidgetTester tester) async { find.byKey(const Key('custom-seed-dialog-input')); final Finder customSeedDialogOkButton = find.byKey(const Key('custom-seed-dialog-ok-button')); - const String confirmCustomSeedText = 'I understand'; + const String confirmCustomSeedText = 'I Understand'; await tester.pumpAndSettle(); isMobile @@ -45,27 +45,33 @@ Future restoreWalletToTest(WidgetTester tester) async { await tester.tap(nameField); await tester.enterText(nameField, walletName); await tester.enterText(importSeedField, testSeed); + await tester.pump(); + + // Change focus from the text field to allow the text error to appear + // TODO: look into more aggressive input validation + await tester.tap(eulaCheckBox); + await tester.pump(); + await tester.tap(tocCheckBox); + await tester.pumpNFrames(5); if (!bip39.validateMnemonic(testSeed)) { await tester.tap(allowCustomSeedCheckbox); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); await tester.enterText(customSeedDialogInput, confirmCustomSeedText); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); await tester.tap(customSeedDialogOkButton); - await tester.pumpAndSettle(); + // Works on safari with 5 frames, but requires more on chrome? + await tester.pumpNFrames(10); } - await tester.tap(eulaCheckBox); - await tester.pumpAndSettle(); - await tester.tap(tocCheckBox); await tester.dragUntilVisible( importConfirmButton, walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); await tester.tap(importConfirmButton); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); await tester.enterText(passwordField, password); await tester.enterText(passwordConfirmField, password); await tester.dragUntilVisible( @@ -73,7 +79,7 @@ Future restoreWalletToTest(WidgetTester tester) async { walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); await tester.tap(importConfirmButton); await pumpUntilDisappear(tester, walletsManagerWrapper); } diff --git a/test_integration/helpers/switch_coins_active_state.dart b/test_integration/helpers/switch_coins_active_state.dart index 6c5d493791..cefae9cf1b 100644 --- a/test_integration/helpers/switch_coins_active_state.dart +++ b/test_integration/helpers/switch_coins_active_state.dart @@ -39,7 +39,7 @@ Future switchCoinsActiveState( await tester.pumpAndSettle(const Duration(milliseconds: 250)); final Finder switchButton = - find.byKey(const Key('coins-manager-switch-button')); + find.byKey(const Key('back-button')); await tester.tap(switchButton); await tester.pumpAndSettle(); } diff --git a/test_integration/tests/dex_tests/dex_tests.dart b/test_integration/tests/dex_tests/dex_tests.dart index 30a392e093..7247518282 100644 --- a/test_integration/tests/dex_tests/dex_tests.dart +++ b/test_integration/tests/dex_tests/dex_tests.dart @@ -4,24 +4,28 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; -import './maker_orders_test.dart'; -import './taker_orders_test.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; +import './maker_orders_test.dart'; +import './taker_orders_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run DEX tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testMakerOrder(tester); - await tester.pumpAndSettle(); - await testTakerOrder(tester); + testWidgets( + 'Run DEX tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await tester.pumpAndSettle(); + await testMakerOrder(tester); + await tester.pumpAndSettle(); + await testTakerOrder(tester); - print('END DEX TESTS'); - }, semanticsEnabled: false); + print('END DEX TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/dex_tests/maker_orders_test.dart b/test_integration/tests/dex_tests/maker_orders_test.dart index 2ec907215c..60974c800a 100644 --- a/test_integration/tests/dex_tests/maker_orders_test.dart +++ b/test_integration/tests/dex_tests/maker_orders_test.dart @@ -7,6 +7,7 @@ import 'package:web_dex/main.dart' as app; import 'package:web_dex/shared/widgets/focusable_widget.dart'; import 'package:web_dex/views/dex/entities_list/orders/order_item.dart'; +import '../../common/pause.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; @@ -27,7 +28,7 @@ Future testMakerOrder(WidgetTester tester) async { matching: find.byKey(const Key('search-field')), ); final Finder sellCoinItem = - find.byKey(const Key('coin-table-item-$sellCoin')); + find.byKey(const Key('Coin-table-item-$sellCoin')); final Finder sellAmountField = find.byKey(const Key('maker-sell-amount-input')); final Finder buyCoinSelectButton = @@ -36,7 +37,7 @@ Future testMakerOrder(WidgetTester tester) async { of: find.byKey(const Key('maker-buy-coins-table')), matching: find.byKey(const Key('search-field')), ); - final Finder buyCoinItem = find.byKey(const Key('coin-table-item-$buyCoin')); + final Finder buyCoinItem = find.byKey(const Key('Coin-table-item-$buyCoin')); final Finder buyAmountField = find.byKey(const Key('maker-buy-amount-input')); final Finder makeOrderButton = find.byKey(const Key('make-order-button')); final Finder makeOrderConfirmButton = @@ -86,6 +87,7 @@ Future testMakerOrder(WidgetTester tester) async { ); await tester.tap(makeOrderConfirmButton); await tester.pumpAndSettle(); + await pause(sec: 5); // Open order details page expect(orderListItem, findsOneWidget); diff --git a/test_integration/tests/dex_tests/taker_orders_test.dart b/test_integration/tests/dex_tests/taker_orders_test.dart index 3ce6b46313..47abd0124f 100644 --- a/test_integration/tests/dex_tests/taker_orders_test.dart +++ b/test_integration/tests/dex_tests/taker_orders_test.dart @@ -27,7 +27,7 @@ Future testTakerOrder(WidgetTester tester) async { of: find.byKey(const Key('taker-sell-coins-table')), matching: find.byKey(const Key('search-field')), ); - final Finder sellCoinItem = find.byKey(Key('coin-table-item-$sellCoin')); + final Finder sellCoinItem = find.byKey(Key('Coin-table-item-$sellCoin')); final Finder sellAmountField = find.descendant( of: find.byKey(const Key('taker-sell-amount')), matching: find.byKey(const Key('amount-input')), @@ -38,7 +38,7 @@ Future testTakerOrder(WidgetTester tester) async { of: find.byKey(const Key('taker-orders-table')), matching: find.byKey(const Key('search-field')), ); - final Finder buyCoinItem = find.byKey(Key('orders-table-item-$buyCoin')); + final Finder buyCoinItem = find.byKey(Key('BestOrder-table-item-$buyCoin')); const String infiniteBidVolume = '2.00'; final bidsTable = find.byKey(const Key('orderbook-bids-list')); @@ -134,20 +134,21 @@ Future testTakerOrder(WidgetTester tester) async { ); expect( - find.descendant( - of: takerFeeSentEventStep, - matching: find.byType(CopiedText), - ), - findsOneWidget, - reason: 'Test error: \'takerFeeSent\' event tx copied text not found'); + find.descendant( + of: takerFeeSentEventStep, + matching: find.byType(CopiedText), + ), + findsOneWidget, + reason: "Test error: 'takerFeeSent' event tx copied text not found", + ); expect( - find.descendant( - of: makerPaymentReceivedEventStep, - matching: find.byType(CopiedText), - ), - findsOneWidget, - reason: - 'Test error: \'makerPaymentReceived\' event tx copied text not found'); + find.descendant( + of: makerPaymentReceivedEventStep, + matching: find.byType(CopiedText), + ), + findsOneWidget, + reason: "Test error: 'makerPaymentReceived' event tx copied text not found", + ); await tester.dragUntilVisible( takerPaymentSentEventStep, @@ -155,11 +156,13 @@ Future testTakerOrder(WidgetTester tester) async { const Offset(0, -10), ); expect( - find.descendant( - of: takerPaymentSentEventStep, matching: find.byType(CopiedText)), - findsOneWidget, - reason: - 'Test error: \'takerPaymentSent\' event tx copied text not found'); + find.descendant( + of: takerPaymentSentEventStep, + matching: find.byType(CopiedText), + ), + findsOneWidget, + reason: "Test error: 'takerPaymentSent' event tx copied text not found", + ); await tester.dragUntilVisible( takerPaymentSpentEventStep, @@ -167,11 +170,13 @@ Future testTakerOrder(WidgetTester tester) async { const Offset(0, -10), ); expect( - find.descendant( - of: takerPaymentSpentEventStep, matching: find.byType(CopiedText)), - findsOneWidget, - reason: - 'Test error: \'takerPaymentSpent\' event tx copied text not found'); + find.descendant( + of: takerPaymentSpentEventStep, + matching: find.byType(CopiedText), + ), + findsOneWidget, + reason: "Test error: 'takerPaymentSpent' event tx copied text not found", + ); await tester.dragUntilVisible( makerPaymentSpentEventStep, @@ -179,11 +184,13 @@ Future testTakerOrder(WidgetTester tester) async { const Offset(0, -10), ); expect( - find.descendant( - of: makerPaymentSpentEventStep, matching: find.byType(CopiedText)), - findsOneWidget, - reason: - 'Test error: \'makerPaymentSpent\' event tx copied text not found'); + find.descendant( + of: makerPaymentSpentEventStep, + matching: find.byType(CopiedText), + ), + findsOneWidget, + reason: "Test error: 'makerPaymentSpent' event tx copied text not found", + ); await tester.dragUntilVisible( backButton, @@ -194,7 +201,7 @@ Future testTakerOrder(WidgetTester tester) async { await tester.tap(backButton); await tester.pumpAndSettle(); await tester.tap(historyTab); - await tester.pump((const Duration(milliseconds: 1000))); + await tester.pump(const Duration(milliseconds: 1000)); expect( find.byType(HistoryItem), findsOneWidget, @@ -204,15 +211,19 @@ Future testTakerOrder(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run taker order tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testTakerOrder(tester); - - print('END TAKER ORDER TESTS'); - }, semanticsEnabled: false); + testWidgets( + 'Run taker order tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await tester.pumpAndSettle(); + await testTakerOrder(tester); + + print('END TAKER ORDER TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/misc_tests/menu_tests.dart b/test_integration/tests/misc_tests/menu_tests.dart index 7d678f53f0..f5d0ed2fb4 100644 --- a/test_integration/tests/misc_tests/menu_tests.dart +++ b/test_integration/tests/misc_tests/menu_tests.dart @@ -47,8 +47,9 @@ Future testMainMenu(WidgetTester tester) async { expect(security, findsOneWidget); expect(feedback, findsOneWidget); - await goto.supportPage(tester); - await tester.pumpAndSettle(); + // TODO: restore if/when support page is added back to a menu + // await goto.supportPage(tester); + // await tester.pumpAndSettle(); await pause(msg: 'END TEST MENU'); } @@ -56,16 +57,20 @@ Future testMainMenu(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run menu tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - print('ACCEPT ALPHA WARNING'); - await restoreWalletToTest(tester); - await testMainMenu(tester); - await tester.pumpAndSettle(); + testWidgets( + 'Run menu tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + print('ACCEPT ALPHA WARNING'); + await restoreWalletToTest(tester); + await testMainMenu(tester); + await tester.pumpAndSettle(); - print('END MAIN MENU TESTS'); - }, semanticsEnabled: false); + print('END MAIN MENU TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/misc_tests/misc_tests.dart b/test_integration/tests/misc_tests/misc_tests.dart index 89009e265f..f4d9bf21ba 100644 --- a/test_integration/tests/misc_tests/misc_tests.dart +++ b/test_integration/tests/misc_tests/misc_tests.dart @@ -4,27 +4,31 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; +import '../../helpers/accept_alpha_warning.dart'; +import '../../helpers/restore_wallet.dart'; import './feedback_tests.dart'; import './menu_tests.dart'; import './theme_test.dart'; -import '../../helpers/accept_alpha_warning.dart'; -import '../../helpers/restore_wallet.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run misc tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await tester.pumpAndSettle(); - await testThemeSwitcher(tester); - await tester.pumpAndSettle(); - await testFeedbackForm(tester); - await tester.pumpAndSettle(); - await restoreWalletToTest(tester); - await testMainMenu(tester); + testWidgets( + 'Run misc tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await tester.pumpAndSettle(); + await testThemeSwitcher(tester); + await tester.pumpAndSettle(); + await testFeedbackForm(tester); + await tester.pumpAndSettle(); + await restoreWalletToTest(tester); + await testMainMenu(tester); - print('END MISC TESTS'); - }, semanticsEnabled: false); + print('END MISC TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/misc_tests/theme_test.dart b/test_integration/tests/misc_tests/theme_test.dart index b8698170cd..0fb214ee7b 100644 --- a/test_integration/tests/misc_tests/theme_test.dart +++ b/test_integration/tests/misc_tests/theme_test.dart @@ -15,42 +15,60 @@ Future testThemeSwitcher(WidgetTester tester) async { final themeSettingsSwitcherDark = find.byKey(const Key('theme-settings-switcher-Dark')); + print( + 'brightness: ${Theme.of(tester.element(themeSwitcherFinder)).brightness}, ' + 'expected: ${Brightness.dark}', + ); // Check default theme (dark) - checkTheme(tester, themeSwitcherFinder, Brightness.dark); + expect( + Theme.of(tester.element(themeSwitcherFinder)).brightness, + equals(Brightness.dark), + reason: 'Default theme should be dark theme', + ); - await tester.tap(themeSwitcherFinder); - await tester.pumpAndSettle(); - checkTheme(tester, themeSwitcherFinder, Brightness.light); + // await tester.tap(themeSwitcherFinder); + // await tester.pumpAndSettle(); + // expect( + // Theme.of(tester.element(themeSwitcherFinder)).brightness, + // equals(Brightness.light), + // reason: 'Current theme should be light theme', + // ); await goto.settingsPage(tester); await tester.tap(themeSettingsSwitcherDark); await tester.pumpAndSettle(); - checkTheme(tester, themeSwitcherFinder, Brightness.dark); + expect( + Theme.of(tester.element(themeSwitcherFinder)).brightness, + equals(Brightness.dark), + reason: 'Current theme should be dark theme', + ); await tester.pumpAndSettle(); await tester.tap(themeSettingsSwitcherLight); await tester.pumpAndSettle(); - checkTheme(tester, themeSwitcherFinder, Brightness.light); -} - -dynamic checkTheme( - WidgetTester tester, Finder testElement, Brightness brightnessExpected) { - expect(Theme.of(tester.element(testElement)).brightness, - equals(brightnessExpected)); + expect( + Theme.of(tester.element(themeSwitcherFinder)).brightness, + equals(Brightness.light), + reason: 'Current theme should be light theme', + ); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run design tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - print('ACCEPT ALPHA WARNING'); - await tester.pumpAndSettle(); - await testThemeSwitcher(tester); - - print('END THEME SWITCH TESTS'); - }, semanticsEnabled: false); + testWidgets( + 'Run design tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + print('ACCEPT ALPHA WARNING'); + await tester.pumpAndSettle(); + await testThemeSwitcher(tester); + + print('END THEME SWITCH TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart index 30264005db..6c2900d70c 100644 --- a/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart +++ b/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart @@ -43,11 +43,11 @@ Future testCreateWallet(WidgetTester tester) async { await tester.enterText(nameField, walletName); await tester.enterText(passwordField, password); await tester.enterText(passwordConfirmField, password); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.tap(eulaCheckBox); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.tap(tocCheckBox); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.tap(confirmButton); await pumpUntilDisappear(tester, walletsManagerWrapper); if (!isMobile) { @@ -57,15 +57,19 @@ Future testCreateWallet(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run Wallet Creation tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); - await acceptAlphaWarning(tester); - await testCreateWallet(tester); - await tester.pumpAndSettle(); + testWidgets( + 'Run Wallet Creation tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + print('ACCEPT ALPHA WARNING'); + await acceptAlphaWarning(tester); + await testCreateWallet(tester); + await tester.pumpAndSettle(); - print('END WALLET CREATION TESTS'); - }, semanticsEnabled: false); + print('END WALLET CREATION TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart index c1a1976945..a9952d2501 100644 --- a/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart +++ b/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart @@ -31,7 +31,7 @@ Future testImportWallet(WidgetTester tester) async { find.byKey(const Key('custom-seed-dialog-input')); final Finder customSeedDialogOkButton = find.byKey(const Key('custom-seed-dialog-ok-button')); - const String confirmCustomSeedText = 'I understand'; + const String confirmCustomSeedText = 'I Understand'; final Finder eulaCheckbox = find.byKey(const Key('checkbox-eula')); final Finder tocCheckbox = find.byKey(const Key('checkbox-toc')); final Finder authorizedWalletButton = @@ -51,17 +51,17 @@ Future testImportWallet(WidgetTester tester) async { await tester.tap(nameField); await tester.enterText(nameField, walletName); await tester.enterText(importSeedField, customSeed); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); + await tester.tap(eulaCheckbox); + await tester.pumpNFrames(10); + await tester.tap(tocCheckbox); + await tester.pumpNFrames(10); await tester.tap(allowCustomSeedCheckbox); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.enterText(customSeedDialogInput, confirmCustomSeedText); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.tap(customSeedDialogOkButton); - await tester.pumpAndSettle(); - await tester.tap(eulaCheckbox); - await tester.pumpAndSettle(); - await tester.tap(tocCheckbox); - await tester.pumpAndSettle(); + await tester.pumpNFrames(20); await tester.tap(importConfirmButton); await tester.pumpAndSettle(); @@ -77,15 +77,19 @@ Future testImportWallet(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run Wallet Import tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); - await acceptAlphaWarning(tester); - await testImportWallet(tester); - await tester.pumpAndSettle(); + testWidgets( + 'Run Wallet Import tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + print('ACCEPT ALPHA WARNING'); + await acceptAlphaWarning(tester); + await testImportWallet(tester); + await tester.pumpAndSettle(); - print('END WALLET IMPORT TESTS'); - }, semanticsEnabled: false); + print('END WALLET IMPORT TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_tests/test_cex_prices.dart b/test_integration/tests/wallets_tests/test_cex_prices.dart index 9adf6cc060..4f1da4b932 100644 --- a/test_integration/tests/wallets_tests/test_cex_prices.dart +++ b/test_integration/tests/wallets_tests/test_cex_prices.dart @@ -24,18 +24,12 @@ Future testCexPrices(WidgetTester tester) async { final Finder coinDetailsReturnButton = find.byKey( const Key('back-button'), ); - final Finder docCoinActive = find.byKey( - const Key('active-coin-item-doc'), - ); final Finder kmdBep20CoinActive = find.byKey( const Key('active-coin-item-kmd-bep20'), ); final Finder kmdBep20Price = find.byKey( const Key('fiat-price-kmd-bep20'), ); - final Finder docPrice = find.byKey( - const Key('fiat-price-doc'), - ); final Finder list = find.byKey( const Key('wallet-page-coins-list'), ); @@ -68,10 +62,12 @@ Future testCexPrices(WidgetTester tester) async { } // Check KMD-BEP20 cex price - final hasKmdBep20 = await filterAsset(tester, - asset: kmdBep20CoinActive, - text: kmdBep20ByTicker, - searchField: searchCoinsField); + final hasKmdBep20 = await filterAsset( + tester, + asset: kmdBep20CoinActive, + text: kmdBep20ByTicker, + searchField: searchCoinsField, + ); if (hasKmdBep20) { await testerTap(tester, kmdBep20CoinActive); @@ -83,8 +79,9 @@ Future testCexPrices(WidgetTester tester) async { } // Check DOC cex price (does not exist) - await testerTap(tester, docCoinActive); - expect(docPrice, findsNothing); + // TODO: re-enable this after the doc/marty changes have been decided on + // await testerTap(tester, docCoinActive); + // expect(docPrice, findsNothing); await goto.walletPage(tester); @@ -95,16 +92,20 @@ Future testCexPrices(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run cex prices tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await testCexPrices(tester); - await tester.pumpAndSettle(); + testWidgets( + 'Run cex prices tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + print('ACCEPT ALPHA WARNING'); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testCexPrices(tester); + await tester.pumpAndSettle(); - print('END CEX PRICES TESTS'); - }, semanticsEnabled: false); + print('END CEX PRICES TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_tests/test_withdraw.dart b/test_integration/tests/wallets_tests/test_withdraw.dart index eb3ad068c7..3ad3dd85e9 100644 --- a/test_integration/tests/wallets_tests/test_withdraw.dart +++ b/test_integration/tests/wallets_tests/test_withdraw.dart @@ -6,6 +6,8 @@ import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; +import '../../common/pause.dart'; +import '../../common/pump_and_settle.dart'; import '../../common/tester_utils.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/get_funded_wif.dart'; @@ -62,6 +64,8 @@ Future testWithdraw(WidgetTester tester) async { ); await addAsset(tester, asset: martyCoinItem, search: 'marty'); + await pause(sec: 5); + await tester.pumpNFrames(20); expect(martyCoinActive, findsOneWidget); await testerTap(tester, martyCoinActive); @@ -90,20 +94,25 @@ Future testWithdraw(WidgetTester tester) async { await testerTap(tester, addressInput); await enterText(tester, finder: addressInput, text: getRandomAddress()); + await testerTap(tester, amountInput); await enterText(tester, finder: amountInput, text: '0.01'); await testerTap(tester, sendEnterButton); + await tester.pumpAndSettle(); // skip all loading & transition frames expect(confirmBackButton, findsOneWidget); expect(confirmAgreeButton, findsOneWidget); await testerTap(tester, confirmAgreeButton); + await tester.pumpAndSettle(); // skip all loading & transition frames expect(completeButtons, findsOneWidget); expect(viewOnExplorerButton, findsOneWidget); expect(doneButton, findsOneWidget); await testerTap(tester, doneButton); + await tester.pumpAndSettle(); // skip all loading & transition frames expect(exitButton, findsOneWidget); await testerTap(tester, exitButton); + await tester.pumpAndSettle(); // skip all loading & transition frames await removeAsset(tester, asset: martyCoinItem, search: 'marty'); @@ -112,16 +121,20 @@ Future testWithdraw(WidgetTester tester) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run withdraw tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await testWithdraw(tester); - await tester.pumpAndSettle(); - - print('END WITHDARW TESTS'); - }, semanticsEnabled: false); + testWidgets( + 'Run withdraw tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + print('ACCEPT ALPHA WARNING'); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testWithdraw(tester); + await tester.pumpAndSettle(); + + print('END WITHDARW TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_tests/wallet_tools.dart b/test_integration/tests/wallets_tests/wallet_tools.dart index 38c039039a..b0688b5ec7 100644 --- a/test_integration/tests/wallets_tests/wallet_tools.dart +++ b/test_integration/tests/wallets_tests/wallet_tools.dart @@ -5,10 +5,14 @@ import 'package:flutter_test/flutter_test.dart'; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; +import '../../common/pump_and_settle.dart'; import '../../common/tester_utils.dart'; -Future removeAsset(WidgetTester tester, - {required Finder asset, required String search}) async { +Future removeAsset( + WidgetTester tester, { + required Finder asset, + required String search, +}) async { final Finder removeAssetsButton = find.byKey( const Key('remove-assets-button'), ); @@ -16,7 +20,7 @@ Future removeAsset(WidgetTester tester, const Key('coins-manager-list'), ); final Finder switchButton = find.byKey( - const Key('coins-manager-switch-button'), + const Key('back-button'), ); final Finder searchCoinsField = find.byKey( const Key('coins-manager-search-field'), @@ -24,7 +28,8 @@ Future removeAsset(WidgetTester tester, await goto.walletPage(tester); - await testerTap(tester, removeAssetsButton); + await tester.tap(removeAssetsButton); + await tester.pumpNFrames(10); expect(list, findsOneWidget); try { @@ -54,8 +59,11 @@ Future removeAsset(WidgetTester tester, await pause(sec: 5); } -Future addAsset(WidgetTester tester, - {required Finder asset, required String search}) async { +Future addAsset( + WidgetTester tester, { + required Finder asset, + required String search, +}) async { final Finder list = find.byKey( const Key('coins-manager-list'), ); @@ -66,7 +74,7 @@ Future addAsset(WidgetTester tester, const Key('coins-manager-search-field'), ); final Finder switchButton = find.byKey( - const Key('coins-manager-switch-button'), + const Key('back-button'), ); await goto.walletPage(tester); @@ -93,7 +101,7 @@ Future addAsset(WidgetTester tester, const Offset(-250, 0), ); await tester.pumpAndSettle(); - await testerTap(tester, asset); + await tester.tap(asset); try { expect(switchButton, findsOneWidget); @@ -101,7 +109,7 @@ Future addAsset(WidgetTester tester, print('**Error** addAsset(): switchButton: $switchButton'); } - await testerTap(tester, switchButton); + await tester.tap(switchButton); await tester.pumpAndSettle(); } @@ -123,9 +131,12 @@ Future filterAsset( return true; } -Future enterText(WidgetTester tester, - {required Finder finder, required String text}) async { +Future enterText( + WidgetTester tester, { + required Finder finder, + required String text, +}) async { await tester.enterText(finder, text); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await pause(); } diff --git a/test_integration/tests/wallets_tests/wallets_tests.dart b/test_integration/tests/wallets_tests/wallets_tests.dart index 14b41f6be0..577ae94aed 100644 --- a/test_integration/tests/wallets_tests/wallets_tests.dart +++ b/test_integration/tests/wallets_tests/wallets_tests.dart @@ -15,25 +15,29 @@ import 'test_withdraw.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run wallet tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); + testWidgets( + 'Run wallet tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); - print('RESTORE WALLET TO TEST'); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testCoinIcons(tester); - await tester.pumpAndSettle(); - await testActivateCoins(tester); - await tester.pumpAndSettle(); - await testCexPrices(tester); - await tester.pumpAndSettle(); - await testWithdraw(tester); - await tester.pumpAndSettle(); - await testFilters(tester); + print('RESTORE WALLET TO TEST'); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await tester.pumpAndSettle(); + await testCoinIcons(tester); + await tester.pumpAndSettle(); + await testActivateCoins(tester); + await tester.pumpAndSettle(); + await testCexPrices(tester); + await tester.pumpAndSettle(); + await testWithdraw(tester); + await tester.pumpAndSettle(); + await testFilters(tester); - print('END WALLET TESTS'); - }, semanticsEnabled: false); + print('END WALLET TESTS'); + }, + semanticsEnabled: false, + ); } diff --git a/test_units/main.dart b/test_units/main.dart index 088b076f64..a08b9f77b3 100644 --- a/test_units/main.dart +++ b/test_units/main.dart @@ -1,5 +1,6 @@ import 'package:test/test.dart'; +import 'tests/cex_market_data/binance_repository_test.dart'; import 'tests/cex_market_data/charts_test.dart'; import 'tests/cex_market_data/generate_demo_data_test.dart'; import 'tests/cex_market_data/profit_loss_repository_test.dart'; @@ -81,6 +82,7 @@ void main() { group('CexMarketData: ', () { testCharts(); + testFailingBinanceRepository(); testProfitLossRepository(); testGenerateDemoData(); }); diff --git a/test_units/tests/cex_market_data/binance_repository_test.dart b/test_units/tests/cex_market_data/binance_repository_test.dart new file mode 100644 index 0000000000..554eaabd7f --- /dev/null +++ b/test_units/tests/cex_market_data/binance_repository_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; + +import 'mocks/mock_failing_binance_provider.dart'; + +void testFailingBinanceRepository() { + late BinanceRepository binanceRepository; + + setUp(() { + binanceRepository = BinanceRepository( + binanceProvider: const MockFailingBinanceProvider(), + ); + }); + + group('Failing BinanceRepository Requests', () { + test('Coin list is empty if all requests to binance fail', () async { + final response = await binanceRepository.getCoinList(); + expect(response, isEmpty); + }); + + test( + 'OHLC request rethrows [UnsupportedError] if all requests fail', + () async { + expect( + () async { + final response = await binanceRepository.getCoinOhlc( + const CexCoinPair.usdtPrice('KMD'), + GraphInterval.oneDay, + ); + return response; + }, + throwsUnsupportedError, + ); + }); + + test('Coin fiat price throws [UnsupportedError] if all requests fail', + () async { + expect( + () async { + final response = await binanceRepository.getCoinFiatPrice('KMD'); + return response; + }, + throwsUnsupportedError, + ); + }); + + test('Coin fiat prices throws [UnsupportedError] if all requests fail', + () async { + expect( + () async { + final response = await binanceRepository + .getCoinFiatPrices('KMD', [DateTime.now()]); + return response; + }, + throwsUnsupportedError, + ); + }); + }); +} diff --git a/test_units/tests/cex_market_data/generate_demo_data_test.dart b/test_units/tests/cex_market_data/generate_demo_data_test.dart index d236b2b973..47f92d01ae 100644 --- a/test_units/tests/cex_market_data/generate_demo_data_test.dart +++ b/test_units/tests/cex_market_data/generate_demo_data_test.dart @@ -3,83 +3,106 @@ import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/generate_demo_data.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; +import 'mocks/mock_binance_provider.dart'; + +void main() { + testGenerateDemoData(); +} + void testGenerateDemoData() { late DemoDataGenerator generator; - late BinanceRepository binanceRepository; + late CexRepository cexRepository; - setUp(() { - binanceRepository = BinanceRepository( - binanceProvider: const BinanceProvider(), + setUp(() async { + // TODO: Replace with a mock repository + cexRepository = BinanceRepository( + binanceProvider: const MockBinanceProvider(), ); + // Pre-fetch & cache the coins list to avoid making multiple requests + await cexRepository.getCoinList(); + generator = DemoDataGenerator( - binanceRepository, - randomSeed: 42, + cexRepository, ); }); - group('DemoDataGenerator', () { - test('generateTransactions returns correct number of transactions', - () async { - final transactions = - await generator.generateTransactions('KMD', PerformanceMode.good); - expect( - transactions.length, - closeTo(generator.transactionsPerMode[PerformanceMode.good] ?? 0, 4), - ); - }); - - test('generateTransactions returns empty list for invalid coin', () async { - final transactions = await generator.generateTransactions( - 'INVALID_COIN', - PerformanceMode.good, - ); - expect(transactions, isEmpty); - }); - - test('generateTransactions respects performance mode', () async { - final goodTransactions = - await generator.generateTransactions('KMD', PerformanceMode.good); - final badTransactions = - await generator.generateTransactions('KMD', PerformanceMode.veryBad); - - double goodBalance = generator.initialBalance; - double badBalance = generator.initialBalance; - - for (var tx in goodTransactions) { - goodBalance += double.parse(tx.myBalanceChange); - } - - for (var tx in badTransactions) { - badBalance += double.parse(tx.myBalanceChange); - } - - expect(goodBalance, greaterThan(badBalance)); - }); - - test('generateTransactions produces valid transaction objects', () async { - final transactions = - await generator.generateTransactions('KMD', PerformanceMode.mediocre); - - for (var tx in transactions) { - expect(tx.coin, equals('KMD')); - expect(tx.confirmations, inInclusiveRange(1, 3)); - expect(tx.feeDetails.coin, equals('USDT')); - expect(tx.from, isNotEmpty); - expect(tx.to, isNotEmpty); - expect(tx.internalId, isNotEmpty); - expect(tx.txHash, isNotEmpty); - expect(double.tryParse(tx.myBalanceChange), isNotNull); - expect(double.tryParse(tx.totalAmount), isNotNull); - } - }); - - test('fetchOhlcData returns data for all specified coin pairs', () async { - final ohlcData = await generator.fetchOhlcData(); - - for (var coinPair in generator.coinPairs) { - expect(ohlcData[coinPair], isNotNull); - expect(ohlcData[coinPair]!, isNotEmpty); - } - }); - }); + group( + 'DemoDataGenerator with live BinanceAPI repository', + () { + test('generateTransactions returns correct number of transactions', + () async { + final transactions = + await generator.generateTransactions('BTC', PerformanceMode.good); + expect( + transactions.length, + closeTo(generator.transactionsPerMode[PerformanceMode.good] ?? 0, 4), + ); + }); + + test('generateTransactions returns empty list for invalid coin', + () async { + final transactions = await generator.generateTransactions( + 'INVALID_COIN', + PerformanceMode.good, + ); + expect(transactions, isEmpty); + }); + + test('generateTransactions respects performance mode', () async { + final goodTransactions = + await generator.generateTransactions('BTC', PerformanceMode.good); + final badTransactions = await generator.generateTransactions( + 'BTC', PerformanceMode.veryBad); + + double goodBalance = generator.initialBalance; + double badBalance = generator.initialBalance; + + for (final tx in goodTransactions) { + goodBalance += double.parse(tx.myBalanceChange); + } + + for (final tx in badTransactions) { + badBalance += double.parse(tx.myBalanceChange); + } + + expect(goodBalance, greaterThan(badBalance)); + }); + + test('generateTransactions produces valid transaction objects', () async { + final transactions = await generator.generateTransactions( + 'BTC', PerformanceMode.mediocre); + + for (final tx in transactions) { + expect(tx.coin, equals('BTC')); + expect(tx.confirmations, inInclusiveRange(1, 3)); + expect(tx.feeDetails.coin, equals('USDT')); + expect(tx.from, isNotEmpty); + expect(tx.to, isNotEmpty); + expect(tx.internalId, isNotEmpty); + expect(tx.txHash, isNotEmpty); + expect(double.tryParse(tx.myBalanceChange), isNotNull); + expect(double.tryParse(tx.totalAmount), isNotNull); + } + }); + + test('fetchOhlcData returns data for all supported coin pairs', () async { + final ohlcData = await generator.fetchOhlcData(); + final supportedCoins = await cexRepository.getCoinList(); + + for (final coinPair in generator.coinPairs) { + final supportedCoin = supportedCoins.where( + (coin) => coin.id == coinPair.baseCoinTicker, + ); + if (supportedCoin.isEmpty) { + expect(ohlcData[coinPair], isNull); + continue; + } + + expect(ohlcData[coinPair], isNotNull); + expect(ohlcData[coinPair]!, isNotEmpty); + } + }); + }, + skip: true, + ); } diff --git a/test_units/tests/cex_market_data/mocks/mock_binance_provider.dart b/test_units/tests/cex_market_data/mocks/mock_binance_provider.dart new file mode 100644 index 0000000000..6e41b31646 --- /dev/null +++ b/test_units/tests/cex_market_data/mocks/mock_binance_provider.dart @@ -0,0 +1,161 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart'; + +/// A mock class for testing a failing Binance provider +/// - all IPs blocked, or network issues +class MockBinanceProvider implements IBinanceProvider { + const MockBinanceProvider(); + + @override + Future fetchExchangeInfo({String? baseUrl}) { + throw UnsupportedError( + 'Full binance exchange info response is not supported', + ); + } + + @override + Future fetchExchangeInfoReduced({ + String? baseUrl, + }) { + return Future.value( + BinanceExchangeInfoResponseReduced( + timezone: 'utc+0', + serverTime: DateTime.now().millisecondsSinceEpoch, + symbols: [ + SymbolReduced( + baseAsset: 'BTC', + quoteAsset: 'USDT', + baseAssetPrecision: 8, + quotePrecision: 8, + status: 'TRADING', + isSpotTradingAllowed: true, + quoteAssetPrecision: 8, + symbol: 'BTCUSDT', + ), + SymbolReduced( + baseAsset: 'ETH', + quoteAsset: 'USDT', + baseAssetPrecision: 8, + quotePrecision: 8, + status: 'TRADING', + isSpotTradingAllowed: true, + quoteAssetPrecision: 8, + symbol: 'ETHUSDT', + ), + SymbolReduced( + baseAsset: 'KMD', + quoteAsset: 'USDT', + baseAssetPrecision: 8, + quotePrecision: 8, + status: 'TRADING', + isSpotTradingAllowed: true, + quoteAssetPrecision: 8, + symbol: 'KMDUSDT', + ), + SymbolReduced( + baseAsset: 'LTC', + quoteAsset: 'USDT', + baseAssetPrecision: 8, + quotePrecision: 8, + status: 'TRADING', + isSpotTradingAllowed: true, + quoteAssetPrecision: 8, + symbol: 'LTCUSDT', + ), + ], + ), + ); + } + + @override + Future fetchKlines( + String symbol, + String interval, { + int? startUnixTimestampMilliseconds, + int? endUnixTimestampMilliseconds, + int? limit, + String? baseUrl, + }) { + List ohlc = [ + const Ohlc( + openTime: 1708646400000, + open: 50740.50, + high: 50740.50, + low: 50740.50, + close: 50740.50, + closeTime: 1708646400000, + ), + const Ohlc( + openTime: 1708984800000, + open: 50740.50, + high: 50740.50, + low: 50740.50, + close: 50740.50, + closeTime: 1708984800000, + ), + const Ohlc( + openTime: 1714435200000, + open: 60666.60, + high: 60666.60, + low: 60666.60, + close: 60666.60, + closeTime: 1714435200000, + ), + Ohlc( + openTime: DateTime.now() + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch, + open: 60666.60, + high: 60666.60, + low: 60666.60, + close: 60666.60, + closeTime: DateTime.now() + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch, + ), + Ohlc( + openTime: DateTime.now().millisecondsSinceEpoch, + open: 60666.60, + high: 60666.60, + low: 60666.60, + close: 60666.60, + closeTime: DateTime.now().millisecondsSinceEpoch, + ), + Ohlc( + openTime: + DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch, + open: 60666.60, + high: 60666.60, + low: 60666.60, + close: 60666.60, + closeTime: + DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch, + ), + ]; + + if (startUnixTimestampMilliseconds != null) { + ohlc = ohlc + .where((ohlc) => ohlc.closeTime >= startUnixTimestampMilliseconds) + .toList(); + } + + if (endUnixTimestampMilliseconds != null) { + ohlc = ohlc + .where((ohlc) => ohlc.closeTime <= endUnixTimestampMilliseconds) + .toList(); + } + + if (limit != null && limit > 0) { + ohlc = ohlc.take(limit).toList(); + } + + ohlc.sort((a, b) => a.closeTime.compareTo(b.closeTime)); + + return Future.value( + CoinOhlc( + ohlc: ohlc, + ), + ); + } +} diff --git a/test_units/tests/cex_market_data/mocks/mock_failing_binance_provider.dart b/test_units/tests/cex_market_data/mocks/mock_failing_binance_provider.dart new file mode 100644 index 0000000000..397a7c6e77 --- /dev/null +++ b/test_units/tests/cex_market_data/mocks/mock_failing_binance_provider.dart @@ -0,0 +1,33 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart'; + +/// A mock class for testing a failing Binance provider +/// - all IPs blocked, or network issues +class MockFailingBinanceProvider implements IBinanceProvider { + const MockFailingBinanceProvider(); + + @override + Future fetchExchangeInfo({String? baseUrl}) { + throw UnsupportedError('Intentional exception'); + } + + @override + Future fetchExchangeInfoReduced({ + String? baseUrl, + }) { + throw UnsupportedError('Intentional exception'); + } + + @override + Future fetchKlines( + String symbol, + String interval, { + int? startUnixTimestampMilliseconds, + int? endUnixTimestampMilliseconds, + int? limit, + String? baseUrl, + }) { + throw UnsupportedError('Intentional exception'); + } +} diff --git a/test_units/tests/cex_market_data/profit_loss_repository_test.dart b/test_units/tests/cex_market_data/profit_loss_repository_test.dart index 5f03d19a99..0a4097bea8 100644 --- a/test_units/tests/cex_market_data/profit_loss_repository_test.dart +++ b/test_units/tests/cex_market_data/profit_loss_repository_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart'; +import 'mocks/mock_binance_provider.dart'; import 'transaction_generation.dart'; void main() { @@ -20,10 +21,11 @@ void testNetProfitLossRepository() { late double currentBtcPrice; setUp(() async { - // TODO: Implement a mock CexRepository cexRepository = BinanceRepository( - binanceProvider: const BinanceProvider(), + binanceProvider: const MockBinanceProvider(), ); + // Pre-fetch & cache the coins list to avoid making multiple requests + await cexRepository.getCoinList(); profitLossRepository = ProfitLossCalculator( cexRepository, ); @@ -59,10 +61,10 @@ void testNetProfitLossRepository() { fiatCoinId: 'USD', ); - final expectedProfitLoss = (currentBtcPrice * 1.0) - (51288.42 * 1.0); + final expectedProfitLoss = (currentBtcPrice * 1.0) - (50740.50 * 1.0); expect(result.length, 1); - expect(result[0].profitLoss, closeTo(expectedProfitLoss, 100)); + expect(result[0].profitLoss, closeTo(expectedProfitLoss, 1000)); }); test('return profit/loss for a 50% sale', () async { @@ -76,11 +78,10 @@ void testNetProfitLossRepository() { coinId: 'BTC', fiatCoinId: 'USD', ); + final expectedProfitLossT1 = (currentBtcPrice * 1.0) - (50740.50 * 1.0); - final expectedProfitLossT1 = (currentBtcPrice * 1.0) - (51288.42 * 1.0); - - const t2CostBasis = 51288.42 * 0.5; - const t2SaleProceeds = 63866 * 0.5; + const t2CostBasis = 50740.50 * 0.5; + const t2SaleProceeds = 60666.60 * 0.5; const t2RealizedProfitLoss = t2SaleProceeds - t2CostBasis; final t2UnrealisedProfitLoss = (currentBtcPrice * 0.5) - t2CostBasis; final expectedTotalProfitLoss = @@ -89,11 +90,11 @@ void testNetProfitLossRepository() { expect(result.length, 2); expect( result[0].profitLoss, - closeTo(expectedProfitLossT1, 100), + closeTo(expectedProfitLossT1, 1000), ); expect( result[1].profitLoss, - closeTo(expectedTotalProfitLoss, 100), + closeTo(expectedTotalProfitLoss, 1000), ); }); @@ -110,11 +111,11 @@ void testNetProfitLossRepository() { fiatCoinId: 'USD', ); - final expectedProfitLossT1 = (currentBtcPrice * 1.0) - (51288.42 * 1.0); + final expectedProfitLossT1 = (currentBtcPrice * 1.0) - (50740.50 * 1.0); const t3LeftoverBalance = 0.5; - const t3CostBasis = 51288.42 * t3LeftoverBalance; - const t3SaleProceeds = 63866 * 0.5; + const t3CostBasis = 50740.50 * t3LeftoverBalance; + const t3SaleProceeds = 60666.60 * 0.5; const t3RealizedProfitLoss = t3SaleProceeds - t3CostBasis; final t3CurrentBalancePrice = currentBtcPrice * t3LeftoverBalance; final t3UnrealisedProfitLoss = t3CurrentBalancePrice - t3CostBasis; @@ -124,17 +125,17 @@ void testNetProfitLossRepository() { expect(result.length, 2); expect( result[0].profitLoss, - closeTo(expectedProfitLossT1, 100), + closeTo(expectedProfitLossT1, 1000), ); expect( result[1].profitLoss, - closeTo(expectedTotalProfitLoss, 100), + closeTo(expectedTotalProfitLoss, 1000), ); }); test('should zero same day transfer of balance without fees', () async { final transactions = [ - createBuyTransaction(1.0, timeStamp: 1708646400), + createBuyTransaction(1.0), createSellTransaction(1.0, timeStamp: 1708646500), ]; @@ -159,13 +160,13 @@ void testRealisedProfitLossRepository() { late CexRepository cexRepository; setUp(() async { - // TODO: Implement a mock CexRepository cexRepository = BinanceRepository( - binanceProvider: const BinanceProvider(), + binanceProvider: const MockBinanceProvider(), ); profitLossRepository = RealisedProfitLossCalculator( cexRepository, ); + await cexRepository.getCoinList(); }); test('return the unrealised profit/loss for a single transaction', @@ -197,14 +198,14 @@ void testRealisedProfitLossRepository() { fiatCoinId: 'USD', ); - const t2CostBasis = 51288.42 * 0.5; - const t2SaleProceeds = 63866 * 0.5; + const t2CostBasis = 50740.50 * 0.5; + const t2SaleProceeds = 60666.60 * 0.5; const expectedRealizedProfitLoss = t2SaleProceeds - t2CostBasis; expect(result.length, 2); expect( result[1].profitLoss, - closeTo(expectedRealizedProfitLoss, 100), + closeTo(expectedRealizedProfitLoss, 1000), ); }); @@ -222,20 +223,20 @@ void testRealisedProfitLossRepository() { ); const t3LeftoverBalance = 0.5; - const t3CostBasis = 51288.42 * t3LeftoverBalance; - const t3SaleProceeds = 63866 * 0.5; + const t3CostBasis = 50740.50 * t3LeftoverBalance; + const t3SaleProceeds = 60666.60 * 0.5; const t3RealizedProfitLoss = t3SaleProceeds - t3CostBasis; expect(result.length, 2); expect( result[1].profitLoss, - closeTo(t3RealizedProfitLoss, 100), + closeTo(t3RealizedProfitLoss, 1000), ); }); test('should zero same day transfer of balance without fees', () async { final transactions = [ - createBuyTransaction(1.0, timeStamp: 1708646400), + createBuyTransaction(1.0), createSellTransaction(1.0, timeStamp: 1708646500), ]; diff --git a/test_units/tests/cex_market_data/transaction_generation.dart b/test_units/tests/cex_market_data/transaction_generation.dart index 3a209e60d2..6fb675c212 100644 --- a/test_units/tests/cex_market_data/transaction_generation.dart +++ b/test_units/tests/cex_market_data/transaction_generation.dart @@ -1,11 +1,9 @@ import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; import 'package:web_dex/model/withdraw_details/fee_details.dart'; -// TODO: copy over the mock transaction data generator from lib - Transaction createBuyTransaction( double balanceChange, { - int timeStamp = 1708646400, + int timeStamp = 1708646400, // $50,740.50 usd }) { final String value = balanceChange.toString(); return Transaction( @@ -18,7 +16,7 @@ Transaction createBuyTransaction( myBalanceChange: value, receivedByMe: value, spentByMe: '0.0', - timestamp: timeStamp, // $50,740.50 usd + timestamp: timeStamp, to: ['1XYZ...'], totalAmount: value, txHash: 'hash1', @@ -29,12 +27,13 @@ Transaction createBuyTransaction( Transaction createSellTransaction( double balanceChange, { - int timeStamp = 1714435200, + int timeStamp = 1714435200, // $60,666.60 usd }) { - if (!balanceChange.isNegative) { - balanceChange = -balanceChange; + double adjustedBalanceChange = balanceChange; + if (!adjustedBalanceChange.isNegative) { + adjustedBalanceChange = -adjustedBalanceChange; } - final String value = balanceChange.toString(); + final String value = adjustedBalanceChange.toString(); return Transaction( blockHeight: 100200, coin: 'BTC', @@ -44,8 +43,8 @@ Transaction createSellTransaction( internalId: 'internal3', myBalanceChange: value, receivedByMe: '0.0', - spentByMe: balanceChange.abs().toString(), - timestamp: timeStamp, // $60,666.60 usd + spentByMe: adjustedBalanceChange.abs().toString(), + timestamp: timeStamp, to: ['1GHI...'], totalAmount: value, txHash: 'hash3', diff --git a/web/src/index.js b/web/index.js similarity index 96% rename from web/src/index.js rename to web/index.js index 1e6ae09152..ac80dfcb17 100644 --- a/web/src/index.js +++ b/web/index.js @@ -6,7 +6,7 @@ // will "boot" the module and make it ready to use. Currently browsers // don't support natively imported WebAssembly as an ES module, but // eventually the manual initialization won't be required! -import init, { LogLevel, Mm2MainErr, Mm2RpcErr, mm2_main, mm2_main_status, mm2_rpc, mm2_version } from "./mm2/kdflib.js"; +import init, { LogLevel, Mm2MainErr, Mm2RpcErr, mm2_main, mm2_main_status, mm2_rpc, mm2_version } from "./src/mm2/kdflib.js"; import './services/theme_checker/theme_checker.js'; import zip from './services/zip/zip.js'; @@ -70,7 +70,7 @@ window.rpc_request = async function (request_js) { alert(`Unexpected error: ${e}`); break; } - throw(e); + throw (e); } } diff --git a/web/src/services/theme_checker/theme_checker.js b/web/services/theme_checker/theme_checker.js similarity index 100% rename from web/src/services/theme_checker/theme_checker.js rename to web/services/theme_checker/theme_checker.js diff --git a/web/services/zip/zip.js b/web/services/zip/zip.js new file mode 100644 index 0000000000..c5053fc815 --- /dev/null +++ b/web/services/zip/zip.js @@ -0,0 +1,27 @@ +// @ts-check +class Zip { + constructor() { + this.encode = this.encode.bind(this); + this.worker = new Worker(new URL('./zip_worker.js', import.meta.url)); + } + + /** + * @param {String} fileName + * @param {String} fileContent + * @returns {Promise} + */ + async encode(fileName, fileContent) { + /** @type {Worker} */ + return new Promise((resolve, reject) => { + this.worker.postMessage({ fileContent, fileName }); + this.worker.onmessage = (event) => resolve(event.data); + this.worker.onerror = (e) => reject(e); + }).catch((e) => { + return null; + }); + } +} +/** @type {Zip} */ +const zip = new Zip(); + +export default zip; \ No newline at end of file diff --git a/web/services/zip/zip_worker.js b/web/services/zip/zip_worker.js new file mode 100644 index 0000000000..acb6868c4e --- /dev/null +++ b/web/services/zip/zip_worker.js @@ -0,0 +1,19 @@ +// @ts-check +import 'jszip'; +import JSZip from 'jszip'; +/** @param {MessageEvent<{fileContent: String, fileName: String}>} event */ +onmessage = async (event) => { + const zip = new JSZip(); + const textContent = event.data.fileContent; + const fileName = event.data.fileName; + + zip.file(fileName, textContent); + const compressed = await zip.generateAsync({ + type: "base64", + compression: "DEFLATE", + compressionOptions: { + level: 9 + }, + }); + postMessage(compressed); +}; \ No newline at end of file diff --git a/web/src/services/zip/zip.js b/web/src/services/zip/zip.js deleted file mode 100644 index 7d4acbd024..0000000000 --- a/web/src/services/zip/zip.js +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-check -class Zip { - constructor() { - this.encode = this.encode.bind(this); - this.worker = new Worker(new URL('./zip_worker.js', import.meta.url)); - } - - /** - * @param {String} fileName - * @param {String} fileContent - * @returns {Promise} - */ - async encode(fileName, fileContent) { - /** @type {Worker} */ - return new Promise((resolve, reject) => { - this.worker.postMessage({ fileContent, fileName}); - this.worker.onmessage = (event) => resolve(event.data); - this.worker.onerror = (e) => reject(e); - }).catch((e) => { - return null; - }); - } - } - /** @type {Zip} */ - const zip = new Zip(); - - export default zip; \ No newline at end of file diff --git a/web/src/services/zip/zip_worker.js b/web/src/services/zip/zip_worker.js deleted file mode 100644 index d6d74824b9..0000000000 --- a/web/src/services/zip/zip_worker.js +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -import 'jszip'; -import JSZip from 'jszip'; -/** @param {MessageEvent<{fileContent: String, fileName: String}>} event */ -onmessage = async (event) => { - const zip = new JSZip(); - const textContent = event.data.fileContent; - const fileName = event.data.fileName; - - zip.file(fileName, textContent); - const compressed = await zip.generateAsync({ - type: "base64", - compression: "DEFLATE", - compressionOptions: { - level: 9 - }, - }); - postMessage(compressed); -}; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index df340f2a9f..c71791fdda 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = (env, argv) => { return { - entry: './web/src/index.js', + entry: './web/index.js', output: { path: path.resolve(__dirname, 'web/dist'), filename: 'script.[contenthash].js', From 3a29513f6dfec4664c529ec908740b1ca18dc556 Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Thu, 3 Oct 2024 16:32:28 +0300 Subject: [PATCH 03/34] add arbitrum coin type (#166) --- lib/app_config/app_config.dart | 1 + lib/bloc/fiat/base_fiat_provider.dart | 5 +++++ lib/model/coin.dart | 6 ++++++ lib/model/coin_type.dart | 1 + lib/model/coin_utils.dart | 4 ++++ lib/shared/constants.dart | 2 ++ lib/shared/utils/prominent_colors.dart | 1 + lib/shared/utils/utils.dart | 14 ++++++++++++++ lib/shared/widgets/coin_item/coin_logo.dart | 2 ++ lib/shared/widgets/coin_type_tag.dart | 2 ++ .../komodo_ui_kit/lib/src/images/coin_icon.dart | 1 + 11 files changed, 39 insertions(+) diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 36657c64eb..121ae76ff3 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -43,6 +43,7 @@ Map priorityCoinsAbbrMap = { 'DASH': 11, 'MATIC': 10, 'FTM': 10, + 'ARB': 10, 'AVAX': 10, 'HT': 10, 'MOVR': 10, diff --git a/lib/bloc/fiat/base_fiat_provider.dart b/lib/bloc/fiat/base_fiat_provider.dart index 3b3b4f064d..19f0d885a0 100644 --- a/lib/bloc/fiat/base_fiat_provider.dart +++ b/lib/bloc/fiat/base_fiat_provider.dart @@ -29,6 +29,7 @@ class Currency { (t == CoinType.avx20 && symbol == 'AVAX') || (t == CoinType.etc && symbol == 'ETC') || (t == CoinType.ftm20 && symbol == 'FTM') || + (t == CoinType.arb20 && symbol == 'ARB') || (t == CoinType.hrc20 && symbol == 'ONE') || (t == CoinType.plg20 && symbol == 'MATIC') || (t == CoinType.mvr20 && symbol == 'MOVR')) return symbol; @@ -155,6 +156,8 @@ abstract class BaseFiatProvider { return 'ETC'; case CoinType.ftm20: return 'FTM'; + case CoinType.arb20: + return 'ARB'; case CoinType.hrc20: return 'HARMONY'; case CoinType.plg20: @@ -261,6 +264,8 @@ abstract class BaseFiatProvider { return CoinType.etc; case "FTM": return CoinType.ftm20; + case "ARB": + return CoinType.arb20; case "HARMONY": return CoinType.hrc20; case "MATIC": diff --git a/lib/model/coin.dart b/lib/model/coin.dart index bb836d59f8..3e0a8e4011 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -455,6 +455,12 @@ CoinType? getCoinType(String? jsonType, String coinAbbr) { } else { continue; } + case CoinType.arb20: + if (jsonType == 'Arbitrum') { + return value; + } else { + continue; + } case CoinType.etc: if (jsonType == 'Ethereum Classic') { return value; diff --git a/lib/model/coin_type.dart b/lib/model/coin_type.dart index 8dd1f359f0..a0d9009baa 100644 --- a/lib/model/coin_type.dart +++ b/lib/model/coin_type.dart @@ -7,6 +7,7 @@ enum CoinType { bep20, qrc20, ftm20, + arb20, avx20, hrc20, mvr20, diff --git a/lib/model/coin_utils.dart b/lib/model/coin_utils.dart index 0cbdf9e67e..a4e53e9b17 100644 --- a/lib/model/coin_utils.dart +++ b/lib/model/coin_utils.dart @@ -122,6 +122,8 @@ String getCoinTypeName(CoinType type) { return 'Smart Chain'; case CoinType.ftm20: return 'FTM-20'; + case CoinType.arb20: + return 'ARB-20'; case CoinType.etc: return 'ETC'; case CoinType.avx20: @@ -163,6 +165,8 @@ String getCoinTypeNameLong(CoinType type) { return 'Smart Chain'; case CoinType.ftm20: return 'Fantom'; + case CoinType.arb20: + return 'Arbitrum'; case CoinType.etc: return 'Ethereum Classic'; case CoinType.avx20: diff --git a/lib/shared/constants.dart b/lib/shared/constants.dart index f29902f768..6ac40f8e40 100644 --- a/lib/shared/constants.dart +++ b/lib/shared/constants.dart @@ -17,6 +17,8 @@ const String bnbUrl = '$ercTxHistoryUrl/v1/bnb_tx_history'; const String bepUrl = '$ercTxHistoryUrl/v2/bep_tx_history'; const String ftmUrl = '$ercTxHistoryUrl/v1/ftm_tx_history'; const String ftmTokenUrl = '$ercTxHistoryUrl/v2/ftm_tx_history'; +const String arbUrl = '$ercTxHistoryUrl/v1/arbitrum_tx_history'; +const String arbTokenUrl = '$ercTxHistoryUrl/v2/arbitrum_tx_history'; const String etcUrl = '$ercTxHistoryUrl/v1/etc_tx_history'; const String avaxUrl = '$ercTxHistoryUrl/v1/avx_tx_history'; const String avaxTokenUrl = '$ercTxHistoryUrl/v2/avx_tx_history'; diff --git a/lib/shared/utils/prominent_colors.dart b/lib/shared/utils/prominent_colors.dart index 769c316f3a..411b888a44 100644 --- a/lib/shared/utils/prominent_colors.dart +++ b/lib/shared/utils/prominent_colors.dart @@ -74,6 +74,7 @@ final Map _prominentColors = { "cds": "#a67116", "mil": "#49b1e6", "ftm": "#14b4ec", + "arb": "#28a0f0", "sol": "#63f89e", "joy": "#7f4ddf", "band": "#4f68f9", diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index a7771d3423..98574b37f4 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -285,6 +285,7 @@ String abbr2Ticker(String abbr) { 'BEP20', 'QRC20', 'FTM20', + 'ARB20', 'HRC20', 'MVR20', 'AVX20', @@ -370,6 +371,15 @@ String? getErcTransactionHistoryUrl(Coin coin) { contractAddress, coin.isTestCoin, ); // 'FTM', 'FTMT' + case CoinType.arb20: + return _getErcTransactionHistoryUrl( + coin.protocolType, + arbUrl, + arbTokenUrl, + address, + contractAddress, + coin.isTestCoin, + ); // 'ARB' case CoinType.etc: return _getErcTransactionHistoryUrl( coin.protocolType, @@ -489,6 +499,8 @@ Color getProtocolColor(CoinType type) { return const Color.fromRGBO(0, 168, 226, 1); case CoinType.ftm20: return const Color.fromRGBO(25, 105, 255, 1); + case CoinType.arb20: + return const Color.fromRGBO(0, 168, 226, 1); case CoinType.hrc20: return const Color.fromRGBO(29, 195, 219, 1); case CoinType.etc: @@ -534,6 +546,7 @@ bool hasTxHistorySupport(Coin coin) { case CoinType.bep20: case CoinType.qrc20: case CoinType.ftm20: + case CoinType.arb20: case CoinType.etc: case CoinType.avx20: case CoinType.mvr20: @@ -561,6 +574,7 @@ String getNativeExplorerUrlByCoin(Coin coin, String? address) { case CoinType.bep20: case CoinType.qrc20: case CoinType.ftm20: + case CoinType.arb20: case CoinType.avx20: case CoinType.mvr20: case CoinType.hco20: diff --git a/lib/shared/widgets/coin_item/coin_logo.dart b/lib/shared/widgets/coin_item/coin_logo.dart index ace45c2a74..72aa610894 100644 --- a/lib/shared/widgets/coin_item/coin_logo.dart +++ b/lib/shared/widgets/coin_item/coin_logo.dart @@ -141,6 +141,8 @@ String getProtocolIcon(Coin coin) { return 'qtum'; case CoinType.ftm20: return 'ftm'; + case CoinType.arb20: + return 'arb'; case CoinType.etc: return 'etc'; case CoinType.avx20: diff --git a/lib/shared/widgets/coin_type_tag.dart b/lib/shared/widgets/coin_type_tag.dart index ac19eac9f2..f3f04128ce 100644 --- a/lib/shared/widgets/coin_type_tag.dart +++ b/lib/shared/widgets/coin_type_tag.dart @@ -50,6 +50,8 @@ class CoinTypeTag extends StatelessWidget { return 'QRC20'; case CoinType.ftm20: return 'FTM20'; + case CoinType.arb20: + return 'ARB20'; case CoinType.etc: return 'ETC'; case CoinType.avx20: diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 7b16c3bd8e..1d4cd18526 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -139,6 +139,7 @@ String abbr2Ticker(String abbr) { 'BEP20', 'QRC20', 'FTM20', + 'ARB20', 'HRC20', 'MVR20', 'AVX20', From 2a0e9b3a47dfa7a3c87c24df2fdb98d6da425405 Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Thu, 3 Oct 2024 16:33:41 +0300 Subject: [PATCH 04/34] Enable Test Coins Setting (#165) * add enable test coins to settings * test coins hide if toggled off in settings --- assets/translations/en.json | 2 + lib/bloc/settings/settings_bloc.dart | 11 ++++ lib/bloc/settings/settings_event.dart | 5 ++ lib/bloc/settings/settings_state.dart | 6 +++ lib/generated/codegen_loader.g.dart | 2 + lib/model/coin_utils.dart | 8 +++ lib/model/stored_settings.dart | 7 +++ lib/views/dex/dex_helpers.dart | 9 ++++ .../coins_table/coins_table_content.dart | 8 ++- .../orders_table/orders_table_content.dart | 9 +++- .../dex/simple/form/tables/table_utils.dart | 20 ++++++-- .../coin_selection_and_amount_input.dart | 8 ++- .../general_settings/general_settings.dart | 3 ++ .../settings_manage_test_coins.dart | 50 +++++++++++++++++++ .../coins_manager_list_wrapper.dart | 9 +++- .../wallet_main/active_coins_list.dart | 8 ++- .../wallet_main/all_coins_list.dart | 13 +++-- 17 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 lib/views/settings/widgets/general_settings/settings_manage_test_coins.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 9e5ad838aa..08f09c375d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -559,7 +559,9 @@ "margin": "Margin", "updateInterval": "Update interval", "expertMode": "Expert mode", + "testCoins": "Test coins", "enableTradingBot": "Enable Trading Bot", + "enableTestCoins": "Enable Test Coins", "makeMarket": "Make Market", "custom": "Custom", "edit": "Edit", diff --git a/lib/bloc/settings/settings_bloc.dart b/lib/bloc/settings/settings_bloc.dart index cd0fc32fc1..48f859f927 100644 --- a/lib/bloc/settings/settings_bloc.dart +++ b/lib/bloc/settings/settings_bloc.dart @@ -17,6 +17,7 @@ class SettingsBloc extends Bloc { on(_onThemeModeChanged); on(_onMarketMakerBotSettingsChanged); + on(_onTestCoinsEnabledChanged); } late StoredSettings _storedSettings; @@ -45,4 +46,14 @@ class SettingsBloc extends Bloc { ); emitter(state.copyWith(marketMakerBotSettings: event.settings)); } + + Future _onTestCoinsEnabledChanged( + TestCoinsEnabledChanged event, + Emitter emitter, + ) async { + await _settingsRepo.updateSettings( + _storedSettings.copyWith(testCoinsEnabled: event.testCoinsEnabled), + ); + emitter(state.copyWith(testCoinsEnabled: event.testCoinsEnabled)); + } } diff --git a/lib/bloc/settings/settings_event.dart b/lib/bloc/settings/settings_event.dart index a5a153a391..d307388e02 100644 --- a/lib/bloc/settings/settings_event.dart +++ b/lib/bloc/settings/settings_event.dart @@ -14,6 +14,11 @@ class ThemeModeChanged extends SettingsEvent { final ThemeMode mode; } +class TestCoinsEnabledChanged extends SettingsEvent { + const TestCoinsEnabledChanged({required this.testCoinsEnabled}); + final bool testCoinsEnabled; +} + class MarketMakerBotSettingsChanged extends SettingsEvent { const MarketMakerBotSettingsChanged(this.settings); diff --git a/lib/bloc/settings/settings_state.dart b/lib/bloc/settings/settings_state.dart index 084a37cb4e..e793b44808 100644 --- a/lib/bloc/settings/settings_state.dart +++ b/lib/bloc/settings/settings_state.dart @@ -7,31 +7,37 @@ class SettingsState extends Equatable { const SettingsState({ required this.themeMode, required this.mmBotSettings, + required this.testCoinsEnabled, }); factory SettingsState.fromStored(StoredSettings stored) { return SettingsState( themeMode: stored.mode, mmBotSettings: stored.marketMakerBotSettings, + testCoinsEnabled: stored.testCoinsEnabled, ); } final ThemeMode themeMode; final MarketMakerBotSettings mmBotSettings; + final bool testCoinsEnabled; @override List get props => [ themeMode, mmBotSettings, + testCoinsEnabled, ]; SettingsState copyWith({ ThemeMode? mode, MarketMakerBotSettings? marketMakerBotSettings, + bool? testCoinsEnabled, }) { return SettingsState( themeMode: mode ?? themeMode, mmBotSettings: marketMakerBotSettings ?? mmBotSettings, + testCoinsEnabled: testCoinsEnabled ?? this.testCoinsEnabled, ); } } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index a4e32f3193..7d2602a5f4 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -590,7 +590,9 @@ abstract class LocaleKeys { static const margin = 'margin'; static const updateInterval = 'updateInterval'; static const expertMode = 'expertMode'; + static const testCoins = 'testCoins'; static const enableTradingBot = 'enableTradingBot'; + static const enableTestCoins = 'enableTestCoins'; static const makeMarket = 'makeMarket'; static const custom = 'custom'; static const edit = 'edit'; diff --git a/lib/model/coin_utils.dart b/lib/model/coin_utils.dart index a4e53e9b17..935ed12120 100644 --- a/lib/model/coin_utils.dart +++ b/lib/model/coin_utils.dart @@ -27,6 +27,14 @@ List sortFiatBalance(List coins) { return list; } +List removeTestCoins(List coins) { + final List list = List.from(coins); + + list.removeWhere((Coin coin) => coin.isTestCoin); + + return list; +} + List removeWalletOnly(List coins) { final List list = List.from(coins); diff --git a/lib/model/stored_settings.dart b/lib/model/stored_settings.dart index 7aa7671812..2d66e9ea28 100644 --- a/lib/model/stored_settings.dart +++ b/lib/model/stored_settings.dart @@ -8,17 +8,20 @@ class StoredSettings { required this.mode, required this.analytics, required this.marketMakerBotSettings, + required this.testCoinsEnabled, }); final ThemeMode mode; final AnalyticsSettings analytics; final MarketMakerBotSettings marketMakerBotSettings; + final bool testCoinsEnabled; static StoredSettings initial() { return StoredSettings( mode: ThemeMode.dark, analytics: AnalyticsSettings.initial(), marketMakerBotSettings: MarketMakerBotSettings.initial(), + testCoinsEnabled: true, ); } @@ -31,6 +34,7 @@ class StoredSettings { marketMakerBotSettings: MarketMakerBotSettings.fromJson( json[storedMarketMakerSettingsKey], ), + testCoinsEnabled: json['testCoinsEnabled'] ?? true, ); } @@ -39,6 +43,7 @@ class StoredSettings { 'themeModeIndex': mode.index, storedAnalyticsSettingsKey: analytics.toJson(), storedMarketMakerSettingsKey: marketMakerBotSettings.toJson(), + 'testCoinsEnabled': testCoinsEnabled, }; } @@ -46,12 +51,14 @@ class StoredSettings { ThemeMode? mode, AnalyticsSettings? analytics, MarketMakerBotSettings? marketMakerBotSettings, + bool? testCoinsEnabled, }) { return StoredSettings( mode: mode ?? this.mode, analytics: analytics ?? this.analytics, marketMakerBotSettings: marketMakerBotSettings ?? this.marketMakerBotSettings, + testCoinsEnabled: testCoinsEnabled ?? this.testCoinsEnabled, ); } } diff --git a/lib/views/dex/dex_helpers.dart b/lib/views/dex/dex_helpers.dart index c0ad3b4b23..22fa2a7b22 100644 --- a/lib/views/dex/dex_helpers.dart +++ b/lib/views/dex/dex_helpers.dart @@ -149,6 +149,15 @@ int getCoinPairsCountFromCoinAbbrMap(Map> coinAbbrMap, .length; } +void removeTestCoinOrders(List orders) { + orders.removeWhere((BestOrder order) { + final Coin? coin = coinsBloc.getCoin(order.coin); + if (coin == null) return true; + + return coin.isTestCoin; + }); +} + void removeSuspendedCoinOrders( List orders, AuthorizeMode authorizeMode) { if (authorizeMode == AuthorizeMode.noLogin) return; diff --git a/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart b/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart index 01158b139b..805c760efa 100644 --- a/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart +++ b/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/tables/nothing_found.dart'; @@ -22,7 +24,11 @@ class CoinsTableContent extends StatelessWidget { stream: coinsBloc.outKnownCoins, initialData: coinsBloc.knownCoins, builder: (context, snapshot) { - final coins = prepareCoinsForTable(coinsBloc.knownCoins, searchString); + final coins = prepareCoinsForTable( + coinsBloc.knownCoins, + searchString, + testCoinsEnabled: context.read().state.testCoinsEnabled, + ); if (coins.isEmpty) return const NothingFound(); return GroupedListView( 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 771ef1ec3d..f499f449dd 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 @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; @@ -43,8 +44,12 @@ class OrdersTableContent extends StatelessWidget { final Map> ordersMap = bestOrders.result!; final AuthorizeMode mode = context.watch().state.mode; - final List orders = - prepareOrdersForTable(ordersMap, searchString, mode); + final List orders = prepareOrdersForTable( + ordersMap, + searchString, + mode, + testCoinsEnabled: context.read().state.testCoinsEnabled, + ); if (orders.isEmpty) return const NothingFound(); diff --git a/lib/views/dex/simple/form/tables/table_utils.dart b/lib/views/dex/simple/form/tables/table_utils.dart index 7a17e3b958..99ccc67152 100644 --- a/lib/views/dex/simple/form/tables/table_utils.dart +++ b/lib/views/dex/simple/form/tables/table_utils.dart @@ -6,8 +6,13 @@ import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/shared/utils/balances_formatter.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; -List prepareCoinsForTable(List coins, String? searchString) { +List prepareCoinsForTable( + List coins, + String? searchString, { + bool testCoinsEnabled = true, +}) { coins = List.from(coins); + if (!testCoinsEnabled) coins = removeTestCoins(coins); coins = removeWalletOnly(coins); coins = removeSuspended(coins); coins = sortFiatBalance(coins); @@ -15,12 +20,21 @@ List prepareCoinsForTable(List coins, String? searchString) { return coins; } -List prepareOrdersForTable(Map>? orders, - String? searchString, AuthorizeMode mode) { +List prepareOrdersForTable( + Map>? orders, + String? searchString, + AuthorizeMode mode, { + bool testCoinsEnabled = true, +}) { if (orders == null) return []; final List sorted = _sortBestOrders(orders); if (sorted.isEmpty) return []; + if (!testCoinsEnabled) { + removeTestCoinOrders(sorted); + if (sorted.isEmpty) return []; + } + removeSuspendedCoinOrders(sorted, mode); if (sorted.isEmpty) return []; diff --git a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart index e9a2f9a2c3..51256524c0 100644 --- a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart +++ b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_icon.dart'; @@ -54,7 +56,11 @@ class _CoinSelectionAndAmountInputState } void _prepareItems() { - _items = prepareCoinsForTable(widget.coins, null) + _items = prepareCoinsForTable( + widget.coins, + null, + testCoinsEnabled: context.read().state.testCoinsEnabled, + ) .map( (coin) => CoinSelectItem( name: coin.name, diff --git a/lib/views/settings/widgets/general_settings/general_settings.dart b/lib/views/settings/widgets/general_settings/general_settings.dart index f8d00ca97c..4fc19b40f2 100644 --- a/lib/views/settings/widgets/general_settings/general_settings.dart +++ b/lib/views/settings/widgets/general_settings/general_settings.dart @@ -5,6 +5,7 @@ import 'package:web_dex/shared/widgets/hidden_without_wallet.dart'; import 'package:web_dex/views/settings/widgets/general_settings/import_swaps.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_download_logs.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_analytics.dart'; +import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_test_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_trading_bot.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_reset_activated_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_theme_switcher.dart'; @@ -24,6 +25,8 @@ class GeneralSettings extends StatelessWidget { const SizedBox(height: 25), const SettingsManageAnalytics(), const SizedBox(height: 25), + const SettingsManageTestCoins(), + const SizedBox(height: 25), const HiddenWithoutWallet( child: SettingsManageTradingBot(), ), diff --git a/lib/views/settings/widgets/general_settings/settings_manage_test_coins.dart b/lib/views/settings/widgets/general_settings/settings_manage_test_coins.dart new file mode 100644 index 0000000000..6c90d02522 --- /dev/null +++ b/lib/views/settings/widgets/general_settings/settings_manage_test_coins.dart @@ -0,0 +1,50 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_event.dart'; +import 'package:web_dex/bloc/settings/settings_state.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/settings/widgets/common/settings_section.dart'; + +class SettingsManageTestCoins extends StatelessWidget { + const SettingsManageTestCoins({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SettingsSection( + title: LocaleKeys.testCoins.tr(), + child: const EnableTestCoinsSwitcher(), + ); + } +} + +class EnableTestCoinsSwitcher extends StatelessWidget { + const EnableTestCoinsSwitcher({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + UiSwitcher( + key: const Key('enable-test-coins-switcher'), + value: state.testCoinsEnabled, + onChanged: (value) => _onSwitcherChanged(context, value), + ), + const SizedBox(width: 15), + Text(LocaleKeys.enableTestCoins.tr()), + ], + ), + ); + } + + void _onSwitcherChanged(BuildContext context, bool value) { + context + .read() + .add(TestCoinsEnabledChanged(testCoinsEnabled: value)); + } +} diff --git a/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart b/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart index d5acf9aa2a..7210cefbde 100644 --- a/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart +++ b/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart @@ -4,10 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_state.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/router/state/wallet_state.dart'; import 'package:web_dex/shared/widgets/information_popup.dart'; @@ -49,7 +51,12 @@ class _CoinsManagerListWrapperState extends State { }, child: BlocBuilder( builder: (BuildContext context, CoinsManagerState state) { - final List sortedCoins = _sortCoins([...state.coins]); + List sortedCoins = _sortCoins([...state.coins]); + + if (!context.read().state.testCoinsEnabled) { + sortedCoins = removeTestCoins(sortedCoins); + } + final bool isAddAssets = state.action == CoinsManagerAction.add; return Column( diff --git a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart index 42f898fce4..83911d93f6 100644 --- a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart @@ -1,5 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -42,7 +44,11 @@ class ActiveCoinsList extends StatelessWidget { ); } - final sorted = sortFiatBalance(displayedCoins.toList()); + List sorted = sortFiatBalance(displayedCoins.toList()); + + if (!context.read().state.testCoinsEnabled) { + sorted = removeTestCoins(sorted); + } return WalletCoinsList( coins: sorted, diff --git a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart index 507557f373..a8cad9a5ef 100644 --- a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; @@ -28,10 +30,15 @@ class AllCoinsList extends StatelessWidget { return const SliverToBoxAdapter(child: UiSpinner()); } - final displayedCoins = - sortByPriority(filterCoinsByPhrase(coins, searchPhrase)); + List displayedCoins = + sortByPriority(filterCoinsByPhrase(coins, searchPhrase)).toList(); + + if (!context.read().state.testCoinsEnabled) { + displayedCoins = removeTestCoins(displayedCoins); + } + return WalletCoinsList( - coins: displayedCoins.toList(), + coins: displayedCoins, onCoinItemTap: onCoinItemTap, ); }); From 70907c0dd3d98a1ff119242213df2626cc64800b Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 7 Oct 2024 13:33:15 +0200 Subject: [PATCH 05/34] chore(deps): Upgrade deps/Flutter and add support for native mobile builds (#163) * Update Flutter to 3.24.3 (stable) Only web build works with this configuration * Bump Flutter version in workflows to `3.24.x` * Remove `desktop_webview_window` dependency * Switch to in-app webview in popup-window * Upgrade `file_picker` to 8.1.2 to fix ios&macos build error win32 v5 removes references to deprecated APIs, which https://github.com/jonataslaw/get_cli/issues/263 * Upgrade `url_launcher` to 6.3.0 to fix iOS build error * Skip web defi fetch step if target is iOS * Bump CI Flutter version to `3.24.x` --- .github/actions/flutter-deps/action.yml | 2 +- .gitignore | 5 +- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 276 +++++++++--------- ios/Runner.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/views/bitrefill/bitrefill_button.dart | 40 +-- .../bitrefill_desktop_webview_button.dart | 86 ------ .../bitrefill_inappbrowser_button.dart | 9 +- .../bitrefill_inappwebview_button.dart | 124 ++++++++ linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - macos/Podfile.lock | 26 +- macos/Runner.xcodeproj/project.pbxproj | 2 - pubspec.lock | 151 +++++----- pubspec.yaml | 18 +- .../flutter/generated_plugin_registrant.cc | 6 +- windows/flutter/generated_plugins.cmake | 2 +- 20 files changed, 398 insertions(+), 382 deletions(-) delete mode 100644 lib/views/bitrefill/bitrefill_desktop_webview_button.dart create mode 100644 lib/views/bitrefill/bitrefill_inappwebview_button.dart diff --git a/.github/actions/flutter-deps/action.yml b/.github/actions/flutter-deps/action.yml index bc075a3fee..b11e27c2d1 100644 --- a/.github/actions/flutter-deps/action.yml +++ b/.github/actions/flutter-deps/action.yml @@ -11,7 +11,7 @@ runs: - name: Get stable flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.22.x" + flutter-version: "3.24.x" channel: "stable" - name: Prepare build directory diff --git a/.gitignore b/.gitignore index 5f1d07731d..59fe278c23 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,7 @@ assets/config/coins_ci.json assets/coin_icons/ # Python -venv/ \ No newline at end of file +venv/ + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d2456..8c6e56146e 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index 88359b225f..279576f388 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index faa7a9e1af..2c9caebf95 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,77 +1,77 @@ PODS: - - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/Core (4.3.9): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.4) - - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): - DKImagePickerController/Core - DKPhotoGallery - - DKImagePickerController/Resource (4.3.4) - - DKPhotoGallery (0.0.17): - - DKPhotoGallery/Core (= 0.0.17) - - DKPhotoGallery/Model (= 0.0.17) - - DKPhotoGallery/Preview (= 0.0.17) - - DKPhotoGallery/Resource (= 0.0.17) + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) - SDWebImage - SwiftyGif - - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Core (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Preview - SDWebImage - SwiftyGif - - DKPhotoGallery/Model (0.0.17): + - DKPhotoGallery/Model (0.0.19): - SDWebImage - SwiftyGif - - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Preview (0.0.19): - DKPhotoGallery/Model - DKPhotoGallery/Resource - SDWebImage - SwiftyGif - - DKPhotoGallery/Resource (0.0.17): + - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter - - Firebase/Analytics (10.10.0): + - Firebase/Analytics (10.25.0): - Firebase/Core - - Firebase/Core (10.10.0): + - Firebase/Core (10.25.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.10.0) - - Firebase/CoreOnly (10.10.0): - - FirebaseCore (= 10.10.0) - - firebase_analytics (10.4.3): - - Firebase/Analytics (= 10.10.0) + - FirebaseAnalytics (~> 10.25.0) + - Firebase/CoreOnly (10.25.0): + - FirebaseCore (= 10.25.0) + - firebase_analytics (10.10.5): + - Firebase/Analytics (= 10.25.0) - firebase_core - Flutter - - firebase_core (2.14.0): - - Firebase/CoreOnly (= 10.10.0) + - firebase_core (2.31.0): + - Firebase/CoreOnly (= 10.25.0) - Flutter - - FirebaseAnalytics (10.10.0): - - FirebaseAnalytics/AdIdSupport (= 10.10.0) + - FirebaseAnalytics (10.25.0): + - FirebaseAnalytics/AdIdSupport (= 10.25.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.10.0): + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (10.25.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.10.0) + - GoogleAppMeasurement (= 10.25.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (10.10.0): + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseCore (10.25.0): - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.11.0): + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.11.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -80,121 +80,123 @@ PODS: - flutter_inappwebview_ios (0.0.1): - Flutter - flutter_inappwebview_ios/Core (= 0.0.1) - - OrderedSet (~> 5.0) + - OrderedSet (~> 6.0.3) - flutter_inappwebview_ios/Core (0.0.1): - Flutter - - OrderedSet (~> 5.0) - - GoogleAppMeasurement (10.10.0): - - GoogleAppMeasurement/AdIdSupport (= 10.10.0) + - OrderedSet (~> 6.0.3) + - GoogleAppMeasurement (10.25.0): + - GoogleAppMeasurement/AdIdSupport (= 10.25.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.10.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.10.0) + - nanopb (< 2.30911.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.25.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.25.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.10.0): + - nanopb (< 2.30911.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.25.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.3): + - nanopb (< 2.30911.0, >= 2.30908.0) + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (4.0.0): + - GoogleMLKit/BarcodeScanning (6.0.0): - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 3.0.0) - - GoogleMLKit/MLKitCore (4.0.0): - - MLKitCommon (~> 9.0.0) - - GoogleToolboxForMac/DebugUtils (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - GoogleToolboxForMac/Defines (2.3.2) - - GoogleToolboxForMac/Logger (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSData+zlib (2.3.2)": - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": - - GoogleToolboxForMac/DebugUtils (= 2.3.2) - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - MLKitBarcodeScanning (~> 5.0.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - GoogleUtilitiesComponents (1.1.0): - GoogleUtilities/Logger - - GTMSessionFetcher/Core (2.3.0) + - GTMSessionFetcher/Core (3.5.0) - integration_test (0.0.1): - Flutter - - MLImage (1.0.0-beta4) - - MLKitBarcodeScanning (3.0.0): - - MLKitCommon (~> 9.0) - - MLKitVision (~> 5.0) - - MLKitCommon (9.0.0): - - GoogleDataTransport (~> 9.0) - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - - GoogleUtilities/UserDefaults (~> 7.0) + - MLImage (1.0.0-beta5) + - MLKitBarcodeScanning (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLKitVision (5.0.0): - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLImage (= 1.0.0-beta4) - - MLKitCommon (~> 9.0) - - mobile_scanner (3.2.0): + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - mobile_scanner (5.2.3): - Flutter - - GoogleMLKit/BarcodeScanning (~> 4.0.0) - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) - - OrderedSet (5.0.0) + - GoogleMLKit/BarcodeScanning (~> 6.0.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - PromisesObjC (2.2.0) - - SDWebImage (5.16.0): - - SDWebImage/Core (= 5.16.0) - - SDWebImage/Core (5.16.0) + - PromisesObjC (2.4.0) + - SDWebImage (5.19.7): + - SDWebImage/Core (= 5.19.7) + - SDWebImage/Core (5.19.7) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - SwiftyGif (5.4.4) + - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter - video_player_avfoundation (0.0.1): - Flutter + - FlutterMacOS DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -209,7 +211,7 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) SPEC REPOS: trunk: @@ -263,46 +265,46 @@ EXTERNAL SOURCES: url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/ios" + :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 - Firebase: facd334e557a979bd03a0b58d90fd56b52b8aba0 - firebase_analytics: 1a5ad75876257318ba5fdc6bf7aae73b6e98d0cf - firebase_core: 85b6664038311940ad60584eaabc73103c61f5de - FirebaseAnalytics: 7bc7de519111dae802f5bc0c9c083918f8b8870d - FirebaseCore: d027ff503d37edb78db98429b11f580a24a7df2a - FirebaseCoreInternal: 9e46c82a14a3b3a25be4e1e151ce6d21536b89c0 - FirebaseInstallations: 2a2c6859354cbec0a228a863d4daf6de7c74ced4 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 - GoogleAppMeasurement: bbbfd4bcb2b40ae9b772c3b0823a58c1e3d618f9 - GoogleDataTransport: f0308f5905a745f94fb91fea9c6cbaf3831cb1bd - GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e - GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + Firebase: 0312a2352584f782ea56f66d91606891d4607f06 + firebase_analytics: 0b2b459312872129295c3f9c44225eda552fd8f3 + firebase_core: 0b39f4f424e02eecabb2356ddf331fa07b772af8 + FirebaseAnalytics: ec00fe8b93b41dc6fe4a28784b8e51da0647a248 + FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - integration_test: 13825b8a9334a850581300559b8839134b124670 - MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b - MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 - MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 - MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 47056db0c04027ea5f41a716385542da28574662 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef - SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 52fdfe96da..44092eac7a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FC78CCE93902D5D826DCE20C /* [CP] Embed Pods Frameworks */, + 629DFBD49650E4794D77944D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -189,7 +190,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -270,6 +271,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 629DFBD49650E4794D77944D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a335..5e31d3d342 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { return Column( children: [ - if (kIsWeb) - // Temporary solution for web until this PR is approved and released: - // https://github.com/pichillilorenzo/flutter_inappwebview/pull/2058 - BitrefillButtonView( - onPressed: isEnabled - ? () => _openBitrefillInNewTab(context, url) - : null, - ) - else if (isInAppBrowserSupported) - BitrefillInAppBrowserButton( - windowTitle: widget.windowTitle, - url: url, - enabled: isEnabled, - onMessage: handleMessage, - ) - else - BitrefillDesktopWebviewButton( - windowTitle: widget.windowTitle, - url: url, - enabled: isEnabled, - onMessage: handleMessage, - ), + BitrefillInAppWebviewButton( + windowTitle: widget.windowTitle, + url: url, + enabled: isEnabled, + onMessage: handleMessage, + ), ], ); }, ); } - void _openBitrefillInNewTab(BuildContext context, String url) { - launchURL(url, inSeparateTab: true); - context.read().add(const BitrefillLaunchRequested()); - } - /// Handles messages from the Bitrefill widget. /// The message is a JSON string that contains the event name and event data. /// The event name is used to create a [BitrefillWidgetEvent] object. diff --git a/lib/views/bitrefill/bitrefill_desktop_webview_button.dart b/lib/views/bitrefill/bitrefill_desktop_webview_button.dart deleted file mode 100644 index c5fb68a705..0000000000 --- a/lib/views/bitrefill/bitrefill_desktop_webview_button.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:io'; - -import 'package:desktop_webview_window/desktop_webview_window.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; -import 'package:web_dex/views/bitrefill/bitrefill_button_view.dart'; - -/// A button that opens the provided [url] in a Browser window on Desktop platforms. -/// This widget uses the desktop_webview_window package to open a new window. -/// The window is closed when a BitrefillPaymentInProgress event is received. -/// -/// NOTE: this widget only works on Windows, Linux and macOS -class BitrefillDesktopWebviewButton extends StatefulWidget { - /// The [onMessage] callback is called when a message is received from the webview. - /// The [enabled] property determines if the button is enabled. - /// The [windowTitle] property is used as the title of the window. - /// The [url] property is the URL to open in the window. - const BitrefillDesktopWebviewButton({ - super.key, - required this.url, - required this.windowTitle, - required this.enabled, - required this.onMessage, - }); - - /// The title of the pop-up browser window. - final String windowTitle; - - /// The URL to open in the pop-up browser window. - final String url; - - /// Determines if the button is enabled. - final bool enabled; - - /// The callback function that is called when a message is received from the webview. - final dynamic Function(String) onMessage; - - @override - BitrefillDesktopWebviewButtonState createState() => - BitrefillDesktopWebviewButtonState(); -} - -class BitrefillDesktopWebviewButtonState - extends State { - Webview? webview; - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (BuildContext context, BitrefillState state) { - if (state is BitrefillPaymentInProgress) { - webview?.close(); - } - }, - child: BitrefillButtonView( - onPressed: widget.enabled ? _openWebview : null, - ), - ); - } - - void _openWebview() { - WebviewWindow.isWebviewAvailable().then((bool value) { - _createWebview(); - }); - } - - Future _createWebview() async { - webview?.close(); - webview = await WebviewWindow.create( - configuration: CreateConfiguration( - title: widget.windowTitle, - titleBarTopPadding: Platform.isMacOS ? 20 : 0, - ), - ); - webview - ?..registerJavaScriptMessageHandler('test', (String name, dynamic body) { - widget.onMessage(body as String); - }) - ..addOnWebMessageReceivedCallback( - (String body) => widget.onMessage(body), - ) - ..setApplicationNameForUserAgent(' WebviewExample/1.0.0') - ..launch(widget.url); - } -} diff --git a/lib/views/bitrefill/bitrefill_inappbrowser_button.dart b/lib/views/bitrefill/bitrefill_inappbrowser_button.dart index f08b43a8f8..2955422df8 100644 --- a/lib/views/bitrefill/bitrefill_inappbrowser_button.dart +++ b/lib/views/bitrefill/bitrefill_inappbrowser_button.dart @@ -11,16 +11,16 @@ import 'package:web_dex/views/bitrefill/bitrefill_button_view.dart'; /// /// NOTE: this widget only works on Web, Android, iOS, and macOS (for now). class BitrefillInAppBrowserButton extends StatefulWidget { - /// The [onMessage] callback is called when a message is received from the webview. + /// The [onMessage] is called when a message is received from the webview. /// The [enabled] property determines if the button is enabled. /// The [windowTitle] property is used as the title of the window. /// The [url] property is the URL to open in the window. const BitrefillInAppBrowserButton({ - super.key, required this.url, required this.windowTitle, required this.enabled, required this.onMessage, + super.key, }); /// The title of the pop-up browser window. @@ -32,7 +32,8 @@ class BitrefillInAppBrowserButton extends StatefulWidget { /// Determines if the button is enabled. final bool enabled; - /// The callback function that is called when a message is received from the webview. + /// The callback function that is called when a message is received from the + /// webview. final dynamic Function(String) onMessage; @override @@ -71,7 +72,7 @@ class BitrefillInAppBrowserButtonState } Future _openBrowserWindow() async { - browser?.openUrlRequest( + await browser?.openUrlRequest( urlRequest: URLRequest( url: WebUri(widget.url), ), diff --git a/lib/views/bitrefill/bitrefill_inappwebview_button.dart b/lib/views/bitrefill/bitrefill_inappwebview_button.dart new file mode 100644 index 0000000000..ac44a13e85 --- /dev/null +++ b/lib/views/bitrefill/bitrefill_inappwebview_button.dart @@ -0,0 +1,124 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; +import 'package:web_dex/views/bitrefill/bitrefill_button_view.dart'; + +/// A button that opens the provided url in an embedded InAppWebview widget. +/// This widget uses the flutter_inappwebview package to open the url using +/// platform-specific webview implementations to embed the website inside a +/// widget. +/// +/// NOTE: this widget only works on Web, Android, iOS, and macOS (for now). +class BitrefillInAppWebviewButton extends StatefulWidget { + /// [onMessage] is called when a message is received from the webview. + /// The [enabled] property determines if the button is clickable. + /// The [windowTitle] property is used as the title of the window. + /// The [url] property is the URL to open in the window. + const BitrefillInAppWebviewButton({ + required this.url, + required this.windowTitle, + required this.enabled, + required this.onMessage, + super.key, + }); + + /// The title of the pop-up browser window. + final String windowTitle; + + /// The URL to open in the pop-up browser window. + final String url; + + /// Determines if the button is enabled. + final bool enabled; + + /// The callback function that is called when a message is received from the + /// webview as a console message. + final dynamic Function(String) onMessage; + + @override + BitrefillInAppWebviewButtonState createState() => + BitrefillInAppWebviewButtonState(); +} + +class BitrefillInAppWebviewButtonState + extends State { + InAppWebViewController? webViewController; + InAppWebViewSettings settings = InAppWebViewSettings( + isInspectable: kDebugMode, + mediaPlaybackRequiresUserGesture: false, + iframeAllow: 'same-origin; popups; scripts; forms', + iframeAllowFullscreen: false, + ); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (BuildContext context, BitrefillState state) { + if (state is BitrefillPaymentInProgress) { + // Close the browser window when a payment is in progress. + } + }, + child: BitrefillButtonView( + onPressed: widget.enabled ? _openDialog : null, + ), + ); + } + + Future _openDialog() async { + await showDialog( + context: context, + builder: (BuildContext context) { + final size = MediaQuery.of(context).size; + final width = size.width * 0.8; + final height = size.height * 0.8; + + return AlertDialog( + title: const Text('Bitrefill'), + content: SizedBox( + width: width, + height: height, + child: Column( + children: [ + Expanded( + child: InAppWebView( + key: const Key('bitrefill-inappwebview'), + initialUrlRequest: _createUrlRequest(), + initialSettings: settings, + onWebViewCreated: _onCreated, + onConsoleMessage: _onConsoleMessage, + ), + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + // ignore: use_setters_to_change_properties + void _onCreated(InAppWebViewController controller) { + webViewController = controller; + } + + void _onConsoleMessage( + InAppWebViewController controller, + ConsoleMessage consoleMessage, + ) { + widget.onMessage(consoleMessage.message); + } + + URLRequest _createUrlRequest() { + return URLRequest(url: WebUri(widget.url)); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 55e53518fd..6adbd2c46b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,10 @@ #include "generated_plugin_registrant.h" -#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); - desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8d2c737526..f32b910ed9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window url_launcher_linux window_size ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c287d6c379..0256bc5cab 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,6 @@ import FlutterMacOS import Foundation -import desktop_webview_window import firebase_analytics import firebase_core import flutter_inappwebview_macos @@ -19,7 +18,6 @@ import video_player_avfoundation import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 156a72c5ad..de0fa55b64 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,6 +1,4 @@ PODS: - - desktop_webview_window (0.0.1): - - FlutterMacOS - Firebase/Analytics (10.25.0): - Firebase/Core - Firebase/Core (10.25.0): @@ -37,16 +35,16 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.28.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.28.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - - OrderedSet (~> 5.0) + - OrderedSet (~> 6.0.3) - FlutterMacOS (1.0.0) - GoogleAppMeasurement (10.25.0): - GoogleAppMeasurement/AdIdSupport (= 10.25.0) @@ -96,14 +94,14 @@ PODS: - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - mobile_scanner (5.1.1): + - mobile_scanner (5.2.3): - FlutterMacOS - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) - nanopb/encode (= 2.30910.0) - nanopb/decode (2.30910.0) - nanopb/encode (2.30910.0) - - OrderedSet (5.0.0) + - OrderedSet (6.0.3) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -124,7 +122,6 @@ PODS: - FlutterMacOS DEPENDENCIES: - - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -152,8 +149,6 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: - desktop_webview_window: - :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos firebase_analytics: :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos firebase_core: @@ -180,21 +175,20 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: - desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2 Firebase: 0312a2352584f782ea56f66d91606891d4607f06 firebase_analytics: 25af54d88e440c4f65ae10a31f3a57268416ce82 firebase_core: fdf12e0c4349815c2e832d9dcad59fbff0ff394b FirebaseAnalytics: ec00fe8b93b41dc6fe4a28784b8e51da0647a248 FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 - FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 - FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e - flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b + mobile_scanner: 0a05256215b047af27b9495db3b77640055e8824 nanopb: 438bc412db1928dac798aa6fd75726007be04262 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 2119f7e23a..c001eee039 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -361,7 +361,6 @@ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", - "${BUILT_PRODUCTS_DIR}/desktop_webview_window/desktop_webview_window.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_macos/flutter_inappwebview_macos.framework", "${BUILT_PRODUCTS_DIR}/mobile_scanner/mobile_scanner.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", @@ -381,7 +380,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/desktop_webview_window.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mobile_scanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", diff --git a/pubspec.lock b/pubspec.lock index fbee4470a9..cc9d53e808 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -150,10 +150,10 @@ packages: dependency: "direct main" description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.4+2" crypto: dependency: "direct main" description: @@ -170,14 +170,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.3" - desktop_webview_window: - dependency: "direct main" - description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" - url: "https://pub.dev" - source: hosted - version: "0.2.3" dragon_charts_flutter: dependency: "direct main" description: @@ -190,10 +182,10 @@ packages: dependency: "direct main" description: name: dragon_logs - sha256: "00bec36566176f7f6c243142530a1abf22416d728a283949b92408a0d1f6a442" + sha256: "797b1ce653ce308f455a8b5d38c87d8cf5ad62bdf3cd70bab621a0087a751750" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" easy_localization: dependency: "direct main" description: @@ -255,12 +247,11 @@ packages: file_picker: dependency: "direct main" description: - path: "." - ref: "85ecbae83eca8d200f869403928d2bf7e6806c67" - resolved-ref: "85ecbae83eca8d200f869403928d2bf7e6806c67" - url: "https://github.com/KomodoPlatform/flutter_file_picker.git" - source: git - version: "5.3.1" + name: file_picker + sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + url: "https://pub.dev" + source: hosted + version: "8.1.2" file_system_access_api: dependency: transitive description: @@ -305,18 +296,18 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.17.0" + version: "2.18.1" flutter: dependency: "direct main" description: flutter @@ -340,18 +331,18 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + sha256: "274edbb07196944e316722d9f6f641c77d0e71261200869887e10f59614c0458" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.3" flutter_inappwebview_android: dependency: transitive description: name: flutter_inappwebview_android - sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + sha256: f48203a11c5eb0c23dd5a3cb3638ae678056b6ceae22819373e36c6cb4f1d46a url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.1" flutter_inappwebview_internal_annotations: dependency: transitive description: @@ -364,34 +355,42 @@ packages: dependency: transitive description: name: flutter_inappwebview_ios - sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + sha256: f6f88d464b38f2fc1c5f2ae74024498115eb1470715bd8b40f902dd4ac99ccc8 url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.1" flutter_inappwebview_macos: dependency: transitive description: name: flutter_inappwebview_macos - sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + sha256: "68e0c3785d8d789710cda7d7efe6effa337c91bf300dd28af7efc2d358fa1a98" url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.1.1" flutter_inappwebview_platform_interface: dependency: transitive description: name: flutter_inappwebview_platform_interface - sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + sha256: "97b4ab116d949ede20c90c7e3d15d24afaf1b706cc0af96b060770293cd6c49d" url: "https://pub.dev" source: hosted - version: "1.0.10" + version: "1.2.0" flutter_inappwebview_web: dependency: transitive description: name: flutter_inappwebview_web - sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + sha256: f7f97b6faa39416e4e86da1184edd4de6c27b271d036f0838ea3ff9a250a1de2 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "86702d2109384311f8ea634855e90ee143b9bfabddd3858696d905a2c28808aa" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "0.4.0" flutter_lints: dependency: "direct dev" description: @@ -605,18 +604,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -661,18 +660,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -685,10 +684,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 + sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.3" nested: dependency: transitive description: @@ -806,10 +805,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -944,10 +943,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: @@ -1069,26 +1068,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" typed_data: dependency: transitive description: @@ -1118,26 +1117,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1158,18 +1157,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: @@ -1254,18 +1253,18 @@ packages: dependency: transitive description: name: video_player_web - sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" vm_service: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -1278,18 +1277,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.0" webdriver: dependency: transitive description: @@ -1310,10 +1309,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.5.4" window_size: dependency: "direct main" description: @@ -1348,5 +1347,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <3.5.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index f0368a553d..39e697a202 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.8.1+0 environment: - sdk: ">=3.4.0 <3.5.0" # The recent 3.5.0 breaks the build. We will resolve this after this release. + sdk: ">=3.4.0 <4.0.0" # The recent 3.5.0 breaks the build. We will resolve this after this release. dependencies: ## ---- Flutter SDK @@ -48,7 +48,7 @@ dependencies: ## ---- KomodoPlatform pub.dev packages (First-party) - dragon_logs: 1.0.3 # Secure code review PR URL: TBD + dragon_logs: 1.0.4 # Secure code review PR URL: TBD ## ---- Dart.dev, Flutter.dev @@ -58,10 +58,10 @@ dependencies: intl: 0.19.0 # dart.dev js: 0.6.7 # dart.dev shared_preferences: 2.1.1 # flutter.dev - url_launcher: 6.1.11 # flutter.dev + url_launcher: 6.3.0 # flutter.dev crypto: 3.0.3 # dart.dev path_provider: 2.1.1 # flutter.dev - cross_file: 0.3.3+4 # flutter.dev + cross_file: 0.3.4+2 # flutter.dev video_player: 2.7.0 # flutter.dev ## ---- google.com @@ -160,11 +160,6 @@ dependencies: url: https://github.com/KomodoPlatform/universal_html.git ref: 6a1bc7d9e6ed735ab9f7b319f9eedb138ce8b0e5 #2.2.2 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - file_picker: - git: - url: https://github.com/KomodoPlatform/flutter_file_picker.git - ref: 85ecbae83eca8d200f869403928d2bf7e6806c67 #5.3.1 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: @@ -194,8 +189,7 @@ dependencies: # Embedded web view # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/3 - flutter_inappwebview: 6.0.0 # Android, iOS, macOS, Web (currently broke, open issue) - desktop_webview_window: 0.2.3 # Windows, Linux + flutter_inappwebview: 6.1.3 # Android, iOS, macOS, Web (currently broke, open issue) # Newly added, not yet reviewed # TODO: review required @@ -208,7 +202,7 @@ dependencies: # TODO: review required dragon_charts_flutter: ^0.1.1-dev.1 bloc_concurrency: ^0.2.5 - + file_picker: ^8.1.2 # Updated from git ref @5.1.4 dev_dependencies: integration_test: # SDK diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3e41859bd6..91ae331f0e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,17 +6,17 @@ #include "generated_plugin_registrant.h" -#include #include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b36f8e33be..c759be6653 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window firebase_core + flutter_inappwebview_windows share_plus url_launcher_windows window_size From 654530e2c69340813a761b7281b552f7ce67c11a Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Wed, 9 Oct 2024 15:20:13 +0300 Subject: [PATCH 06/34] DEX Swap form gets filled with URL parameters (#162) * router fix uri query parameters were lost at parse * router add dexroute parameters * router dex state notifyListeners for new fields * router dex process order_type param * router dex clean params without notifying * router dex maker process ticker and amount params * router dex fix typo * dex TakerSetSellCoin event add setOnlyIfNotSet * router dex maker consumes only their own params * router dex allow order_type being maker by default * router dex taker process from/to currencies * dex repository waitOrderbookAvailability function * router dex form wait for orderbooks before process * router dex taker process from_amount * router dex code cleanup * mobile/widget layout taker form dropdown position * maker form parse error check --- lib/bloc/dex_repository.dart | 25 +++++++++ lib/bloc/taker_form/taker_bloc.dart | 12 ++--- lib/bloc/taker_form/taker_event.dart | 4 +- lib/blocs/maker_form_bloc.dart | 12 ++++- .../navigators/app_router_delegate.dart | 13 ++++- lib/router/parsers/dex_route_parser.dart | 9 ++++ lib/router/parsers/root_route_parser.dart | 6 +-- lib/router/routes.dart | 23 ++++++-- lib/router/state/dex_state.dart | 53 ++++++++++++++++++- .../dex/entities_list/dex_list_wrapper.dart | 30 ++++++++++- .../simple/form/maker/maker_form_layout.dart | 36 +++++++++++++ .../dex/simple/form/taker/taker_form.dart | 41 ++++++++++++++ .../simple/form/taker/taker_form_layout.dart | 4 +- 13 files changed, 246 insertions(+), 22 deletions(-) diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index 82bc7e44e4..c1833f6aa5 100644 --- a/lib/bloc/dex_repository.dart +++ b/lib/bloc/dex_repository.dart @@ -1,4 +1,5 @@ import 'package:rational/rational.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; @@ -139,4 +140,28 @@ class DexRepository { return Swap.fromJson(response['result']); } + + Future waitOrderbookAvailability({ + int retries = 10, + int interval = 300, + }) async { + BestOrders orders; + + for (int attempt = 0; attempt < retries; attempt++) { + orders = await getBestOrders( + BestOrdersRequest( + coin: defaultDexCoin, + type: BestOrdersRequestType.number, + number: 1, + action: 'sell', + ), + ); + + if (orders.result?.isNotEmpty ?? false) { + return; + } + + await Future.delayed(Duration(milliseconds: interval)); + } + } } diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index f1b8878cae..97f8159d89 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -240,18 +240,18 @@ class TakerBloc extends Bloc { TakerSetDefaults event, Emitter emit, ) async { - if (state.sellCoin == null) await _setDefaultSellCoin(); - } - - Future _setDefaultSellCoin() async { - final Coin? defaultCoin = _coinsRepo.getCoin(defaultDexCoin); - add(TakerSetSellCoin(defaultCoin)); + if (state.sellCoin == null) { + final Coin? defaultCoin = _coinsRepo.getCoin(defaultDexCoin); + add(TakerSetSellCoin(defaultCoin, setOnlyIfNotSet: true)); + } } Future _onSetSellCoin( TakerSetSellCoin event, Emitter emit, ) async { + if (event.setOnlyIfNotSet && state.sellCoin != null) return; + emit(state.copyWith( sellCoin: () => event.coin, showCoinSelector: () => false, diff --git a/lib/bloc/taker_form/taker_event.dart b/lib/bloc/taker_form/taker_event.dart index f80d65abcb..0c127f2eec 100644 --- a/lib/bloc/taker_form/taker_event.dart +++ b/lib/bloc/taker_form/taker_event.dart @@ -25,10 +25,12 @@ class TakerCoinSelectorClick extends TakerEvent {} class TakerOrderSelectorClick extends TakerEvent {} class TakerSetSellCoin extends TakerEvent { - TakerSetSellCoin(this.coin, {this.autoSelectOrderAbbr}); + TakerSetSellCoin(this.coin, + {this.autoSelectOrderAbbr, this.setOnlyIfNotSet = false}); final Coin? coin; final String? autoSelectOrderAbbr; + final bool setOnlyIfNotSet; } class TakerSelectOrder extends TakerEvent { diff --git a/lib/blocs/maker_form_bloc.dart b/lib/blocs/maker_form_bloc.dart index 967ac51688..c07c2d1aa2 100644 --- a/lib/blocs/maker_form_bloc.dart +++ b/lib/blocs/maker_form_bloc.dart @@ -526,7 +526,11 @@ class MakerFormBloc implements BlocBase { if (amountStr.isEmpty) { amount = null; } else { - amount = Rational.parse(amountStr); + try { + amount = Rational.parse(amountStr); + } catch (_) { + amount = null; + } } isMaxActive = false; @@ -544,7 +548,11 @@ class MakerFormBloc implements BlocBase { if (amountStr.isEmpty) { amount = null; } else { - amount = Rational.parse(amountStr); + try { + amount = Rational.parse(amountStr); + } catch (_) { + amount = null; + } } if (amount == buyAmount) return; diff --git a/lib/router/navigators/app_router_delegate.dart b/lib/router/navigators/app_router_delegate.dart index 11568af7bc..2b18b3bf01 100644 --- a/lib/router/navigators/app_router_delegate.dart +++ b/lib/router/navigators/app_router_delegate.dart @@ -104,6 +104,11 @@ class AppRouterDelegate extends RouterDelegate routingState.selectedMenu = MainMenuValue.dex; routingState.dexState.action = path.action; routingState.dexState.uuid = path.uuid; + routingState.dexState.fromCurrency = path.fromCurrency; + routingState.dexState.fromAmount = path.fromAmount; + routingState.dexState.toCurrency = path.toCurrency; + routingState.dexState.toAmount = path.toAmount; + routingState.dexState.orderType = path.orderType; } void _setNewMarketMakerBotRoutePath(MarketMakerBotRoutePath path) { @@ -165,7 +170,13 @@ class AppRouterDelegate extends RouterDelegate ); } - return DexRoutePath.dex(); + return DexRoutePath.dex( + fromAmount: routingState.dexState.fromAmount, + fromCurrency: routingState.dexState.fromCurrency, + toAmount: routingState.dexState.toAmount, + toCurrency: routingState.dexState.toCurrency, + orderType: routingState.dexState.orderType, + ); } AppRoutePath get _currentMarketMakerBotConfiguration { diff --git a/lib/router/parsers/dex_route_parser.dart b/lib/router/parsers/dex_route_parser.dart index 8beeb65c47..394626e0c4 100644 --- a/lib/router/parsers/dex_route_parser.dart +++ b/lib/router/parsers/dex_route_parser.dart @@ -14,6 +14,15 @@ class _DexRouteParser implements BaseRouteParser { } } + if (uri.pathSegments.length == 1) { + return DexRoutePath.dex( + fromCurrency: uri.queryParameters['from_currency'] ?? '', + fromAmount: uri.queryParameters['from_amount'] ?? '', + toCurrency: uri.queryParameters['to_currency'] ?? '', + toAmount: uri.queryParameters['to_amount'] ?? '', + orderType: uri.queryParameters['order_type'] ?? '', + ); + } return DexRoutePath.dex(); } } diff --git a/lib/router/parsers/root_route_parser.dart b/lib/router/parsers/root_route_parser.dart index 56b59321a3..dc19a34730 100644 --- a/lib/router/parsers/root_route_parser.dart +++ b/lib/router/parsers/root_route_parser.dart @@ -22,10 +22,10 @@ class RootRouteInformationParser extends RouteInformationParser { @override Future parseRouteInformation( RouteInformation routeInformation) async { - final uri = Uri.parse(routeInformation.uri.path); - final BaseRouteParser parser = _getRoutParser(uri); + final BaseRouteParser parser = + _getRoutParser(Uri.parse(routeInformation.uri.path)); - return parser.getRoutePath(uri); + return parser.getRoutePath(routeInformation.uri); } @override diff --git a/lib/router/routes.dart b/lib/router/routes.dart index cb4d3050ff..5f94d3cb40 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -39,16 +39,33 @@ class FiatRoutePath implements AppRoutePath { } class DexRoutePath implements AppRoutePath { - DexRoutePath.dex() - : location = '/${firstUriSegment.dex}', + DexRoutePath.dex({ + this.fromCurrency = '', + this.fromAmount = '', + this.toCurrency = '', + this.toAmount = '', + this.orderType = '', + }) : location = '/${firstUriSegment.dex}', uuid = ''; + DexRoutePath.swapDetails(this.action, this.uuid) - : location = '/${firstUriSegment.dex}/trading_details/$uuid'; + : location = '/${firstUriSegment.dex}/trading_details/$uuid', + fromCurrency = '', + fromAmount = '', + toCurrency = '', + toAmount = '', + orderType = ''; @override final String location; final String uuid; DexAction action = DexAction.none; + + final String fromCurrency; + final String fromAmount; + final String toCurrency; + final String toAmount; + final String orderType; } class BridgeRoutePath implements AppRoutePath { diff --git a/lib/router/state/dex_state.dart b/lib/router/state/dex_state.dart index a31aa70f0e..c6f27659a4 100644 --- a/lib/router/state/dex_state.dart +++ b/lib/router/state/dex_state.dart @@ -4,11 +4,22 @@ import 'package:web_dex/router/state/menu_state_interface.dart'; class DexState extends ChangeNotifier implements IResettableOnLogout { DexState() : _action = DexAction.none, - _uuid = ''; + _uuid = '', + _fromCurrency = '', + _fromAmount = '', + _toCurrency = '', + _toAmount = '', + _orderType = ''; DexAction _action; String _uuid; + String _fromCurrency; + String _fromAmount; + String _toCurrency; + String _toAmount; + String _orderType; + set action(DexAction action) { if (_action == action) { return; @@ -35,6 +46,36 @@ class DexState extends ChangeNotifier implements IResettableOnLogout { String get uuid => _uuid; + String get fromCurrency => _fromCurrency; + set fromCurrency(String fromCurrency) { + _fromCurrency = fromCurrency; + notifyListeners(); + } + + String get fromAmount => _fromAmount; + set fromAmount(String fromAmount) { + _fromAmount = fromAmount; + notifyListeners(); + } + + String get toCurrency => _toCurrency; + set toCurrency(String toCurrency) { + _toCurrency = toCurrency; + notifyListeners(); + } + + String get toAmount => _toAmount; + set toAmount(String toAmount) { + _toAmount = toAmount; + notifyListeners(); + } + + String get orderType => _orderType; + set orderType(String orderType) { + _orderType = orderType; + notifyListeners(); + } + @override void reset() { action = DexAction.none; @@ -42,7 +83,15 @@ class DexState extends ChangeNotifier implements IResettableOnLogout { @override void resetOnLogOut() { - action = DexAction.none; + reset(); + } + + void clearDexParams() { + _fromCurrency = ''; + _fromAmount = ''; + _toCurrency = ''; + _toAmount = ''; + _orderType = ''; } } diff --git a/lib/views/dex/entities_list/dex_list_wrapper.dart b/lib/views/dex/entities_list/dex_list_wrapper.dart index fc9227d573..337294e87c 100644 --- a/lib/views/dex/entities_list/dex_list_wrapper.dart +++ b/lib/views/dex/entities_list/dex_list_wrapper.dart @@ -33,11 +33,37 @@ class _DexListWrapperState extends State { bool _isFilterShown = false; DexListType? previouseType; + final TradingKindBloc tradingKindBloc = + TradingKindBloc(TradingKindState.initial()); + + @override + void initState() { + routingState.dexState.addListener(_onRouteChange); + super.initState(); + } + + @override + void dispose() { + routingState.dexState.removeListener(_onRouteChange); + super.dispose(); + } + + void _onRouteChange() { + if (mounted) { + final type = routingState.dexState.orderType; + if (type.isNotEmpty || + (routingState.dexState.fromCurrency.isNotEmpty || + routingState.dexState.toCurrency.isNotEmpty)) { + tradingKindBloc + .setKind(type == 'taker' ? TradingKind.taker : TradingKind.maker); + } + } + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => - TradingKindBloc(TradingKindState.initial()), + create: (_) => tradingKindBloc, child: BlocBuilder( builder: (context, state) { final filter = filters[widget.listType]; diff --git a/lib/views/dex/simple/form/maker/maker_form_layout.dart b/lib/views/dex/simple/form/maker/maker_form_layout.dart index 0dcb78e24a..921c3f73d7 100644 --- a/lib/views/dex/simple/form/maker/maker_form_layout.dart +++ b/lib/views/dex/simple/form/maker/maker_form_layout.dart @@ -8,6 +8,8 @@ import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/views/dex/simple/confirm/maker_order_confirmation.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_buy_coin_table.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_content.dart'; @@ -25,9 +27,43 @@ class _MakerFormLayoutState extends State { @override void initState() { makerFormBloc.setDefaultSellCoin(); + _consumeRouteParameters(); + super.initState(); } + void _consumeRouteParameters() { + if (routingState.dexState.orderType != 'taker') { + if (routingState.dexState.fromCurrency.isNotEmpty) { + final Coin? sellCoin = + coinsBloc.getCoin(routingState.dexState.fromCurrency); + + if (sellCoin != null) { + makerFormBloc.sellCoin = sellCoin; + + if (routingState.dexState.fromAmount.isNotEmpty) { + makerFormBloc.setSellAmount(routingState.dexState.fromAmount); + } + } + } + + if (routingState.dexState.toCurrency.isNotEmpty) { + final Coin? buyCoin = + coinsBloc.getCoin(routingState.dexState.toCurrency); + + if (buyCoin != null) { + makerFormBloc.buyCoin = buyCoin; + + if (routingState.dexState.toAmount.isNotEmpty) { + makerFormBloc.setBuyAmount(routingState.dexState.toAmount); + } + } + } + + routingState.dexState.clearDexParams(); + } + } + @override Widget build(BuildContext context) { final DexTabBarBloc bloc = context.read(); diff --git a/lib/views/dex/simple/form/taker/taker_form.dart b/lib/views/dex/simple/form/taker/taker_form.dart index 93492a707f..3afddf36ca 100644 --- a/lib/views/dex/simple/form/taker/taker_form.dart +++ b/lib/views/dex/simple/form/taker/taker_form.dart @@ -2,9 +2,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/views/dex/simple/form/taker/taker_form_layout.dart'; class TakerForm extends StatefulWidget { @@ -26,12 +30,49 @@ class _TakerFormState extends State { takerBloc.add(TakerSetWalletIsReady(value)); }); + routingState.dexState.addListener(_consumeRouteParameters); super.initState(); } + void _consumeRouteParameters() async { + if (routingState.dexState.orderType == 'taker') { + final fromCurrency = routingState.dexState.fromCurrency; + final toCurrency = routingState.dexState.toCurrency; + final fromAmount = routingState.dexState.fromAmount; + routingState.dexState.clearDexParams(); + + await dexRepository.waitOrderbookAvailability(); + + if (mounted) { + final takerBloc = context.read(); + Coin? sellCoin = + fromCurrency.isNotEmpty ? coinsBloc.getCoin(fromCurrency) : null; + Coin? buyCoin = + toCurrency.isNotEmpty ? coinsBloc.getCoin(toCurrency) : null; + + if (sellCoin != null || buyCoin != null) { + takerBloc.add( + TakerSetSellCoin(sellCoin, autoSelectOrderAbbr: buyCoin?.abbr)); + + if (fromAmount.isNotEmpty) { + Rational? sellAmount; + try { + sellAmount = Rational.parse(fromAmount); + } catch (_) {} + + if (sellAmount != null) { + takerBloc.add(TakerSetSellAmount(sellAmount)); + } + } + } + } + } + } + @override void dispose() { _coinsListener?.cancel(); + routingState.dexState.removeListener(_consumeRouteParameters); super.dispose(); } diff --git a/lib/views/dex/simple/form/taker/taker_form_layout.dart b/lib/views/dex/simple/form/taker/taker_form_layout.dart index f2560a8bd7..79572faf3c 100644 --- a/lib/views/dex/simple/form/taker/taker_form_layout.dart +++ b/lib/views/dex/simple/form/taker/taker_form_layout.dart @@ -103,11 +103,11 @@ class _TakerFormMobileLayout extends StatelessWidget { ], ), Padding( - padding: const EdgeInsets.fromLTRB(16, 37, 16, 0), + padding: const EdgeInsets.fromLTRB(16, 52, 16, 0), child: TakerSellCoinsTable(), ), Padding( - padding: const EdgeInsets.fromLTRB(16, 134, 16, 0), + padding: const EdgeInsets.fromLTRB(16, 167, 16, 0), child: TakerOrdersTable(), ), ], From 210aecca3d9cb86ffc0a2bd3098d05de9c1b46f0 Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Thu, 17 Oct 2024 17:07:06 +0300 Subject: [PATCH 07/34] fix onPopPage deprecated (#172) --- .../navigators/app_router_delegate.dart | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/router/navigators/app_router_delegate.dart b/lib/router/navigators/app_router_delegate.dart index 2b18b3bf01..7f18cbb2ff 100644 --- a/lib/router/navigators/app_router_delegate.dart +++ b/lib/router/navigators/app_router_delegate.dart @@ -25,24 +25,25 @@ class AppRouterDelegate extends RouterDelegate Widget build(BuildContext context) { updateScreenType(context); + final MaterialPage page1 = MaterialPage( + key: const ValueKey('MainPage'), + child: Builder( + builder: (context) { + materialPageContext = context; + return GestureDetector( + onTap: () => globalCancelBloc.runDropdownDismiss(context: context), + child: MainLayout(), + ); + }, + ), + ); + + final List> pages = >[page1]; + return Navigator( key: navigatorKey, - pages: [ - MaterialPage( - key: const ValueKey('MainPage'), - child: Builder( - builder: (context) { - materialPageContext = context; - return GestureDetector( - onTap: () => - globalCancelBloc.runDropdownDismiss(context: context), - child: MainLayout(), - ); - }, - ), - ), - ], - onPopPage: (route, dynamic result) => route.didPop(result), + pages: pages, + onDidRemovePage: (Page page) => pages.remove(page), ); } From e21561059396b60308c54c9c79331e57994f171d Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Thu, 17 Oct 2024 17:07:38 +0300 Subject: [PATCH 08/34] add are you sure prompt at close/refresh (#173) --- lib/views/main_layout/main_layout.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 4699a91a54..d8c5aaac05 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,3 +1,6 @@ +import 'package:web/web.dart' as web; +import 'dart:js_interop'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/app_config/app_config.dart'; @@ -23,6 +26,11 @@ class MainLayout extends StatefulWidget { class _MainLayoutState extends State { @override void initState() { + web.window.onbeforeunload = (web.BeforeUnloadEvent event) { + event.preventDefault(); + event.returnValue = 'Are you sure?'; + }.toJS; + WidgetsBinding.instance.addPostFrameCallback((_) async { await AlphaVersionWarningService().run(); updateBloc.init(); From 4b9d31db830b970217b6bc121bd05323284cf34a Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 23 Oct 2024 12:01:44 +0200 Subject: [PATCH 09/34] fix(log-export): cross platform log export (#174) * change extension to `.gz` on native platforms * add single-file zip archive support credit for zip.dart goes to gpt-4o --- lib/services/file_loader/file_loader.dart | 6 +- .../file_loader_native_desktop.dart | 18 +-- lib/services/file_loader/file_loader_web.dart | 2 - .../mobile/file_loader_native_android.dart | 34 +++-- .../mobile/file_loader_native_ios.dart | 20 +-- lib/shared/utils/zip.dart | 140 ++++++++++++++++++ 6 files changed, 175 insertions(+), 45 deletions(-) create mode 100644 lib/shared/utils/zip.dart diff --git a/lib/services/file_loader/file_loader.dart b/lib/services/file_loader/file_loader.dart index c2542ae6e2..02af467e21 100644 --- a/lib/services/file_loader/file_loader.dart +++ b/lib/services/file_loader/file_loader.dart @@ -8,8 +8,8 @@ abstract class FileLoader { required LoadFileType type, }); Future upload({ - required Function(String name, String? content) onUpload, - required Function(String) onError, + required void Function(String name, String? content) onUpload, + required void Function(String) onError, LoadFileType? fileType, }); } @@ -32,8 +32,6 @@ enum LoadFileType { return 'application/zip'; case LoadFileType.text: return 'text/plain'; - default: - return '*/*'; } } diff --git a/lib/services/file_loader/file_loader_native_desktop.dart b/lib/services/file_loader/file_loader_native_desktop.dart index 09530a02c5..7f271c06cb 100644 --- a/lib/services/file_loader/file_loader_native_desktop.dart +++ b/lib/services/file_loader/file_loader_native_desktop.dart @@ -1,8 +1,8 @@ -import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; +import 'package:web_dex/shared/utils/zip.dart'; class FileLoaderNativeDesktop implements FileLoader { const FileLoaderNativeDesktop(); @@ -15,18 +15,16 @@ class FileLoaderNativeDesktop implements FileLoader { }) async { switch (type) { case LoadFileType.text: - _saveAsTextFile(fileName, data); - return; + await _saveAsTextFile(fileName, data); case LoadFileType.compressed: - _saveAsCompressedFile(fileName, data); - return; + await _saveAsCompressedFile(fileName, data); } } @override Future upload({ - required Function(String name, String content) onUpload, - required Function(String) onError, + required void Function(String name, String content) onUpload, + required void Function(String) onError, LoadFileType? fileType, }) async { try { @@ -63,10 +61,8 @@ class FileLoaderNativeDesktop implements FileLoader { await FilePicker.platform.saveFile(fileName: '$fileName.zip'); if (fileFullPath == null) return; - final List fileBytes = utf8.encode(data); - - // Using ZLibCodec for compression - final compressedBytes = ZLibEncoder().convert(fileBytes); + final compressedBytes = + createZipOfSingleFile(fileName: fileName, fileContent: data); final File compressedFile = File(fileFullPath); await compressedFile.writeAsBytes(compressedBytes); diff --git a/lib/services/file_loader/file_loader_web.dart b/lib/services/file_loader/file_loader_web.dart index bc99d23bcc..1316d92182 100644 --- a/lib/services/file_loader/file_loader_web.dart +++ b/lib/services/file_loader/file_loader_web.dart @@ -16,10 +16,8 @@ class FileLoaderWeb implements FileLoader { switch (type) { case LoadFileType.text: await _saveAsTextFile(filename: fileName, data: data); - return; case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); - return; } } diff --git a/lib/services/file_loader/mobile/file_loader_native_android.dart b/lib/services/file_loader/mobile/file_loader_native_android.dart index c69605b84a..c3b0c74d14 100644 --- a/lib/services/file_loader/mobile/file_loader_native_android.dart +++ b/lib/services/file_loader/mobile/file_loader_native_android.dart @@ -1,8 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/utils/zip.dart'; class FileLoaderNativeAndroid implements FileLoader { const FileLoaderNativeAndroid(); @@ -16,10 +19,8 @@ class FileLoaderNativeAndroid implements FileLoader { switch (type) { case LoadFileType.text: await _saveAsTextFile(fileName: fileName, data: data); - break; case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); - break; } } @@ -27,37 +28,38 @@ class FileLoaderNativeAndroid implements FileLoader { required String fileName, required String data, }) async { + // On mobile, the file bytes are used to create the file to be saved. + // On desktop a file is created first, then a file is saved. + final Uint8List fileBytes = utf8.encode(data); final String? fileFullPath = await FilePicker.platform.saveFile( fileName: '$fileName.txt', + bytes: fileBytes, ); - if (fileFullPath == null) return; - - final File file = File(fileFullPath)..createSync(recursive: true); - await file.writeAsString(data); + if (fileFullPath == null || fileFullPath.isEmpty == true) { + log('error: output filepath for $fileName is empty'); + } } Future _saveAsCompressedFile({ required String fileName, required String data, }) async { + final Uint8List compressedBytes = + createZipOfSingleFile(fileName: fileName, fileContent: data); final String? fileFullPath = await FilePicker.platform.saveFile( fileName: '$fileName.zip', + bytes: compressedBytes, ); - if (fileFullPath == null) return; - - final List fileBytes = utf8.encode(data); - // Using ZLibCodec for compression - final compressedBytes = ZLibEncoder().convert(fileBytes); - - final File compressedFile = File(fileFullPath); - await compressedFile.writeAsBytes(compressedBytes); + if (fileFullPath == null || fileFullPath.isEmpty == true) { + log('error: output filepath for $fileName is empty'); + } } @override Future upload({ - required Function(String name, String content) onUpload, - required Function(String) onError, + required void Function(String name, String content) onUpload, + required void Function(String) onError, LoadFileType? fileType, }) async { try { 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 b434410454..5f6cf830a7 100644 --- a/lib/services/file_loader/mobile/file_loader_native_ios.dart +++ b/lib/services/file_loader/mobile/file_loader_native_ios.dart @@ -1,11 +1,11 @@ -import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; -import 'package:web_dex/services/file_loader/file_loader.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:path_provider/path_provider.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/services/file_loader/file_loader.dart'; +import 'package:web_dex/shared/utils/zip.dart'; class FileLoaderNativeIOS implements FileLoader { const FileLoaderNativeIOS(); @@ -19,10 +19,8 @@ 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; } } @@ -45,10 +43,8 @@ class FileLoaderNativeIOS implements FileLoader { final directory = await getApplicationDocumentsDirectory(); final filePath = path.join(directory.path, '$fileName.zip'); - final List fileBytes = utf8.encode(data); - - // Using ZLibCodec for compression - final compressedBytes = ZLibEncoder().convert(fileBytes); + final compressedBytes = + createZipOfSingleFile(fileName: fileName, fileContent: data); final File compressedFile = File(filePath); await compressedFile.writeAsBytes(compressedBytes); @@ -58,8 +54,8 @@ class FileLoaderNativeIOS implements FileLoader { @override Future upload({ - required Function(String name, String content) onUpload, - required Function(String) onError, + required void Function(String name, String content) onUpload, + required void Function(String) onError, LoadFileType? fileType, }) async { try { diff --git a/lib/shared/utils/zip.dart b/lib/shared/utils/zip.dart new file mode 100644 index 0000000000..467df41ad3 --- /dev/null +++ b/lib/shared/utils/zip.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +Uint8List createZipOfSingleFile({ + required String fileName, + required String fileContent, +}) { + final fileNameWithExtension = '$fileName.txt'; + + final originalBytes = utf8.encode(fileContent); + // use `raw: true` to exclude zlip header and trailer data that causes + // zip decompression to fail + final compressedBytes = + Uint8List.fromList(ZLibCodec(raw: true).encode(originalBytes)); + final crc32 = _crc32(originalBytes); + + final ByteData localFileHeader = _createZipHeader( + fileNameWithExtension, + crc32, + originalBytes, + compressedBytes.length, + ); + + final ByteData centralDirectoryHeader = _createZipDirectoryHeader( + fileNameWithExtension, + crc32, + originalBytes, + compressedBytes.length, + ); + + final ByteData endOfCentralDirectory = _createEndOfCentralDirectory( + centralDirectoryHeader, + localFileHeader, + compressedBytes, + ); + + final zipData = BytesBuilder() + ..add(localFileHeader.buffer.asUint8List()) + ..add(compressedBytes) + ..add(centralDirectoryHeader.buffer.asUint8List()) + ..add(endOfCentralDirectory.buffer.asUint8List()); + return zipData.takeBytes(); +} + +ByteData _createEndOfCentralDirectory( + ByteData centralDirectoryHeader, + ByteData localFileHeader, + Uint8List bytes, +) { + final endOfCentralDirectory = ByteData(22) + // End of central directory signature + ..setUint32(0, 0x06054b50, Endian.little) + // Number of this disk + ..setUint16(4, 0, Endian.little) + // Disk with the start of the central directory + ..setUint16(6, 0, Endian.little) + // Total number of entries in the central directory on this disk + ..setUint16(8, 1, Endian.little) + // Total number of entries in the central directory + ..setUint16(10, 1, Endian.little) + // Size of the central directory + ..setUint32(12, centralDirectoryHeader.lengthInBytes, Endian.little) + // Offset of start of central directory + ..setUint32(16, localFileHeader.lengthInBytes + bytes.length, Endian.little) + // Comment length + ..setUint16(20, 0, Endian.little); + return endOfCentralDirectory; +} + +ByteData _createZipDirectoryHeader( + String fileName, + int crc32, + Uint8List originalBytes, + int compressedSize, +) { + final centralDirectoryHeader = ByteData(46 + fileName.length) + ..setUint32( + 0, + 0x02014b50, + Endian.little, + ) // Central directory file header signature + ..setUint16(4, 0, Endian.little) // Version made by + ..setUint16(6, 20, Endian.little) // Version needed to extract + ..setUint16(8, 0, Endian.little) // General purpose bit flag + ..setUint16(10, 8, Endian.little) // Compression method (8: Deflate) + ..setUint16(12, 0, Endian.little) // File last modification time + ..setUint16(14, 0, Endian.little) // File last modification date + ..setUint32(16, crc32, Endian.little) // CRC-32 + ..setUint32(20, compressedSize, Endian.little) // Compressed size + ..setUint32(24, originalBytes.length, Endian.little) // Uncompressed size + ..setUint16(28, fileName.length, Endian.little) // File name length + ..setUint16(30, 0, Endian.little) // Extra field length + ..setUint16(32, 0, Endian.little) // File comment length + ..setUint16(34, 0, Endian.little) // Disk number start + ..setUint16(36, 0, Endian.little) // Internal file attributes + ..setUint32(38, 0, Endian.little) // External file attributes + ..setUint32(42, 0, Endian.little) // Relative offset of local header + ..buffer.asUint8List().setAll(46, utf8.encode(fileName)); // File name + return centralDirectoryHeader; +} + +ByteData _createZipHeader( + String fileName, + int crc32, + Uint8List originalBytes, + int compressedSize, +) { + final localFileHeader = ByteData(30 + fileName.length) + ..setUint32(0, 0x04034b50, Endian.little) // Local file header signature + ..setUint16(4, 20, Endian.little) // Version needed to extract + ..setUint16(6, 0, Endian.little) // General purpose bit flag + ..setUint16(8, 8, Endian.little) // Compression method (8: Deflate) + ..setUint16(10, 0, Endian.little) // File last modification time + ..setUint16(12, 0, Endian.little) // File last modification date + ..setUint32(14, crc32, Endian.little) // CRC-32 + ..setUint32(18, compressedSize, Endian.little) // Compressed size + ..setUint32(22, originalBytes.length, Endian.little) // Uncompressed size + ..setUint16(26, fileName.length, Endian.little) // File name length + ..buffer.asUint8List().setAll(30, utf8.encode(fileName)); // File name + return localFileHeader; +} + +int _crc32(List bytes) { + // Simple implementation of CRC-32 checksum algorithm + const polynomial = 0xEDB88320; + var crc = 0xFFFFFFFF; + for (final byte in bytes) { + var currentByte = byte; + for (int j = 0; j < 8; j++) { + final isBitSet = (crc ^ currentByte) & 1; + crc >>= 1; + if (isBitSet != 0) { + crc ^= polynomial; + } + currentByte >>= 1; + } + } + return ~crc & 0xFFFFFFFF; +} From 8dff320fd2e995dd1f24aea77b80b2d5f82a24d3 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 28 Oct 2024 14:46:54 +0200 Subject: [PATCH 10/34] feat(fiat-on-ramp): cross-platform fiat on-ramp (#170) * Update Flutter to 3.24.3 (stable) Only web build works with this configuration * Bump Flutter version in workflows to `3.24.x` * Remove `desktop_webview_window` dependency * Switch to in-app webview in popup-window * Upgrade `file_picker` to 8.1.2 to fix ios&macos build error win32 v5 removes references to deprecated APIs, which https://github.com/jonataslaw/get_cli/issues/263 * Upgrade `url_launcher` to 6.3.0 to fix iOS build error * Skip web defi fetch step if target is iOS * Bump CI Flutter version to `3.24.x` * Fix mobile coin details buttons layout - Also fix Bitrefill button in preparation for cross-platform fiat onramp in a similar fashion * Add fullscreen in-app-webview for native platforms * Close the browser if redirected to web app This is a failure condition for the `checkout_status_redirect.html` page * Move payment status events to BLoC The async onConsole/onMessage callback used by the `flutter_inappwebview` package is incompatible with the previous watcher implementation. * Migrate bitrefill provider & watcher to package:web * refactor fullscreen webview to webview dialog * add fiat onramp html page fixes issues with reading `onmessage` and `window.console` from an iframe or another window * apply patch to `web_support.js` to fix web callbacks from https://github.com/pichillilorenzo/flutter_inappwebview/pull/2058 * migrate remaining fiat & bitrefill html references to package:web * WIP: add initial fiat onramp bloc implementation * fix arb merge issue: add arb to currency class * fix type conversion bugs and add more error logging add stacktrace to logs when in kDebug mode * replace onCheckoutComplete callback with BlocListener * improve form state management and add default payment methods list for initial user input * fix cross-platform compilation with conditional imports package:web and js_interop only work on web, so use conditional exports * fix cocoapods build warnings * use url instead of proxy page on native platforms only web requires the proxy page because of CORS restrictions. onConsole, and onMessage works on native platforms * fix hive runtime init exception `Hive.initFlutter` failed on macOS and appears to be a web-specific function that produces an exception on native platforms * add error parsing for banxa order creation * localize fiat error and popup messages * fix status message parsing in wrapper html page * move getCoinAddress to coins bloc * update testing and setup docs add example launch.json and iOS crash logs location add notes about linux setup * fix fiat amount injecting decimals removed unnecessary string `error` field, since the status fields suffice * fix onramp error on linux & banxa parsing bug * fix fiat form overlapping issues on mobile - use autoscrolltext, expand, and align - fix fiat icon errors when scrolling quickly - fix webview platform check * fix flutter analyze warnings * add fiat onramp form integration test * bump build transformer package commit * improve status parsing on native platforms - parsing errors from escaped json strings on Windows - re-enable banxa order status watching * fix coins bloc and Hive init race condition on macos Runtime updates Hive boxes have to be initialised on native platforms before coins bloc executes, which was not happening consistently across all platforms * move confirmation prompt behind conditional import `web` and `js_interop` package imports do not compile on native platforms, so they have to be hidden behind conditional imports to allow for cross-platform support --- .gitignore | 1 + app_build/build_config.json | 3 +- .../assets/web/web_support.js | 10 +- assets/translations/en.json | 10 + assets/web_pages/fiat_widget.html | 110 +++ docs/BUILD_RUN_APP.md | 28 +- docs/MANUAL_TESTING_DEBUGGING.md | 129 ++- ios/Flutter/Release.xcconfig | 3 +- ios/Podfile | 2 +- ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/project.pbxproj | 5 +- lib/bloc/app_bloc_root.dart | 1 - lib/bloc/bitrefill/bloc/bitrefill_bloc.dart | 14 +- .../bitrefill/data/bitrefill_provider.dart | 14 +- .../data/bitrefill_purchase_watcher.dart | 95 -- .../bitrefill/data/bitrefill_repository.dart | 10 - .../models/embedded_bitrefill_url.dart | 7 +- .../portfolio_growth_bloc.dart | 2 +- .../profit_loss/profit_loss_bloc.dart | 8 +- lib/bloc/fiat/banxa_fiat_provider.dart | 160 ++-- lib/bloc/fiat/base_fiat_provider.dart | 130 +-- .../fiat/fiat_onramp_form/fiat_form_bloc.dart | 470 ++++++++++ .../fiat_onramp_form/fiat_form_event.dart | 98 ++ .../fiat_onramp_form/fiat_form_state.dart | 150 +++ lib/bloc/fiat/fiat_order_status.dart | 37 + lib/bloc/fiat/fiat_repository.dart | 166 ++-- .../fiat/models/fiat_buy_order_error.dart | 37 + lib/bloc/fiat/models/fiat_buy_order_info.dart | 174 ++++ lib/bloc/fiat/models/fiat_mode.dart | 25 + lib/bloc/fiat/models/fiat_order.dart | 0 lib/bloc/fiat/models/fiat_payment_method.dart | 168 ++++ lib/bloc/fiat/models/fiat_price_info.dart | 77 ++ .../fiat/models/fiat_transaction_fee.dart | 42 + .../fiat/models/fiat_transaction_limit.dart | 43 + lib/bloc/fiat/models/i_currency.dart | 67 ++ lib/bloc/fiat/models/models.dart | 9 + lib/bloc/fiat/ramp/ramp_fiat_provider.dart | 178 ++-- lib/bloc/fiat/ramp/ramp_purchase_watcher.dart | 258 ------ .../market_maker_trade_form_bloc.dart | 2 +- lib/blocs/coins_bloc.dart | 52 +- lib/generated/codegen_loader.g.dart | 123 +-- lib/main.dart | 1 + lib/model/forms/fiat/currency_input.dart | 38 + lib/model/forms/fiat/fiat_amount_input.dart | 52 ++ .../initializer/app_bootstrapper.dart | 5 +- .../utils/extensions/string_extensions.dart | 11 + lib/shared/utils/utils.dart | 50 +- lib/shared/utils/window/window.dart | 3 + lib/shared/utils/window/window_native.dart | 9 + lib/shared/utils/window/window_stub.dart | 7 + lib/shared/utils/window/window_web.dart | 15 + lib/views/bitrefill/bitrefill_button.dart | 5 - .../bitrefill_inappbrowser_button.dart | 7 - .../bitrefill_inappwebview_button.dart | 52 +- lib/views/fiat/address_bar.dart | 48 + lib/views/fiat/custom_fiat_input_field.dart | 81 ++ lib/views/fiat/fiat_asset_icon.dart | 34 + lib/views/fiat/fiat_currency_item.dart | 54 ++ lib/views/fiat/fiat_currency_list_tile.dart | 55 ++ lib/views/fiat/fiat_form.dart | 851 ++++-------------- lib/views/fiat/fiat_icon.dart | 31 +- lib/views/fiat/fiat_inputs.dart | 489 ++-------- lib/views/fiat/fiat_page.dart | 115 ++- ...hod.dart => fiat_payment_method_card.dart} | 68 +- lib/views/fiat/fiat_payment_method_group.dart | 73 ++ lib/views/fiat/fiat_payment_methods_grid.dart | 95 ++ lib/views/fiat/fiat_select_button.dart | 101 +++ lib/views/fiat/webview_dialog.dart | 222 +++++ lib/views/main_layout/main_layout.dart | 10 +- .../coin_details_common_buttons.dart | 255 ++++-- .../coin_details_info/coin_details_info.dart | 57 +- macos/Runner/AppDelegate.swift | 2 +- .../lib/src/komodo_coin_updater.dart | 20 +- packages/komodo_coin_updates/pubspec.yaml | 2 + pubspec.lock | 2 +- run_integration_tests.dart | 1 + .../fiat_onramp_tests/fiat_onramp_tests.dart | 27 + .../tests/fiat_onramp_tests/form_tests.dart | 137 +++ 78 files changed, 3931 insertions(+), 2074 deletions(-) create mode 100644 assets/web_pages/fiat_widget.html delete mode 100644 lib/bloc/bitrefill/data/bitrefill_purchase_watcher.dart create mode 100644 lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart create mode 100644 lib/bloc/fiat/fiat_onramp_form/fiat_form_event.dart create mode 100644 lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart create mode 100644 lib/bloc/fiat/models/fiat_buy_order_error.dart create mode 100644 lib/bloc/fiat/models/fiat_buy_order_info.dart create mode 100644 lib/bloc/fiat/models/fiat_mode.dart create mode 100644 lib/bloc/fiat/models/fiat_order.dart create mode 100644 lib/bloc/fiat/models/fiat_payment_method.dart create mode 100644 lib/bloc/fiat/models/fiat_price_info.dart create mode 100644 lib/bloc/fiat/models/fiat_transaction_fee.dart create mode 100644 lib/bloc/fiat/models/fiat_transaction_limit.dart create mode 100644 lib/bloc/fiat/models/i_currency.dart create mode 100644 lib/bloc/fiat/models/models.dart delete mode 100644 lib/bloc/fiat/ramp/ramp_purchase_watcher.dart create mode 100644 lib/model/forms/fiat/currency_input.dart create mode 100644 lib/model/forms/fiat/fiat_amount_input.dart create mode 100644 lib/shared/utils/window/window.dart create mode 100644 lib/shared/utils/window/window_native.dart create mode 100644 lib/shared/utils/window/window_stub.dart create mode 100644 lib/shared/utils/window/window_web.dart create mode 100644 lib/views/fiat/address_bar.dart create mode 100644 lib/views/fiat/custom_fiat_input_field.dart create mode 100644 lib/views/fiat/fiat_asset_icon.dart create mode 100644 lib/views/fiat/fiat_currency_item.dart create mode 100644 lib/views/fiat/fiat_currency_list_tile.dart rename lib/views/fiat/{fiat_payment_method.dart => fiat_payment_method_card.dart} (57%) create mode 100644 lib/views/fiat/fiat_payment_method_group.dart create mode 100644 lib/views/fiat/fiat_payment_methods_grid.dart create mode 100644 lib/views/fiat/fiat_select_button.dart create mode 100644 lib/views/fiat/webview_dialog.dart create mode 100644 test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart create mode 100644 test_integration/tests/fiat_onramp_tests/form_tests.dart diff --git a/.gitignore b/.gitignore index 59fe278c23..ac512ae593 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ .pub/ /build/ contrib/coins_config.json +devtools_options.yaml # Web related web/index.html diff --git a/app_build/build_config.json b/app_build/build_config.json index 54d64808bf..4969bf516f 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -60,8 +60,9 @@ } }, "coins": { + "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "83794c99944ae451d7898a8db78898be7a63de8a", + "bundled_coins_repo_commit": "7d2ca04e250b59362641da69f752aec8cbd83838", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://komodoplatform.github.io/coins", "coins_repo_branch": "master", diff --git a/assets/packages/flutter_inappwebview_web/assets/web/web_support.js b/assets/packages/flutter_inappwebview_web/assets/web/web_support.js index 9997396268..dd6a6dc22c 100644 --- a/assets/packages/flutter_inappwebview_web/assets/web/web_support.js +++ b/assets/packages/flutter_inappwebview_web/assets/web/web_support.js @@ -1,6 +1,12 @@ window.flutter_inappwebview = { webViews: {}, - createFlutterInAppWebView: function (viewId, iframeId) { + /** + * @param viewId {number | string} + * @param iframe {HTMLIFrameElement} + * @param iframeContainer {HTMLDivElement} + */ + createFlutterInAppWebView: function (viewId, iframe, iframeContainer) { + const iframeId = iframe.id; var webView = { viewId: viewId, iframeId: iframeId, @@ -19,8 +25,6 @@ window.flutter_inappwebview = { }, prepare: function (settings) { webView.settings = settings; - var iframe = document.getElementById(iframeId); - var iframeContainer = document.getElementById(iframeId + '-container'); document.addEventListener('fullscreenchange', function (event) { // document.fullscreenElement will point to the element that diff --git a/assets/translations/en.json b/assets/translations/en.json index 08f09c375d..8015037428 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -552,6 +552,16 @@ "fiatCantCompleteOrder": "Cannot complete order. Please try again later or contact support", "fiatPriceCanChange": "Price is subject to change depending on the selected provider", "fiatConnectWallet": "Please connect your wallet to purchase coins", + "fiatMinimumAmount": "Please enter a value greater than {} {}", + "fiatMaximumAmount": "Please enter a value less than {} {}", + "fiatPaymentSubmittedTitle": "Your payment request was opened in another window or tab", + "fiatPaymentSubmittedMessage": "Please complete the payment in another window or tab.", + "fiatPaymentSuccessTitle": "Order successful!", + "fiatPaymentSuccessMessage": "Your coins have been deposited to your wallet.", + "fiatPaymentFailedTitle": "Payment failed", + "fiatPaymentFailedMessage": "Your payment has failed. Please check your email for more information or contact the provider's support.", + "fiatPaymentInProgressTitle": "Payment received", + "fiatPaymentInProgressMessage": "Congratulations! Your payment has been received and the coins are on the way to your wallet. \n\nYou will receive your coins in 1-60 minutes.", "pleaseWait": "Please wait", "bitrefillPaymentSuccessfull": "Bitrefill payment succssfull", "bitrefillPaymentSuccessfullInstruction": "You should receive an email from Bitrefill shortly.\n\nPlease check your email for further information.\n\nInvoice ID: {}\n", diff --git a/assets/web_pages/fiat_widget.html b/assets/web_pages/fiat_widget.html new file mode 100644 index 0000000000..a42d14db13 --- /dev/null +++ b/assets/web_pages/fiat_widget.html @@ -0,0 +1,110 @@ + + + + + Fiat OnRamp + + + + + + + + + + \ No newline at end of file diff --git a/docs/BUILD_RUN_APP.md b/docs/BUILD_RUN_APP.md index 7a3cbdb468..3f4409f42c 100644 --- a/docs/BUILD_RUN_APP.md +++ b/docs/BUILD_RUN_APP.md @@ -161,10 +161,34 @@ The Linux dependencies, [according to flutter.dev](https://docs.flutter.dev/get- > - libstdc++-12-dev > - webkit2gtk-4.1 (Webview support) -To install on Ubuntu 22.04 or later, run: +To install on Ubuntu 20.04 or later, run: ```bash -sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev webkit2gtk-4.1 +sudo apt-get install -y clang cmake git ninja-build pkg-config \ + libgtk-3-dev liblzma-dev libstdc++-12-dev webkit2gtk-4.1 \ + curl git unzip xz-utils zip libglu1-mesa +``` + +Users of Ubuntu 24.04 (Noble) or later, might need to install additional dependencies, and add `PKG_CONFIG_PATH` to their bash configuration. If that doesn't work, then try adding it to `/etc/environment` as well. + +```bash +# Install xproto & xorg development libraries (xorg is precautionary, so can be excluded) +sudo apt-get install -y x11proto-dev xorg-dev libgl1-mesa-dev + +# Check if PKG_CONFIG_PATH exists first before modifying or overwriting it +echo $PKG_CONFIG_PATH + +# Add PKG_CONFIG_PATH to .bashrc only if it doesn't exist or is empty +if [ -z "$PKG_CONFIG_PATH" ]; then + echo "PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/local/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig" | sudo tee -a ~/.bashrc + source ~/.bashrc + pkg-config --cflags --libs gtk+-3.0 +else + echo "PKG_CONFIG_PATH is already set." +fi + +# Confirm that gtk+-3.0 is found in the output +flutter doctor ``` ```bash diff --git a/docs/MANUAL_TESTING_DEBUGGING.md b/docs/MANUAL_TESTING_DEBUGGING.md index 70d01c39bb..930a49d75d 100644 --- a/docs/MANUAL_TESTING_DEBUGGING.md +++ b/docs/MANUAL_TESTING_DEBUGGING.md @@ -27,7 +27,7 @@ File structure example bellow: [Manual testing plan](https://docs.google.com/spreadsheets/d/1EiFwI00VJFj5lRm-x-ybRoV8r17EW3GnhzTBR628XjM/edit#gid=0) -## Debugging web version on desktop +## Debugging web version on desktop ## HTTP @@ -40,7 +40,7 @@ flutter run -d chrome --web-hostname=0.0.0.0 --web-port=7777 ### Generate self-signed certificate with openssl ```bash -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname" +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha512 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=http://localhost.com" ``` ### Run flutter with self-signed certificate @@ -126,3 +126,128 @@ At the time of writing used branch [gen-recoverable-swap](https://github.com/Kom 2. BOB_PASSPHRASE uses for maker 3. TAKER_FAIL_AT values see [here](https://github.com/KomodoPlatform/atomicDEX-API/pull/1428/files#diff-3b58e25a3c557aa8a502011591e9a7d56441fd147c2ab072e108902a06ef3076R481) 4. MAKER_FAIL_AT values see [here](https://github.com/KomodoPlatform/atomicDEX-API/pull/1428/files#diff-608240539630bec8eb43b211b0b74ec3580b34dda66e339bac21c04b1db6da43R1861) + +### iOS Crash Logs + +Look for entries starting with or containing "Runner" or "Komodo" + +- On a real device: Go to Settings -> Privacy -> Analytics & Improvements -> Analytics Data +- In Simulator: ~/Library/Logs/DiagnosticReports/ + +## Visual Studio Code Configuration + +### launch.json + +Replace `$HOME` with your home directory if there are any issues with the path. + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "KW (debug)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "args": [ + "--web-port", + "6969" + ] + }, + { + "name": "KW (debug,https)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "args": [ + "--web-port", + "6970", + "--web-tls-cert-path", + "$HOME/.ssh/debug/server.crt", + "--web-tls-cert-key-path", + "$HOME/.ssh/debug/server.key" + ] + }, + { + "name": "KW (debug,https,no-web-security)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "args": [ + "--web-port", + "6971", + "--web-browser-flag", + "--disable-web-security", + "--web-tls-cert-path", + "$HOME/.ssh/debug/server.crt", + "--web-tls-cert-key-path", + "$HOME/.ssh/debug/server.key" + ] + }, + { + "name": "KW (profile)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--web-port", + "6972" + ] + }, + { + "name": "KW (profile,https)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--web-port", + "6973", + "--web-tls-cert-path", + "$HOME/.ssh/debug/server.crt", + "--web-tls-cert-key-path", + "$HOME/.ssh/debug/server.key" + ] + }, + { + "name": "KW (release)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "args": [ + "--web-port", + "8080" + ] + }, + { + "name": "KW (release,https)", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "args": [ + "--web-port", + "8081", + "--web-tls-cert-path", + "$HOME/.ssh/debug/server.crt", + "--web-tls-cert-key-path", + "$HOME/.ssh/debug/server.key" + ] + } + ] +} +``` + +### settings.json + +```json +{ + "dart.flutterSdkPath": ".fvm/versions/stable", + "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code", + "editor.formatOnSave": true + } +} +``` diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 6f07b31e9e..2f2c1098a8 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile index 279576f388..2c068c404b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2c9caebf95..80e40ed665 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -305,6 +305,6 @@ SPEC CHECKSUMS: url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 -PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 +PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 44092eac7a..f028d3b5b0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -301,7 +301,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; FC78CCE93902D5D826DCE20C /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -415,6 +415,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -555,6 +556,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -587,6 +589,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 1487b5215c..20e05effa9 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -107,7 +107,6 @@ class AppBlocRoot extends StatelessWidget { demoDataGenerator: DemoDataCache.withDefaults(), ) : TransactionHistoryRepo(api: mm2Api, client: Client()); - final profitLossRepo = ProfitLossRepository.withDefaults( transactionHistoryRepo: transactionsRepo, diff --git a/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart b/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart index 01c8a53f4d..bcfbae80b6 100644 --- a/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart +++ b/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart @@ -20,7 +20,6 @@ class BitrefillBloc extends Bloc { } final BitrefillRepository _bitrefillRepository; - StreamSubscription? _paymentIntentSubscription; Future _onBitrefillLoadRequested( BitrefillLoadRequested event, @@ -41,12 +40,7 @@ class BitrefillBloc extends Bloc { BitrefillEvent event, Emitter emit, ) { - _paymentIntentSubscription?.cancel(); - _paymentIntentSubscription = _bitrefillRepository - .watchPaymentIntent() - .listen((BitrefillPaymentIntentEvent event) { - add(BitrefillPaymentIntentReceived(event)); - }); + // previously handled payment intent watching here } void _onBitrefillPaymentIntentReceived( @@ -70,10 +64,4 @@ class BitrefillBloc extends Bloc { .toString(); emit(BitrefillPaymentSuccess(invoiceId: invoiceId)); } - - @override - Future close() { - _paymentIntentSubscription?.cancel(); - return super.close(); - } } diff --git a/lib/bloc/bitrefill/data/bitrefill_provider.dart b/lib/bloc/bitrefill/data/bitrefill_provider.dart index a7de500690..a8140002a7 100644 --- a/lib/bloc/bitrefill/data/bitrefill_provider.dart +++ b/lib/bloc/bitrefill/data/bitrefill_provider.dart @@ -1,6 +1,5 @@ -import 'package:flutter/foundation.dart'; -import 'package:universal_html/html.dart' as html; import 'package:web_dex/bloc/bitrefill/models/embedded_bitrefill_url.dart'; +import 'package:web_dex/shared/utils/window/window.dart'; class BitrefillProvider { /// A map of supported coin abbreviations to their corresponding Bitrefill @@ -58,14 +57,7 @@ class BitrefillProvider { /// Returns the URL of the Bitrefill widget page without any query parameters. String baseEmbeddedBitrefillUrl() { - if (kIsWeb) { - final String baseUrl = - '${html.window.location.origin}/assets/assets/web_pages/bitrefill_widget.html'; - return baseUrl; - } - - return kDebugMode - ? 'http://localhost:42069/assets/web_pages/bitrefill_widget.html' - : 'https://app.komodoplatform.com/assets/assets/web_pages/bitrefill_widget.html'; + return '${getOriginUrl()}/assets/assets/' + 'web_pages/bitrefill_widget.html'; } } diff --git a/lib/bloc/bitrefill/data/bitrefill_purchase_watcher.dart b/lib/bloc/bitrefill/data/bitrefill_purchase_watcher.dart deleted file mode 100644 index 21435b3645..0000000000 --- a/lib/bloc/bitrefill/data/bitrefill_purchase_watcher.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:universal_html/html.dart' as html; -import 'package:web_dex/bloc/bitrefill/models/bitrefill_payment_intent_event.dart'; - -class BitrefillPurchaseWatcher { - bool _isDisposed = false; - - /// Watches for the payment intent event from the Bitrefill checkout page - /// using a [scheduleMicrotask] to listen for events asynchronously. - /// - /// NB: This will only work if the Bitrefill page was opened from the app - - /// similar to [RampPurchaseWatcher]. JavaScript's `window.opener.postMessage` - /// is used to send the payment intent data to the app. - /// I.e. If the user copies the checkout URL and opens it in a new tab, - /// we will not receive events. - Stream watchPaymentIntent() { - _assertNotDisposed(); - - final StreamController controller = - StreamController(); - scheduleMicrotask(() async { - final Stream> stream = watchBitrefillPaymentIntent() - .takeWhile((Map element) => !controller.isClosed); - try { - await for (final Map event in stream) { - final BitrefillPaymentIntentEvent paymentIntentEvent = - BitrefillPaymentIntentEvent.fromJson(event); - controller.add(paymentIntentEvent); - } - } catch (e) { - controller.addError(e); - } finally { - _cleanup(); - } - }); - - return controller.stream; - } - - void _cleanup() { - _isDisposed = true; - // Close any other resources if necessary - } - - void _assertNotDisposed() { - if (_isDisposed) { - throw Exception('RampOrderWatcher has already been disposed'); - } - } - - /// Watches for the payment intent event from the Bitrefill checkout page. - /// - /// NB: This will only work if the Bitrefill page was opened from the app - - /// similar to [RampPurchaseWatcher]. JavaScript's `window.opener.postMessage` - /// is used to send the payment intent data to the app. - /// I.e. If the user copies the checkout URL and opens it in a new tab, - /// we will not receive events. - Stream> watchBitrefillPaymentIntent() async* { - final StreamController> paymentIntentsController = - StreamController>(); - - void handlerFunction(html.Event event) { - if (paymentIntentsController.isClosed) { - return; - } - final html.MessageEvent messageEvent = event as html.MessageEvent; - if (messageEvent.data is String) { - try { - // TODO(Francois): convert to a model here (payment intent or invoice created atm) - final Map dataJson = - jsonDecode(messageEvent.data as String) as Map; - paymentIntentsController.add(dataJson); - } catch (e) { - paymentIntentsController.addError(e); - } - } - } - - try { - html.window.addEventListener('message', handlerFunction); - - yield* paymentIntentsController.stream; - } catch (e) { - paymentIntentsController.addError(e); - } finally { - html.window.removeEventListener('message', handlerFunction); - - if (!paymentIntentsController.isClosed) { - await paymentIntentsController.close(); - } - } - } -} diff --git a/lib/bloc/bitrefill/data/bitrefill_repository.dart b/lib/bloc/bitrefill/data/bitrefill_repository.dart index 487e7f600a..3fb93fcfc6 100644 --- a/lib/bloc/bitrefill/data/bitrefill_repository.dart +++ b/lib/bloc/bitrefill/data/bitrefill_repository.dart @@ -1,18 +1,8 @@ -import 'dart:async'; - import 'package:web_dex/bloc/bitrefill/data/bitrefill_provider.dart'; -import 'package:web_dex/bloc/bitrefill/data/bitrefill_purchase_watcher.dart'; -import 'package:web_dex/bloc/bitrefill/models/bitrefill_payment_intent_event.dart'; class BitrefillRepository { - final BitrefillPurchaseWatcher _bitrefillPurchaseWatcher = - BitrefillPurchaseWatcher(); final BitrefillProvider _bitrefillProvider = BitrefillProvider(); - Stream watchPaymentIntent() { - return _bitrefillPurchaseWatcher.watchPaymentIntent(); - } - /// Returns the supported coins for Bitrefill. List get bitrefillSupportedCoins => _bitrefillProvider.supportedCoinAbbrs; diff --git a/lib/bloc/bitrefill/models/embedded_bitrefill_url.dart b/lib/bloc/bitrefill/models/embedded_bitrefill_url.dart index 3c664fed25..6daa34090b 100644 --- a/lib/bloc/bitrefill/models/embedded_bitrefill_url.dart +++ b/lib/bloc/bitrefill/models/embedded_bitrefill_url.dart @@ -34,8 +34,10 @@ class EmbeddedBitrefillUrl { /// This defaults to 'Komodo Platform'. final String companyName; - /// Whether to show payment info when opening the Bitrefill widget. - /// The default is false. + /// Whether to display the recipient address, amount, and QR code in the + /// payment widget. This can be useful for the user to verify the payment + /// details before making the payment. This is false by default, however, to + /// reduce the visual clutter during the payment process. final bool showPaymentInfo; /// The refund address to use when opening the Bitrefill widget. @@ -55,6 +57,7 @@ class EmbeddedBitrefillUrl { 'theme': theme, 'language': language, 'company_name': companyName, + 'show_payment_info': showPaymentInfo ? 'true' : 'false', }; if (paymentMethods != null) { diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart index 6a8f8ddfaf..8678e8a8df 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart @@ -188,7 +188,7 @@ class PortfolioGrowthBloc isError: true, trace: s, path: 'PortfolioGrowthBloc', - ); + ).ignore(); return ChartData.empty(); } } diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index 4bc82319ed..836de88e95 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -107,7 +107,13 @@ class ProfitLossBloc extends Bloc { ); }, onError: (e, s) { - logger.log('Failed to load portfolio profit/loss: $e', isError: true); + logger + .log( + 'Failed to load portfolio profit/loss: $e', + isError: true, + trace: s, + ) + .ignore(); return ProfitLossLoadFailure( error: TextError(error: 'Failed to load portfolio profit/loss: $e'), selectedPeriod: event.selectedPeriod, diff --git a/lib/bloc/fiat/banxa_fiat_provider.dart b/lib/bloc/fiat/banxa_fiat_provider.dart index 7533d4acba..e28cbef7a5 100644 --- a/lib/bloc/fiat/banxa_fiat_provider.dart +++ b/lib/bloc/fiat/banxa_fiat_provider.dart @@ -3,14 +3,14 @@ import 'dart:convert'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/shared/utils/utils.dart'; class BanxaFiatProvider extends BaseFiatProvider { - final String providerId = "Banxa"; - final String apiEndpoint = "/api/v1/banxa"; - BanxaFiatProvider(); + final String providerId = 'Banxa'; + final String apiEndpoint = '/api/v1/banxa'; @override String getProviderId() { @@ -26,9 +26,9 @@ class BanxaFiatProvider extends BaseFiatProvider { return _parseOrderStatus(statusString ?? ''); } - Future _getPaymentMethods( + Future _getPaymentMethods( String source, - Currency target, { + ICurrency target, { String? sourceAmount, }) => apiRequest( @@ -37,15 +37,15 @@ class BanxaFiatProvider extends BaseFiatProvider { queryParams: { 'endpoint': '/api/payment-methods', 'source': source, - 'target': target.symbol + 'target': target.symbol, }, ); - Future _getPricesWithPaymentMethod( + Future _getPricesWithPaymentMethod( String source, - Currency target, + ICurrency target, String sourceAmount, - Map paymentMethod, + FiatPaymentMethod paymentMethod, ) => apiRequest( 'GET', @@ -55,24 +55,29 @@ class BanxaFiatProvider extends BaseFiatProvider { 'source': source, 'target': target.symbol, 'source_amount': sourceAmount, - 'payment_method_id': paymentMethod['id'].toString(), + 'payment_method_id': paymentMethod.id, }, ); - Future _createOrder(Map payload) => - apiRequest('POST', apiEndpoint, - queryParams: { - 'endpoint': '/api/orders', - }, - body: payload); + Future _createOrder(Map payload) => apiRequest( + 'POST', + apiEndpoint, + queryParams: { + 'endpoint': '/api/orders', + }, + body: payload, + ); - Future _getOrder(String orderId) => - apiRequest('GET', apiEndpoint, queryParams: { - 'endpoint': '/api/orders', - 'order_id': orderId, - }); + Future _getOrder(String orderId) => apiRequest( + 'GET', + apiEndpoint, + queryParams: { + 'endpoint': '/api/orders', + 'order_id': orderId, + }, + ); - Future _getFiats() => apiRequest( + Future _getFiats() => apiRequest( 'GET', apiEndpoint, queryParams: { @@ -81,7 +86,7 @@ class BanxaFiatProvider extends BaseFiatProvider { }, ); - Future _getCoins() => apiRequest( + Future _getCoins() => apiRequest( 'GET', apiEndpoint, queryParams: { @@ -129,11 +134,12 @@ class BanxaFiatProvider extends BaseFiatProvider { // needs to be re-implemented for mobile/desktop. while (true) { final response = await _getOrder(orderId) - .catchError((e) => Future.error('Error fetching order: $e')); + .catchError((e) => Future.error('Error fetching order: $e')); - log('Fiat order status response:\n${jsonEncode(response)}'); + log('Fiat order status response:\n${jsonEncode(response)}').ignore(); - final status = _parseStatusFromResponse(response); + final status = + _parseStatusFromResponse(response as Map? ?? {}); final isCompleted = status == FiatOrderStatus.success || status == FiatOrderStatus.failed; @@ -146,41 +152,46 @@ class BanxaFiatProvider extends BaseFiatProvider { if (isCompleted) break; - await Future.delayed(const Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 5)); } } @override - Future> getFiatList() async { + Future> getFiatList() async { final response = await _getFiats(); final data = response['data']['fiats'] as List; return data - .map((item) => Currency( - item['fiat_code'] as String, - item['fiat_name'] as String, - isFiat: true, - )) + .map( + (item) => FiatCurrency( + item['fiat_code'] as String, + item['fiat_name'] as String, + ), + ) .toList(); } @override - Future> getCoinList() async { + Future> getCoinList() async { final response = await _getCoins(); final data = response['data']['coins'] as List; - List currencyList = []; + final List currencyList = []; for (final item in data) { final coinCode = item['coin_code'] as String; final coinName = item['coin_name'] as String; final blockchains = item['blockchains'] as List; for (final blockchain in blockchains) { + final coinType = getCoinType(blockchain['code'] as String); + if (coinType == null) { + continue; + } + currencyList.add( - Currency( + CryptoCurrency( coinCode, coinName, - chainType: getCoinType(blockchain['code'] as String), - isFiat: false, + coinType, ), ); } @@ -190,18 +201,23 @@ class BanxaFiatProvider extends BaseFiatProvider { } @override - Future>> getPaymentMethodsList( + Future> getPaymentMethodsList( String source, - Currency target, + ICurrency target, String sourceAmount, ) async { try { final response = await _getPaymentMethods(source, target, sourceAmount: sourceAmount); - List> paymentMethods = - List>.from(response['data']['payment_methods']); - - List>> priceFutures = []; + final List paymentMethods = (response['data'] + ['payment_methods'] as List) + .map( + (json) => + FiatPaymentMethod.fromJson(json as Map? ?? {}), + ) + .toList(); + + final List> priceFutures = []; for (final paymentMethod in paymentMethods) { final futurePrice = getPaymentMethodPrice( source, @@ -213,11 +229,13 @@ class BanxaFiatProvider extends BaseFiatProvider { } // Wait for all futures to complete - List> prices = await Future.wait(priceFutures); + final List prices = await Future.wait(priceFutures); // Combine price information with payment methods for (int i = 0; i < paymentMethods.length; i++) { - paymentMethods[i]['price_info'] = prices[i]; + paymentMethods[i] = paymentMethods[i].copyWith( + priceInfo: prices[i], + ); } return paymentMethods; @@ -227,30 +245,42 @@ class BanxaFiatProvider extends BaseFiatProvider { } @override - Future> getPaymentMethodPrice( + Future getPaymentMethodPrice( String source, - Currency target, + ICurrency target, String sourceAmount, - Map paymentMethod, + FiatPaymentMethod paymentMethod, ) async { try { final response = await _getPricesWithPaymentMethod( - source, - target, - sourceAmount, - paymentMethod, + source, + target, + sourceAmount, + paymentMethod, + ) as Map? ?? + {}; + final responseData = response['data'] as Map? ?? {}; + final prices = responseData['prices'] as List; + return FiatPriceInfo.fromJson( + prices.first as Map? ?? {}, ); - return Map.from(response['data']['prices'][0]); - } catch (e) { - return {}; + } catch (e, s) { + log( + 'Failed to get payment method price: $e', + isError: true, + trace: s, + // leaving path here until we figure out how to include stack trace + path: 'Banxa.getPaymentMethodPrice', + ).ignore(); + return const FiatPriceInfo.zero(); } } @override - Future> buyCoin( + Future buyCoin( String accountReference, String source, - Currency target, + ICurrency target, String walletAddress, String paymentMethodId, String sourceAmount, @@ -260,23 +290,23 @@ class BanxaFiatProvider extends BaseFiatProvider { 'account_reference': accountReference, 'source': source, 'target': target.symbol, - "wallet_address": walletAddress, + 'wallet_address': walletAddress, 'payment_method_id': paymentMethodId, 'source_amount': sourceAmount, 'return_url_on_success': returnUrlOnSuccess, }; - log('Fiat buy coin order payload:'); - log(jsonEncode(payload)); + await log('Fiat buy coin order payload:'); + await log(jsonEncode(payload)); final response = await _createOrder(payload); - log('Fiat buy coin order response:'); - log(jsonEncode(response)); + await log('Fiat buy coin order response:'); + await log(jsonEncode(response)); - return Map.from(response); + return FiatBuyOrderInfo.fromJson(response as Map? ?? {}); } @override - String? getCoinChainId(Currency currency) { + String? getCoinChainId(CryptoCurrency currency) { switch (currency.chainType) { case CoinType.bep20: return 'BNB'; // It's BSC usually, different for this provider diff --git a/lib/bloc/fiat/base_fiat_provider.dart b/lib/bloc/fiat/base_fiat_provider.dart index 19f0d885a0..80b29ca157 100644 --- a/lib/bloc/fiat/base_fiat_provider.dart +++ b/lib/bloc/fiat/base_fiat_provider.dart @@ -3,45 +3,11 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; import 'package:web_dex/model/coin_type.dart'; -import 'package:web_dex/model/coin_utils.dart'; +import 'package:web_dex/shared/utils/window/window.dart'; -const String domain = "https://fiat-ramps-proxy.komodo.earth"; - -class Currency { - final String symbol; - final String name; - final CoinType? chainType; - final bool isFiat; - - Currency(this.symbol, this.name, {this.chainType, required this.isFiat}); - - String getAbbr() { - if (chainType == null) return symbol; - - final t = chainType; - if (t == null || - t == CoinType.utxo || - (t == CoinType.cosmos && symbol == 'ATOM') || - (t == CoinType.cosmos && symbol == 'ATOM') || - (t == CoinType.erc20 && symbol == 'ETH') || - (t == CoinType.bep20 && symbol == 'BNB') || - (t == CoinType.avx20 && symbol == 'AVAX') || - (t == CoinType.etc && symbol == 'ETC') || - (t == CoinType.ftm20 && symbol == 'FTM') || - (t == CoinType.arb20 && symbol == 'ARB') || - (t == CoinType.hrc20 && symbol == 'ONE') || - (t == CoinType.plg20 && symbol == 'MATIC') || - (t == CoinType.mvr20 && symbol == 'MOVR')) return symbol; - - return '$symbol-${getCoinTypeName(chainType!).replaceAll('-', '')}'; - } - - /// Returns the short name of the coin including the chain type (if any). - String formatNameShort() { - return '$name${chainType != null ? ' (${getCoinTypeName(chainType!)})' : ''}'; - } -} +const String domain = 'https://fiat-ramps-proxy.komodo.earth'; abstract class BaseFiatProvider { String getProviderId(); @@ -50,27 +16,27 @@ abstract class BaseFiatProvider { Stream watchOrderStatus(String orderId); - Future> getFiatList(); + Future> getFiatList(); - Future> getCoinList(); + Future> getCoinList(); - Future>> getPaymentMethodsList( + Future> getPaymentMethodsList( String source, - Currency target, + ICurrency target, String sourceAmount, ); - Future> getPaymentMethodPrice( + Future getPaymentMethodPrice( String source, - Currency target, + ICurrency target, String sourceAmount, - Map paymentMethod, + FiatPaymentMethod paymentMethod, ); - Future> buyCoin( + Future buyCoin( String accountReference, String source, - Currency target, + ICurrency target, String walletAddress, String paymentMethodId, String sourceAmount, @@ -130,15 +96,15 @@ abstract class BaseFiatProvider { return json.decode(response.body); } else { return Future.error( - json.decode(response.body), + json.decode(response.body) as Object, ); } } catch (e) { - return Future.error("Network error: $e"); + return Future.error('Network error: $e'); } } - String? getCoinChainId(Currency currency) { + String? getCoinChainId(CryptoCurrency currency) { switch (currency.chainType) { // These exist in the current fiat provider coin lists: case CoinType.utxo: @@ -164,6 +130,7 @@ abstract class BaseFiatProvider { return 'MATIC'; case CoinType.mvr20: return 'MOVR'; + // ignore: no_default_cases default: return null; } @@ -246,34 +213,69 @@ abstract class BaseFiatProvider { CoinType? getCoinType(String chain) { switch (chain) { - case "BTC": - case "BCH": - case "DOGE": - case "LTC": + case 'BTC': + case 'BCH': + case 'DOGE': + case 'LTC': return CoinType.utxo; - case "ETH": + case 'ETH': return CoinType.erc20; - case "BSC": - case "BNB": + case 'BSC': + case 'BNB': return CoinType.bep20; - case "ATOM": + case 'ATOM': return CoinType.cosmos; - case "AVAX": + case 'AVAX': return CoinType.avx20; - case "ETC": + case 'ETC': return CoinType.etc; - case "FTM": + case 'FTM': return CoinType.ftm20; - case "ARB": + case 'ARB': return CoinType.arb20; - case "HARMONY": + case 'HARMONY': return CoinType.hrc20; - case "MATIC": + case 'MATIC': return CoinType.plg20; - case "MOVR": + case 'MOVR': return CoinType.mvr20; default: return null; } } + + /// Provides the base URL to the intermediate html page that is used to + /// bypass CORS restrictions so that console.log and postMessage events + /// can be received and handled. + static String fiatWrapperPageUrl(String providerUrl) { + final encodedUrl = base64Encode(utf8.encode(providerUrl)); + + return '${getOriginUrl()}/assets/assets/' + 'web_pages/fiat_widget.html?fiatUrl=$encodedUrl'; + } + + /// Provides the URL to the checkout handler HTML page that posts the payment + /// status received from the fiat provider to the Komodo Wallet app. The + /// `window.opener.postMessage` function is used for this purpose, and should + /// be handled by the Komodo Wallet app. + static String checkoutCallbackUrl() { + const pagePath = 'assets/assets/web_pages/checkout_status_redirect.html'; + return '${getOriginUrl()}/$pagePath'; + } + + /// Provides the URL to the checkout handler HTML page that posts the payment + /// status received from the fiat provider to the Komodo Wallet app. + static String successUrl(String accountReference) { + final baseUrl = checkoutCallbackUrl(); + + final queryString = { + 'account_reference': accountReference, + 'status': 'success', + } + .entries + .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') + .join('&'); + + return '$baseUrl?$queryString'; + } } diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart new file mode 100644 index 0000000000..ca2b53f509 --- /dev/null +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart @@ -0,0 +1,470 @@ +import 'dart:convert'; + +import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:formz/formz.dart'; +import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; +import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; +import 'package:web_dex/bloc/fiat/fiat_repository.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; +import 'package:web_dex/bloc/transformers.dart'; +import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/coins_bloc.dart'; +import 'package:web_dex/model/coin_type.dart'; +import 'package:web_dex/model/forms/fiat/currency_input.dart'; +import 'package:web_dex/model/forms/fiat/fiat_amount_input.dart'; +import 'package:web_dex/shared/utils/extensions/string_extensions.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +part 'fiat_form_event.dart'; +part 'fiat_form_state.dart'; + +class FiatFormBloc extends Bloc { + FiatFormBloc({ + FiatRepository? repository, + // TODO: update to respository reference once refactored + CoinsBloc? coinsRepository, + }) : _fiatRepository = repository ?? fiatRepository, + _coinsRepository = coinsRepository ?? coinsBloc, + super(const FiatFormState.initial()) { + // all user input fields are debounced using the debounce stream transformer + on( + _onChangeSelectedFiatCoin, + transformer: debounce(500), + ); + on(_onChangeSelectedCoin, transformer: debounce(500)); + on(_onUpdateFiatAmount, transformer: debounce(500)); + on(_onSelectPaymentMethod); + on(_onSubmitForm); + on(_onPaymentStatusMessage); + on(_onFiatModeChanged); + on(_onAccountInformationChanged); + on(_onClearAccountInformation); + // debounce used here instead of restartable, since multiple user actions + // can trigger this event, and restartable resulted in hitching + on(_onRefreshForm, transformer: debounce(500)); + on( + _onLoadCurrencyLists, + transformer: restartable(), + ); + on( + _onWatchOrderStatus, + transformer: restartable(), + ); + } + + final FiatRepository _fiatRepository; + final CoinsBloc _coinsRepository; + + Future _onChangeSelectedFiatCoin( + SelectedFiatCurrencyChanged event, + Emitter emit, + ) async { + emit( + state.copyWith( + selectedFiat: CurrencyInput.dirty(event.selectedFiat), + ), + ); + } + + Future _onChangeSelectedCoin( + SelectedCoinChanged event, + Emitter emit, + ) async { + emit( + state.copyWith( + selectedCoin: CurrencyInput.dirty(event.selectedCoin), + ), + ); + } + + Future _onUpdateFiatAmount( + FiatAmountChanged event, + Emitter emit, + ) async { + emit( + state.copyWith( + fiatAmount: _getAmountInputWithBounds(event.fiatAmount), + ), + ); + } + + FiatAmountInput _getAmountInputWithBounds( + String amount, { + FiatPaymentMethod? selectedPaymentMethod, + }) { + double? minAmount; + double? maxAmount; + final paymentMethod = selectedPaymentMethod ?? state.selectedPaymentMethod; + final firstLimit = paymentMethod.transactionLimits.firstOrNull; + if (firstLimit != null) { + minAmount = firstLimit.min; + maxAmount = firstLimit.max; + } + + return FiatAmountInput.dirty( + amount, + minValue: minAmount, + maxValue: maxAmount, + ); + } + + void _onSelectPaymentMethod( + PaymentMethodSelected event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedPaymentMethod: event.paymentMethod, + fiatAmount: _getAmountInputWithBounds( + state.fiatAmount.value, + selectedPaymentMethod: event.paymentMethod, + ), + fiatOrderStatus: FiatOrderStatus.pending, + status: FiatFormStatus.initial, + ), + ); + } + + Future _onSubmitForm( + FormSubmissionRequested event, + Emitter emit, + ) async { + final formValidationError = getFormIssue(); + if (formValidationError != null || !state.isValid) { + log('Form validation failed. Validation: ${state.isValid}').ignore(); + return; + } + + if (state.checkoutUrl.isNotEmpty) { + emit(state.copyWith(checkoutUrl: '')); + } + + try { + final newOrder = await _fiatRepository.buyCoin( + state.accountReference, + state.selectedFiat.value!.symbol, + state.selectedCoin.value!, + state.coinReceiveAddress, + state.selectedPaymentMethod, + state.fiatAmount.value, + BaseFiatProvider.successUrl(state.accountReference), + ); + + if (!newOrder.error.isNone) { + return emit(_parseOrderError(newOrder.error)); + } + + final checkoutUrl = newOrder.checkoutUrl as String? ?? ''; + if (checkoutUrl.isEmpty) { + log('Invalid checkout URL received.').ignore(); + return emit( + state.copyWith( + fiatOrderStatus: FiatOrderStatus.failed, + ), + ); + } + + emit( + state.copyWith( + checkoutUrl: checkoutUrl, + orderId: newOrder.id, + status: FiatFormStatus.success, + fiatOrderStatus: FiatOrderStatus.submitted, + ), + ); + } catch (e, s) { + log( + 'Error loading currency list: $e', + path: 'FiatFormBloc._onSubmitForm', + trace: s, + isError: true, + ).ignore(); + emit( + state.copyWith( + status: FiatFormStatus.failure, + checkoutUrl: '', + ), + ); + } + } + + Future _onRefreshForm( + RefreshFormRequested event, + Emitter emit, + ) async { + // If the entered fiat amount is empty or invalid, then return a placeholder + // list of payment methods + String sourceAmount = '10000'; + if (state.fiatAmount.valueAsDouble == null || + state.fiatAmount.valueAsDouble == 0) { + emit(_defaultPaymentMethods()); + } else { + emit(state.copyWith(status: FiatFormStatus.loading)); + sourceAmount = state.fiatAmount.value; + } + + emit( + state.copyWith( + fiatAmount: _getAmountInputWithBounds(state.fiatAmount.value), + ), + ); + + // Prefetch required form data based on updated state information + await _fetchAccountInfo(emit); + try { + final methods = _fiatRepository.getPaymentMethodsList( + state.selectedFiat.value!.symbol, + state.selectedCoin.value!, + sourceAmount, + ); + // await here in case of unhandled errors, but `onError` should handle + // all exceptions/errors in the stream + return await emit.forEach( + methods, + onData: (data) => _updatePaymentMethods( + data, + forceUpdate: event.forceRefresh, + ), + onError: (e, s) { + log( + 'Error fetching and updating payment methods: $e', + path: 'FiatFormBloc._onRefreshForm', + trace: s, + isError: true, + ).ignore(); + return state.copyWith(paymentMethods: []); + }, + ); + } catch (error, stacktrace) { + log( + 'Error loading currency list: $error', + path: 'FiatFormBloc._onRefreshForm', + trace: stacktrace, + isError: true, + ).ignore(); + emit( + state.copyWith( + paymentMethods: [], + status: FiatFormStatus.failure, + ), + ); + } + } + + FiatFormState _updatePaymentMethods( + List methods, { + bool forceUpdate = false, + }) { + try { + final shouldUpdate = forceUpdate || state.selectedPaymentMethod.isNone; + if (shouldUpdate && methods.isNotEmpty) { + final method = state.selectedPaymentMethod.isNone + ? methods.first + : methods.firstWhere( + (method) => method.id == state.selectedPaymentMethod.id, + orElse: () => methods.first, + ); + + return state.copyWith( + paymentMethods: methods, + selectedPaymentMethod: method, + status: FiatFormStatus.success, + fiatAmount: _getAmountInputWithBounds( + state.fiatAmount.value, + selectedPaymentMethod: method, + ), + ); + } + + return state.copyWith( + status: FiatFormStatus.success, + ); + } catch (e, s) { + log( + 'Error loading currency list: $e', + path: 'FiatFormBloc._onRefreshForm', + trace: s, + isError: true, + ).ignore(); + return state.copyWith(paymentMethods: []); + } + } + + Future _onAccountInformationChanged( + AccountInformationChanged event, + Emitter emit, + ) async { + final accountRerference = await _coinsRepository.getCoinAddress('KMD'); + final address = await _coinsRepository + .getCoinAddress(state.selectedCoin.value!.getAbbr()); + + emit( + state.copyWith( + accountReference: accountRerference, + coinReceiveAddress: address, + ), + ); + } + + void _onClearAccountInformation( + ClearAccountInformationRequested event, + Emitter emit, + ) { + emit( + state.copyWith( + accountReference: '', + coinReceiveAddress: '', + ), + ); + } + + Future _fetchAccountInfo(Emitter emit) async { + final address = + await _coinsRepository.getCoinAddress(state.selectedCoin.value!.symbol); + emit( + state.copyWith(accountReference: address, coinReceiveAddress: address), + ); + } + + void _onPaymentStatusMessage( + FiatOnRampPaymentStatusMessageReceived event, + Emitter emit, + ) { + if (!event.message.isJson()) { + log('Invalid json console message received'); + return; + } + + try { + // Escaped strings are decoded to unescaped strings instead of json + // objects :( + String message = event.message; + if (jsonDecode(event.message) is String) { + message = jsonDecode(message) as String; + } + final data = jsonDecode(message) as Map; + if (_isRampNewPurchaseMessage(data)) { + emit(state.copyWith(fiatOrderStatus: FiatOrderStatus.success)); + } else if (_isCheckoutStatusMessage(data)) { + final status = data['status'] as String? ?? 'declined'; + emit( + state.copyWith( + fiatOrderStatus: FiatOrderStatus.fromString(status), + ), + ); + } + } catch (e, s) { + log( + 'Error parsing payment status message: $e', + path: 'FiatFormBloc._onPaymentStatusMessage', + trace: s, + isError: true, + ).ignore(); + } + } + + void _onFiatModeChanged(FiatModeChanged event, Emitter emit) { + emit(state.copyWith(fiatMode: event.mode)); + } + + Future _onLoadCurrencyLists( + LoadCurrencyListsRequested event, + Emitter emit, + ) async { + try { + final fiatList = await fiatRepository.getFiatList(); + final coinList = await fiatRepository.getCoinList(); + emit(state.copyWith(fiatList: fiatList, coinList: coinList)); + } catch (e, s) { + log( + 'Error loading currency list: $e', + path: 'FiatFormBloc._onLoadCurrencyLists', + trace: s, + isError: true, + ).ignore(); + } + } + + Future _onWatchOrderStatus( + WatchOrderStatusRequested event, + Emitter emit, + ) async { + // banxa implementation monitors status using their API, so watch the order + // status via the existing repository methods + if (state.selectedPaymentMethod.providerId != 'Banxa') { + return; + } + + final orderStatusStream = _fiatRepository.watchOrderStatus( + state.selectedPaymentMethod, + state.orderId, + ); + + return emit.forEach( + orderStatusStream, + onData: (data) { + return state.copyWith(fiatOrderStatus: data); + }, + onError: (error, stackTrace) { + log( + 'Error watching order status: $error', + path: 'FiatFormBloc._onWatchOrderStatus', + trace: stackTrace, + isError: true, + ).ignore(); + return state.copyWith(fiatOrderStatus: FiatOrderStatus.failed); + }, + ); + } + + bool _isRampNewPurchaseMessage(Map data) { + return data.containsKey('type') && data['type'] == 'PURCHASE_CREATED'; + } + + bool _isCheckoutStatusMessage(Map data) { + return data.containsKey('type') && (data['type'] == 'PAYMENT-STATUS'); + } + + FiatFormState _parseOrderError(FiatBuyOrderError error) { + // TODO? banxa can return an error indicating that a higher fiat amount is + // required, which could be indicated to the user. The only issue is that + // it is text-based and does not match the value returned in their payment + // method list + return state.copyWith( + checkoutUrl: '', + status: FiatFormStatus.failure, + fiatOrderStatus: FiatOrderStatus.failed, + ); + } + + String? getFormIssue() { + // TODO: ? show on the UI and localise? These are currently used as more of + // a boolean "is there an error?" rather than "what is the error?" + if (!_coinsRepository.isLoggedIn) { + return 'Please connect your wallet to purchase coins'; + } + if (state.paymentMethods.isEmpty) { + return 'No payment method for this pair'; + } + if (state.coinReceiveAddress.isEmpty) { + return 'No wallet, or coin/network might not be supported'; + } + if (state.accountReference.isEmpty) { + return 'Account reference (KMD Address) could not be fetched'; + } + + return null; + } + + FiatFormState _defaultPaymentMethods() { + return state.copyWith( + paymentMethods: defaultFiatPaymentMethods, + selectedPaymentMethod: defaultFiatPaymentMethods.first, + status: FiatFormStatus.initial, + fiatOrderStatus: FiatOrderStatus.pending, + ); + } +} diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_event.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_event.dart new file mode 100644 index 0000000000..b1d9f5fbd7 --- /dev/null +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_event.dart @@ -0,0 +1,98 @@ +part of 'fiat_form_bloc.dart'; + +sealed class FiatFormEvent extends Equatable { + const FiatFormEvent(); + + @override + List get props => []; +} + +final class FiatOnRampPaymentStatusMessageReceived extends FiatFormEvent { + const FiatOnRampPaymentStatusMessageReceived(this.message); + + final String message; + + @override + List get props => [message]; +} + +final class SelectedFiatCurrencyChanged extends FiatFormEvent { + const SelectedFiatCurrencyChanged(this.selectedFiat); + + final ICurrency selectedFiat; + + @override + List get props => [selectedFiat]; +} + +final class SelectedCoinChanged extends FiatFormEvent { + const SelectedCoinChanged(this.selectedCoin); + + final ICurrency selectedCoin; + + @override + List get props => [selectedCoin]; +} + +final class FiatAmountChanged extends FiatFormEvent { + const FiatAmountChanged(this.fiatAmount); + + final String fiatAmount; + + @override + List get props => [fiatAmount]; +} + +final class PaymentMethodSelected extends FiatFormEvent { + const PaymentMethodSelected(this.paymentMethod); + + final FiatPaymentMethod paymentMethod; + + @override + List get props => [paymentMethod]; +} + +final class FormSubmissionRequested extends FiatFormEvent {} + +final class FiatModeChanged extends FiatFormEvent { + const FiatModeChanged(this.mode); + + FiatModeChanged.fromTabIndex(int tabIndex) + : mode = FiatMode.fromTabIndex(tabIndex); + + final FiatMode mode; + + @override + List get props => [mode]; +} + +final class PaymentStatusClearRequested extends FiatFormEvent { + const PaymentStatusClearRequested(); +} + +final class AccountInformationChanged extends FiatFormEvent { + const AccountInformationChanged(); +} + +final class ClearAccountInformationRequested extends FiatFormEvent { + const ClearAccountInformationRequested(); +} + +final class RefreshFormRequested extends FiatFormEvent { + const RefreshFormRequested({ + this.forceRefresh = false, + }); + + final bool forceRefresh; + + @override + List get props => [forceRefresh]; +} + +final class LoadCurrencyListsRequested extends FiatFormEvent { + const LoadCurrencyListsRequested(); +} + +final class WatchOrderStatusRequested extends FiatFormEvent { + const WatchOrderStatusRequested(); +} diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart new file mode 100644 index 0000000000..f34e3aa544 --- /dev/null +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart @@ -0,0 +1,150 @@ +part of 'fiat_form_bloc.dart'; + +enum FiatFormStatus { initial, loading, success, failure } + +final class FiatFormState extends Equatable with FormzMixin { + const FiatFormState({ + required this.selectedFiat, + required this.selectedCoin, + required this.fiatAmount, + required this.paymentMethods, + required this.selectedPaymentMethod, + required this.accountReference, + required this.coinReceiveAddress, + required this.checkoutUrl, + required this.orderId, + required this.fiatList, + required this.coinList, + this.status = FiatFormStatus.initial, + this.fiatOrderStatus = FiatOrderStatus.pending, + this.fiatMode = FiatMode.onramp, + }); + + const FiatFormState.initial() + : selectedFiat = const CurrencyInput.dirty( + FiatCurrency('USD', 'United States Dollar'), + ), + selectedCoin = const CurrencyInput.dirty( + CryptoCurrency('BTC', 'Bitcoin', CoinType.utxo), + ), + fiatAmount = const FiatAmountInput.pure(), + selectedPaymentMethod = const FiatPaymentMethod.none(), + accountReference = '', + coinReceiveAddress = '', + checkoutUrl = '', + orderId = '', + status = FiatFormStatus.initial, + paymentMethods = const [], + fiatList = const [], + coinList = const [], + fiatOrderStatus = FiatOrderStatus.pending, + fiatMode = FiatMode.onramp; + + /// The selected fiat currency to use to purchase [selectedCoin]. + final CurrencyInput selectedFiat; + /// The selected crypto currency to purchase. + final CurrencyInput selectedCoin; + /// The amount of [selectedFiat] to use to purchase [selectedCoin]. + final FiatAmountInput fiatAmount; + /// The selected payment method to use to purchase [selectedCoin]. + final FiatPaymentMethod selectedPaymentMethod; + /// The account reference to use to purchase [selectedCoin]. + final String accountReference; + /// The crypto receive address to use to purchase [selectedCoin]. + final String coinReceiveAddress; + /// The callback url to return to once checkout is completed. + final String checkoutUrl; + /// The order id for the fiat purchase (Only supported by Banxa). + final String orderId; + /// The current status of the form (loading, success, failure). + final FiatFormStatus status; + /// The list of payment methods available for the [selectedFiat], + /// [selectedCoin], and [fiatAmount]. + final Iterable paymentMethods; + /// The list of fiat currencies that can be used to purchase [selectedCoin]. + final Iterable fiatList; + /// The list of crypto currencies that can be purchased. + final Iterable coinList; + /// The current status of the fiat order. + final FiatOrderStatus fiatOrderStatus; + /// The current mode of the fiat form (onramp, offramp). This is currently + /// used to determine the tab to show. The implementation will likely change + /// once the order history tab is implemented + final FiatMode fiatMode; + + /// Gets the transaction limit from the selected payment method + FiatTransactionLimit? get transactionLimit => + selectedPaymentMethod.transactionLimits.firstOrNull; + /// The minimum fiat amount that is allowed for the selected payment method + double? get minFiatAmount => transactionLimit?.min; + /// The maximum fiat amount that is allowed for the selected payment method + double? get maxFiatAmount => transactionLimit?.max; + bool get isLoadingCurrencies => fiatList.length < 2 || coinList.length < 2; + bool get isLoading => isLoadingCurrencies || status == FiatFormStatus.loading; + bool get canSubmit => + !isLoading && + accountReference.isNotEmpty && + status != FiatFormStatus.failure && + !fiatOrderStatus.isSubmitting && + isValid; + + FiatFormState copyWith({ + CurrencyInput? selectedFiat, + CurrencyInput? selectedCoin, + FiatAmountInput? fiatAmount, + FiatPaymentMethod? selectedPaymentMethod, + String? accountReference, + String? coinReceiveAddress, + String? checkoutUrl, + String? orderId, + FiatFormStatus? status, + Iterable? paymentMethods, + Iterable? fiatList, + Iterable? coinList, + FiatOrderStatus? fiatOrderStatus, + FiatMode? fiatMode, + }) { + return FiatFormState( + selectedFiat: selectedFiat ?? this.selectedFiat, + selectedCoin: selectedCoin ?? this.selectedCoin, + selectedPaymentMethod: + selectedPaymentMethod ?? this.selectedPaymentMethod, + accountReference: accountReference ?? this.accountReference, + coinReceiveAddress: coinReceiveAddress ?? this.coinReceiveAddress, + checkoutUrl: checkoutUrl ?? this.checkoutUrl, + orderId: orderId ?? this.orderId, + fiatAmount: fiatAmount ?? this.fiatAmount, + status: status ?? this.status, + paymentMethods: paymentMethods ?? this.paymentMethods, + fiatList: fiatList ?? this.fiatList, + coinList: coinList ?? this.coinList, + fiatOrderStatus: fiatOrderStatus ?? this.fiatOrderStatus, + fiatMode: fiatMode ?? this.fiatMode, + ); + } + + @override + List> get inputs => [ + selectedFiat, + selectedCoin, + fiatAmount, + ]; + + @override + List get props => [ + selectedFiat, + selectedCoin, + selectedPaymentMethod, + accountReference, + coinReceiveAddress, + checkoutUrl, + orderId, + fiatAmount, + status, + paymentMethods, + fiatList, + coinList, + fiatOrderStatus, + fiatMode, + ]; +} diff --git a/lib/bloc/fiat/fiat_order_status.dart b/lib/bloc/fiat/fiat_order_status.dart index 6980e8f712..0432e9c097 100644 --- a/lib/bloc/fiat/fiat_order_status.dart +++ b/lib/bloc/fiat/fiat_order_status.dart @@ -3,6 +3,9 @@ enum FiatOrderStatus { /// User has not yet started the payment process pending, + /// User has started the process, and a payment request has been submitted + submitted, + /// Payment has been submitted and is being processed inProgress, @@ -14,4 +17,38 @@ enum FiatOrderStatus { bool get isTerminal => this == FiatOrderStatus.success || this == FiatOrderStatus.failed; + bool get isSubmitting => + this == FiatOrderStatus.inProgress || this == FiatOrderStatus.submitted; + bool get isFailed => this == FiatOrderStatus.failed; + bool get isSuccess => this == FiatOrderStatus.success; + + /// Parses the fiat order status form string + /// Throws [Exception] if the string is not a valid status + static FiatOrderStatus fromString(String status) { + // The case statements are references to Banxa's order statuses. See the + // docs link here for more info: https://docs.banxa.com/docs/order-status + switch (status) { + case 'complete': + return FiatOrderStatus.success; + + case 'cancelled': + case 'declined': + case 'expired': + case 'refunded': + return FiatOrderStatus.failed; + + case 'extraVerification': + case 'pendingPayment': + case 'waitingPayment': + return FiatOrderStatus.pending; + + case 'paymentReceived': + case 'inProgress': + case 'coinTransferred': + return FiatOrderStatus.inProgress; + + default: + throw Exception('Unknown status: $status'); + } + } } diff --git a/lib/bloc/fiat/fiat_repository.dart b/lib/bloc/fiat/fiat_repository.dart index 0c65fb8971..63546d3c19 100644 --- a/lib/bloc/fiat/fiat_repository.dart +++ b/lib/bloc/fiat/fiat_repository.dart @@ -2,6 +2,7 @@ import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/fiat/banxa_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; import 'package:web_dex/bloc/fiat/ramp/ramp_fiat_provider.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -9,17 +10,17 @@ final fiatRepository = FiatRepository([BanxaFiatProvider(), RampFiatProvider()]); class FiatRepository { - final List fiatProviders; FiatRepository(this.fiatProviders); + final List fiatProviders; String? _paymentMethodFiat; - Currency? _paymentMethodsCoin; - List>? _paymentMethodsList; + ICurrency? _paymentMethodsCoin; + List? _paymentMethodsList; BaseFiatProvider? _getPaymentMethodProvider( - Map paymentMethod, + FiatPaymentMethod paymentMethod, ) { - return _getProvider(paymentMethod['provider_id'].toString()); + return _getProvider(paymentMethod.providerId); } BaseFiatProvider? _getProvider( @@ -34,7 +35,7 @@ class FiatRepository { } Stream watchOrderStatus( - Map paymentMethod, + FiatPaymentMethod paymentMethod, String orderId, ) async* { final provider = _getPaymentMethodProvider(paymentMethod); @@ -43,13 +44,14 @@ class FiatRepository { yield* provider!.watchOrderStatus(orderId); } - Future> _getListFromProviders( - Future> Function(BaseFiatProvider) getList, - bool isCoin) async { + Future> _getListFromProviders( + Future> Function(BaseFiatProvider) getList, + bool isCoin, + ) async { final futures = fiatProviders.map(getList); final results = await Future.wait(futures); - final currencyMap = {}; + final currencyMap = {}; Set? knownCoinAbbreviations; @@ -62,7 +64,7 @@ class FiatRepository { for (final currency in currencyList) { // Skip unsupported chains and coins if (isCoin && - (currency.chainType == null || + (currency.isFiat || !knownCoinAbbreviations!.contains(currency.getAbbr()))) { continue; } @@ -76,9 +78,11 @@ class FiatRepository { ..sort((a, b) => a.symbol.compareTo(b.symbol)); } - Future> getFiatList() async { + Future> getFiatList() async { return (await _getListFromProviders( - (provider) => provider.getFiatList(), false)) + (provider) => provider.getFiatList(), + false, + )) ..sort((a, b) => currencySorter(a.getAbbr(), b.getAbbr())); } @@ -98,12 +102,15 @@ class FiatRepository { } } - Future> getCoinList() async { + Future> getCoinList() async { return _getListFromProviders((provider) => provider.getCoinList(), true); } - String? _calculateCoinAmount(String fiatAmount, String spotPriceIncludingFee, - {int decimalPoints = 8}) { + String? _calculateCoinAmount( + String fiatAmount, + String spotPriceIncludingFee, { + int decimalPoints = 8, + }) { if (fiatAmount.isEmpty || spotPriceIncludingFee.isEmpty) { return null; } @@ -120,13 +127,11 @@ class FiatRepository { } } - String _calculateSpotPriceIncludingFee(Map paymentMethod) { + String _calculateSpotPriceIncludingFee(FiatPaymentMethod paymentMethod) { // Use the previous coin and fiat amounts to estimate the spot price // including fee. - final coinAmount = - double.parse(paymentMethod['price_info']['coin_amount'] as String); - final fiatAmount = - double.parse(paymentMethod['price_info']['fiat_amount'] as String); + final coinAmount = paymentMethod.priceInfo.coinAmount; + final fiatAmount = paymentMethod.priceInfo.fiatAmount; final spotPriceIncludingFee = fiatAmount / coinAmount; return spotPriceIncludingFee.toString(); } @@ -139,10 +144,10 @@ class FiatRepository { return amount.substring(decimalPointIndex + 1).length; } - List>? _getPaymentListEstimate( - List> paymentMethodsList, + List? _getPaymentListEstimate( + List paymentMethodsList, String sourceAmount, - Currency target, + ICurrency target, String source, ) { if (target != _paymentMethodsCoin || source != _paymentMethodFiat) { @@ -156,8 +161,8 @@ class FiatRepository { return paymentMethodsList.map((method) { String? spotPriceIncludingFee; spotPriceIncludingFee = _calculateSpotPriceIncludingFee(method); - int decimalAmount = - _getDecimalPoints(method['price_info']['coin_amount']) ?? 8; + final int decimalAmount = + _getDecimalPoints(method.priceInfo.coinAmount.toString()) ?? 8; final coinAmount = _calculateCoinAmount( sourceAmount, @@ -165,25 +170,27 @@ class FiatRepository { decimalPoints: decimalAmount, ); - return { - ...method, - "price_info": { - ...method['price_info'], - "coin_amount": coinAmount, - "fiat_amount": sourceAmount, - }.map((key, value) => MapEntry(key as String, value)), - }; + return method.copyWith( + priceInfo: method.priceInfo.copyWith( + coinAmount: double.tryParse(coinAmount ?? '0') ?? 0, + fiatAmount: double.tryParse(sourceAmount) ?? 0, + ), + ); }).toList(); - } catch (e) { - log('Fiat payment list estimation failed', - isError: true, trace: StackTrace.current, path: 'fiat_repository'); + } catch (e, s) { + log( + 'Fiat payment list estimation failed', + isError: true, + trace: s, + path: 'fiat_repository', + ); return null; } } - Stream>> getPaymentMethodsList( + Stream> getPaymentMethodsList( String source, - Currency target, + ICurrency target, String sourceAmount, ) async* { if (_paymentMethodsList != null) { @@ -191,7 +198,11 @@ class FiatRepository { // This is to display temporary values while the new list is being fetched // This is not a perfect solution _paymentMethodsList = _getPaymentListEstimate( - _paymentMethodsList!, sourceAmount, target, source); + _paymentMethodsList!, + sourceAmount, + target, + source, + ); if (_paymentMethodsList != null) { _paymentMethodsCoin = target; _paymentMethodFiat = source; @@ -203,11 +214,12 @@ class FiatRepository { final paymentMethods = await provider.getPaymentMethodsList(source, target, sourceAmount); return paymentMethods - .map((method) => { - ...method, - 'provider_id': provider.getProviderId(), - 'provider_icon_asset_path': provider.providerIconPath, - }) + .map( + (method) => method.copyWith( + providerId: provider.getProviderId(), + providerIconAssetPath: provider.providerIconPath, + ), + ) .toList(); }); @@ -220,16 +232,16 @@ class FiatRepository { yield _paymentMethodsList!; } - Future> getPaymentMethodPrice( + Future getPaymentMethodPrice( String source, - Currency target, + ICurrency target, String sourceAmount, - Map buyPaymentMethod, + FiatPaymentMethod buyPaymentMethod, ) async { final provider = _getPaymentMethodProvider(buyPaymentMethod); - if (provider == null) return Future.error("Provider not found"); + if (provider == null) return Future.error('Provider not found'); - return await provider.getPaymentMethodPrice( + return provider.getPaymentMethodPrice( source, target, sourceAmount, @@ -237,67 +249,69 @@ class FiatRepository { ); } - Future> buyCoin( + Future buyCoin( String accountReference, String source, - Currency target, + ICurrency target, String walletAddress, - Map paymentMethod, + FiatPaymentMethod paymentMethod, String sourceAmount, String returnUrlOnSuccess, ) async { final provider = _getPaymentMethodProvider(paymentMethod); - if (provider == null) return Future.error("Provider not found"); + if (provider == null) return Future.error('Provider not found'); - return await provider.buyCoin( + return provider.buyCoin( accountReference, source, target, walletAddress, - paymentMethod['id'].toString(), + paymentMethod.id, sourceAmount, returnUrlOnSuccess, ); } - List> _addRelativePercentField( - List> paymentMethodsList) { + List _addRelativePercentField( + List paymentMethodsList, + ) { + if (paymentMethodsList.isEmpty) { + return paymentMethodsList; + } + // Add a relative percent value to each payment method // based on the payment method with the highest `coin_amount` try { final coinAmounts = _paymentMethodsList! - .map((method) => double.parse(method['price_info']['coin_amount'])) + .map((method) => method.priceInfo.coinAmount) .toList(); final maxCoinAmount = coinAmounts.reduce((a, b) => a > b ? a : b); return _paymentMethodsList!.map((method) { - final coinAmount = double.parse(method['price_info']['coin_amount']); + final coinAmount = method.priceInfo.coinAmount; if (coinAmount == 0) { return method; } if (coinAmount == maxCoinAmount) { - return { - ...method, - 'relative_percent': null, - }; + return method.copyWith(relativePercent: 0); } final relativeValue = - (coinAmount - maxCoinAmount) / (maxCoinAmount).abs(); + (coinAmount - maxCoinAmount) / maxCoinAmount.abs(); - return { - ...method, - 'relative_percent': relativeValue, //0 to -1 - }; + return method.copyWith(relativePercent: relativeValue); }).toList() ..sort((a, b) { - if (a['relative_percent'] == null) return -1; - if (b['relative_percent'] == null) return 1; - return (b['relative_percent'] as double) - .compareTo(a['relative_percent'] as double); + if (a.relativePercent == 0) return -1; + if (b.relativePercent == 0) return 1; + return b.relativePercent.compareTo(a.relativePercent); }); - } catch (e) { - log('Failed to add relative percent field to payment methods list', - isError: true, trace: StackTrace.current, path: 'fiat_repository'); + } catch (e, s) { + log( + 'Failed to add relative percent field to payment methods list', + isError: true, + trace: s, + path: 'fiat_repository', + ); return paymentMethodsList; } } diff --git a/lib/bloc/fiat/models/fiat_buy_order_error.dart b/lib/bloc/fiat/models/fiat_buy_order_error.dart new file mode 100644 index 0000000000..d4d436e8d4 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_buy_order_error.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class FiatBuyOrderError extends Equatable { + const FiatBuyOrderError({ + required this.code, + required this.status, + required this.title, + }); + + factory FiatBuyOrderError.fromJson(Map json) { + return FiatBuyOrderError( + code: assertInt(json['code']) ?? 0, + status: assertInt(json['status']) ?? 0, + title: json['title'] as String? ?? '', + ); + } + + const FiatBuyOrderError.none() : this(code: 0, status: 0, title: ''); + + bool get isNone => this == const FiatBuyOrderError.none(); + + final int code; + final int status; + final String title; + + Map toJson() { + return { + 'code': code, + 'status': status, + 'title': title, + }; + } + + @override + List get props => [code, status, title]; +} diff --git a/lib/bloc/fiat/models/fiat_buy_order_info.dart b/lib/bloc/fiat/models/fiat_buy_order_info.dart new file mode 100644 index 0000000000..a4bdb67482 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_buy_order_info.dart @@ -0,0 +1,174 @@ +import 'package:equatable/equatable.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_buy_order_error.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class FiatBuyOrderInfo extends Equatable { + const FiatBuyOrderInfo({ + required this.id, + required this.accountId, + required this.accountReference, + required this.orderType, + required this.fiatCode, + required this.fiatAmount, + required this.coinCode, + required this.walletAddress, + required this.extAccountId, + required this.network, + required this.paymentCode, + required this.checkoutUrl, + required this.createdAt, + required this.error, + }); + + const FiatBuyOrderInfo.none() + : this( + id: '', + accountId: '', + accountReference: '', + orderType: '', + fiatCode: '', + fiatAmount: 0.0, + coinCode: '', + walletAddress: '', + extAccountId: '', + network: '', + paymentCode: '', + checkoutUrl: '', + createdAt: '', + error: const FiatBuyOrderError.none(), + ); + + const FiatBuyOrderInfo.fromCheckoutUrl(String url) + : this( + id: '', + accountId: '', + accountReference: '', + orderType: '', + fiatCode: '', + fiatAmount: 0.0, + coinCode: '', + walletAddress: '', + extAccountId: '', + network: '', + paymentCode: '', + checkoutUrl: url, + createdAt: '', + error: const FiatBuyOrderError.none(), + ); + + factory FiatBuyOrderInfo.fromJson(Map json) { + Map data = json; + if (json['data'] != null) { + final orderData = json['data'] as Map? ?? {}; + data = orderData['order'] as Map? ?? {}; + } + + return FiatBuyOrderInfo( + id: data['id'] as String? ?? '', + accountId: data['account_id'] as String? ?? '', + accountReference: data['account_reference'] as String? ?? '', + orderType: data['order_type'] as String? ?? '', + fiatCode: data['fiat_code'] as String? ?? '', + fiatAmount: assertDouble(data['fiat_amount']), + coinCode: data['coin_code'] as String? ?? '', + walletAddress: data['wallet_address'] as String? ?? '', + extAccountId: data['ext_account_id'] as String? ?? '', + network: data['network'] as String? ?? '', + paymentCode: data['payment_code'] as String? ?? '', + checkoutUrl: data['checkout_url'] as String? ?? '', + createdAt: assertString(data['created_at']) ?? '', + error: data['errors'] != null + ? FiatBuyOrderError.fromJson(data['errors'] as Map) + : const FiatBuyOrderError.none(), + ); + } + final String id; + final String accountId; + final String accountReference; + final String orderType; + final String fiatCode; + final double fiatAmount; + final String coinCode; + final String walletAddress; + final String extAccountId; + final String network; + final String paymentCode; + final String checkoutUrl; + final String createdAt; + final FiatBuyOrderError error; + + @override + List get props => [ + id, + accountId, + accountReference, + orderType, + fiatCode, + fiatAmount, + coinCode, + walletAddress, + extAccountId, + network, + paymentCode, + checkoutUrl, + createdAt, + error, + ]; + + Map toJson() { + return { + 'data': { + 'order': { + 'id': id, + 'account_id': accountId, + 'account_reference': accountReference, + 'order_type': orderType, + 'fiat_code': fiatCode, + 'fiat_amount': fiatAmount, + 'coin_code': coinCode, + 'wallet_address': walletAddress, + 'ext_account_id': extAccountId, + 'network': network, + 'payment_code': paymentCode, + 'checkout_url': checkoutUrl, + 'created_at': createdAt, + 'errors': error.toJson(), + }, + }, + }; + } + + FiatBuyOrderInfo copyWith({ + String? id, + String? accountId, + String? accountReference, + String? orderType, + String? fiatCode, + double? fiatAmount, + String? coinCode, + String? walletAddress, + String? extAccountId, + String? network, + String? paymentCode, + String? checkoutUrl, + String? createdAt, + FiatBuyOrderError? error, + }) { + return FiatBuyOrderInfo( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + accountReference: accountReference ?? this.accountReference, + orderType: orderType ?? this.orderType, + fiatCode: fiatCode ?? this.fiatCode, + fiatAmount: fiatAmount ?? this.fiatAmount, + coinCode: coinCode ?? this.coinCode, + walletAddress: walletAddress ?? this.walletAddress, + extAccountId: extAccountId ?? this.extAccountId, + network: network ?? this.network, + paymentCode: paymentCode ?? this.paymentCode, + checkoutUrl: checkoutUrl ?? this.checkoutUrl, + createdAt: createdAt ?? this.createdAt, + error: error ?? this.error, + ); + } +} diff --git a/lib/bloc/fiat/models/fiat_mode.dart b/lib/bloc/fiat/models/fiat_mode.dart new file mode 100644 index 0000000000..8634e76b4b --- /dev/null +++ b/lib/bloc/fiat/models/fiat_mode.dart @@ -0,0 +1,25 @@ +enum FiatMode { + onramp, + offramp; + + const FiatMode(); + + factory FiatMode.fromTabIndex(int tabIndex) { + if (tabIndex == 0) { + return onramp; + } else if (tabIndex == 1) { + return offramp; + } else { + throw Exception('Unknown FiatMode'); + } + } + + int get tabIndex { + switch (this) { + case onramp: + return 0; + case offramp: + return 1; + } + } +} diff --git a/lib/bloc/fiat/models/fiat_order.dart b/lib/bloc/fiat/models/fiat_order.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/bloc/fiat/models/fiat_payment_method.dart b/lib/bloc/fiat/models/fiat_payment_method.dart new file mode 100644 index 0000000000..c3d3301f02 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_payment_method.dart @@ -0,0 +1,168 @@ +import 'package:equatable/equatable.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_price_info.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_transaction_fee.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_transaction_limit.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class FiatPaymentMethod extends Equatable { + const FiatPaymentMethod({ + required this.providerId, + required this.id, + required this.name, + required this.priceInfo, + required this.relativePercent, + required this.providerIconAssetPath, + required this.transactionLimits, + required this.transactionFees, + }); + + const FiatPaymentMethod.none() + : providerId = 'none', + id = '', + name = '', + priceInfo = const FiatPriceInfo.zero(), + relativePercent = 0, + providerIconAssetPath = '', + transactionLimits = const [], + transactionFees = const []; + + factory FiatPaymentMethod.fromJson(Map json) { + final limitsJson = json['transaction_limits'] as List? ?? []; + final List limits = limitsJson + .map( + (e) => + FiatTransactionLimit.fromJson(e as Map? ?? {}), + ) + .toList(); + + final feesJson = json['transaction_fees'] as List? ?? []; + final List fees = feesJson + .map( + (e) => FiatTransactionFee.fromJson(e as Map? ?? {}), + ) + .toList(); + + return FiatPaymentMethod( + providerId: json['provider_id'] as String? ?? '', + id: assertString(json['id']) ?? '', + name: json['name'] as String? ?? '', + priceInfo: FiatPriceInfo.fromJson( + json['price_info'] as Map? ?? {}, + ), + relativePercent: double.parse(json['relative_percent'] as String? ?? '0'), + providerIconAssetPath: json['provider_icon_asset_path'] as String? ?? '', + transactionLimits: limits, + transactionFees: fees, + ); + } + + final String providerId; + final String id; + final String name; + final FiatPriceInfo priceInfo; + final double relativePercent; + final String providerIconAssetPath; + final List transactionLimits; + final List transactionFees; + + bool get isNone => providerId == 'none'; + + FiatPaymentMethod copyWith({ + String? providerId, + String? id, + String? name, + FiatPriceInfo? priceInfo, + double? relativePercent, + String? providerIconAssetPath, + List? transactionLimits, + List? transactionFees, + }) { + return FiatPaymentMethod( + providerId: providerId ?? this.providerId, + id: id ?? this.id, + name: name ?? this.name, + priceInfo: priceInfo ?? this.priceInfo, + relativePercent: relativePercent ?? this.relativePercent, + providerIconAssetPath: + providerIconAssetPath ?? this.providerIconAssetPath, + transactionLimits: transactionLimits ?? this.transactionLimits, + transactionFees: transactionFees ?? this.transactionFees, + ); + } + + Map toJson() { + return { + 'provider_id': providerId, + 'id': id, + 'name': name, + 'price_info': priceInfo.toJson(), + 'relative_percent': relativePercent, + 'provider_icon_asset_path': providerIconAssetPath, + 'transaction_limits': transactionLimits.map((e) => e.toJson()).toList(), + 'transaction_fees': transactionFees.map((e) => e.toJson()).toList(), + }; + } + + @override + List get props => [ + providerId, + id, + name, + priceInfo, + relativePercent, + providerIconAssetPath, + transactionLimits, + transactionFees, + ]; +} + +const List defaultFiatPaymentMethods = [ + FiatPaymentMethod( + id: 'CARD_PAYMENT', + name: 'Card Payment', + providerId: 'Ramp', + priceInfo: FiatPriceInfo( + fiatAmount: 0, + coinAmount: 0, + fiatCode: 'USD', + coinCode: 'BTC', + spotPriceIncludingFee: 0, + ), + relativePercent: 0, + providerIconAssetPath: 'assets/fiat/providers/ramp_icon.svg', + transactionLimits: [], + transactionFees: [], + ), + FiatPaymentMethod( + id: 'APPLE_PAY', + name: 'Apple Pay', + providerId: 'Ramp', + priceInfo: FiatPriceInfo( + fiatAmount: 0, + coinAmount: 0, + fiatCode: 'USD', + coinCode: 'BTC', + spotPriceIncludingFee: 0, + ), + relativePercent: -0.04126038522159592, + providerIconAssetPath: 'assets/fiat/providers/ramp_icon.svg', + transactionLimits: [], + transactionFees: [], + ), + FiatPaymentMethod( + id: '7554', + name: 'Visa/Mastercard', + providerId: 'Banxa', + priceInfo: FiatPriceInfo( + fiatAmount: 0, + coinAmount: 0, + fiatCode: 'USD', + coinCode: 'BTC', + spotPriceIncludingFee: 0, + ), + relativePercent: -0.017942476775854282, + providerIconAssetPath: 'assets/fiat/providers/banxa_icon.svg', + transactionLimits: [], + transactionFees: [], + ), +]; diff --git a/lib/bloc/fiat/models/fiat_price_info.dart b/lib/bloc/fiat/models/fiat_price_info.dart new file mode 100644 index 0000000000..09af6aa779 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_price_info.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class FiatPriceInfo extends Equatable { + const FiatPriceInfo({ + required this.fiatAmount, + required this.coinAmount, + required this.fiatCode, + required this.coinCode, + required this.spotPriceIncludingFee, + }); + + const FiatPriceInfo.zero() + : fiatAmount = 0, + coinAmount = 0, + fiatCode = '', + coinCode = '', + spotPriceIncludingFee = 0; + + factory FiatPriceInfo.fromJson(Map json) { + return FiatPriceInfo( + fiatAmount: _parseFiatAmount(json), + coinAmount: _parseCoinAmount(json), + fiatCode: json['fiat_code'] as String? ?? '', + coinCode: json['coin_code'] as String? ?? '', + spotPriceIncludingFee: assertDouble(json['spot_price_including_fee']), + ); + } + + static double _parseFiatAmount(Map json) => + double.parse(json['fiat_amount'] as String? ?? '0'); + + static double _parseCoinAmount(Map json) => + double.parse(json['coin_amount'] as String? ?? '0'); + + final double fiatAmount; + final double coinAmount; + final String fiatCode; + final String coinCode; + final double spotPriceIncludingFee; + + FiatPriceInfo copyWith({ + double? fiatAmount, + double? coinAmount, + String? fiatCode, + String? coinCode, + double? spotPriceIncludingFee, + }) { + return FiatPriceInfo( + fiatAmount: fiatAmount ?? this.fiatAmount, + coinAmount: coinAmount ?? this.coinAmount, + fiatCode: fiatCode ?? this.fiatCode, + coinCode: coinCode ?? this.coinCode, + spotPriceIncludingFee: + spotPriceIncludingFee ?? this.spotPriceIncludingFee, + ); + } + + Map toJson() { + return { + 'fiat_amount': fiatAmount, + 'coin_amount': coinAmount, + 'fiat_code': fiatCode, + 'coin_code': coinCode, + 'spot_price_including_fee': spotPriceIncludingFee, + }; + } + + @override + List get props => [ + fiatAmount, + coinAmount, + fiatCode, + coinCode, + spotPriceIncludingFee, + ]; +} diff --git a/lib/bloc/fiat/models/fiat_transaction_fee.dart b/lib/bloc/fiat/models/fiat_transaction_fee.dart new file mode 100644 index 0000000000..dce7d94e57 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_transaction_fee.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; + +class FiatTransactionFee extends Equatable { + const FiatTransactionFee({required this.fees}); + + factory FiatTransactionFee.fromJson(Map json) { + final feesJson = json['fees'] as List? ?? []; + final List feesList = feesJson + .map((e) => FeeDetail.fromJson(e as Map)) + .toList(); + + return FiatTransactionFee(fees: feesList); + } + final List fees; + + Map toJson() { + return { + 'fees': fees.map((fee) => fee.toJson()).toList(), + }; + } + + @override + List get props => [fees]; +} + +class FeeDetail extends Equatable { + const FeeDetail({required this.amount}); + + factory FeeDetail.fromJson(Map json) { + return FeeDetail(amount: (json['amount'] ?? 0.0) as double); + } + final double amount; + + Map toJson() { + return { + 'amount': amount, + }; + } + + @override + List get props => [amount]; +} diff --git a/lib/bloc/fiat/models/fiat_transaction_limit.dart b/lib/bloc/fiat/models/fiat_transaction_limit.dart new file mode 100644 index 0000000000..e01bb58634 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_transaction_limit.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +class FiatTransactionLimit extends Equatable { + const FiatTransactionLimit({ + required this.min, + required this.max, + required this.fiatCode, + required this.weekly, + }); + + factory FiatTransactionLimit.fromJson(Map json) { + double parseDouble(String? value) { + if (value == null || value.isEmpty) { + return 0.0; + } + return double.tryParse(value) ?? 0.0; + } + + return FiatTransactionLimit( + min: parseDouble(json['min'] as String?), + max: parseDouble(json['max'] as String?), + weekly: parseDouble(json['weekly'] as String?), + fiatCode: json['fiat_code'] as String? ?? '', + ); + } + + Map toJson() { + return { + 'min': min.toString(), + 'max': max.toString(), + 'weekly': weekly.toString(), + 'fiat_code': fiatCode, + }; + } + + final double min; + final double max; + final double weekly; + final String fiatCode; + + @override + List get props => [min, max, weekly, fiatCode]; +} diff --git a/lib/bloc/fiat/models/i_currency.dart b/lib/bloc/fiat/models/i_currency.dart new file mode 100644 index 0000000000..54c697ce62 --- /dev/null +++ b/lib/bloc/fiat/models/i_currency.dart @@ -0,0 +1,67 @@ +import 'package:web_dex/model/coin_type.dart'; +import 'package:web_dex/model/coin_utils.dart'; + +/// Base class for all currencies +abstract class ICurrency { + const ICurrency(this.symbol, this.name); + + final String symbol; + final String name; + + /// Returns true if the currency is a fiat currency (e.g. USD) + bool get isFiat; + /// Returns true if the currency is a crypto currency (e.g. BTC) + bool get isCrypto; + + /// Returns the abbreviation of the currency (e.g. BTC, USD). + String getAbbr() => symbol; + /// Returns the full name of the currency (e.g. Bitcoin). + String formatNameShort() => name; +} + +class FiatCurrency extends ICurrency { + const FiatCurrency(super.symbol, super.name); + + @override + bool get isFiat => true; + + @override + bool get isCrypto => false; +} + +class CryptoCurrency extends ICurrency { + const CryptoCurrency(super.symbol, super.name, this.chainType); + + final CoinType chainType; + + @override + bool get isFiat => false; + + @override + bool get isCrypto => true; + + @override + String getAbbr() { + if (chainType == CoinType.utxo || + (chainType == CoinType.cosmos && symbol == 'ATOM') || + (chainType == CoinType.erc20 && symbol == 'ETH') || + (chainType == CoinType.bep20 && symbol == 'BNB') || + (chainType == CoinType.avx20 && symbol == 'AVAX') || + (chainType == CoinType.etc && symbol == 'ETC') || + (chainType == CoinType.ftm20 && symbol == 'FTM') || + (chainType == CoinType.arb20 && symbol == 'ARB') || + (chainType == CoinType.hrc20 && symbol == 'ONE') || + (chainType == CoinType.plg20 && symbol == 'MATIC') || + (chainType == CoinType.mvr20 && symbol == 'MOVR')) { + return symbol; + } + + return '$symbol-${getCoinTypeName(chainType).replaceAll('-', '')}'; + } + + @override + String formatNameShort() { + final coinType = ' (${getCoinTypeName(chainType)})'; + return '$name$coinType'; + } +} diff --git a/lib/bloc/fiat/models/models.dart b/lib/bloc/fiat/models/models.dart new file mode 100644 index 0000000000..7a612528a8 --- /dev/null +++ b/lib/bloc/fiat/models/models.dart @@ -0,0 +1,9 @@ +export 'fiat_buy_order_error.dart'; +export 'fiat_buy_order_info.dart'; +export 'fiat_mode.dart'; +export 'fiat_order.dart'; +export 'fiat_payment_method.dart'; +export 'fiat_price_info.dart'; +export 'fiat_transaction_fee.dart'; +export 'fiat_transaction_limit.dart'; +export 'i_currency.dart'; diff --git a/lib/bloc/fiat/ramp/ramp_fiat_provider.dart b/lib/bloc/fiat/ramp/ramp_fiat_provider.dart index ec362509e6..49a4dd2edc 100644 --- a/lib/bloc/fiat/ramp/ramp_fiat_provider.dart +++ b/lib/bloc/fiat/ramp/ramp_fiat_provider.dart @@ -5,13 +5,14 @@ import 'package:flutter/foundation.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; -import 'package:web_dex/bloc/fiat/ramp/ramp_purchase_watcher.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; -const komodoLogoUrl = 'https://komodoplatform.com/assets/img/logo-dark.png'; +const komodoLogoUrl = 'https://app.komodoplatform.com/icons/logo_icon.png'; class RampFiatProvider extends BaseFiatProvider { - final String providerId = "Ramp"; - final String apiEndpoint = "/api/v1/ramp"; + RampFiatProvider(); + final String providerId = 'Ramp'; + final String apiEndpoint = '/api/v1/ramp'; String get orderDomain => kDebugMode ? 'https://app.demo.ramp.network' : 'https://app.ramp.network'; @@ -22,20 +23,18 @@ class RampFiatProvider extends BaseFiatProvider { @override String get providerIconPath => '$assetsPath/fiat/providers/ramp_icon.svg'; - RampFiatProvider(); - @override String getProviderId() { return providerId; } - String getFullCoinCode(Currency target) { - return '${getCoinChainId(target)}_${target.symbol}'; + String getFullCoinCode(ICurrency target) { + return '${getCoinChainId(target as CryptoCurrency)}_${target.symbol}'; } - Future _getPaymentMethods( + Future _getPaymentMethods( String source, - Currency target, { + ICurrency target, { String? sourceAmount, }) => apiRequest( @@ -47,15 +46,15 @@ class RampFiatProvider extends BaseFiatProvider { body: { 'fiatCurrency': source, 'cryptoAssetSymbol': getFullCoinCode(target), - "fiatValue": double.tryParse(sourceAmount!), + 'fiatValue': double.tryParse(sourceAmount!), }, ); - Future _getPricesWithPaymentMethod( + Future _getPricesWithPaymentMethod( String source, - Currency target, + ICurrency target, String sourceAmount, - Map paymentMethod, + FiatPaymentMethod paymentMethod, ) => apiRequest( 'POST', @@ -70,7 +69,7 @@ class RampFiatProvider extends BaseFiatProvider { }, ); - Future _getFiats() => apiRequest( + Future _getFiats() => apiRequest( 'GET', apiEndpoint, queryParams: { @@ -78,7 +77,7 @@ class RampFiatProvider extends BaseFiatProvider { }, ); - Future _getCoins({String? currencyCode}) => apiRequest( + Future _getCoins({String? currencyCode}) => apiRequest( 'GET', apiEndpoint, queryParams: { @@ -88,41 +87,38 @@ class RampFiatProvider extends BaseFiatProvider { ); @override - Stream watchOrderStatus([String? orderId]) { - assert( - orderId == null || orderId.isEmpty == true, - 'Ramp Order ID is only available after the user starts the checkout.', - ); - - final rampOrderWatcher = RampPurchaseWatcher(); - - return rampOrderWatcher.watchOrderStatus(); - } - - @override - Future> getFiatList() async { + Future> getFiatList() async { final response = await _getFiats(); final data = response as List; return data .where((item) => item['onrampAvailable'] as bool) - .map((item) => Currency( - item['fiatCurrency'] as String, - item['name'] as String, - isFiat: true, - )) + .map( + (item) => FiatCurrency( + item['fiatCurrency'] as String, + item['name'] as String, + ), + ) .toList(); } @override - Future> getCoinList() async { + Future> getCoinList() async { final response = await _getCoins(); final data = response['assets'] as List; return data .map((item) { - return Currency(item['symbol'] as String, item['name'] as String, - chainType: getCoinType(item['chain'] as String), isFiat: false); + final coinType = getCoinType(item['chain'] as String); + if (coinType == null) { + return null; + } + return CryptoCurrency( + item['symbol'] as String, + item['name'] as String, + coinType, + ); }) - .where((item) => item.chainType != null) + .where((e) => e != null) + .cast() .toList(); } @@ -135,13 +131,13 @@ class RampFiatProvider extends BaseFiatProvider { } @override - Future>> getPaymentMethodsList( + Future> getPaymentMethodsList( String source, - Currency target, + ICurrency target, String sourceAmount, ) async { try { - List> paymentMethodsList = []; + final List paymentMethodsList = []; final paymentMethodsFuture = _getPaymentMethods(source, target, sourceAmount: sourceAmount); @@ -149,7 +145,7 @@ class RampFiatProvider extends BaseFiatProvider { final results = await Future.wait([paymentMethodsFuture, coinsFuture]); - final paymentMethods = results[0]; + final paymentMethods = results[0] as Map; final coins = results[1] as Map; final asset = paymentMethods['asset']; @@ -162,43 +158,45 @@ class RampFiatProvider extends BaseFiatProvider { asset == null ? null : asset['maxPurchaseAmount']; if (asset != null) { - paymentMethods.forEach((key, value) { - if (key != "asset") { + paymentMethods.forEach((String key, dynamic value) { + if (key != 'asset') { final method = { - "id": key, - "name": _formatMethodName(key), - "transaction_fees": [ + 'id': key, + 'name': _formatMethodName(key), + 'transaction_fees': [ { - "fees": [ + 'fees': [ { - "amount": - value["baseRampFee"] / double.tryParse(sourceAmount) + 'amount': + value['baseRampFee'] / double.tryParse(sourceAmount), }, ], } ], - "transaction_limits": [ + 'transaction_limits': [ { - "fiat_code": source, - "min": (assetMinPurchaseAmount != null && + 'fiat_code': source, + 'min': (assetMinPurchaseAmount != null && assetMinPurchaseAmount != -1 ? assetMinPurchaseAmount : globalMinPurchaseAmount) .toString(), - "max": (assetMaxPurchaseAmount != null && + 'max': (assetMaxPurchaseAmount != null && assetMaxPurchaseAmount != -1 ? assetMaxPurchaseAmount : globalMaxPurchaseAmount) .toString(), } ], - "price_info": { - 'coin_amount': - getCryptoAmount(value['cryptoAmount'], asset['decimals']), - "fiat_amount": value['fiatValue'].toString(), - } + 'price_info': { + 'coin_amount': getCryptoAmount( + value['cryptoAmount'] as String, + asset['decimals'] as int, + ), + 'fiat_amount': value['fiatValue'].toString(), + }, }; - paymentMethodsList.add(method); + paymentMethodsList.add(FiatPaymentMethod.fromJson(method)); } }); } @@ -210,12 +208,12 @@ class RampFiatProvider extends BaseFiatProvider { } } - double _getPaymentMethodFee(Map paymentMethod) { - return paymentMethod['transaction_fees'][0]['fees'][0]['amount']; + double _getPaymentMethodFee(FiatPaymentMethod paymentMethod) { + return paymentMethod.transactionFees.first.fees.first.amount; } double _getFeeAdjustedPrice( - Map paymentMethod, + FiatPaymentMethod paymentMethod, double price, ) { return price / (1 - _getPaymentMethodFee(paymentMethod)); @@ -227,11 +225,11 @@ class RampFiatProvider extends BaseFiatProvider { } @override - Future> getPaymentMethodPrice( + Future getPaymentMethodPrice( String source, - Currency target, + ICurrency target, String sourceAmount, - Map paymentMethod, + FiatPaymentMethod paymentMethod, ) async { final response = await _getPricesWithPaymentMethod( source, @@ -240,7 +238,7 @@ class RampFiatProvider extends BaseFiatProvider { paymentMethod, ); final asset = response['asset']; - final prices = asset['price']; + final prices = asset['price'] as Map? ?? {}; if (!prices.containsKey(source)) { return Future.error( 'Price information not available for the currency: $source', @@ -251,19 +249,22 @@ class RampFiatProvider extends BaseFiatProvider { 'fiat_code': source, 'coin_code': target.symbol, 'spot_price_including_fee': - _getFeeAdjustedPrice(paymentMethod, prices[source]).toString(), + _getFeeAdjustedPrice(paymentMethod, prices[source] as double) + .toString(), 'coin_amount': getCryptoAmount( - response[paymentMethod['id']]['cryptoAmount'], asset['decimals']), + response[paymentMethod.id]['cryptoAmount'] as String, + asset['decimals'] as int, + ), }; - return Map.from(priceInfo); + return FiatPriceInfo.fromJson(priceInfo); } @override - Future> buyCoin( + Future buyCoin( String accountReference, String source, - Currency target, + ICurrency target, String walletAddress, String paymentMethodId, String sourceAmount, @@ -273,32 +274,31 @@ class RampFiatProvider extends BaseFiatProvider { 'hostApiKey': hostId, 'hostAppName': appShortTitle, 'hostLogoUrl': komodoLogoUrl, - "userAddress": walletAddress, - "finalUrl": returnUrlOnSuccess, - "defaultFlow": 'ONRAMP', - "enabledFlows": '[ONRAMP]', - "fiatCurrency": source, - "fiatValue": sourceAmount, - "defaultAsset": getFullCoinCode(target), + 'userAddress': walletAddress, + 'finalUrl': returnUrlOnSuccess, + 'defaultFlow': 'ONRAMP', + 'enabledFlows': '[ONRAMP]', + 'fiatCurrency': source, + 'fiatValue': sourceAmount, + 'defaultAsset': getFullCoinCode(target), // if(coinsBloc.walletCoins.isNotEmpty) // "swapAsset": coinsBloc.walletCoins.map((e) => e.abbr).toList().toString(), // "swapAsset": fullAssetCode, // This limits the crypto asset list at the redirect page }; final queryString = payload.entries.map((entry) { - return '${Uri.encodeComponent(entry.key)}=${Uri.encodeComponent(entry.value.toString())}'; + return '${Uri.encodeComponent(entry.key)}=${Uri.encodeComponent(entry.value)}'; }).join('&'); final checkoutUrl = '$orderDomain?$queryString'; + return FiatBuyOrderInfo.fromCheckoutUrl(checkoutUrl); + } - final orderInfo = { - 'data': { - 'order': { - 'checkout_url': checkoutUrl, - }, - }, - }; - - return Map.from(orderInfo); + @override + Stream watchOrderStatus(String orderId) { + throw UnsupportedError( + 'Ramp integration relies on console.log and/or postMessage ' + 'callbacks from a webpage', + ); } } diff --git a/lib/bloc/fiat/ramp/ramp_purchase_watcher.dart b/lib/bloc/fiat/ramp/ramp_purchase_watcher.dart deleted file mode 100644 index 03690c7fe8..0000000000 --- a/lib/bloc/fiat/ramp/ramp_purchase_watcher.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:universal_html/html.dart' as html; -import 'package:http/http.dart' as http; -import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; - -class RampPurchaseWatcher { - FiatOrderStatus? _lastStatus; - bool _isDisposed = false; - - /// Watches the status of new Ramp purchases. - /// - /// NB: Will only work if the Ramp checkout tab was opened by the app. I.e. - /// the user copies the checkout URL and opens it in a new tab, we will not - /// be able to track the status of that purchase. Implementing a microservice - /// that can receive webhooks is a possible solution. - /// - /// [watchFirstPurchaseOnly] - if true, will only listen for status updates - /// for the first purchase. If false, will we will no longer listen for - /// status updates of the first purchase and will start listening for status - /// updates of the new purchase. The ramp purchase is created in one of the - /// last checkout steps, so if the user creates the purchase and goes back to - /// the first step, Ramp will create a new purchase. - Stream watchOrderStatus({ - bool watchFirstPurchaseOnly = false, - }) { - _assertNotDisposed(); - - RampPurchaseDetails? purchaseDetails; - - final controller = StreamController(); - - scheduleMicrotask(() async { - StreamSubscription? subscription; - - final stream = watchNewRampOrdersCreated().takeWhile((purchase) => - !controller.isClosed && - (purchaseDetails == null || !watchFirstPurchaseOnly)); - try { - subscription = stream.listen( - (newPurchaseJson) => purchaseDetails = - RampPurchaseDetails.tryFromMessage(newPurchaseJson), - cancelOnError: false, - ); - - while (!controller.isClosed) { - if (purchaseDetails != null) { - final status = await _getPurchaseStatus(purchaseDetails!); - if (status != _lastStatus) { - _lastStatus = status; - controller.add(status); - } - - if (status.isTerminal || controller.isClosed) break; - } - await Future.delayed(const Duration(seconds: 10)); - } - } catch (e) { - controller.addError(e); - debugPrint('RampOrderWatcher: Error: $e'); - } finally { - subscription?.cancel().ignore(); - _cleanup(); - } - }); - - return controller.stream; - } - - Stream> watchNewRampOrdersCreated() async* { - _assertNotDisposed(); - - final purchaseStartedController = StreamController>(); - - void handlerFunction(html.Event event) { - if (purchaseStartedController.isClosed) return; - final messageEvent = event as html.MessageEvent; - if (messageEvent.data is Map) { - try { - final dataJson = (messageEvent.data as Map).cast(); - - if (_isRampNewPurchaseMessage(dataJson)) { - purchaseStartedController.add(dataJson); - } - } catch (e) { - purchaseStartedController.addError(e); - } - } - } - - final handler = handlerFunction; - - try { - html.window.addEventListener('message', handler); - - yield* purchaseStartedController.stream; - } catch (e) { - purchaseStartedController.addError(e); - } finally { - html.window.removeEventListener('message', handler); - - if (!purchaseStartedController.isClosed) { - await purchaseStartedController.close(); - } - } - } - - /// Checks if the JS message is a new Ramp purchase message. - bool _isRampNewPurchaseMessage(Map data) { - return data.containsKey('type') && data['type'] == 'PURCHASE_CREATED'; - } - - void _cleanup() { - _isDisposed = true; - // Close any other resources if necessary - } - - void _assertNotDisposed() { - if (_isDisposed) { - throw Exception('RampOrderWatcher has already been disposed'); - } - } - - static Future _getPurchaseStatus( - RampPurchaseDetails purchase) async { - final response = await http.get(purchase.purchaseUrl); - - if (response.statusCode != 200) { - throw Exception('Could not get Ramp purchase status'); - } - - final data = json.decode(response.body) as _JsonMap; - final rampStatus = data['status'] as String; - final status = _mapRampStatusToFiatOrderStatus(rampStatus); - if (status != null) { - return status; - } else { - throw Exception('Could not parse Ramp status: $rampStatus'); - } - } - - static FiatOrderStatus? _mapRampStatusToFiatOrderStatus(String rampStatus) { - // See here for all possible statuses: - // https://docs.ramp.network/sdk-reference#on-ramp-purchase-status - switch (rampStatus) { - case 'INITIALIZED': - case 'PAYMENT_STARTED': - case 'PAYMENT_IN_PROGRESS': - return FiatOrderStatus.pending; - - case 'FIAT_SENT': - case 'FIAT_RECEIVED': - case 'RELEASING': - return FiatOrderStatus.inProgress; - case 'PAYMENT_EXECUTED': - case 'RELEASED': - return FiatOrderStatus.success; - case 'PAYMENT_FAILED': - case 'EXPIRED': - case 'CANCELLED': - return FiatOrderStatus.failed; - default: - return null; - } - } -} - -typedef _JsonMap = Map; - -class RampPurchaseDetails { - RampPurchaseDetails({ - required this.orderId, - required this.apiUrl, - required this.purchaseViewToken, - }); - - final String orderId; - final String apiUrl; - final String purchaseViewToken; - - Uri get purchaseUrl => - Uri.parse('$apiUrl/host-api/purchase/$orderId?secret=$purchaseViewToken'); - - static RampPurchaseDetails? tryFromMessage(Map message) { - if (!message.containsKey('type') || message['type'] != 'PURCHASE_CREATED') { - return null; - } - - try { - final payload = message['payload'] as Map; - final Map purchase = - Map.from(payload['purchase'] as Map); - final String purchaseViewToken = payload['purchaseViewToken'] as String; - final String apiUrl = payload['apiUrl'] as String; - final String orderId = purchase['id'] as String; - - return RampPurchaseDetails( - orderId: orderId, - apiUrl: apiUrl, - purchaseViewToken: purchaseViewToken, - ); - } catch (e) { - debugPrint('RampOrderWatcher: Error parsing RampPurchaseDetails: $e'); - return null; - } - } - -//==== RampPurchase MESSAGE FORMAT: -// { -// type: 'PURCHASE_CREATED', -// payload: { -// purchase: RampPurchase, -// purchaseViewToken: string, -// apiUrl: string -// }, -// widgetInstanceId: string, -// } - -//==== RampPurchase MESSAGE EXAMPLE: -// { -// "type": "PURCHASE_CREATED", -// "payload": { -// "purchase": { -// "endTime": "2023-11-26T13:24:20.177Z", -// "cryptoAmount": "110724180593676737247", -// "fiatCurrency": "GBP", -// "fiatValue": 100, -// "assetExchangeRateEur": 1, -// "fiatExchangeRateEur": 1.1505242363683013, -// "baseRampFee": 3.753282987574591, -// "networkFee": 0.00869169, -// "appliedFee": 3.761974677574591, -// "createdAt": "2023-11-23T13:24:20.271Z", -// "updatedAt": "2023-11-23T13:24:21.040Z", -// "id": "s73gxbn6jotrvqj", -// "asset": { -// "address": "0x5248dDdC7857987A2EfD81522AFBA1fCb017A4b7", -// "symbol": "MATIC_TEST", -// "apiV3Symbol": "TEST", -// "name": "Test Token on Polygon Mumbai", -// "decimals": 18, -// "type": "MATIC_ERC20", -// "apiV3Type": "ERC20", -// "chain": "MATIC" -// }, -// "receiverAddress": "0xbbabc29087c7ef37a59da76896d7740a43dcb371", -// "assetExchangeRate": 0.869169, -// "purchaseViewToken": "56grvvsvu3mae27t", -// "status": "INITIALIZED", -// "paymentMethodType": "CARD_PAYMENT" -// }, -// "purchaseViewToken": "56grvvsvu3mae27t", -// "apiUrl": "https://api.demo.ramp.network/api" -// }, -// "widgetInstanceId": "KNWgVtLoPwMM0v2sllOeE" -// } -} 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 a400cc54ca..1512609ffa 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 @@ -66,7 +66,7 @@ class MarketMakerTradeFormBloc Future _onSellCoinChanged( MarketMakerTradeFormSellCoinChanged event, - Emitter emit, + Emitter emit, ) async { final identicalBuyAndSellCoins = state.buyCoin.value == event.sellCoin; final sellCoinBalance = event.sellCoin?.balance ?? 0; diff --git a/lib/blocs/coins_bloc.dart b/lib/blocs/coins_bloc.dart index 2ed9bf8fbd..625a6c1174 100644 --- a/lib/blocs/coins_bloc.dart +++ b/lib/blocs/coins_bloc.dart @@ -38,7 +38,9 @@ class CoinsBloc implements BlocBase { trezorRepo: trezorRepo, walletRepo: currentWalletBloc, ); + } + Future init() async { _authorizationSubscription = authRepo.authMode.listen((event) async { switch (event) { case AuthorizeMode.noLogin: @@ -371,8 +373,9 @@ class CoinsBloc implements BlocBase { ); } - final WithdrawDetails withdrawDetails = - WithdrawDetails.fromJson(response['result']); + final WithdrawDetails withdrawDetails = WithdrawDetails.fromJson( + response['result'] as Map? ?? {}, + ); return BlocResponse( result: withdrawDetails, @@ -456,6 +459,51 @@ class CoinsBloc implements BlocBase { walletCoins = _walletCoins; } + Future getCoinAddress(String abbr) async { + final loggedIn = isLoggedIn && currentWalletBloc.wallet != null; + if (!loggedIn) { + return null; + } + + final accountKey = currentWalletBloc.wallet!.id; + final abbrKey = abbr; + + if (addressCache.containsKey(accountKey) && + addressCache[accountKey]!.containsKey(abbrKey)) { + return addressCache[accountKey]![abbrKey]; + } else { + await activateCoins([getCoin(abbr)!]); + final coin = walletCoins.firstWhereOrNull((c) => c.abbr == abbr); + + if (coin != null && coin.address != null) { + if (!addressCache.containsKey(accountKey)) { + addressCache[accountKey] = {}; + } + + // Cache this wallet's addresses + for (final walletCoin in walletCoins) { + if (walletCoin.address != null && + !addressCache[accountKey]!.containsKey(walletCoin.abbr)) { + // Exit if the address already exists in a different account + // Address belongs to another account, this is a bug, + // gives outdated data + for (final entry in addressCache.entries) { + if (entry.key != accountKey && + entry.value.containsValue(walletCoin.address)) { + return null; + } + } + + addressCache[accountKey]![walletCoin.abbr] = walletCoin.address!; + } + } + + return addressCache[accountKey]![abbrKey]; + } + } + return null; + } + @override void dispose() { _walletCoinsController.close(); diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 7d2602a5f4..6503af80fc 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -1,6 +1,6 @@ // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart -abstract class LocaleKeys { +abstract class LocaleKeys { static const plsActivateKmd = 'plsActivateKmd'; static const rewardClaiming = 'rewardClaiming'; static const noKmdAddress = 'noKmdAddress'; @@ -101,50 +101,39 @@ abstract class LocaleKeys { static const seedPhrase = 'seedPhrase'; static const assetNumber = 'assetNumber'; static const clipBoard = 'clipBoard'; - static const walletsManagerCreateWalletButton = - 'walletsManagerCreateWalletButton'; - static const walletsManagerImportWalletButton = - 'walletsManagerImportWalletButton'; - static const walletsManagerStepBuilderCreationWalletError = - 'walletsManagerStepBuilderCreationWalletError'; + static const walletsManagerCreateWalletButton = 'walletsManagerCreateWalletButton'; + static const walletsManagerImportWalletButton = 'walletsManagerImportWalletButton'; + static const walletsManagerStepBuilderCreationWalletError = 'walletsManagerStepBuilderCreationWalletError'; static const walletCreationTitle = 'walletCreationTitle'; static const walletImportTitle = 'walletImportTitle'; static const walletImportByFileTitle = 'walletImportByFileTitle'; - static const walletImportCreatePasswordTitle = - 'walletImportCreatePasswordTitle'; + static const walletImportCreatePasswordTitle = 'walletImportCreatePasswordTitle'; static const walletImportByFileDescription = 'walletImportByFileDescription'; static const walletLogInTitle = 'walletLogInTitle'; static const walletCreationNameHint = 'walletCreationNameHint'; static const walletCreationPasswordHint = 'walletCreationPasswordHint'; - static const walletCreationConfirmPasswordHint = - 'walletCreationConfirmPasswordHint'; + static const walletCreationConfirmPasswordHint = 'walletCreationConfirmPasswordHint'; static const walletCreationConfirmPassword = 'walletCreationConfirmPassword'; static const walletCreationUploadFile = 'walletCreationUploadFile'; static const walletCreationEmptySeedError = 'walletCreationEmptySeedError'; static const walletCreationExistNameError = 'walletCreationExistNameError'; static const walletCreationNameLengthError = 'walletCreationNameLengthError'; - static const walletCreationFormatPasswordError = - 'walletCreationFormatPasswordError'; - static const walletCreationConfirmPasswordError = - 'walletCreationConfirmPasswordError'; + static const walletCreationFormatPasswordError = 'walletCreationFormatPasswordError'; + static const walletCreationConfirmPasswordError = 'walletCreationConfirmPasswordError'; static const invalidPasswordError = 'invalidPasswordError'; static const importSeedEnterSeedPhraseHint = 'importSeedEnterSeedPhraseHint'; static const passphraseCheckingTitle = 'passphraseCheckingTitle'; static const passphraseCheckingDescription = 'passphraseCheckingDescription'; static const passphraseCheckingEnterWord = 'passphraseCheckingEnterWord'; - static const passphraseCheckingEnterWordHint = - 'passphraseCheckingEnterWordHint'; + static const passphraseCheckingEnterWordHint = 'passphraseCheckingEnterWordHint'; static const back = 'back'; static const settingsMenuGeneral = 'settingsMenuGeneral'; static const settingsMenuLanguage = 'settingsMenuLanguage'; static const settingsMenuSecurity = 'settingsMenuSecurity'; static const settingsMenuAbout = 'settingsMenuAbout'; - static const seedPhraseSettingControlsViewSeed = - 'seedPhraseSettingControlsViewSeed'; - static const seedPhraseSettingControlsDownloadSeed = - 'seedPhraseSettingControlsDownloadSeed'; - static const debugSettingsResetActivatedCoins = - 'debugSettingsResetActivatedCoins'; + static const seedPhraseSettingControlsViewSeed = 'seedPhraseSettingControlsViewSeed'; + static const seedPhraseSettingControlsDownloadSeed = 'seedPhraseSettingControlsDownloadSeed'; + static const debugSettingsResetActivatedCoins = 'debugSettingsResetActivatedCoins'; static const debugSettingsDownloadButton = 'debugSettingsDownloadButton'; static const or = 'or'; static const passwordTitle = 'passwordTitle'; @@ -154,19 +143,16 @@ abstract class LocaleKeys { static const changePasswordSpan1 = 'changePasswordSpan1'; static const updatePassword = 'updatePassword'; static const passwordHasChanged = 'passwordHasChanged'; - static const confirmationForShowingSeedPhraseTitle = - 'confirmationForShowingSeedPhraseTitle'; + static const confirmationForShowingSeedPhraseTitle = 'confirmationForShowingSeedPhraseTitle'; static const saveAndRemember = 'saveAndRemember'; static const seedPhraseShowingTitle = 'seedPhraseShowingTitle'; static const seedPhraseShowingWarning = 'seedPhraseShowingWarning'; static const seedPhraseShowingShowPhrase = 'seedPhraseShowingShowPhrase'; static const seedPhraseShowingCopySeed = 'seedPhraseShowingCopySeed'; - static const seedPhraseShowingSavedPhraseButton = - 'seedPhraseShowingSavedPhraseButton'; + static const seedPhraseShowingSavedPhraseButton = 'seedPhraseShowingSavedPhraseButton'; static const seedAccessSpan1 = 'seedAccessSpan1'; static const backupSeedNotificationTitle = 'backupSeedNotificationTitle'; - static const backupSeedNotificationDescription = - 'backupSeedNotificationDescription'; + static const backupSeedNotificationDescription = 'backupSeedNotificationDescription'; static const backupSeedNotificationButton = 'backupSeedNotificationButton'; static const swapConfirmationTitle = 'swapConfirmationTitle'; static const swapConfirmationYouReceive = 'swapConfirmationYouReceive'; @@ -174,44 +160,33 @@ abstract class LocaleKeys { static const tradingDetailsTitleFailed = 'tradingDetailsTitleFailed'; static const tradingDetailsTitleCompleted = 'tradingDetailsTitleCompleted'; static const tradingDetailsTitleInProgress = 'tradingDetailsTitleInProgress'; - static const tradingDetailsTitleOrderMatching = - 'tradingDetailsTitleOrderMatching'; + static const tradingDetailsTitleOrderMatching = 'tradingDetailsTitleOrderMatching'; static const tradingDetailsTotalSpentTime = 'tradingDetailsTotalSpentTime'; - static const tradingDetailsTotalSpentTimeWithHours = - 'tradingDetailsTotalSpentTimeWithHours'; + static const tradingDetailsTotalSpentTimeWithHours = 'tradingDetailsTotalSpentTimeWithHours'; static const swapRecoverButtonTitle = 'swapRecoverButtonTitle'; static const swapRecoverButtonText = 'swapRecoverButtonText'; static const swapRecoverButtonErrorMessage = 'swapRecoverButtonErrorMessage'; - static const swapRecoverButtonSuccessMessage = - 'swapRecoverButtonSuccessMessage'; + static const swapRecoverButtonSuccessMessage = 'swapRecoverButtonSuccessMessage'; static const swapProgressStatusFailed = 'swapProgressStatusFailed'; static const swapDetailsStepStatusFailed = 'swapDetailsStepStatusFailed'; static const disclaimerAcceptEulaCheckbox = 'disclaimerAcceptEulaCheckbox'; - static const disclaimerAcceptTermsAndConditionsCheckbox = - 'disclaimerAcceptTermsAndConditionsCheckbox'; + static const disclaimerAcceptTermsAndConditionsCheckbox = 'disclaimerAcceptTermsAndConditionsCheckbox'; static const disclaimerAcceptDescription = 'disclaimerAcceptDescription'; - static const swapDetailsStepStatusInProcess = - 'swapDetailsStepStatusInProcess'; - static const swapDetailsStepStatusTimeSpent = - 'swapDetailsStepStatusTimeSpent'; + static const swapDetailsStepStatusInProcess = 'swapDetailsStepStatusInProcess'; + static const swapDetailsStepStatusTimeSpent = 'swapDetailsStepStatusTimeSpent'; static const milliseconds = 'milliseconds'; static const seconds = 'seconds'; static const minutes = 'minutes'; static const hours = 'hours'; - static const coinAddressDetailsNotificationTitle = - 'coinAddressDetailsNotificationTitle'; - static const coinAddressDetailsNotificationDescription = - 'coinAddressDetailsNotificationDescription'; + static const coinAddressDetailsNotificationTitle = 'coinAddressDetailsNotificationTitle'; + static const coinAddressDetailsNotificationDescription = 'coinAddressDetailsNotificationDescription'; static const swapFeeDetailsPaidFromBalance = 'swapFeeDetailsPaidFromBalance'; static const swapFeeDetailsSendCoinTxFee = 'swapFeeDetailsSendCoinTxFee'; - static const swapFeeDetailsReceiveCoinTxFee = - 'swapFeeDetailsReceiveCoinTxFee'; + static const swapFeeDetailsReceiveCoinTxFee = 'swapFeeDetailsReceiveCoinTxFee'; static const swapFeeDetailsTradingFee = 'swapFeeDetailsTradingFee'; - static const swapFeeDetailsSendTradingFeeTxFee = - 'swapFeeDetailsSendTradingFeeTxFee'; + static const swapFeeDetailsSendTradingFeeTxFee = 'swapFeeDetailsSendTradingFeeTxFee'; static const swapFeeDetailsNone = 'swapFeeDetailsNone'; - static const swapFeeDetailsPaidFromReceivedVolume = - 'swapFeeDetailsPaidFromReceivedVolume'; + static const swapFeeDetailsPaidFromReceivedVolume = 'swapFeeDetailsPaidFromReceivedVolume'; static const logoutPopupTitle = 'logoutPopupTitle'; static const logoutPopupDescription = 'logoutPopupDescription'; static const transactionDetailsTitle = 'transactionDetailsTitle'; @@ -287,8 +262,7 @@ abstract class LocaleKeys { static const sell = 'sell'; static const buy = 'buy'; static const changingWalletPassword = 'changingWalletPassword'; - static const changingWalletPasswordDescription = - 'changingWalletPasswordDescription'; + static const changingWalletPasswordDescription = 'changingWalletPasswordDescription'; static const dark = 'dark'; static const darkMode = 'darkMode'; static const light = 'light'; @@ -311,8 +285,7 @@ abstract class LocaleKeys { static const email = 'email'; static const emailValidatorError = 'emailValidatorError'; static const feedbackValidatorEmptyError = 'feedbackValidatorEmptyError'; - static const feedbackValidatorMaxLengthError = - 'feedbackValidatorMaxLengthError'; + static const feedbackValidatorMaxLengthError = 'feedbackValidatorMaxLengthError'; static const yourFeedback = 'yourFeedback'; static const sendFeedback = 'sendFeedback'; static const sendFeedbackError = 'sendFeedbackError'; @@ -360,8 +333,7 @@ abstract class LocaleKeys { static const noSenderAddress = 'noSenderAddress'; static const confirmOnTrezor = 'confirmOnTrezor'; static const alphaVersionWarningTitle = 'alphaVersionWarningTitle'; - static const alphaVersionWarningDescription = - 'alphaVersionWarningDescription'; + static const alphaVersionWarningDescription = 'alphaVersionWarningDescription'; static const sendToAnalytics = 'sendToAnalytics'; static const backToWallet = 'backToWallet'; static const backToDex = 'backToDex'; @@ -407,16 +379,13 @@ abstract class LocaleKeys { static const bridgeMaxSendAmountError = 'bridgeMaxSendAmountError'; static const bridgeMinOrderAmountError = 'bridgeMinOrderAmountError'; static const bridgeMaxOrderAmountError = 'bridgeMaxOrderAmountError'; - static const bridgeInsufficientBalanceError = - 'bridgeInsufficientBalanceError'; + static const bridgeInsufficientBalanceError = 'bridgeInsufficientBalanceError'; static const lowTradeVolumeError = 'lowTradeVolumeError'; static const bridgeSelectReceiveCoinError = 'bridgeSelectReceiveCoinError'; static const withdrawNoParentCoinError = 'withdrawNoParentCoinError'; static const withdrawTopUpBalanceError = 'withdrawTopUpBalanceError'; - static const withdrawNotEnoughBalanceForGasError = - 'withdrawNotEnoughBalanceForGasError'; - static const withdrawNotSufficientBalanceError = - 'withdrawNotSufficientBalanceError'; + static const withdrawNotEnoughBalanceForGasError = 'withdrawNotEnoughBalanceForGasError'; + static const withdrawNotSufficientBalanceError = 'withdrawNotSufficientBalanceError'; static const withdrawZeroBalanceError = 'withdrawZeroBalanceError'; static const withdrawAmountTooLowError = 'withdrawAmountTooLowError'; static const withdrawNoSuchCoinError = 'withdrawNoSuchCoinError'; @@ -472,10 +441,8 @@ abstract class LocaleKeys { static const availableForSwaps = 'availableForSwaps'; static const swapNow = 'swapNow'; static const passphrase = 'passphrase'; - static const enterPassphraseHiddenWalletTitle = - 'enterPassphraseHiddenWalletTitle'; - static const enterPassphraseHiddenWalletDescription = - 'enterPassphraseHiddenWalletDescription'; + static const enterPassphraseHiddenWalletTitle = 'enterPassphraseHiddenWalletTitle'; + static const enterPassphraseHiddenWalletDescription = 'enterPassphraseHiddenWalletDescription'; static const skip = 'skip'; static const activateToSeeFunds = 'activateToSeeFunds'; static const allowCustomFee = 'allowCustomFee'; @@ -548,10 +515,8 @@ abstract class LocaleKeys { static const noWalletsAvailable = 'noWalletsAvailable'; static const selectWalletToReset = 'selectWalletToReset'; static const qrScannerTitle = 'qrScannerTitle'; - static const qrScannerErrorControllerUninitialized = - 'qrScannerErrorControllerUninitialized'; - static const qrScannerErrorPermissionDenied = - 'qrScannerErrorPermissionDenied'; + static const qrScannerErrorControllerUninitialized = 'qrScannerErrorControllerUninitialized'; + static const qrScannerErrorPermissionDenied = 'qrScannerErrorPermissionDenied'; static const qrScannerErrorGenericError = 'qrScannerErrorGenericError'; static const qrScannerErrorTitle = 'qrScannerErrorTitle'; static const spend = 'spend'; @@ -567,8 +532,8 @@ abstract class LocaleKeys { static const selectFiat = 'selectFiat'; static const selectCoin = 'selectCoin'; static const bestOffer = 'bestOffer'; - static const loadingNfts = 'loadingNfts'; static const komodoWallet = 'komodoWallet'; + static const loadingNfts = 'loadingNfts'; static const coinAssets = 'coinAssets'; static const bundled = 'bundled'; static const updated = 'updated'; @@ -582,10 +547,19 @@ abstract class LocaleKeys { static const fiatCantCompleteOrder = 'fiatCantCompleteOrder'; static const fiatPriceCanChange = 'fiatPriceCanChange'; static const fiatConnectWallet = 'fiatConnectWallet'; + static const fiatMinimumAmount = 'fiatMinimumAmount'; + static const fiatMaximumAmount = 'fiatMaximumAmount'; + static const fiatPaymentSubmittedTitle = 'fiatPaymentSubmittedTitle'; + static const fiatPaymentSubmittedMessage = 'fiatPaymentSubmittedMessage'; + static const fiatPaymentSuccessTitle = 'fiatPaymentSuccessTitle'; + static const fiatPaymentSuccessMessage = 'fiatPaymentSuccessMessage'; + static const fiatPaymentFailedTitle = 'fiatPaymentFailedTitle'; + static const fiatPaymentFailedMessage = 'fiatPaymentFailedMessage'; + static const fiatPaymentInProgressTitle = 'fiatPaymentInProgressTitle'; + static const fiatPaymentInProgressMessage = 'fiatPaymentInProgressMessage'; static const pleaseWait = 'pleaseWait'; static const bitrefillPaymentSuccessfull = 'bitrefillPaymentSuccessfull'; - static const bitrefillPaymentSuccessfullInstruction = - 'bitrefillPaymentSuccessfullInstruction'; + static const bitrefillPaymentSuccessfullInstruction = 'bitrefillPaymentSuccessfullInstruction'; static const tradingBot = 'tradingBot'; static const margin = 'margin'; static const updateInterval = 'updateInterval'; @@ -622,4 +596,5 @@ abstract class LocaleKeys { static const allTimeInvestment = 'allTimeInvestment'; static const allTimeProfit = 'allTimeProfit'; static const profitAndLoss = 'profitAndLoss'; + } diff --git a/lib/main.dart b/lib/main.dart index 52410cfbe1..b2b1c2bd0a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:web_dex/bloc/cex_market_data/cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/runtime_coin_updates/runtime_update_config_provider.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; +import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/blocs/startup_bloc.dart'; import 'package:web_dex/model/stored_settings.dart'; import 'package:web_dex/performance_analytics/performance_analytics.dart'; diff --git a/lib/model/forms/fiat/currency_input.dart b/lib/model/forms/fiat/currency_input.dart new file mode 100644 index 0000000000..ac84232651 --- /dev/null +++ b/lib/model/forms/fiat/currency_input.dart @@ -0,0 +1,38 @@ +import 'package:formz/formz.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; + +/// Validation errors for the currency selection form field. +enum CurrencyValidationError { + /// No currency selected + empty, + + /// Currency is not valid for the current operation (e.g., unsupported) + unsupported, +} + +/// Formz input for selecting a currency. +class CurrencyInput extends FormzInput { + const CurrencyInput.pure() : super.pure(null); + const CurrencyInput.dirty([super.value]) : super.dirty(); + + @override + CurrencyValidationError? validator(ICurrency? value) { + if (value == null) { + return CurrencyValidationError.empty; + } + + // Additional checks can be placed here + if (!isCurrencySupported(value)) { + return CurrencyValidationError.unsupported; + } + + return null; + } + + bool isCurrencySupported(ICurrency currency) { + // Implement your logic for determining if a currency is supported. + // For example, this might check against a list of supported fiat/currencies. + // Here, we assume a placeholder true value, meaning all are supported. + return true; + } +} diff --git a/lib/model/forms/fiat/fiat_amount_input.dart b/lib/model/forms/fiat/fiat_amount_input.dart new file mode 100644 index 0000000000..b319070cbb --- /dev/null +++ b/lib/model/forms/fiat/fiat_amount_input.dart @@ -0,0 +1,52 @@ +import 'package:formz/formz.dart'; + +/// Validation errors for the fiat amount form field. +enum FiatAmountValidationError { + /// Input is empty + empty, + + /// Input is not a valid decimal number + invalid, + + /// Input is below the specified minimum amount + belowMinimum, + + /// Input exceeds the specified maximum amount + aboveMaximum, +} + +/// Formz input for a fiat currency amount. +class FiatAmountInput extends FormzInput { + const FiatAmountInput.pure({this.minValue = 0, this.maxValue}) + : super.pure(''); + const FiatAmountInput.dirty(super.value, {this.minValue = 0, this.maxValue}) + : super.dirty(); + + final double? minValue; + final double? maxValue; + + double? get valueAsDouble => double.tryParse(value); + + @override + FiatAmountValidationError? validator(String value) { + if (value.isEmpty) { + return FiatAmountValidationError.empty; + } + + final amount = double.tryParse(value.replaceAll(',', '')); + + if (amount == null) { + return FiatAmountValidationError.invalid; + } + + if (minValue != null && amount < minValue!) { + return FiatAmountValidationError.belowMinimum; + } + + if (maxValue != null && amount > maxValue!) { + return FiatAmountValidationError.aboveMaximum; + } + + return null; + } +} diff --git a/lib/services/initializer/app_bootstrapper.dart b/lib/services/initializer/app_bootstrapper.dart index bb405a0216..b272b0003a 100644 --- a/lib/services/initializer/app_bootstrapper.dart +++ b/lib/services/initializer/app_bootstrapper.dart @@ -39,7 +39,10 @@ final class AppBootstrapper { RuntimeUpdateConfigProvider() .getRuntimeUpdateConfig() .then((config) => _runtimeUpdateConfig = config), - KomodoCoinUpdater.ensureInitialized(appFolder) + // Hive has to be initialised before runtime coin operations can be used + // in the coins repository + KomodoCoinUpdater.ensureInitialized(appFolder, isWeb: kIsWeb) + .then((_) => coinsBloc.init()) .then((_) => sparklineRepository.init()), ]; } diff --git a/lib/shared/utils/extensions/string_extensions.dart b/lib/shared/utils/extensions/string_extensions.dart index d3e18086cd..483605e51c 100644 --- a/lib/shared/utils/extensions/string_extensions.dart +++ b/lib/shared/utils/extensions/string_extensions.dart @@ -1,6 +1,17 @@ +import 'dart:convert'; + extension StringExtension on String { String toCapitalize() { if (isEmpty) return this; return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } + + bool isJson() { + try { + jsonDecode(this); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 98574b37f4..7d72810dd6 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:app_theme/app_theme.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rational/rational.dart'; @@ -223,7 +224,7 @@ Future launchURL( } } -void log( +Future log( String message, { String? path, StackTrace? trace, @@ -235,11 +236,16 @@ void log( // final String errorTrace = getInfoFromStackTrace(trace); // logger.write('$errorTrace: $errorOrUsefulData'); // } - if (isTestMode && isError) { + const isTestEnv = isTestMode || kDebugMode; + if (isTestEnv && isError) { // ignore: avoid_print print('path: $path'); // ignore: avoid_print print('error: $message'); + if (trace != null) { + // ignore: avoid_print + print('trace: $trace'); + } } try { @@ -260,19 +266,19 @@ void log( } /// Returns the ticker from the coin abbreviation. -/// +/// /// Parameters: /// - [abbr] (String): The abbreviation of the coin, including suffixes like the -/// coin token type (e.g. 'ETH-ERC20', 'BNB-BEP20') and whether the coin is +/// coin token type (e.g. 'ETH-ERC20', 'BNB-BEP20') and whether the coin is /// a test or OLD coin (e.g. 'ETH_OLD', 'BNB-TEST'). -/// +/// /// Return Value: /// - (String): The ticker of the coin, with the suffixes removed. -/// +/// /// Example Usage: /// ```dart /// String abbr = 'ETH-ERC20'; -/// +/// /// String ticker = abbr2Ticker(abbr); /// print(ticker); // Output: "ETH" /// ``` @@ -609,7 +615,7 @@ String? assertString(dynamic value) { case double: return value.toString(); default: - return value; + return value as String?; } } @@ -618,9 +624,33 @@ int? assertInt(dynamic value) { switch (value.runtimeType) { case String: - return int.parse(value); + return int.parse(value as String); + default: + return value as int?; + } +} + +double assertDouble(dynamic value) { + if (value == null) return double.nan; + + switch (value.runtimeType) { + case double: + return value as double; + case int: + return (value as int).toDouble(); + case String: + return double.tryParse(value as String) ?? double.nan; + case bool: + return (value as bool) ? 1.0 : 0.0; + case num: + return (value as num).toDouble(); default: - return value; + try { + return double.parse(value.toString()); + } catch (e, s) { + log('Error converting to double: $e', trace: s, isError: true); + return double.nan; + } } } diff --git a/lib/shared/utils/window/window.dart b/lib/shared/utils/window/window.dart new file mode 100644 index 0000000000..8276f49603 --- /dev/null +++ b/lib/shared/utils/window/window.dart @@ -0,0 +1,3 @@ +export 'window_stub.dart' + if (dart.library.io) './window_native.dart' + if (dart.library.html) './window_web.dart'; diff --git a/lib/shared/utils/window/window_native.dart b/lib/shared/utils/window/window_native.dart new file mode 100644 index 0000000000..897f0b43e8 --- /dev/null +++ b/lib/shared/utils/window/window_native.dart @@ -0,0 +1,9 @@ +String getOriginUrl() { + return 'https://app.komodoplatform.com'; +} + +void showMessageBeforeUnload(String message) { + // TODO: implement + // don't throw an exception here, since native platforms should continue + // to work even if we can't prevent closure +} diff --git a/lib/shared/utils/window/window_stub.dart b/lib/shared/utils/window/window_stub.dart new file mode 100644 index 0000000000..ad373f0569 --- /dev/null +++ b/lib/shared/utils/window/window_stub.dart @@ -0,0 +1,7 @@ +String getOriginUrl() { + throw UnsupportedError('stub getOrigin'); +} + +void showMessageBeforeUnload(String message) { + throw UnsupportedError('stub showMessageBeforeUnload'); +} diff --git a/lib/shared/utils/window/window_web.dart b/lib/shared/utils/window/window_web.dart new file mode 100644 index 0000000000..e76613c227 --- /dev/null +++ b/lib/shared/utils/window/window_web.dart @@ -0,0 +1,15 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +String getOriginUrl() { + return web.window.location.origin; +} + +void showMessageBeforeUnload(String message) { + web.window.onbeforeunload = (web.BeforeUnloadEvent event) { + event + ..preventDefault() + ..returnValue = message; + }.toJS; +} diff --git a/lib/views/bitrefill/bitrefill_button.dart b/lib/views/bitrefill/bitrefill_button.dart index 9f8a7e1f4f..b0e9cbc3bb 100644 --- a/lib/views/bitrefill/bitrefill_button.dart +++ b/lib/views/bitrefill/bitrefill_button.dart @@ -1,7 +1,5 @@ import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; @@ -37,9 +35,6 @@ class BitrefillButton extends StatefulWidget { } class _BitrefillButtonState extends State { - final bool isInAppBrowserSupported = - !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); - @override void initState() { context diff --git a/lib/views/bitrefill/bitrefill_inappbrowser_button.dart b/lib/views/bitrefill/bitrefill_inappbrowser_button.dart index 2955422df8..380a45c5f9 100644 --- a/lib/views/bitrefill/bitrefill_inappbrowser_button.dart +++ b/lib/views/bitrefill/bitrefill_inappbrowser_button.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -44,12 +43,6 @@ class BitrefillInAppBrowserButton extends StatefulWidget { class BitrefillInAppBrowserButtonState extends State { CustomInAppBrowser? browser; - final InAppBrowserClassSettings settings = InAppBrowserClassSettings( - browserSettings: InAppBrowserSettings(), - webViewSettings: InAppWebViewSettings( - isInspectable: kDebugMode, - ), - ); @override void initState() { diff --git a/lib/views/bitrefill/bitrefill_inappwebview_button.dart b/lib/views/bitrefill/bitrefill_inappwebview_button.dart index ac44a13e85..4533476d8d 100644 --- a/lib/views/bitrefill/bitrefill_inappwebview_button.dart +++ b/lib/views/bitrefill/bitrefill_inappwebview_button.dart @@ -67,6 +67,14 @@ class BitrefillInAppWebviewButtonState } Future _openDialog() async { + if (kIsWeb) { + await _showWebDialog(); + } else { + await _showFullScreenDialog(); + } + } + + Future _showWebDialog() async { await showDialog( context: context, builder: (BuildContext context) { @@ -79,18 +87,12 @@ class BitrefillInAppWebviewButtonState content: SizedBox( width: width, height: height, - child: Column( - children: [ - Expanded( - child: InAppWebView( - key: const Key('bitrefill-inappwebview'), - initialUrlRequest: _createUrlRequest(), - initialSettings: settings, - onWebViewCreated: _onCreated, - onConsoleMessage: _onConsoleMessage, - ), - ), - ], + child: InAppWebView( + key: const Key('bitrefill-inappwebview'), + initialUrlRequest: _createUrlRequest(), + initialSettings: settings, + onWebViewCreated: _onCreated, + onConsoleMessage: _onConsoleMessage, ), ), actions: [ @@ -106,6 +108,32 @@ class BitrefillInAppWebviewButtonState ); } + Future _showFullScreenDialog() async { + await Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.windowTitle), + foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, + elevation: 0, + ), + body: SafeArea( + child: InAppWebView( + key: const Key('bitrefill-inappwebview'), + initialUrlRequest: _createUrlRequest(), + initialSettings: settings, + onWebViewCreated: _onCreated, + onConsoleMessage: _onConsoleMessage, + ), + ), + ); + }, + ), + ); + } + // ignore: use_setters_to_change_properties void _onCreated(InAppWebViewController controller) { webViewController = controller; diff --git a/lib/views/fiat/address_bar.dart b/lib/views/fiat/address_bar.dart new file mode 100644 index 0000000000..4311a67393 --- /dev/null +++ b/lib/views/fiat/address_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class AddressBar extends StatelessWidget { + const AddressBar({ + required this.receiveAddress, + super.key, + }); + + final String? receiveAddress; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Card( + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + onTap: () => copyToClipBoard(context, receiveAddress!), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + if (receiveAddress != null && receiveAddress!.isNotEmpty) + const Icon(Icons.copy, size: 16) + else + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + truncateMiddleSymbols(receiveAddress ?? ''), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/fiat/custom_fiat_input_field.dart b/lib/views/fiat/custom_fiat_input_field.dart new file mode 100644 index 0000000000..050b3d661f --- /dev/null +++ b/lib/views/fiat/custom_fiat_input_field.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:web_dex/shared/constants.dart'; + +class CustomFiatInputField extends StatelessWidget { + const CustomFiatInputField({ + required this.controller, + required this.hintText, + required this.onTextChanged, + required this.assetButton, + super.key, + this.label, + this.readOnly = false, + this.inputError, + }); + + final TextEditingController controller; + final String hintText; + final Widget? label; + final void Function(String?) onTextChanged; + final bool readOnly; + final Widget assetButton; + final String? inputError; + + @override + Widget build(BuildContext context) { + final textColor = Theme.of(context).colorScheme.onSurfaceVariant; + + final inputStyle = Theme.of(context).textTheme.headlineLarge?.copyWith( + fontSize: 18, + fontWeight: FontWeight.w300, + color: textColor, + letterSpacing: 1.1, + ); + + final InputDecoration inputDecoration = InputDecoration( + label: label, + labelStyle: inputStyle, + fillColor: Theme.of(context).colorScheme.onSurface, + floatingLabelStyle: + Theme.of(context).inputDecorationTheme.floatingLabelStyle, + floatingLabelBehavior: FloatingLabelBehavior.always, + contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + hintText: hintText, + border: const OutlineInputBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(4), + topLeft: Radius.circular(4), + bottomRight: Radius.circular(18), + topRight: Radius.circular(18), + ), + ), + errorText: inputError, + errorMaxLines: 1, + helperText: '', + ); + + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.centerRight, + children: [ + TextField( + autofocus: true, + controller: controller, + style: inputStyle, + decoration: inputDecoration, + readOnly: readOnly, + onChanged: onTextChanged, + inputFormatters: [FilteringTextInputFormatter.allow(numberRegExp)], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + Positioned( + right: 16, + bottom: 26, + top: 2, + child: assetButton, + ), + ], + ); + } +} diff --git a/lib/views/fiat/fiat_asset_icon.dart b/lib/views/fiat/fiat_asset_icon.dart new file mode 100644 index 0000000000..a927a4aa41 --- /dev/null +++ b/lib/views/fiat/fiat_asset_icon.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; +import 'package:web_dex/shared/widgets/coin_icon.dart'; +import 'package:web_dex/views/fiat/fiat_icon.dart'; + +class FiatAssetIcon extends StatelessWidget { + const FiatAssetIcon({ + required this.currency, + required this.icon, + required this.onTap, + required this.assetExists, + super.key, + }); + + final ICurrency currency; + final Widget icon; + final VoidCallback onTap; + final bool? assetExists; + + @override + Widget build(BuildContext context) { + const double size = 36.0; + + if (currency.isFiat) { + return FiatIcon(symbol: currency.symbol); + } + + if (assetExists ?? false) { + return CoinIcon(currency.symbol, size: size); + } else { + return icon; + } + } +} diff --git a/lib/views/fiat/fiat_currency_item.dart b/lib/views/fiat/fiat_currency_item.dart new file mode 100644 index 0000000000..12ed88c311 --- /dev/null +++ b/lib/views/fiat/fiat_currency_item.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; +import 'package:web_dex/shared/widgets/coin_icon.dart'; +import 'package:web_dex/views/fiat/fiat_currency_list_tile.dart'; +import 'package:web_dex/views/fiat/fiat_select_button.dart'; + +class FiatCurrencyItem extends StatelessWidget { + const FiatCurrencyItem({ + required this.foregroundColor, + required this.disabled, + required this.currency, + required this.icon, + required this.onTap, + required this.isListTile, + super.key, + }); + + final Color foregroundColor; + final bool disabled; + final ICurrency currency; + final Widget icon; + final VoidCallback onTap; + final bool isListTile; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: currency.isFiat + ? Future.value(true) + : checkIfAssetExists(currency.symbol), + builder: (context, snapshot) { + final assetExists = snapshot.connectionState == ConnectionState.done + ? snapshot.data ?? false + : null; + return isListTile + ? FiatCurrencyListTile( + currency: currency, + icon: icon, + onTap: onTap, + assetExists: assetExists, + ) + : FiatSelectButton( + context: context, + foregroundColor: foregroundColor, + enabled: !disabled, + currency: currency, + icon: icon, + onTap: onTap, + assetExists: assetExists, + ); + }, + ); + } +} diff --git a/lib/views/fiat/fiat_currency_list_tile.dart b/lib/views/fiat/fiat_currency_list_tile.dart new file mode 100644 index 0000000000..a10d707b6e --- /dev/null +++ b/lib/views/fiat/fiat_currency_list_tile.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; +import 'package:web_dex/model/coin_utils.dart'; +import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; +import 'package:web_dex/views/fiat/fiat_asset_icon.dart'; + +class FiatCurrencyListTile extends StatelessWidget { + const FiatCurrencyListTile({ + required this.currency, + required this.icon, + required this.onTap, + required this.assetExists, + super.key, + }); + + final ICurrency currency; + final Widget icon; + final VoidCallback onTap; + final bool? assetExists; + + @override + Widget build(BuildContext context) { + final coinType = currency.isCrypto + ? getCoinTypeName((currency as CryptoCurrency).chainType) + : ''; + + return ListTile( + leading: FiatAssetIcon( + currency: currency, + icon: icon, + onTap: onTap, + assetExists: assetExists, + ), + title: Row( + children: [ + // Use Expanded to let AutoScrollText take all available space + Expanded( + child: AutoScrollText( + text: '${currency.name}$coinType', + ), + ), + // Align the text to the right + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text(currency.symbol), + ), + ), + ], + ), + onTap: onTap, + ); + } +} diff --git a/lib/views/fiat/fiat_form.dart b/lib/views/fiat/fiat_form.dart index e95de03aee..86d9af0ede 100644 --- a/lib/views/fiat/fiat_form.dart +++ b/lib/views/fiat/fiat_form.dart @@ -1,101 +1,54 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:universal_html/html.dart' - as html; //TODO! Non-web implementation import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; +import 'package:web_dex/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; -import 'package:web_dex/bloc/fiat/fiat_repository.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_mode.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; +import 'package:web_dex/model/forms/fiat/fiat_amount_input.dart'; import 'package:web_dex/shared/ui/gradient_border.dart'; -import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/connect_wallet/connect_wallet_wrapper.dart'; -import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/views/fiat/fiat_action_tab.dart'; import 'package:web_dex/views/fiat/fiat_inputs.dart'; -import 'package:web_dex/views/fiat/fiat_payment_method.dart'; +import 'package:web_dex/views/fiat/fiat_payment_methods_grid.dart'; +import 'package:web_dex/views/fiat/webview_dialog.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; class FiatForm extends StatefulWidget { - const FiatForm({required this.onCheckoutComplete, super.key}); - - // TODO: Remove this when we have a proper bloc for this page - final Function({required bool isSuccess}) onCheckoutComplete; + const FiatForm({super.key}); @override State createState() => _FiatFormState(); } -enum FiatMode { onramp, offramp } - class _FiatFormState extends State { - int _activeTabIndex = 0; - - Currency _selectedFiat = Currency( - "USD", - 'United States Dollar', - isFiat: true, - ); - - Currency _selectedCoin = Currency( - "BTC", - 'Bitcoin', - chainType: CoinType.utxo, - isFiat: false, - ); - - Map? selectedPaymentMethod; - Map? selectedPaymentMethodPrice; - String? accountReference; - String? coinReceiveAddress; // null if not set, '' if not found - String? fiatAmount; - String? checkoutUrl; - bool loading = false; - bool orderFailed = false; - Map? error; - List>? paymentMethods; - Timer? _fiatInputDebounce; - - static const bool useSimpleLoadingSpinner = true; - - static const fillerFiatAmount = '100000'; - bool _isLoggedIn = currentWalletBloc.wallet != null; StreamSubscription>? _walletCoinsListener; StreamSubscription? _loginActivationListener; - StreamSubscription>>? _paymentMethodsListener; - - FiatMode get selectedFiatMode => [ - FiatMode.onramp, - FiatMode.offramp, - ].elementAt(_activeTabIndex); @override void dispose() { _walletCoinsListener?.cancel(); _loginActivationListener?.cancel(); - _paymentMethodsListener?.cancel(); - _fiatInputDebounce?.cancel(); super.dispose(); } - void _setActiveTab(int i) { - setState(() { - _activeTabIndex = i; - }); - } + void _setActiveTab(int i) => + context.read().add(FiatModeChanged.fromTabIndex(i)); - void _handleAccountStatusChange(bool isLoggedIn) async { + Future _handleAccountStatusChange(bool isLoggedIn) async { if (_isLoggedIn != isLoggedIn) { setState(() => _isLoggedIn = isLoggedIn); } @@ -103,7 +56,9 @@ class _FiatFormState extends State { if (isLoggedIn) { await fillAccountInformation(); } else { - await _clearAccountData(); + context + .read() + .add(const ClearAccountInformationRequested()); } } @@ -112,434 +67,79 @@ class _FiatFormState extends State { super.initState(); _walletCoinsListener = coinsBloc.outWalletCoins.listen((walletCoins) async { - _handleAccountStatusChange(walletCoins.isNotEmpty); + await _handleAccountStatusChange(walletCoins.isNotEmpty); }); _loginActivationListener = coinsBloc.outLoginActivationFinished.listen((isLoggedIn) async { - _handleAccountStatusChange(isLoggedIn); - }); - - // Prefetch the hardcoded pair (like USD/BTC) - _refreshForm(); - } - - Future getCoinAddress(String abbr) async { - if (_isLoggedIn && currentWalletBloc.wallet != null) { - final accountKey = currentWalletBloc.wallet!.id; - final abbrKey = abbr; - - // Cache check - if (coinsBloc.addressCache.containsKey(accountKey) && - coinsBloc.addressCache[accountKey]!.containsKey(abbrKey)) { - return coinsBloc.addressCache[accountKey]![abbrKey]; - } else { - await activateCoinIfNeeded(abbr); - final coin = - coinsBloc.walletCoins.firstWhereOrNull((c) => c.abbr == abbr); - - if (coin != null && coin.address != null) { - if (!coinsBloc.addressCache.containsKey(accountKey)) { - coinsBloc.addressCache[accountKey] = {}; - } - - // Cache this wallet's addresses - for (final walletCoin in coinsBloc.walletCoins) { - if (walletCoin.address != null && - !coinsBloc.addressCache[accountKey]! - .containsKey(walletCoin.abbr)) { - // Exit if the address already exists in a different account - // Address belongs to another account, this is a bug, gives outdated data - for (final entry in coinsBloc.addressCache.entries) { - if (entry.key != accountKey && - entry.value.containsValue(walletCoin.address)) { - return null; - } - } - - coinsBloc.addressCache[accountKey]![walletCoin.abbr] = - walletCoin.address!; - } - } - - return coinsBloc.addressCache[accountKey]![abbrKey]; - } - } - } - - return null; - } - - Future _updateFiatAmount(String? value) async { - setState(() { - fiatAmount = value; - }); - - if (_fiatInputDebounce?.isActive ?? false) { - _paymentMethodsListener?.cancel(); - _fiatInputDebounce!.cancel(); - } - _fiatInputDebounce = Timer(const Duration(milliseconds: 500), () async { - fillPaymentMethods(_selectedFiat.symbol, _selectedCoin, - forceUpdate: true); + await _handleAccountStatusChange(isLoggedIn); }); - } - - String? getNonZeroFiatAmount() { - if (fiatAmount == null) return null; - final amount = double.tryParse(fiatAmount!); - - if (amount == null || amount < 10) return null; - return fiatAmount; - } - - Future _updateSelectedOptions( - Currency selectedFiat, - Currency selectedCoin, { - bool forceUpdate = false, - }) async { - bool coinChanged = _selectedCoin != selectedCoin; - bool fiatChanged = _selectedFiat != selectedFiat; - - // Set coins - setState(() { - _selectedFiat = selectedFiat; - _selectedCoin = selectedCoin; - - // Clear the previous data - if (forceUpdate || coinChanged || fiatChanged) { - selectedPaymentMethod = null; - selectedPaymentMethodPrice = null; - _paymentMethodsListener?.cancel(); - paymentMethods = null; - } - - if (forceUpdate) accountReference = null; - - if (forceUpdate || coinChanged) coinReceiveAddress = null; - }); - - // Fetch new payment methods based on the selected options - if (forceUpdate || (fiatChanged || coinChanged)) { - fillAccountInformation(); - fillPaymentMethods(_selectedFiat.symbol, _selectedCoin, - forceUpdate: true); - } - } - Future fillAccountReference() async { - final address = await getCoinAddress('KMD'); - - if (!mounted) return; - setState(() { - accountReference = address; - }); - } - - Future fillCoinReceiveAddress() async { - final address = await getCoinAddress(_selectedCoin.getAbbr()); - - if (!mounted) return; - setState(() { - coinReceiveAddress = address; - }); + context.read() + ..add(const LoadCurrencyListsRequested()) + ..add(const RefreshFormRequested(forceRefresh: true)); } Future fillAccountInformation() async { - fillAccountReference(); - fillCoinReceiveAddress(); + context.read().add(const AccountInformationChanged()); } - Future fillPaymentMethods(String fiat, Currency coin, - {bool forceUpdate = false}) async { - try { - final sourceAmount = getNonZeroFiatAmount(); - _paymentMethodsListener = fiatRepository - .getPaymentMethodsList(fiat, coin, sourceAmount ?? fillerFiatAmount) - .listen((newPaymentMethods) { - setState(() { - paymentMethods = newPaymentMethods; - }); - - // if fiat amount has changed, exit early - final fiatChanged = sourceAmount != getNonZeroFiatAmount(); - final coinChanged = _selectedCoin != coin; - if (fiatChanged || coinChanged) { - return; - } - - if ((forceUpdate || selectedPaymentMethod == null) && - paymentMethods!.isNotEmpty) { - final method = selectedPaymentMethod == null - ? paymentMethods!.first - : paymentMethods!.firstWhere( - (method) => method['id'] == selectedPaymentMethod!['id'], - orElse: () => paymentMethods!.first); - changePaymentMethod(method); - } - }); - } catch (e) { - setState(() { - paymentMethods = []; - }); - } - } - - Future changePaymentMethod(Map method) async { - setState(() { - selectedPaymentMethod = method; - - if (selectedPaymentMethod != null) { - final sourceAmount = getNonZeroFiatAmount(); - final currentPriceInfo = selectedPaymentMethod!['price_info']; - final priceInfo = sourceAmount == null || - currentPriceInfo == null || - double.parse(sourceAmount) != - double.parse( - selectedPaymentMethod!['price_info']['fiat_amount']) - ? null - : currentPriceInfo; - - selectedPaymentMethodPrice = priceInfo; - } else { - // selectedPaymentMethodPrice = null; - } - }); - } - - String? getFormIssue() { - if (!_isLoggedIn) { - return 'Please connect your wallet to purchase coins'; - } - if (paymentMethods == null) { - return 'Payment methods not fetched yet'; - } - if (paymentMethods!.isEmpty) { - return 'No payment method for this pair'; - } - if (coinReceiveAddress == null) { - return 'Wallet adress is not fetched yet or no login'; - } - if (coinReceiveAddress!.isEmpty) { - return 'No wallet, or coin/network might not be supported'; - } - if (accountReference == null) { - return 'Account reference (KMD Address) is not fetched yet'; - } - if (accountReference!.isEmpty) { - return 'Account reference (KMD Address) could not be fetched'; - } - if (fiatAmount == null) { - return 'Fiat amount is not set'; - } - if (fiatAmount!.isEmpty) { - return 'Fiat amount is empty'; - } - - final fiatAmountValue = getFiatAmountValue(); - - if (fiatAmountValue == null) { - return 'Invalid fiat amount'; - } - if (fiatAmountValue <= 0) { - return 'Fiat amount should be higher than zero'; - } - - if (selectedPaymentMethod == null) { - return 'Fiat not selected'; - } - - final boundariesError = getBoundariesError(); - if (boundariesError != null) return boundariesError; - - return null; - } - - String? getBoundariesError() { - return isFiatAmountTooLow() - ? 'Please enter more than ${getMinFiatAmount()} ${_selectedFiat.symbol}' - : isFiatAmountTooHigh() - ? 'Please enter less than ${getMaxFiatAmount()} ${_selectedFiat.symbol}' - : null; - } - - double? getFiatAmountValue() { - if (fiatAmount == null) return null; - return double.tryParse(fiatAmount!); - } - - Map? getFiatLimitData() { - if (selectedPaymentMethod == null) return null; - - final txLimits = - selectedPaymentMethod!['transaction_limits'] as List?; - if (txLimits == null || txLimits.isEmpty) return null; - - final limitData = txLimits.first; - if (limitData.isEmpty) return null; - - final fiatCode = limitData['fiat_code']; - if (fiatCode == null || fiatCode != _selectedFiat.symbol) return null; - - return limitData; - } - - double? getMinFiatAmount() { - final limitData = getFiatLimitData(); - if (limitData == null) return null; - return double.tryParse(limitData['min']); + void _showOrderFailedSnackbar() { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocaleKeys.orderFailedTryAgain.tr()), + ), + ); } - double? getMaxFiatAmount() { - final limitData = getFiatLimitData(); - if (limitData == null) return null; - return double.tryParse(limitData['max']); - } + void completeOrder() => + context.read().add(FormSubmissionRequested()); - bool isFiatAmountTooHigh() { - final fiatAmountValue = getFiatAmountValue(); - if (fiatAmountValue == null) return false; + Future openCheckoutPage(String checkoutUrl, String orderId) async { + if (checkoutUrl.isEmpty) return; - final limit = getMaxFiatAmount(); - if (limit == null) return false; + // Only web requires the intermediate html page to satisfy cors rules and + // allow for console.log and postMessage events to be handled. + final url = + kIsWeb ? BaseFiatProvider.fiatWrapperPageUrl(checkoutUrl) : checkoutUrl; - return fiatAmountValue > limit; + return WebViewDialog.show( + context, + url: url, + title: LocaleKeys.buy.tr(), + onConsoleMessage: _onConsoleMessage, + onCloseWindow: _onCloseWebView, + ); } - bool isFiatAmountTooLow() { - final fiatAmountValue = getFiatAmountValue(); - if (fiatAmountValue == null) return false; - - final limit = getMinFiatAmount(); - if (limit == null) return false; + void _onConsoleMessage(String message) => context + .read() + .add(FiatOnRampPaymentStatusMessageReceived(message)); - return fiatAmountValue < limit; + void _onCloseWebView() { + // TODO: decide whether to consider a closed webview as "failed" } - //TODO! Non-web native implementation - String successUrl() { - // Base URL to the HTML redirect page - final baseUrl = '${html.window.location.origin}/assets' - '/web_pages/checkout_status_redirect.html'; - - final queryString = { - 'account_reference': accountReference!, - 'status': 'success', - } - .entries - .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') - .join('&'); - - return '$baseUrl?$queryString'; - } + Future _handlePaymentStatusUpdate(FiatFormState stateSnapshot) async { + //TODO? We can still show the alerts if we're no mounted by using the + // app's navigator key. This will be useful if the user has navigated + // to another page before completing the order. + if (!mounted) return; - Future completeOrder() async { - final formIssue = getFormIssue(); - if (formIssue != null) { - log('Fiat order form is not complete: $formIssue'); + final status = stateSnapshot.fiatOrderStatus; + if (status == FiatOrderStatus.submitted) { + // ignore: use_build_context_synchronously + context.read().add(const WatchOrderStatusRequested()); + await openCheckoutPage(stateSnapshot.checkoutUrl, stateSnapshot.orderId); return; } - setState(() { - checkoutUrl = null; - orderFailed = false; - loading = true; - error = null; - }); - - try { - final newOrder = await fiatRepository.buyCoin( - accountReference!, - _selectedFiat.symbol, - _selectedCoin, - coinReceiveAddress!, - selectedPaymentMethod!, - fiatAmount!, - successUrl(), - ); - - setState(() { - checkoutUrl = newOrder['data']?['order']?['checkout_url']; - orderFailed = checkoutUrl == null; - loading = false; - error = null; - - if (!orderFailed) { - return openCheckoutPage(); - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(LocaleKeys.orderFailedTryAgain.tr()), - ), - ); - }); - - log('New order failed: $newOrder'); - - if (error != null) { - log( - 'Error message: ${'${error!['code'] ?? ''} ' - '- ${error!['title']}${error!['description'] != null ? ' - Details:' - ' ${error!['description']}' : ''}'}', - ); - } - - // Ramp does not have an order ID - // TODO: Abstract out provider-specific order ID parsing. - final maybeOrderId = newOrder['data']['order']['id'] as String? ?? ''; - showOrderStatusUpdates(selectedPaymentMethod!, maybeOrderId); - } catch (e) { - setState(() { - checkoutUrl = null; - orderFailed = true; - loading = false; - if (e is Map && e.containsKey('errors')) { - error = e['errors']; - } else { - error = null; - } - }); + if (status == FiatOrderStatus.failed) { + _showOrderFailedSnackbar(); } - } - - void openCheckoutPage() { - if (checkoutUrl == null) return; - launchURL(checkoutUrl!, inSeparateTab: true); - } - void showOrderStatusUpdates( - Map paymentMethod, - String orderId, - ) async { - FiatOrderStatus? lastStatus; - // TODO: Move to bloc & use bloc listener to show changes. - final statusStream = - fiatRepository.watchOrderStatus(paymentMethod, orderId); - - await for (final status in statusStream) { - //TODO? We can still show the alerts if we're no mounted by using the - // app's navigator key. This will be useful if the user has navigated - // to another page before completing the order. - if (!mounted) return; - - if (lastStatus == status) continue; - lastStatus = status; - - if (status != FiatOrderStatus.pending && checkoutUrl != null) { - setState(() => checkoutUrl = null); - } - - if (status == FiatOrderStatus.failed) setState(() => orderFailed = true); - - if (status != FiatOrderStatus.pending) { - showPaymentStatusDialog(status); - - // TODO: Differentiate between inProgress and success callback in bloc. - // That will deftermine whether they are changed to the "In Progress" - // tab or the "History" tab. - widget.onCheckoutComplete(isSuccess: true); - } + if (status != FiatOrderStatus.pending) { + showPaymentStatusDialog(status); } } @@ -556,32 +156,30 @@ class _FiatFormState extends State { case FiatOrderStatus.pending: throw Exception('Pending status should not be shown in dialog.'); + case FiatOrderStatus.submitted: + title = LocaleKeys.fiatPaymentSubmittedTitle.tr(); + content = LocaleKeys.fiatPaymentSubmittedMessage.tr(); + icon = const Icon(Icons.open_in_new); + case FiatOrderStatus.success: - title = 'Order successful!'; - content = 'Your coins have been deposited to your wallet.'; + title = LocaleKeys.fiatPaymentSuccessTitle.tr(); + content = LocaleKeys.fiatPaymentSuccessMessage.tr(); icon = const Icon(Icons.check_circle_outline); - break; case FiatOrderStatus.failed: - title = 'Payment failed'; - // TODO: Localise all [FiatOrderStatus] messages. If we implement - // provider-specific error messages, we can include support details. - content = 'Your payment has failed. Please check your email for ' - 'more information or contact the provider\'s support.'; + title = LocaleKeys.fiatPaymentFailedTitle.tr(); + // TODO: If we implement provider-specific error messages, + // we can include support details. + content = LocaleKeys.fiatPaymentFailedMessage.tr(); icon = const Icon(Icons.error_outline, color: Colors.red); - break; case FiatOrderStatus.inProgress: - title = 'Payment received'; - content = 'Congratulations! Your payment has been received and the ' - 'coins are on the way to your wallet. \n\n' - 'You will receive your coins in 1-60 minutes.'; + title = LocaleKeys.fiatPaymentInProgressTitle.tr(); + content = LocaleKeys.fiatPaymentInProgressMessage.tr(); icon = const Icon(Icons.hourglass_bottom_outlined); - break; } - //TODO: Localize - showAdaptiveDialog( + showAdaptiveDialog( context: context, builder: (context) => AlertDialog.adaptive( title: Text(title!), @@ -597,141 +195,8 @@ class _FiatFormState extends State { ).ignore(); } - Widget buildPaymentMethodsSection() { - final isLoading = paymentMethods == null; - if (isLoading) { - return useSimpleLoadingSpinner - ? const UiSpinner( - width: 36, - height: 36, - strokeWidth: 4, - ) - : _buildSkeleton(); - } - - final hasPaymentMethods = paymentMethods?.isNotEmpty ?? false; - if (!hasPaymentMethods) { - return Center( - child: Text( - LocaleKeys.noOptionsToPurchase - .tr(args: [_selectedCoin.symbol, _selectedFiat.symbol]), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - ); - } else { - final groupedPaymentMethods = - groupPaymentMethodsByProviderId(paymentMethods!); - return Column( - children: [ - for (var entry in groupedPaymentMethods.entries) ...[ - _buildPaymentMethodGroup(entry.key, entry.value), - const SizedBox(height: 16), - ], - ], - ); - } - } - - Map>> groupPaymentMethodsByProviderId( - List> paymentMethods) { - final groupedMethods = >>{}; - for (final method in paymentMethods) { - final providerId = method['provider_id']; - if (!groupedMethods.containsKey(providerId)) { - groupedMethods[providerId] = []; - } - groupedMethods[providerId]!.add(method); - } - return groupedMethods; - } - - Widget _buildPaymentMethodGroup( - String providerId, List>? methods) { - return Card( - margin: const EdgeInsets.all(0), - color: Theme.of(context).colorScheme.onSurface, - elevation: 4, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: Theme.of(context).primaryColor.withOpacity( - selectedPaymentMethod != null && - selectedPaymentMethod!['provider_id'] == providerId - ? 1 - : 0.25)), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(providerId), - const SizedBox(height: 16), - GridView.builder( - shrinkWrap: true, - itemCount: methods!.length, - // TODO: Improve responsiveness by making crossAxisCount dynamic based on - // min and max child width. - gridDelegate: _gridDelegate, - itemBuilder: (context, index) { - return FiatPaymentMethod( - key: ValueKey(index), - fiatAmount: getNonZeroFiatAmount(), - paymentMethodData: methods[index], - selectedPaymentMethod: selectedPaymentMethod, - onSelect: changePaymentMethod, - ); - }, - ), - ], - ), - ), - ); - } - - Widget _buildSkeleton() { - return GridView( - shrinkWrap: true, - gridDelegate: _gridDelegate, - children: - List.generate(4, (index) => const Card(child: SkeletonListTile())), - ); - } - - SliverGridDelegate get _gridDelegate => - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isMobile ? 1 : 2, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - mainAxisExtent: 90, - ); - - Future _refreshForm() async { - await _updateSelectedOptions( - _selectedFiat, - _selectedCoin, - forceUpdate: true, - ); - } - - Future _clearAccountData() async { - setState(() { - coinReceiveAddress = null; - accountReference = null; - }); - } - @override Widget build(BuildContext context) { - final formIssue = getFormIssue(); - - final canSubmit = !loading && - accountReference != null && - formIssue == null && - error == null; - // TODO: Add optimisations to re-use the generated checkout URL if the user // submits the form again without changing any data. When the user presses // the "Buy Now" button, we create the checkout URL and open it in a new @@ -746,72 +211,116 @@ class _FiatFormState extends State { // orders that were never completed. final scrollController = ScrollController(); - return DexScrollbar( - isMobile: isMobile, - scrollController: scrollController, - child: SingleChildScrollView( - key: const Key('fiat-form-scroll'), - controller: scrollController, - child: Column( - children: [ - FiatActionTabBar( - currentTabIndex: _activeTabIndex, - onTabClick: _setActiveTab, - ), - const SizedBox(height: 16), - if (selectedFiatMode == FiatMode.offramp) - Center(child: Text(LocaleKeys.comingSoon.tr())) - else - GradientBorder( - innerColor: dexPageColors.frontPlate, - gradient: dexPageColors.formPlateGradient, - child: Container( - padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), - child: Column( - children: [ - FiatInputs( - onUpdate: _updateSelectedOptions, - onFiatAmountUpdate: _updateFiatAmount, - initialFiat: _selectedFiat, - initialCoin: _selectedCoin, - selectedPaymentMethodPrice: selectedPaymentMethodPrice, - receiveAddress: coinReceiveAddress, - isLoggedIn: _isLoggedIn, - fiatMinAmount: getMinFiatAmount(), - fiatMaxAmount: getMaxFiatAmount(), - boundariesError: getBoundariesError(), - ), - const SizedBox(height: 16), - buildPaymentMethodsSection(), - const SizedBox(height: 16), - ConnectWalletWrapper( - key: const Key('connect-wallet-fiat-form'), - eventType: WalletsManagerEventType.fiat, - child: UiPrimaryButton( - height: 40, - text: loading - ? '${LocaleKeys.submitting.tr()}...' - : LocaleKeys.buyNow.tr(), - onPressed: canSubmit ? completeOrder : null, + return BlocConsumer( + listenWhen: (previous, current) => + previous.fiatOrderStatus != current.fiatOrderStatus, + listener: (context, state) => _handlePaymentStatusUpdate(state), + builder: (context, state) => DexScrollbar( + isMobile: isMobile, + scrollController: scrollController, + child: SingleChildScrollView( + key: const Key('fiat-form-scroll'), + controller: scrollController, + child: Column( + children: [ + FiatActionTabBar( + currentTabIndex: state.fiatMode.tabIndex, + onTabClick: _setActiveTab, + ), + const SizedBox(height: 16), + if (state.fiatMode == FiatMode.offramp) + Center(child: Text(LocaleKeys.comingSoon.tr())) + else + GradientBorder( + innerColor: dexPageColors.frontPlate, + gradient: dexPageColors.formPlateGradient, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), + child: Column( + children: [ + FiatInputs( + onFiatCurrencyChanged: _onFiatChanged, + onCoinChanged: _onCoinChanged, + onFiatAmountUpdate: _onFiatAmountChanged, + initialFiat: state.selectedFiat.value!, + initialCoin: state.selectedCoin.value!, + initialFiatAmount: state.fiatAmount.valueAsDouble, + fiatList: state.fiatList, + coinList: state.coinList, + selectedPaymentMethodPrice: + state.selectedPaymentMethod.priceInfo, + receiveAddress: state.coinReceiveAddress, + isLoggedIn: _isLoggedIn, + fiatMinAmount: state.minFiatAmount, + fiatMaxAmount: state.maxFiatAmount, + boundariesError: state.fiatAmount.error?.text(state), + ), + const SizedBox(height: 16), + FiatPaymentMethodsGrid(state: state), + const SizedBox(height: 16), + ConnectWalletWrapper( + key: const Key('connect-wallet-fiat-form'), + eventType: WalletsManagerEventType.fiat, + child: UiPrimaryButton( + key: const Key('fiat-onramp-submit-button'), + height: 40, + text: state.fiatOrderStatus.isSubmitting + ? '${LocaleKeys.submitting.tr()}...' + : LocaleKeys.buyNow.tr(), + onPressed: state.canSubmit ? completeOrder : null, + ), ), - ), - const SizedBox(height: 16), - Text( - _isLoggedIn - ? error != null - ? LocaleKeys.fiatCantCompleteOrder.tr() - : LocaleKeys.fiatPriceCanChange.tr() - : LocaleKeys.fiatConnectWallet.tr(), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, - ) - ], + const SizedBox(height: 16), + Text( + _isLoggedIn + ? state.fiatOrderStatus.isFailed + ? LocaleKeys.fiatCantCompleteOrder.tr() + : LocaleKeys.fiatPriceCanChange.tr() + : LocaleKeys.fiatConnectWallet.tr(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), ), - ) - ], + ], + ), ), ), ); } + + void _onFiatChanged(ICurrency value) => context.read() + ..add(SelectedFiatCurrencyChanged(value)) + ..add(const RefreshFormRequested(forceRefresh: true)); + + void _onCoinChanged(ICurrency value) => context.read() + ..add(SelectedCoinChanged(value)) + ..add(const RefreshFormRequested(forceRefresh: true)); + + void _onFiatAmountChanged(String? value) => context.read() + ..add(FiatAmountChanged(value ?? '0')) + ..add(const RefreshFormRequested(forceRefresh: true)); +} + +extension on FiatAmountValidationError { + String? text(FiatFormState state) { + final fiatId = state.selectedFiat.value?.symbol ?? ''; + switch (this) { + case FiatAmountValidationError.aboveMaximum: + return LocaleKeys.fiatMaximumAmount + .tr(args: [state.maxFiatAmount?.toString() ?? '', fiatId]); + case FiatAmountValidationError.invalid: + case FiatAmountValidationError.belowMinimum: + return LocaleKeys.fiatMinimumAmount.tr( + args: [ + state.minFiatAmount?.toString() ?? '', + fiatId, + ], + ); + case FiatAmountValidationError.empty: + return null; + } + } } diff --git a/lib/views/fiat/fiat_icon.dart b/lib/views/fiat/fiat_icon.dart index 4e0a30cca2..e81bdaae07 100644 --- a/lib/views/fiat/fiat_icon.dart +++ b/lib/views/fiat/fiat_icon.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:web_dex/app_config/app_config.dart'; @@ -17,7 +17,7 @@ class FiatIcon extends StatefulWidget { } class _FiatIconState extends State { - bool? _assetExists; + bool _assetExists = false; @override void initState() { @@ -37,9 +37,11 @@ class _FiatIconState extends State { void setOrFetchAssetExistence() { if (_knownAssetExistence != null) { - setState(() => _assetExists = _knownAssetExistence); + setState(() => _assetExists = _knownAssetExistence!); } else { _checkIfAssetExists(context).then((exists) { + if (!mounted) return; + setState(() => _assetExists = exists); }); } @@ -54,11 +56,10 @@ class _FiatIconState extends State { return _knownAssetExistence != null ? Future.value(_knownAssetExistence) : Future(() async { - // ignore: use_build_context_synchronously - final bundle = await _loadAssetManifest(context); + if (!mounted) return false; - // Check if asset exists in the asset bundle - final assetExists = bundle.contains(_assetPath); + // ignore: use_build_context_synchronously + final assetExists = await _doesAssetExist(context, _assetPath); FiatIcon._assetExistenceCache[_assetPath] = assetExists; @@ -66,11 +67,15 @@ class _FiatIconState extends State { }); } - Future> _loadAssetManifest(BuildContext context) async { - String manifestContent = - await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); - Map manifestMap = json.decode(manifestContent); - return manifestMap.keys.toSet(); + Future _doesAssetExist(BuildContext context, String assetPath) async { + try { + // Try to load the image asset + await precacheImage(AssetImage(assetPath), context); + return true; + } catch (e) { + // Asset could not be loaded, return false + return false; + } } @override @@ -82,7 +87,7 @@ class _FiatIconState extends State { child: Container( alignment: Alignment.center, width: 36, - child: (_assetExists == true) + child: _assetExists ? Image.asset( '${FiatIcon._fiatAssetsFolder}/${widget.symbol.toLowerCase()}.webp', key: Key(widget.symbol), diff --git a/lib/views/fiat/fiat_inputs.dart b/lib/views/fiat/fiat_inputs.dart index 74e1744543..799bbbac30 100644 --- a/lib/views/fiat/fiat_inputs.dart +++ b/lib/views/fiat/fiat_inputs.dart @@ -1,41 +1,48 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; -import 'package:web_dex/bloc/fiat/fiat_repository.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_price_info.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin_utils.dart'; -import 'package:web_dex/shared/constants.dart'; -import 'package:web_dex/shared/utils/formatters.dart'; -import 'package:web_dex/shared/utils/utils.dart'; -import 'package:web_dex/shared/widgets/coin_icon.dart'; +import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; +import 'package:web_dex/views/fiat/address_bar.dart'; +import 'package:web_dex/views/fiat/custom_fiat_input_field.dart'; +import 'package:web_dex/views/fiat/fiat_currency_item.dart'; import 'package:web_dex/views/fiat/fiat_icon.dart'; class FiatInputs extends StatefulWidget { - final Function(Currency, Currency) onUpdate; - final Function(String?) onFiatAmountUpdate; - final Currency initialFiat; - final Currency initialCoin; - final Map? selectedPaymentMethodPrice; - final bool isLoggedIn; - final String? receiveAddress; - final double? fiatMinAmount; - final double? fiatMaxAmount; - final String? boundariesError; - const FiatInputs({ - required this.onUpdate, - required this.onFiatAmountUpdate, required this.initialFiat, + required this.initialFiatAmount, required this.initialCoin, + required this.fiatList, + required this.coinList, required this.receiveAddress, required this.isLoggedIn, + required this.onFiatCurrencyChanged, + required this.onCoinChanged, + required this.onFiatAmountUpdate, + super.key, this.selectedPaymentMethodPrice, this.fiatMinAmount, this.fiatMaxAmount, this.boundariesError, }); + final ICurrency initialFiat; + final double? initialFiatAmount; + final ICurrency initialCoin; + final Iterable fiatList; + final Iterable coinList; + final FiatPriceInfo? selectedPaymentMethodPrice; + final bool isLoggedIn; + final String? receiveAddress; + final double? fiatMinAmount; + final double? fiatMaxAmount; + final String? boundariesError; + final void Function(ICurrency) onFiatCurrencyChanged; + final void Function(ICurrency) onCoinChanged; + final void Function(String?) onFiatAmountUpdate; + @override FiatInputsState createState() => FiatInputsState(); } @@ -43,16 +50,6 @@ class FiatInputs extends StatefulWidget { class FiatInputsState extends State { TextEditingController fiatController = TextEditingController(); - late Currency selectedFiat; - late Currency selectedCoin; - List fiatList = []; - List coinList = []; - - // As part of refactoring, we need to move this to a bloc state. In this - // instance, we wanted to show the loading indicator in the parent widget - // but that's not possible with the current implementation. - bool get isLoading => fiatList.length < 2 || coinList.length < 2; - @override void dispose() { fiatController.dispose(); @@ -63,65 +60,43 @@ class FiatInputsState extends State { @override void initState() { super.initState(); - setState(() { - selectedFiat = widget.initialFiat; - selectedCoin = widget.initialCoin; - fiatList = [widget.initialFiat]; - coinList = [widget.initialCoin]; - }); - initFiatList(); - initCoinList(); - } - - void initFiatList() async { - final list = await fiatRepository.getFiatList(); - if (mounted) { - setState(() { - fiatList = list; - }); - } + fiatController.text = widget.initialFiatAmount?.toString() ?? ''; } - void initCoinList() async { - final list = await fiatRepository.getCoinList(); - if (mounted) { - setState(() { - coinList = list; - }); + @override + void didUpdateWidget(FiatInputs oldWidget) { + super.didUpdateWidget(oldWidget); + + final double? newFiatAmount = widget.initialFiatAmount; + + // Convert the current text to double for comparison + final double currentFiatAmount = + double.tryParse(fiatController.text) ?? 0.0; + + // Compare using double values + if (newFiatAmount != currentFiatAmount) { + final newFiatAmountText = newFiatAmount?.toString() ?? ''; + fiatController + ..text = newFiatAmountText + ..selection = TextSelection.fromPosition( + TextPosition(offset: newFiatAmountText.length), + ); } } - void updateParent() { - widget.onUpdate( - selectedFiat, - selectedCoin, - ); - } - - void changeFiat(Currency? newValue) { + void changeFiat(ICurrency? newValue) { if (newValue == null) return; - if (mounted) { - setState(() { - selectedFiat = newValue; - }); - } - updateParent(); + widget.onFiatCurrencyChanged(newValue); } - void changeCoin(Currency? newValue) { + void changeCoin(ICurrency? newValue) { if (newValue == null) return; - if (mounted) { - setState(() { - selectedCoin = newValue; - }); - } - updateParent(); + widget.onCoinChanged(newValue); } void fiatAmountChanged(String? newValue) { - setState(() {}); widget.onFiatAmountUpdate(newValue); } @@ -129,11 +104,9 @@ class FiatInputsState extends State { Widget build(BuildContext context) { final priceInfo = widget.selectedPaymentMethodPrice; - final coinAmount = priceInfo != null && priceInfo.isNotEmpty - ? priceInfo['coin_amount'] - : null; - final fiatListLoading = fiatList.length <= 1; - final coinListLoading = coinList.length <= 1; + final coinAmount = priceInfo?.coinAmount; + final fiatListLoading = widget.fiatList.length <= 1; + final coinListLoading = widget.coinList.length <= 1; final boundariesString = widget.fiatMaxAmount == null && widget.fiatMinAmount == null @@ -143,16 +116,19 @@ class FiatInputsState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomFiatInputField( + key: const Key('fiat-amount-form-field'), controller: fiatController, hintText: '${LocaleKeys.enterAmount.tr()} $boundariesString', onTextChanged: fiatAmountChanged, label: Text(LocaleKeys.spend.tr()), - assetButton: _buildCurrencyItem( + assetButton: FiatCurrencyItem( + key: const Key('fiat-onramp-fiat-dropdown'), + foregroundColor: foregroundColor, disabled: fiatListLoading, - currency: selectedFiat, + currency: widget.initialFiat, icon: FiatIcon( - key: Key('fiat_icon_${selectedFiat.symbol}'), - symbol: selectedFiat.symbol, + key: Key('fiat_icon_${widget.initialFiat.symbol}'), + symbol: widget.initialFiat.symbol, ), onTap: () => _showAssetSelectionDialog('fiat'), isListTile: false, @@ -160,11 +136,11 @@ class FiatInputsState extends State { inputError: widget.boundariesError, ), AnimatedContainer( - duration: const Duration(milliseconds: 00), + duration: Duration.zero, height: widget.boundariesError == null ? 0 : 8, ), Card( - margin: const EdgeInsets.all(0), + margin: EdgeInsets.zero, color: Theme.of(context).colorScheme.onSurface, child: ListTile( contentPadding: @@ -173,29 +149,32 @@ class FiatInputsState extends State { padding: const EdgeInsets.only(top: 6.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(LocaleKeys.youReceive.tr()), - Text( - fiatController.text.isEmpty || priceInfo == null - ? '0.00' - : coinAmount ?? LocaleKeys.unknown.tr(), - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith(fontSize: 24), - ), - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(LocaleKeys.youReceive.tr()), + AutoScrollText( + text: fiatController.text.isEmpty || priceInfo == null + ? '0.00' + : coinAmount.toString(), + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(fontSize: 24), + ), + ], + ), ), const SizedBox(width: 12), SizedBox( height: 48, - child: _buildCurrencyItem( + child: FiatCurrencyItem( + key: const Key('fiat-onramp-coin-dropdown'), + foregroundColor: foregroundColor, disabled: coinListLoading, - currency: selectedCoin, + currency: widget.initialCoin, icon: Icon(_getDefaultAssetIcon('coin')), onTap: () => _showAssetSelectionDialog('coin'), isListTile: false, @@ -214,15 +193,13 @@ class FiatInputsState extends State { ); } - IconData _getDefaultAssetIcon(String type) { - return type == 'fiat' ? Icons.attach_money : Icons.monetization_on; - } - void _showAssetSelectionDialog(String type) { final isFiat = type == 'fiat'; - List itemList = isFiat ? fiatList : coinList; + final Iterable itemList = + isFiat ? widget.fiatList : widget.coinList; final icon = Icon(_getDefaultAssetIcon(type)); - Function(Currency) onItemSelected = isFiat ? changeFiat : changeCoin; + final void Function(ICurrency) onItemSelected = + isFiat ? changeFiat : changeCoin; _showSelectionDialog( context: context, @@ -236,23 +213,27 @@ class FiatInputsState extends State { void _showSelectionDialog({ required BuildContext context, required String title, - required List itemList, + required Iterable itemList, required Widget icon, - required Function(Currency) onItemSelected, + required void Function(ICurrency) onItemSelected, }) { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return AlertDialog( + key: const Key('fiat-onramp-currency-dialog'), title: Text(title), content: SizedBox( width: 450, child: ListView.builder( + key: const Key('fiat-onramp-currency-list'), shrinkWrap: true, itemCount: itemList.length, itemBuilder: (BuildContext context, int index) { - final item = itemList[index]; - return _buildCurrencyItem( + final item = itemList.elementAt(index); + return FiatCurrencyItem( + key: Key('fiat-onramp-currency-item-${item.symbol}'), + foregroundColor: foregroundColor, disabled: false, currency: item, icon: icon, @@ -270,283 +251,9 @@ class FiatInputsState extends State { ); } - Widget _buildCurrencyItem({ - required bool disabled, - required Currency currency, - required Widget icon, - required VoidCallback onTap, - required bool isListTile, - }) { - return FutureBuilder( - future: currency.isFiat - ? Future.value(true) - : checkIfAssetExists(currency.symbol), - builder: (context, snapshot) { - final assetExists = snapshot.connectionState == ConnectionState.done - ? snapshot.data ?? false - : null; - return isListTile - ? _buildListTile( - currency: currency, - icon: icon, - assetExists: assetExists, - onTap: onTap, - ) - : _buildButton( - enabled: !disabled, - currency: currency, - icon: icon, - assetExists: assetExists, - onTap: onTap, - ); - }, - ); - } - - Widget _getAssetIcon({ - required Currency currency, - required Widget icon, - bool? assetExists, - required VoidCallback onTap, - }) { - double size = 36.0; - - if (currency.isFiat) { - return FiatIcon(symbol: currency.symbol); - } - - if (assetExists != null && assetExists) { - return CoinIcon(currency.symbol, size: size); - } else { - return icon; - } - } - - Widget _buildListTile({ - required Currency currency, - required Widget icon, - bool? assetExists, - required VoidCallback onTap, - }) { - return ListTile( - leading: _getAssetIcon( - currency: currency, - icon: icon, - assetExists: assetExists, - onTap: onTap, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${currency.name}${currency.chainType != null ? ' (${getCoinTypeName(currency.chainType!)})' : ''}', - ), - Text(currency.symbol), - ], - ), - onTap: onTap, - ); - } - Color get foregroundColor => Theme.of(context).colorScheme.onSurfaceVariant; - - Widget _buildButton({ - required bool enabled, - required Currency? currency, - required Widget icon, - bool? assetExists, - required VoidCallback onTap, - }) { - // TODO: Refactor so that [Currency] holds an enum for fiat/coin or create - // a separate class for fiat/coin that extend the same base class. - final isFiat = currency?.isFiat ?? false; - - return FilledButton.icon( - onPressed: enabled ? onTap : null, - label: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - (isFiat ? currency?.getAbbr() : currency?.name) ?? - (isFiat - ? LocaleKeys.selectFiat.tr() - : LocaleKeys.selectCoin.tr()), - style: DefaultTextStyle.of(context).style.copyWith( - fontWeight: FontWeight.w500, - color: enabled - ? foregroundColor - : foregroundColor.withOpacity(0.5), - ), - ), - if (!isFiat && currency != null) - Text( - currency.chainType != null - ? getCoinTypeName(currency.chainType!) - : '', - style: DefaultTextStyle.of(context).style.copyWith( - color: enabled - ? foregroundColor.withOpacity(0.5) - : foregroundColor.withOpacity(0.25), - ), - ), - ], - ), - const SizedBox(width: 4), - Icon( - Icons.keyboard_arrow_down, - size: 28, - color: foregroundColor.withOpacity(enabled ? 1 : 0.5), - ), - ], - ), - style: (Theme.of(context).filledButtonTheme.style ?? const ButtonStyle()) - .copyWith( - backgroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.onSurface), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 0, horizontal: 0), - ), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - ), - icon: currency == null - ? Icon(_getDefaultAssetIcon(isFiat ? 'fiat' : 'coin')) - : _getAssetIcon( - currency: currency, - icon: icon, - assetExists: assetExists, - onTap: onTap, - ), - ); - } -} - -class AddressBar extends StatelessWidget { - const AddressBar({ - super.key, - required this.receiveAddress, - }); - - final String? receiveAddress; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: Card( - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - ), - onTap: () => copyToClipBoard(context, receiveAddress!), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - if (receiveAddress != null && receiveAddress!.isNotEmpty) - const Icon(Icons.copy, size: 16) - else - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - truncateMiddleSymbols(receiveAddress ?? ''), - ), - ), - ], - ), - ), - ), - ), - ); - } } -class CustomFiatInputField extends StatelessWidget { - final TextEditingController controller; - final String hintText; - final Widget? label; - final Function(String?) onTextChanged; - final bool readOnly; - final Widget assetButton; - final String? inputError; - - const CustomFiatInputField({ - required this.controller, - required this.hintText, - required this.onTextChanged, - this.label, - this.readOnly = false, - required this.assetButton, - this.inputError, - }); - - @override - Widget build(BuildContext context) { - final textColor = Theme.of(context).colorScheme.onSurfaceVariant; - - final inputStyle = Theme.of(context).textTheme.headlineLarge?.copyWith( - fontSize: 18, - fontWeight: FontWeight.w300, - color: textColor, - letterSpacing: 1.1, - ); - - InputDecoration inputDecoration = InputDecoration( - label: label, - labelStyle: inputStyle, - fillColor: Theme.of(context).colorScheme.onSurface, - floatingLabelStyle: - Theme.of(context).inputDecorationTheme.floatingLabelStyle, - floatingLabelBehavior: FloatingLabelBehavior.always, - contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - hintText: hintText, - border: const OutlineInputBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(4), - topLeft: Radius.circular(4), - bottomRight: Radius.circular(18), - topRight: Radius.circular(18), - ), - ), - errorText: inputError, - errorMaxLines: 1, - helperText: '', - ); - - return Stack( - clipBehavior: Clip.none, - alignment: Alignment.centerRight, - children: [ - TextField( - autofocus: true, - controller: controller, - style: inputStyle, - decoration: inputDecoration, - readOnly: readOnly, - onChanged: onTextChanged, - inputFormatters: [FilteringTextInputFormatter.allow(numberRegExp)], - keyboardType: const TextInputType.numberWithOptions(decimal: true), - ), - Positioned( - right: 16, - bottom: 26, - top: 2, - child: assetButton, - ), - ], - ); - } +IconData _getDefaultAssetIcon(String type) { + return type == 'fiat' ? Icons.attach_money : Icons.monetization_on; } diff --git a/lib/views/fiat/fiat_page.dart b/lib/views/fiat/fiat_page.dart index bd6052fa75..f79cb9e1cc 100644 --- a/lib/views/fiat/fiat_page.dart +++ b/lib/views/fiat/fiat_page.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; +import 'package:web_dex/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; +import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/swap.dart'; @@ -40,25 +42,77 @@ class _FiatPageState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.mode == AuthorizeMode.noLogin) { - setState(() { - _activeTabIndex = 0; - }); - } - }, - child: _showSwap ? _buildTradingDetails() : _buildFiatPage(), + return BlocProvider( + create: (_) => FiatFormBloc(), + child: MultiBlocListener( + listeners: [ + BlocListener( + listener: _handleAuthStateChange, + ), + BlocListener( + listenWhen: (previous, current) => + previous.fiatOrderStatus != current.fiatOrderStatus, + listener: _handleOrderStatusChange, + ), + ], + child: _showSwap + ? TradingDetails( + uuid: routingState.fiatState.uuid, + ) + : FiatPageLayout( + activeTabIndex: _activeTabIndex, + ), + ), ); } - Widget _buildTradingDetails() { - return TradingDetails( - uuid: routingState.fiatState.uuid, - ); + void _handleOrderStatusChange(BuildContext context, FiatFormState state) { + if (state.fiatOrderStatus == FiatOrderStatus.success) { + _onCheckoutComplete(isSuccess: true); + } + } + + void _handleAuthStateChange(BuildContext context, AuthBlocState state) { + if (state.mode == AuthorizeMode.noLogin) { + setState(() { + _activeTabIndex = 0; + }); + } + } + + // Will be used in the future for switching between tabs when we implement + // the purchase history tab. + // void _setActiveTab(int i) { + // setState(() { + // _activeTabIndex = i; + // }); + // } + + void _onRouteChange() { + setState(() { + _showSwap = routingState.fiatState.action == FiatAction.tradingDetails; + }); + } + + void _onCheckoutComplete({required bool isSuccess}) { + if (isSuccess) { + // In the future, we will navigate to the purchase history tab when the + // purchase is complete. + // _setActiveTab(1); + } } +} + +class FiatPageLayout extends StatelessWidget { + const FiatPageLayout({ + required this.activeTabIndex, + super.key, + }); - Widget _buildFiatPage() { + final int activeTabIndex; + + @override + Widget build(BuildContext context) { return PageLayout( content: Expanded( child: Container( @@ -70,8 +124,6 @@ class _FiatPageState extends State with TickerProviderStateMixin { borderRadius: BorderRadius.circular(18.0), ), child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, children: [ // TODO: Future feature to show fiat purchase history. Until then, // we'll hide the tabs since only the first one is used. @@ -87,8 +139,7 @@ class _FiatPageState extends State with TickerProviderStateMixin { // ), Flexible( child: _TabContent( - activeTabIndex: _activeTabIndex, - onCheckoutComplete: _onCheckoutComplete, + activeTabIndex: activeTabIndex, ), ), ], @@ -98,14 +149,6 @@ class _FiatPageState extends State with TickerProviderStateMixin { ); } - // Will be used in the future for switching between tabs when we implement - // the purchase history tab. - // void _setActiveTab(int i) { - // setState(() { - // _activeTabIndex = i; - // }); - // } - Color? _backgroundColor(BuildContext context) { if (isMobile) { final ThemeMode mode = theme.mode; @@ -113,39 +156,21 @@ class _FiatPageState extends State with TickerProviderStateMixin { } return null; } - - void _onRouteChange() { - setState(() { - _showSwap = routingState.fiatState.action == FiatAction.tradingDetails; - }); - } - - void _onCheckoutComplete({required bool isSuccess}) { - if (isSuccess) { - // In the future, we will navigate to the purchase history tab when the - // purchase is complete. - // _setActiveTab(1); - } - } } class _TabContent extends StatelessWidget { const _TabContent({ required int activeTabIndex, - required this.onCheckoutComplete, // ignore: unused_element super.key, }) : _activeTabIndex = activeTabIndex; - // TODO: Remove this when we have a proper bloc for this page - final Function({required bool isSuccess}) onCheckoutComplete; - final int _activeTabIndex; @override Widget build(BuildContext context) { final List tabContents = [ - FiatForm(onCheckoutComplete: onCheckoutComplete), + const FiatForm(), Padding( padding: const EdgeInsets.only(top: 20), child: InProgressList( diff --git a/lib/views/fiat/fiat_payment_method.dart b/lib/views/fiat/fiat_payment_method_card.dart similarity index 57% rename from lib/views/fiat/fiat_payment_method.dart rename to lib/views/fiat/fiat_payment_method_card.dart index 73bc9de1dc..5162077b70 100644 --- a/lib/views/fiat/fiat_payment_method.dart +++ b/lib/views/fiat/fiat_payment_method_card.dart @@ -1,37 +1,33 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_payment_method.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -class FiatPaymentMethod extends StatefulWidget { - final String? fiatAmount; - final Map paymentMethodData; - final Map? selectedPaymentMethod; - final Function(Map) onSelect; - - const FiatPaymentMethod({ +class FiatPaymentMethodCard extends StatefulWidget { + const FiatPaymentMethodCard({ required this.fiatAmount, required this.paymentMethodData, required this.selectedPaymentMethod, required this.onSelect, super.key, }); + final String? fiatAmount; + final FiatPaymentMethod paymentMethodData; + final FiatPaymentMethod? selectedPaymentMethod; + final void Function(FiatPaymentMethod) onSelect; @override - FiatPaymentMethodState createState() => FiatPaymentMethodState(); + FiatPaymentMethodCardState createState() => FiatPaymentMethodCardState(); } -class FiatPaymentMethodState extends State { +class FiatPaymentMethodCardState extends State { @override Widget build(BuildContext context) { - bool isSelected = widget.selectedPaymentMethod != null && - widget.selectedPaymentMethod!['id'] == widget.paymentMethodData['id']; - - final priceInfo = widget.paymentMethodData['price_info']; - - final relativePercent = - widget.paymentMethodData['relative_percent'] as double?; + final bool isSelected = widget.selectedPaymentMethod != null && + widget.selectedPaymentMethod!.id == widget.paymentMethodData.id; + final relativePercent = widget.paymentMethodData.relativePercent as double?; final isBestOffer = relativePercent == null; return InkWell( @@ -40,16 +36,17 @@ class FiatPaymentMethodState extends State { }, borderRadius: BorderRadius.circular(8), child: Card( - margin: const EdgeInsets.all(0), + margin: EdgeInsets.zero, color: Theme.of(context).colorScheme.onSurface, elevation: 4, clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( - color: Theme.of(context) - .primaryColor - .withOpacity(isSelected ? 1 : 0.25)), + color: Theme.of(context) + .primaryColor + .withOpacity(isSelected ? 1 : 0.25), + ), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -65,13 +62,13 @@ class FiatPaymentMethodState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${widget.paymentMethodData['name']}', + widget.paymentMethodData.name, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), Text( - widget.paymentMethodData['provider_id'], + widget.paymentMethodData.providerId, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, @@ -80,17 +77,17 @@ class FiatPaymentMethodState extends State { ), ), const SizedBox(width: 8), - if (priceInfo != null) - isBestOffer - ? Chip( - label: Text(LocaleKeys.bestOffer.tr()), - backgroundColor: Colors.green, - ) - : Text( - '${(relativePercent * 100).toStringAsFixed(2)}%', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), + if (isBestOffer) + Chip( + label: Text(LocaleKeys.bestOffer.tr()), + backgroundColor: Colors.green, + ) + else + Text( + '${(relativePercent * 100).toStringAsFixed(2)}%', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), ], ), ), @@ -100,11 +97,10 @@ class FiatPaymentMethodState extends State { } Widget get providerLogo { - final assetPath = - widget.paymentMethodData['provider_icon_asset_path'] as String; + final assetPath = widget.paymentMethodData.providerIconAssetPath; //TODO: Additional validation that the asset exists - return SvgPicture.asset(assetPath, fit: BoxFit.contain); + return SvgPicture.asset(assetPath); } } diff --git a/lib/views/fiat/fiat_payment_method_group.dart b/lib/views/fiat/fiat_payment_method_group.dart new file mode 100644 index 0000000000..403abf25bd --- /dev/null +++ b/lib/views/fiat/fiat_payment_method_group.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_payment_method.dart'; +import 'package:web_dex/views/fiat/fiat_payment_method_card.dart'; + +class FiatPaymentMethodGroup extends StatelessWidget { + const FiatPaymentMethodGroup({ + required SliverGridDelegate gridDelegate, + required this.methods, + required this.selectedPaymentMethod, + required this.providerId, + required this.fiatAmount, + super.key, + }) : _gridDelegate = gridDelegate; + + final SliverGridDelegate _gridDelegate; + final String providerId; + final List methods; + final FiatPaymentMethod? selectedPaymentMethod; + final String fiatAmount; + + @override + Widget build(BuildContext context) { + return Card( + margin: EdgeInsets.zero, + color: Theme.of(context).colorScheme.onSurface, + elevation: 4, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: Theme.of(context).primaryColor.withOpacity( + selectedPaymentMethod != null && + selectedPaymentMethod!.providerId == providerId + ? 1 + : 0.25, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(providerId), + const SizedBox(height: 16), + GridView.builder( + shrinkWrap: true, + itemCount: methods.length, + // TODO: Improve responsiveness by making crossAxisCount dynamic based on + // min and max child width. + gridDelegate: _gridDelegate, + itemBuilder: (context, index) { + final method = methods[index]; + final providerId = method.providerId.toLowerCase(); + return FiatPaymentMethodCard( + key: Key('fiat-payment-method-$providerId-$index'), + fiatAmount: fiatAmount, + paymentMethodData: method, + selectedPaymentMethod: selectedPaymentMethod, + onSelect: (method) => context.read().add( + PaymentMethodSelected(method), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/fiat/fiat_payment_methods_grid.dart b/lib/views/fiat/fiat_payment_methods_grid.dart new file mode 100644 index 0000000000..93b9a469e5 --- /dev/null +++ b/lib/views/fiat/fiat_payment_methods_grid.dart @@ -0,0 +1,95 @@ +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/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; +import 'package:web_dex/bloc/fiat/models/fiat_payment_method.dart'; +import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/fiat/fiat_payment_method_group.dart'; + +class FiatPaymentMethodsGrid extends StatelessWidget { + const FiatPaymentMethodsGrid({ + required this.state, + super.key, + this.simpleSpinner = true, + }); + + final FiatFormState state; + final bool simpleSpinner; + + @override + Widget build(BuildContext context) { + final isLoading = state.isLoading; + if (isLoading) { + return simpleSpinner + ? const UiSpinner( + width: 36, + height: 36, + strokeWidth: 4, + ) + : GridView( + shrinkWrap: true, + gridDelegate: _gridDelegate, + children: List.generate( + 4, + (index) => const Card(child: SkeletonListTile()), + ), + ); + } + + final hasPaymentMethods = state.paymentMethods.isNotEmpty; + if (!hasPaymentMethods) { + return Center( + child: Text( + LocaleKeys.noOptionsToPurchase.tr( + args: [ + state.selectedCoin.value!.symbol, + state.selectedFiat.value!.symbol, + ], + ), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } else { + final groupedPaymentMethods = + groupPaymentMethodsByProviderId(state.paymentMethods.toList()); + return Column( + children: [ + for (final entry in groupedPaymentMethods.entries) ...[ + FiatPaymentMethodGroup( + gridDelegate: _gridDelegate, + providerId: entry.key, + methods: entry.value, + fiatAmount: state.fiatAmount.value, + selectedPaymentMethod: state.selectedPaymentMethod, + ), + const SizedBox(height: 16), + ], + ], + ); + } + } + + Map> groupPaymentMethodsByProviderId( + List paymentMethods, + ) { + final groupedMethods = >{}; + for (final method in paymentMethods) { + final providerId = method.providerId; + if (!groupedMethods.containsKey(providerId)) { + groupedMethods[providerId] = []; + } + groupedMethods[providerId]!.add(method); + } + return groupedMethods; + } + + SliverGridDelegate get _gridDelegate => + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isMobile ? 1 : 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + mainAxisExtent: 90, + ); +} diff --git a/lib/views/fiat/fiat_select_button.dart b/lib/views/fiat/fiat_select_button.dart new file mode 100644 index 0000000000..74ccb37798 --- /dev/null +++ b/lib/views/fiat/fiat_select_button.dart @@ -0,0 +1,101 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/fiat/models/i_currency.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin_utils.dart'; +import 'package:web_dex/views/fiat/fiat_asset_icon.dart'; + +class FiatSelectButton extends StatelessWidget { + const FiatSelectButton({ + required this.context, + required this.foregroundColor, + required this.enabled, + required this.currency, + required this.icon, + required this.onTap, + required this.assetExists, + super.key, + }); + + final BuildContext context; + final Color foregroundColor; + final bool enabled; + final ICurrency? currency; + final Widget icon; + final VoidCallback onTap; + final bool? assetExists; + + @override + Widget build(BuildContext context) { + final isFiat = currency?.isFiat ?? false; + + return FilledButton.icon( + onPressed: enabled ? onTap : null, + label: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + (isFiat ? currency?.getAbbr() : currency?.name) ?? + (isFiat + ? LocaleKeys.selectFiat.tr() + : LocaleKeys.selectCoin.tr()), + style: DefaultTextStyle.of(context).style.copyWith( + fontWeight: FontWeight.w500, + color: enabled + ? foregroundColor + : foregroundColor.withOpacity(0.5), + ), + ), + if (!isFiat && currency != null) + Text( + (currency! as CryptoCurrency).isCrypto + ? getCoinTypeName((currency! as CryptoCurrency).chainType) + : '', + style: DefaultTextStyle.of(context).style.copyWith( + color: enabled + ? foregroundColor.withOpacity(0.5) + : foregroundColor.withOpacity(0.25), + ), + ), + ], + ), + const SizedBox(width: 4), + Icon( + Icons.keyboard_arrow_down, + size: 28, + color: foregroundColor.withOpacity(enabled ? 1 : 0.5), + ), + ], + ), + style: (Theme.of(context).filledButtonTheme.style ?? const ButtonStyle()) + .copyWith( + backgroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.onSurface, + ), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + icon: currency == null + ? Icon(_getDefaultAssetIcon(isFiat ? 'fiat' : 'coin')) + : FiatAssetIcon( + currency: currency!, + icon: icon, + onTap: onTap, + assetExists: assetExists, + ), + ); + } +} + +IconData _getDefaultAssetIcon(String type) { + return type == 'fiat' ? Icons.attach_money : Icons.monetization_on; +} diff --git a/lib/views/fiat/webview_dialog.dart b/lib/views/fiat/webview_dialog.dart new file mode 100644 index 0000000000..84212a5c38 --- /dev/null +++ b/lib/views/fiat/webview_dialog.dart @@ -0,0 +1,222 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class WebViewDialog { + static Future show( + BuildContext context, { + required String url, + required String title, + void Function(String)? onConsoleMessage, + VoidCallback? onCloseWindow, + InAppWebViewSettings? settings, + }) async { + // `flutter_inappwebview` does not yet support Linux, so use `url_launcher` + // to launch the URL in the default browser. + if (!kIsWeb && !kIsWasm && Platform.isLinux) { + return launchURL(url); + } + + final webviewSettings = settings ?? + InAppWebViewSettings( + isInspectable: kDebugMode, + iframeSandbox: { + Sandbox.ALLOW_SAME_ORIGIN, + Sandbox.ALLOW_SCRIPTS, + Sandbox.ALLOW_FORMS, + Sandbox.ALLOW_POPUPS, + }, + ); + + if (kIsWeb && !isMobile) { + await showDialog( + context: context, + builder: (BuildContext context) { + return InAppWebviewDialog( + title: title, + webviewSettings: webviewSettings, + onConsoleMessage: onConsoleMessage ?? (_) {}, + onCloseWindow: onCloseWindow, + url: url, + ); + }, + ); + } else { + await Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return FullscreenInAppWebview( + title: title, + webviewSettings: webviewSettings, + onConsoleMessage: onConsoleMessage ?? (_) {}, + onCloseWindow: onCloseWindow, + url: url, + ); + }, + ), + ); + } + } +} + +class InAppWebviewDialog extends StatelessWidget { + const InAppWebviewDialog({ + required this.title, + required this.webviewSettings, + required this.onConsoleMessage, + required this.url, + this.onCloseWindow, + super.key, + }); + + final String title; + final InAppWebViewSettings webviewSettings; + final void Function(String) onConsoleMessage; + final String url; + final VoidCallback? onCloseWindow; + + @override + Widget build(BuildContext context) { + return Dialog( + child: SizedBox( + width: 700, + height: 700, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppBar( + title: Text(title), + foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ), + Expanded( + child: MessageInAppWebView( + key: const Key('dialog-inappwebview'), + settings: webviewSettings, + url: url, + onConsoleMessage: onConsoleMessage, + onCloseWindow: onCloseWindow, + ), + ), + ], + ), + ), + ); + } +} + +class FullscreenInAppWebview extends StatelessWidget { + const FullscreenInAppWebview({ + required this.title, + required this.webviewSettings, + required this.onConsoleMessage, + required this.url, + this.onCloseWindow, + super.key, + }); + + final String title; + final InAppWebViewSettings webviewSettings; + final void Function(String) onConsoleMessage; + final String url; + final VoidCallback? onCloseWindow; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, + elevation: 0, + ), + body: SafeArea( + child: MessageInAppWebView( + key: const Key('fullscreen-inapp-webview'), + settings: webviewSettings, + url: url, + onConsoleMessage: onConsoleMessage, + onCloseWindow: onCloseWindow, + ), + ), + ); + } +} + +class MessageInAppWebView extends StatefulWidget { + const MessageInAppWebView({ + required this.settings, + required this.onConsoleMessage, + required this.url, + this.onCloseWindow, + super.key, + }); + + final InAppWebViewSettings settings; + final void Function(String) onConsoleMessage; + final String url; + final VoidCallback? onCloseWindow; + + @override + State createState() => _MessageInAppWebviewState(); +} + +class _MessageInAppWebviewState extends State { + @override + Widget build(BuildContext context) { + final urlRequest = URLRequest(url: WebUri(widget.url)); + return InAppWebView( + key: const Key('flutter-in-app-webview'), + initialSettings: widget.settings, + initialUrlRequest: urlRequest, + onConsoleMessage: _onConsoleMessage, + onUpdateVisitedHistory: _onUpdateHistory, + onCloseWindow: (_) { + widget.onCloseWindow?.call(); + }, + onLoadStop: (controller, url) async { + await controller.evaluateJavascript( + source: ''' + window.addEventListener("message", (event) => { + let messageData; + try { + messageData = JSON.parse(event.data); + } catch (parseError) { + messageData = event.data; + } + + try { + const messageString = (typeof messageData === 'object') ? JSON.stringify(messageData) : String(messageData); + console.log(messageString); + } catch (postError) { + console.error('Error posting message', postError); + } + }, false); + ''', + ); + }, + ); + } + + void _onConsoleMessage(_, ConsoleMessage consoleMessage) { + widget.onConsoleMessage(consoleMessage.message); + } + + void _onUpdateHistory( + InAppWebViewController controller, + WebUri? url, + bool? isReload, + ) { + if (url.toString() == 'https://app.komodoplatform.com/') { + Navigator.of(context).pop(); + } + } +} diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index d8c5aaac05..434a32ca3d 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,6 +1,3 @@ -import 'package:web/web.dart' as web; -import 'dart:js_interop'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/app_config/app_config.dart'; @@ -14,6 +11,7 @@ import 'package:web_dex/router/navigators/main_layout/main_layout_router.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/services/alpha_version_alert_service/alpha_version_alert_service.dart'; import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/utils/window/window.dart'; import 'package:web_dex/views/common/header/app_header.dart'; import 'package:web_dex/views/common/main_menu/main_menu_bar_mobile.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; @@ -26,10 +24,8 @@ class MainLayout extends StatefulWidget { class _MainLayoutState extends State { @override void initState() { - web.window.onbeforeunload = (web.BeforeUnloadEvent event) { - event.preventDefault(); - event.returnValue = 'Are you sure?'; - }.toJS; + // TODO: localize + showMessageBeforeUnload('Are you sure you want to leave?'); WidgetsBinding.instance.addPostFrameCallback((_) async { await AlphaVersionWarningService().run(); diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index 39cb9ef596..f5947631be 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -14,12 +14,12 @@ import 'package:web_dex/views/wallet/coin_details/faucet/faucet_button.dart'; class CoinDetailsCommonButtons extends StatelessWidget { const CoinDetailsCommonButtons({ - Key? key, required this.isMobile, required this.selectWidget, required this.clickSwapButton, required this.coin, - }) : super(key: key); + super.key, + }); final bool isMobile; final Coin coin; @@ -29,29 +29,151 @@ class CoinDetailsCommonButtons extends StatelessWidget { @override Widget build(BuildContext context) { return isMobile - ? _buildMobileButtons(context) - : _buildDesktopButtons(context); + ? CoinDetailsCommonButtonsMobileLayout( + coin: coin, + isMobile: isMobile, + selectWidget: selectWidget, + clickSwapButton: clickSwapButton, + context: context, + ) + : CoinDetailsCommonButtonsDesktopLayout( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + clickSwapButton: clickSwapButton, + context: context, + ); } +} + +class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { + const CoinDetailsCommonButtonsMobileLayout({ + required this.coin, + required this.isMobile, + required this.selectWidget, + required this.clickSwapButton, + required this.context, + super.key, + }); + + final Coin coin; + final bool isMobile; + final void Function(CoinPageType p1) selectWidget; + final VoidCallback clickSwapButton; + final BuildContext context; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Visibility( + visible: coin.protocolData?.contractAddress.isNotEmpty ?? false, + child: ContractAddressButton(coin), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: CoinDetailsSendButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, + ), + ), + const SizedBox(width: 15), + Flexible( + child: CoinDetailsReceiveButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isBitrefillIntegrationEnabled) + Flexible( + child: BitrefillButton( + key: Key( + 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}', + ), + coin: coin, + onPaymentRequested: (_) => selectWidget(CoinPageType.send), + ), + ), + if (isBitrefillIntegrationEnabled) const SizedBox(width: 15), + if (!coin.walletOnly) + Flexible( + child: CoinDetailsSwapButton( + isMobile: isMobile, + coin: coin, + clickSwapButton: clickSwapButton, + context: context, + ), + ), + ], + ), + ], + ); + } +} + +class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { + const CoinDetailsCommonButtonsDesktopLayout({ + required this.isMobile, + required this.coin, + required this.selectWidget, + required this.clickSwapButton, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final void Function(CoinPageType p1) selectWidget; + final VoidCallback clickSwapButton; + final BuildContext context; - Widget _buildDesktopButtons(BuildContext context) { + @override + Widget build(BuildContext context) { return Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 120), - child: _buildSendButton(context), + child: CoinDetailsSendButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, + ), ), Container( margin: const EdgeInsets.only(left: 21), constraints: const BoxConstraints(maxWidth: 120), - child: _buildReceiveButton(context), + child: CoinDetailsReceiveButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, + ), ), if (!coin.walletOnly) Container( - margin: const EdgeInsets.only(left: 21), - constraints: const BoxConstraints(maxWidth: 120), - child: _buildSwapButton(context)), + margin: const EdgeInsets.only(left: 21), + constraints: const BoxConstraints(maxWidth: 120), + child: CoinDetailsSwapButton( + isMobile: isMobile, + coin: coin, + clickSwapButton: clickSwapButton, + context: context, + ), + ), if (coin.hasFaucet) Container( margin: const EdgeInsets.only(left: 21), @@ -73,86 +195,113 @@ class CoinDetailsCommonButtons extends StatelessWidget { ), ), Flexible( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: coin.protocolData?.contractAddress.isNotEmpty ?? false - ? SizedBox(width: 230, child: ContractAddressButton(coin)) - : null, - )) - ], - ); - } - - Widget _buildMobileButtons(BuildContext context) { - return Column( - children: [ - Visibility( - visible: coin.protocolData?.contractAddress.isNotEmpty ?? false, - child: ContractAddressButton(coin), - ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Flexible(child: _buildSendButton(context)), - const SizedBox(width: 15), - Flexible(child: _buildReceiveButton(context)), - ], + flex: 2, + child: Align( + alignment: Alignment.centerRight, + child: coin.protocolData?.contractAddress.isNotEmpty ?? false + ? SizedBox(width: 230, child: ContractAddressButton(coin)) + : null, + ), ), ], ); } +} + +class CoinDetailsReceiveButton extends StatelessWidget { + const CoinDetailsReceiveButton({ + required this.isMobile, + required this.coin, + required this.selectWidget, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final void Function(CoinPageType p1) selectWidget; + final BuildContext context; - Widget _buildSendButton(BuildContext context) { + @override + Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); return UiPrimaryButton( - key: const Key('coin-details-send-button'), + key: const Key('coin-details-receive-button'), height: isMobile ? 52 : 40, prefix: Container( padding: const EdgeInsets.only(right: 14), child: SvgPicture.asset( - '$assetsPath/others/send.svg', + '$assetsPath/others/receive.svg', ), ), textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, - onPressed: coin.isSuspended || coin.balance == 0 + onPressed: coin.isSuspended ? null : () { - selectWidget(CoinPageType.send); + selectWidget(CoinPageType.receive); }, - text: LocaleKeys.send.tr(), + text: LocaleKeys.receive.tr(), ); } +} + +class CoinDetailsSendButton extends StatelessWidget { + const CoinDetailsSendButton({ + required this.isMobile, + required this.coin, + required this.selectWidget, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final void Function(CoinPageType p1) selectWidget; + final BuildContext context; - Widget _buildReceiveButton(BuildContext context) { + @override + Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); return UiPrimaryButton( - key: const Key('coin-details-receive-button'), + key: const Key('coin-details-send-button'), height: isMobile ? 52 : 40, prefix: Container( padding: const EdgeInsets.only(right: 14), child: SvgPicture.asset( - '$assetsPath/others/receive.svg', + '$assetsPath/others/send.svg', ), ), textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, - onPressed: coin.isSuspended + onPressed: coin.isSuspended || coin.balance == 0 ? null : () { - selectWidget(CoinPageType.receive); + selectWidget(CoinPageType.send); }, - text: LocaleKeys.receive.tr(), + text: LocaleKeys.send.tr(), ); } +} + +class CoinDetailsSwapButton extends StatelessWidget { + const CoinDetailsSwapButton({ + required this.isMobile, + required this.coin, + required this.clickSwapButton, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final VoidCallback clickSwapButton; + final BuildContext context; - Widget _buildSwapButton(BuildContext context) { + @override + Widget build(BuildContext context) { if (currentWalletBloc.wallet?.config.type != WalletType.iguana) { return const SizedBox.shrink(); } diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index 922bbd88fa..7bab57d075 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -34,11 +34,11 @@ import 'package:web_dex/views/wallet/coin_details/transactions/transaction_table class CoinDetailsInfo extends StatefulWidget { const CoinDetailsInfo({ - Key? key, required this.coin, required this.setPageType, required this.onBackButtonPressed, - }) : super(key: key); + super.key, + }); final Coin coin; final void Function(CoinPageType) setPageType; final VoidCallback onBackButtonPressed; @@ -67,7 +67,6 @@ class _CoinDetailsInfoState extends State fiatCoinId: 'USDT', selectedPeriod: selectedDurationInitial, walletId: _walletId!, - updateFrequency: const Duration(minutes: 1), ), ); @@ -234,7 +233,6 @@ class _DesktopCoinDetails extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), child: Column( - mainAxisSize: MainAxisSize.max, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -354,8 +352,6 @@ class _MobileContent extends StatelessWidget { borderRadius: BorderRadius.circular(18.0), ), child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, children: [ CoinIcon( coin.abbr, @@ -381,7 +377,6 @@ class _MobileContent extends StatelessWidget { coin: coin, ), ), - if (!coin.walletOnly) _SwapButton(coin: coin), if (coin.hasFaucet) FaucetButton( onPressed: () => setPageType(CoinPageType.faucet), @@ -478,16 +473,17 @@ class _Balance extends StatelessWidget { isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - isMobile - ? const SizedBox.shrink() - : Text( - LocaleKeys.yourBalance.tr(), - style: themeData.textTheme.titleMedium!.copyWith( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.custom.headerFloatBoxColor, - ), - ), + if (isMobile) + const SizedBox.shrink() + else + Text( + LocaleKeys.yourBalance.tr(), + style: themeData.textTheme.titleMedium!.copyWith( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.custom.headerFloatBoxColor, + ), + ), Flexible( child: Row( mainAxisSize: isMobile ? MainAxisSize.max : MainAxisSize.min, @@ -632,33 +628,6 @@ class _GetRewardsButton extends StatelessWidget { } } -class _SwapButton extends StatelessWidget { - const _SwapButton({required this.coin}); - final Coin coin; - - @override - Widget build(BuildContext context) { - if (currentWalletBloc.wallet?.config.type != WalletType.iguana) { - return const SizedBox.shrink(); - } - - return UiBorderButton( - width: double.infinity, - height: 52, - borderColor: theme.custom.swapButtonColor, - borderWidth: 2, - backgroundColor: Theme.of(context).colorScheme.surface, - text: LocaleKeys.swapCoin.tr(), - textColor: theme.custom.swapButtonColor, - onPressed: coin.isSuspended ? null : () => _goToSwap(context, coin), - prefix: SvgPicture.asset( - '$assetsPath/others/swap.svg', - allowDrawingOutsideViewBox: true, - ), - ); - } -} - void _goToSwap(BuildContext context, Coin coin) { context.read().add(TakerSetSellCoin(coin)); routingState.selectedMenu = MainMenuValue.dex; diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 77cae2b3ff..bc841d0522 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -29,7 +29,7 @@ func performMM2Stop() -> Int32 { } @available(macOS 10.12, *) -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { override func applicationDidFinishLaunching(_ notification: Notification) { diff --git a/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart b/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart index 6dbf468023..ed4cd11261 100644 --- a/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart +++ b/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart @@ -1,12 +1,28 @@ +import 'dart:io'; + import 'package:hive_flutter/hive_flutter.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'models/coin_info.dart'; import 'models/models.dart'; class KomodoCoinUpdater { - static Future ensureInitialized(String appFolder) async { - await Hive.initFlutter(appFolder); + /// Initialises Hive with the path to the app folder. This should be called + /// before any other operations with this package. If [isWeb] is true, then + /// [Hive.initFlutter] is called instead of [Hive.init]. + static Future ensureInitialized( + String appFolder, { + bool isWeb = false, + }) async { + if (isWeb) { + await Hive.initFlutter(appFolder); + } else { + final Directory appDir = await getApplicationDocumentsDirectory(); + final String path = p.join(appDir.path, appFolder); + Hive.init(path); + } initializeAdapters(); } diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index c6ed90e2a2..3d1cb6a681 100644 --- a/packages/komodo_coin_updates/pubspec.yaml +++ b/packages/komodo_coin_updates/pubspec.yaml @@ -9,6 +9,8 @@ environment: # Add regular dependencies here. dependencies: http: 1.2.2 # dart.dev + path_provider: 2.1.1 # flutter.dev + path: 1.9.0 # dart.dev - transitive to direct komodo_persistence_layer: path: ../komodo_persistence_layer/ diff --git a/pubspec.lock b/pubspec.lock index cc9d53e808..e44519a0fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -596,7 +596,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "388f04296a5531c3cdad766269a3040d2b4ee9ac" + resolved-ref: aed015fbec529f806ef40f59c884130d3b9b5806 url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" diff --git a/run_integration_tests.dart b/run_integration_tests.dart index b38f089d6c..bb3b62d6f8 100644 --- a/run_integration_tests.dart +++ b/run_integration_tests.dart @@ -12,6 +12,7 @@ final List testsList = [ 'wallets_manager_tests/wallets_manager_tests.dart', 'dex_tests/dex_tests.dart', 'misc_tests/misc_tests.dart', + 'fiat_onramp_tests/fiat_onramp_tests.dart', ]; //app data path for mac and linux diff --git a/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart b/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart new file mode 100644 index 0000000000..a3e9d226d9 --- /dev/null +++ b/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart @@ -0,0 +1,27 @@ +// ignore_for_file: avoid_print + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:web_dex/main.dart' as app; + +import '../../helpers/accept_alpha_warning.dart'; +import '../../helpers/restore_wallet.dart'; +import 'form_tests.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets( + 'Run Fiat On-Ramp tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testFiatFormInputs(tester); + + print('END Fiat On-Ramp TESTS'); + }, + semanticsEnabled: false, + ); +} diff --git a/test_integration/tests/fiat_onramp_tests/form_tests.dart b/test_integration/tests/fiat_onramp_tests/form_tests.dart new file mode 100644 index 0000000000..5d774eff83 --- /dev/null +++ b/test_integration/tests/fiat_onramp_tests/form_tests.dart @@ -0,0 +1,137 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:web_dex/main.dart' as app; + +import '../../common/pump_and_settle.dart'; +import '../../helpers/accept_alpha_warning.dart'; +import '../../helpers/restore_wallet.dart'; + +Future testFiatFormInputs(WidgetTester tester) async { + final Finder fiatFinder = find.byKey(const Key('main-menu-fiat')); + + await tester.tap(fiatFinder); + // wait for fiat form to load fiat currencies, coin list, and payment methods + await tester.pumpAndSettle(); + + await _testFiatAmountField(tester); + await _testFiatSelection(tester); + await _testCoinSelection(tester); + await _testPaymentMethodSelection(tester); + await _textSubmit(tester); +} + +Future _textSubmit(WidgetTester tester) async { + final Finder submitFinder = + find.byKey(const Key('fiat-onramp-submit-button')); + final Finder webviewFinder = find.byKey(const Key('flutter-in-app-webview')); + + expect(submitFinder, findsOneWidget, reason: 'Submit button not found'); + await tester.tap(submitFinder); + await tester.pumpAndSettle(); + expect(webviewFinder, findsOneWidget, reason: 'Webview not found'); +} + +Future _testFiatAmountField(WidgetTester tester) async { + final Finder fiatAmountFinder = + find.byKey(const Key('fiat-amount-form-field')); + + await tester.tap(fiatAmountFinder); + await tester.pump(); + await tester.enterText(fiatAmountFinder, '50'); + await tester.pump(); + await tester.pumpAndSettle(); // wait for payment methods to populate + + await _testPaymentMethodSelection(tester); +} + +Future _testFiatSelection(WidgetTester tester) async { + final Finder fiatDropdownFinder = + find.byKey(const Key('fiat-onramp-fiat-dropdown')); + final Finder usdIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-USD')); + final Finder eurIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-EUR')); + + await tester.tap(fiatDropdownFinder); + await tester.pumpNFrames(10); + expect(usdIconFinder, findsOneWidget, reason: 'USD icon not found'); + expect(eurIconFinder, findsOneWidget, reason: 'EUR icon not found'); + await tester.tap(eurIconFinder); + await tester.pump(); + await tester.pumpAndSettle(); // wait for payment methods to populate +} + +Future _testCoinSelection(WidgetTester tester) async { + final Finder coinDropdownFinder = + find.byKey(const Key('fiat-onramp-coin-dropdown')); + final Finder btcIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-BTC')); + final Finder maticIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-LTC')); + + await tester.tap(coinDropdownFinder); + await tester.pumpAndSettle(); + expect(btcIconFinder, findsOneWidget, reason: 'BTC icon not found'); + await _tapCurrencyItem(tester, maticIconFinder); + await tester.pump(); + await tester.pumpAndSettle(); // wait for payment methods to populate + + await _testPaymentMethodSelection(tester); +} + +Future _tapCurrencyItem(WidgetTester tester, Finder asset) async { + final Finder list = find.byKey(const Key('fiat-onramp-currency-list')); + final Finder dialog = find.byKey(const Key('fiat-onramp-currency-dialog')); + + expect( + dialog, + findsOneWidget, + reason: 'Fiat onramp currency dialog not found', + ); + expect(list, findsOneWidget, reason: 'Fiat onramp currency list not found'); + await tester.dragUntilVisible(asset, list, const Offset(0, -50)); + await tester.pumpAndSettle(); + await tester.tap(asset); +} + +Future _testPaymentMethodSelection(WidgetTester tester) async { + final Finder rampPaymentMethodFinder = + find.byKey(const Key('fiat-payment-method-ramp-0')); + final Finder banxaPaymentMethodFinder = + find.byKey(const Key('fiat-payment-method-banxa-0')); + + expect( + rampPaymentMethodFinder, + findsOneWidget, + reason: 'Ramp payment method not found', + ); + expect( + banxaPaymentMethodFinder, + findsOneWidget, + reason: 'Banxa payment method not found', + ); + + await tester.tap(rampPaymentMethodFinder); + await tester.pump(); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets( + 'Run fiat form tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testFiatFormInputs(tester); + + print('END fiat form TESTS'); + }, + semanticsEnabled: false, + ); +} From 56ec51d11d9550613903926f95fe0c818fde5435 Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Fri, 15 Nov 2024 15:18:50 +0300 Subject: [PATCH 11/34] Private Key Export (#183) * kdf show_priv_key api * display private keys * private keys QR code dialog * private keys list title * private key dialog width * private key share instead of copy * Revert "private key share instead of copy" This reverts commit a623444ea75e4d074419bcc9689585ca819beceb. * private key clipboard warning --- assets/translations/en.json | 2 + lib/generated/codegen_loader.g.dart | 2 + lib/mm2/mm2_api/mm2_api.dart | 23 +++ .../show_priv_key/show_priv_key_request.dart | 19 ++ .../show_priv_key/show_priv_key_response.dart | 14 ++ .../security_settings_page.dart | 18 +- .../seed_settings/seed_show.dart | 164 +++++++++++++++++- 7 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart create mode 100644 lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 8015037428..904b1bddda 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -463,6 +463,8 @@ "manageAnalytics": "Manage analytics", "logs": "Logs", "resetActivatedCoinsTitle": "Reset Activated Coins", + "privateKeys": "Private Keys", + "copyWarning": "Your Clipboard isn't a safe place for your private key! Copying the seed phrase or private keys can make them vulnerable to clipboard hacks. Please handle with caution and only copy if absolutely necessary.", "seedConfirmTitle": "Let's double check your seed phrase", "seedConfirmDescription": "Your seed phrase is the only way to access Your funds. That's why we want to ensure you saved it safely. Please input your seed phrase into text filed below.", "standardWallet": "Standard wallet", diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 6503af80fc..3fc2a35baf 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -458,6 +458,8 @@ abstract class LocaleKeys { static const manageAnalytics = 'manageAnalytics'; static const logs = 'logs'; static const resetActivatedCoinsTitle = 'resetActivatedCoinsTitle'; + static const privateKeys = 'privateKeys'; + static const copyWarning = 'copyWarning'; static const seedConfirmTitle = 'seedConfirmTitle'; static const seedConfirmDescription = 'seedConfirmDescription'; static const standardWallet = 'standardWallet'; diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 6cf19583bf..5f3b8e8d2a 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -45,6 +45,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/rpc_error.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/stop/stop_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart'; @@ -917,4 +919,25 @@ class Mm2Api { return response; } + + Future showPrivKey( + ShowPrivKeyRequest request, + ) async { + try { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { + return null; + } + return ShowPrivKeyResponse.fromJson(json); + } catch (e, s) { + log( + 'Error getting privkey ${request.coin}: ${e.toString()}', + path: 'api => showPrivKey', + trace: s, + isError: true, + ); + return null; + } + } } diff --git a/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart b/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart new file mode 100644 index 0000000000..1ad5d3765e --- /dev/null +++ b/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart @@ -0,0 +1,19 @@ +import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; + +class ShowPrivKeyRequest implements BaseRequest { + ShowPrivKeyRequest({ + required this.coin, + }); + @override + late String userpass; + @override + final String method = 'show_priv_key'; + final String coin; + + @override + Map toJson() => { + 'method': method, + 'userpass': userpass, + 'coin': coin, + }; +} diff --git a/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart b/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart new file mode 100644 index 0000000000..cfa59b6c98 --- /dev/null +++ b/lib/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart @@ -0,0 +1,14 @@ +class ShowPrivKeyResponse { + ShowPrivKeyResponse({ + required this.coin, + required this.privKey, + }); + + factory ShowPrivKeyResponse.fromJson(Map json) => + ShowPrivKeyResponse( + coin: json['result']['coin'] ?? '', + privKey: json['result']['priv_key'] ?? '', + ); + final String coin; + final String privKey; +} 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 77f9c13cfe..7557f4bc2c 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -5,6 +5,9 @@ import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; import 'package:web_dex/bloc/security_settings/security_settings_state.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart'; +import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/settings_menu_value.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; @@ -28,6 +31,7 @@ class SecuritySettingsPage extends StatefulWidget { class _SecuritySettingsPageState extends State { String _seed = ''; + final Map _privKeys = {}; @override Widget build(BuildContext context) { @@ -70,20 +74,23 @@ class _SecuritySettingsPageState extends State { switch (step) { case SecuritySettingsStep.securityMain: _seed = ''; + _privKeys.clear(); return SecuritySettingsMainPage(onViewSeedPressed: onViewSeedPressed); case SecuritySettingsStep.seedShow: - return SeedShow(seedPhrase: _seed); + return SeedShow(seedPhrase: _seed, privKeys: _privKeys); case SecuritySettingsStep.seedConfirm: return SeedConfirmation(seedPhrase: _seed); case SecuritySettingsStep.seedSuccess: _seed = ''; + _privKeys.clear(); return const SeedConfirmSuccess(); case SecuritySettingsStep.passwordUpdate: _seed = ''; + _privKeys.clear(); return const PasswordUpdatePage(); } } @@ -99,6 +106,15 @@ class _SecuritySettingsPageState extends State { _seed = await wallet.getSeed(pass); if (_seed.isEmpty) return; + _privKeys.clear(); + for (final coin in coinsBloc.walletCoins) { + final result = + await mm2Api.showPrivKey(ShowPrivKeyRequest(coin: coin.abbr)); + if (result != null) { + _privKeys[coin] = result.privKey; + } + } + securitySettingsBloc.add(const ShowSeedEvent()); } } diff --git a/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart b/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart index 894404ed87..219c799a26 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart @@ -8,14 +8,22 @@ import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; import 'package:web_dex/bloc/security_settings/security_settings_state.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/dry_intrinsic.dart'; import 'package:web_dex/views/settings/widgets/security_settings/seed_settings/seed_back_button.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/views/wallet/coin_details/receive/qr_code_address.dart'; class SeedShow extends StatelessWidget { - const SeedShow({required this.seedPhrase}); + const SeedShow({ + required this.seedPhrase, + required this.privKeys, + }); final String seedPhrase; + final Map privKeys; @override Widget build(BuildContext context) { @@ -53,11 +61,13 @@ class SeedShow extends StatelessWidget { ), const SizedBox(height: 16), Flexible(child: _SeedPlace(seedPhrase: seedPhrase)), + const SizedBox(height: 20), + _SeedPhraseConfirmButton(seedPhrase: seedPhrase), + const SizedBox(height: 40), + _PrivateKeysList(privKeys: privKeys), ], ), ), - const SizedBox(height: 20), - _SeedPhraseConfirmButton(seedPhrase: seedPhrase) ], ), ), @@ -65,6 +75,125 @@ class SeedShow extends StatelessWidget { } } +class _PrivateKeysList extends StatelessWidget { + const _PrivateKeysList({ + required this.privKeys, + Key? key, + }) : super(key: key); + + final Map privKeys; + + @override + Widget build(BuildContext context) { + if (privKeys.isEmpty) { + return Container(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.privateKeys.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: privKeys.length, + itemBuilder: (context, index) { + final coin = privKeys.keys.elementAt(index); + final key = privKeys[coin]!; + + return Card( + color: Theme.of(context).colorScheme.onSurface, + margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CoinItem(coin: coin), + const Spacer(), + Text( + truncateMiddleSymbols(key, 5, 5), + style: Theme.of(context).textTheme.bodySmall, + ), + IconButton( + iconSize: 20, + icon: const Icon(Icons.copy), + onPressed: () { + copyToClipBoard(context, key); + }, + ), + IconButton( + iconSize: 20, + icon: const Icon(Icons.qr_code), + onPressed: () { + _showQrDialog(context, coin, key); + }, + ), + ], + ), + ), + ); + }, + ), + ], + ); + } + + void _showQrDialog(BuildContext context, Coin coin, String key) { + showDialog( + context: context, + builder: (context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: SizedBox( + width: 300, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CoinItem(coin: coin), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + const SizedBox(height: 16), + QRCodeAddress(currentAddress: key), + const SizedBox(height: 16), + SelectableText( + key, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + class _TitleRow extends StatelessWidget { const _TitleRow(); @@ -78,8 +207,33 @@ class _TitleRow extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 16), ), const SizedBox(height: 6), - Text(LocaleKeys.seedPhraseMakeSureBody.tr(), - style: Theme.of(context).textTheme.bodyLarge), + Text( + LocaleKeys.seedPhraseMakeSureBody.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.custom.warningColor.withOpacity(0.1), + border: Border.all(color: theme.custom.warningColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.warning, color: theme.custom.warningColor), + const SizedBox(width: 8), + Expanded( + child: Text( + LocaleKeys.copyWarning.tr(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: theme.custom.warningColor, + ), + ), + ), + ], + ), + ), ], ); } From c9f6acc0762b7dba82c0b4d8382add341c54238a Mon Sep 17 00:00:00 2001 From: "Charl (Nitride)" <77973576+CharlVS@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:38:16 +0100 Subject: [PATCH 12/34] Hot-fix for CDN fallback of missing coin icons (#180) * Fallback to remote images CDN for missing coin icons * Add coin icon fallback for all references to icons Add coin icon fallback for other widgets referencing the local image icons. Now all coin icons in the app share the same widget. * Prevent flicker for fallback coin icons Prevent flicker for fallback coin icons by caching the status of the existence on the CDN. * Further coin icon fallback bug fixes --- lib/shared/widgets/coin_item/coin_logo.dart | 52 +---- lib/views/bridge/bridge_protocol_label.dart | 16 +- lib/views/fiat/fiat_inputs.dart | 3 + .../lib/src/images/coin_icon.dart | 184 ++++++++++++------ pubspec.yaml | 2 +- 5 files changed, 144 insertions(+), 113 deletions(-) diff --git a/lib/shared/widgets/coin_item/coin_logo.dart b/lib/shared/widgets/coin_item/coin_logo.dart index 72aa610894..993599053c 100644 --- a/lib/shared/widgets/coin_item/coin_logo.dart +++ b/lib/shared/widgets/coin_item/coin_logo.dart @@ -1,6 +1,5 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -22,45 +21,17 @@ class CoinLogo extends StatelessWidget { return Stack( clipBehavior: Clip.none, children: [ - _CoinIcon( - coin: coin, - logoSize: size, - ), - _ProtocolIcon( - coin: coin, - logoSize: size, - ), + CoinIcon(coin.abbr, size: size), + if (coin.type != CoinType.utxo && coin.protocolData != null) + _ProtocolIcon( + coin: coin, + logoSize: size, + ), ], ); } } -class _CoinIcon extends StatelessWidget { - const _CoinIcon({ - required this.coin, - required this.logoSize, - }); - - final Coin coin; - final double logoSize; - - @override - Widget build(BuildContext context) { - return Container( - width: logoSize, - height: logoSize, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: dexPageColors.emptyPlace, - ), - child: Padding( - padding: const EdgeInsets.only(top: 2), - child: CoinIcon(coin.abbr, size: logoSize), - ), - ); - } -} - class _ProtocolIcon extends StatelessWidget { const _ProtocolIcon({ required this.coin, @@ -74,15 +45,9 @@ class _ProtocolIcon extends StatelessWidget { double get protocolBorder => protocolSizeWithBorder * 0.1; double get protocolLeftPosition => logoSize * 0.55; double get protocolTopPosition => logoSize * 0.55; - String get protocolIconPath => - '$assetsPath/coin_icons/png/${getProtocolIcon(coin)}.png'; @override Widget build(BuildContext context) { - if (coin.type == CoinType.utxo || coin.protocolData == null) { - return const SizedBox.shrink(); - } - return Positioned( left: protocolLeftPosition, top: protocolTopPosition, @@ -100,11 +65,12 @@ class _ProtocolIcon extends StatelessWidget { child: Container( width: protocolSizeWithBorder - protocolBorder, height: protocolSizeWithBorder - protocolBorder, - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white, - image: DecorationImage(image: AssetImage(protocolIconPath)), ), + child: CoinIcon(getProtocolIcon(coin), + size: protocolSizeWithBorder - protocolBorder), ), ), ); diff --git a/lib/views/bridge/bridge_protocol_label.dart b/lib/views/bridge/bridge_protocol_label.dart index 865e798ad0..21a61627f4 100644 --- a/lib/views/bridge/bridge_protocol_label.dart +++ b/lib/views/bridge/bridge_protocol_label.dart @@ -1,11 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/shared/utils/utils.dart'; -import 'package:web_dex/shared/widgets/coin_item/coin_logo.dart'; class BridgeProtocolLabel extends StatelessWidget { const BridgeProtocolLabel(this.coin); @@ -37,7 +36,7 @@ class BridgeProtocolLabel extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIcon(), + CoinIcon(coin.abbr, size: 16), const SizedBox(width: 6), _buildText(backgroundColor), ], @@ -45,17 +44,6 @@ class BridgeProtocolLabel extends StatelessWidget { ); } - Widget _buildIcon() { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: SizedBox( - width: 16, - child: Image.asset( - '$assetsPath/coin_icons/png/${getProtocolIcon(coin)}.png'), - ), - ); - } - Widget _buildText(Color protocolColor) { return Text( coin.type == CoinType.utxo diff --git a/lib/views/fiat/fiat_inputs.dart b/lib/views/fiat/fiat_inputs.dart index 799bbbac30..416945b863 100644 --- a/lib/views/fiat/fiat_inputs.dart +++ b/lib/views/fiat/fiat_inputs.dart @@ -9,6 +9,9 @@ import 'package:web_dex/views/fiat/custom_fiat_input_field.dart'; import 'package:web_dex/views/fiat/fiat_currency_item.dart'; import 'package:web_dex/views/fiat/fiat_icon.dart'; +// TODO(@takenagain): When `dev` is merged into `main`, please refactor this +// to use the `CoinIcon` widget. I'm leaving this unchanged for now to avoid +// merge conflicts with your fiat onramp overhaul. class FiatInputs extends StatefulWidget { const FiatInputs({ required this.initialFiat, diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 1d4cd18526..57bcb448e3 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,10 +2,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -// NB: ENSURE IT STAYS IN SYNC WITH MAIN PROJECT in `lib/src/utils/utils.dart`. -const coinImagesFolder = 'assets/coin_icons/png/'; +const coinImagesFolder = 'coin_icons/png/'; +const mediaCdnUrl = 'https://komodoplatform.github.io/coins/icons/'; final Map _assetExistenceCache = {}; +final Map _cdnExistenceCache = {}; List? _cachedFileList; String _getImagePath(String abbr) { @@ -13,13 +14,23 @@ String _getImagePath(String abbr) { return '$coinImagesFolder$fileName.png'; } +String _getCdnUrl(String abbr) { + final fileName = abbr2Ticker(abbr).toLowerCase(); + return '$mediaCdnUrl$fileName.png'; +} + Future> _getFileList() async { if (_cachedFileList == null) { - final manifestContent = await rootBundle.loadString('AssetManifest.json'); - final manifestMap = json.decode(manifestContent); - _cachedFileList = manifestMap.keys - .where((String key) => key.startsWith(coinImagesFolder)) - .toList(); + try { + final manifestContent = await rootBundle.loadString('AssetManifest.json'); + final Map manifestMap = json.decode(manifestContent); + _cachedFileList = manifestMap.keys + .where((String key) => key.startsWith(coinImagesFolder)) + .toList(); + } catch (e) { + debugPrint('Error loading asset manifest: $e'); + _cachedFileList = []; + } } return _cachedFileList!; } @@ -27,12 +38,32 @@ Future> _getFileList() async { Future checkIfAssetExists(String abbr) async { final filePath = _getImagePath(abbr); - if (!_assetExistenceCache.containsKey(filePath)) { - final fileList = await _getFileList(); - _assetExistenceCache[filePath] = fileList.contains(filePath); + if (_assetExistenceCache.containsKey(filePath)) { + return _assetExistenceCache[filePath]!; } - return _assetExistenceCache[filePath]!; + try { + final fileList = await _getFileList(); + final exists = fileList.contains(filePath); + + if (exists) { + try { + await rootBundle.load(filePath); + _assetExistenceCache[filePath] = true; + } catch (e) { + debugPrint('Asset $filePath found in manifest but failed to load: $e'); + _assetExistenceCache[filePath] = false; + } + } else { + _assetExistenceCache[filePath] = false; + } + + return _assetExistenceCache[filePath]!; + } catch (e) { + debugPrint('Error checking if asset exists for $abbr: $e'); + _assetExistenceCache[filePath] = false; + return false; + } } class CoinIcon extends StatelessWidget { @@ -45,8 +76,6 @@ class CoinIcon extends StatelessWidget { /// Convenience constructor for creating a coin icon from a symbol aka /// abbreviation. This avoids having to call [abbr2Ticker] manually. - /// - /// CoinIcon.ofSymbol( String symbol, { this.size = 20, @@ -60,46 +89,23 @@ class CoinIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final placeHolder = Center(child: Icon(Icons.monetization_on, size: size)); - return Opacity( opacity: suspended ? 0.4 : 1, child: SizedBox.square( dimension: size, - child: _maybeAssetExists() == true - ? _knownImage() - : _maybeAssetExists() == false - ? placeHolder - : FutureBuilder( - future: _getImage(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return placeHolder; - } - }, - ), + child: CoinIconResolverWidget( + key: Key(coinAbbr), + coinAbbr: coinAbbr, + size: size, + ), ), ); } - /// Returns null if the asset existence is unknown. - /// Returns true if the asset exists. - /// Returns false if the asset does not exist. - bool? _maybeAssetExists() => _assetExistenceCache[_getImagePath(coinAbbr)]; - - Image _knownImage() => Image.asset( - _getImagePath(coinAbbr), - filterQuality: FilterQuality.high, - ); - - Future _getImage() async { - if ((await checkIfAssetExists(coinAbbr)) == false) { - return null; - } - - return _knownImage(); + void clearAssetCaches() { + _assetExistenceCache.clear(); + _cdnExistenceCache.clear(); + _cachedFileList = null; } /// Pre-loads the coin icon image into the cache. @@ -114,15 +120,84 @@ class CoinIcon extends StatelessWidget { String abbr, { bool throwExceptions = false, }) async { - final filePath = _getImagePath(abbr); - final image = AssetImage(filePath); - await precacheImage( - image, - context, - onError: !throwExceptions - ? null - : (e, _) => - throw Exception('Failed to pre-cache image for coin $abbr: $e'), + try { + bool? assetExists, cdnExists; + + final filePath = _getImagePath(abbr); + final assetImage = AssetImage(filePath); + final cdn = _getCdnUrl(abbr); + final cdnImage = NetworkImage(cdn); + + assetExists = true; + await precacheImage( + assetImage, + context, + onError: (e, stackTrace) { + assetExists = false; + + if (throwExceptions) { + throw Exception('Failed to pre-cache image for coin $abbr: $e'); + } + }, + ); + if (context.mounted) { + cdnExists = true; + await precacheImage( + cdnImage, + context, + onError: (e, stackTrace) { + cdnExists = false; + + if (throwExceptions) { + throw Exception('Failed to pre-cache image for coin $abbr: $e'); + } + }, + ); + } + + _assetExistenceCache[filePath] = assetExists ?? false; + if (cdnExists != null) _cdnExistenceCache[abbr] = cdnExists!; + } catch (e) { + debugPrint('Error in precacheCoinIcon for $abbr: $e'); + if (throwExceptions) rethrow; + } + } +} + +class CoinIconResolverWidget extends StatelessWidget { + const CoinIconResolverWidget({ + super.key, + required this.coinAbbr, + required this.size, + }); + + final String coinAbbr; + final double size; + + @override + Widget build(BuildContext context) { + // Check local asset first + final filePath = _getImagePath(coinAbbr); + + // if (await checkIfAssetExists(coinAbbr)) { + + _assetExistenceCache[filePath] = true; + return Image.asset( + filePath, + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) { + _assetExistenceCache[filePath] = false; + + _cdnExistenceCache[coinAbbr] ??= true; + return Image.network( + _getCdnUrl(coinAbbr), + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) { + _cdnExistenceCache[coinAbbr] = false; + return Icon(Icons.monetization_on_outlined, size: size); + }, + ); + }, ); } } @@ -155,7 +230,6 @@ String abbr2Ticker(String abbr) { 'IBC_NUCLEUSTEST', ]; - // Join the suffixes with '|' to form the regex pattern String regexPattern = '(${filteredSuffixes.join('|')})'; String ticker = abbr diff --git a/pubspec.yaml b/pubspec.yaml index 39e697a202..57188d2daf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.1+0 +version: 0.8.2+0 environment: sdk: ">=3.4.0 <4.0.0" # The recent 3.5.0 breaks the build. We will resolve this after this release. From 5448183711cefc142b5dcfab1d579fbadf4bb524 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 25 Nov 2024 17:05:51 +0200 Subject: [PATCH 13/34] fix(lp-tools): ux inconsistency and coin amounts while order is being created (#184) * fix trading bot tab order * fix trading bot history onclick event * show estimate trade volume while creating new order --- lib/bloc/app_bloc_root.dart | 1 + ...arket_maker_bot_order_list_repository.dart | 78 +++++++++++++++---- .../market_maker_order_list/trade_pair.dart | 13 +++- .../market_maker_bot_form.dart | 2 +- .../market_maker_bot_page.dart | 1 + .../market_maker_bot_tab_content_wrapper.dart | 2 +- .../market_maker_bot_tab_type.dart | 4 +- .../trade_pair_list_item.dart | 4 +- 8 files changed, 85 insertions(+), 20 deletions(-) diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 20e05effa9..c8e93c5ae6 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -222,6 +222,7 @@ class AppBlocRoot extends StatelessWidget { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), + coinsBloc, ), ), ), diff --git a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart index 45e7fee7ca..6767dc0bda 100644 --- a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart +++ b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart @@ -1,5 +1,7 @@ +import 'package:rational/rational.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; +import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/services/orders_service/my_orders_service.dart'; @@ -7,10 +9,12 @@ import 'package:web_dex/services/orders_service/my_orders_service.dart'; class MarketMakerBotOrderListRepository { final MyOrdersService _ordersService; final SettingsRepository _settingsRepository; + final CoinsBloc _coinsRepository; const MarketMakerBotOrderListRepository( this._ordersService, this._settingsRepository, + this._coinsRepository, ); Future cancelOrders(List tradePairs) async { @@ -42,20 +46,68 @@ class MarketMakerBotOrderListRepository { final makerOrders = (await _ordersService.getOrders()) ?.where((order) => order.orderType == TradeSide.maker); - final tradePairs = configs - .map( - (e) => TradePair( - e, - makerOrders - ?.where( - (order) => - order.base == e.baseCoinId && order.rel == e.relCoinId, - ) - .firstOrNull, - ), - ) - .toList(); + final tradePairs = configs.map((TradeCoinPairConfig config) { + final order = makerOrders + ?.where( + (order) => + order.base == config.baseCoinId && + order.rel == config.relCoinId, + ) + .firstOrNull; + + final Rational baseCoinAmount = _getBaseCoinAmount(config, order); + return TradePair( + config, + order, + baseCoinAmount: baseCoinAmount, + relCoinAmount: _getRelCoinAmount(baseCoinAmount, config, order), + ); + }).toList(); return tradePairs; } + + Rational _getRelCoinAmount( + Rational baseCoinAmount, + TradeCoinPairConfig config, + MyOrder? order, + ) { + return order?.relAmountAvailable ?? + _getRelAmountFromBaseAmount(baseCoinAmount, config, order); + } + + Rational _getBaseCoinAmount(TradeCoinPairConfig config, MyOrder? order) { + return order?.baseAmountAvailable ?? + _getBaseAmountFromVolume(config.baseCoinId, config.maxVolume!.value); + } + + Rational _getBaseAmountFromVolume(String baseCoinId, double maxVolume) { + final double baseCoinBalance = + _coinsRepository.getCoin(baseCoinId)?.balance ?? 0; + final double baseCoinAmount = maxVolume * baseCoinBalance; + return Rational.parse(baseCoinAmount.toString()); + } + + Rational _getRelAmountFromBaseAmount( + Rational baseCoinAmount, + TradeCoinPairConfig config, + MyOrder? order, + ) { + final double? baseUsdPrice = + _coinsRepository.getCoin(config.baseCoinId)?.usdPrice?.price; + final double? relUsdPrice = + _coinsRepository.getCoin(config.relCoinId)?.usdPrice?.price; + final price = relUsdPrice != null && baseUsdPrice != null + ? baseUsdPrice / relUsdPrice + : null; + + Rational relAmount = Rational.zero; + if (price != null) { + final double priceWithMargin = price * (1 + (config.margin / 100)); + final double amount = baseCoinAmount.toDouble() * priceWithMargin; + return Rational.parse(amount.toString()); + } + + return relAmount; + } } diff --git a/lib/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart b/lib/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart index 6584509c36..52a6b8ef02 100644 --- a/lib/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart +++ b/lib/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart @@ -3,11 +3,22 @@ import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config. import 'package:web_dex/model/my_orders/my_order.dart'; class TradePair { - TradePair(this.config, this.order); + TradePair( + this.config, + this.order, { + Rational? baseCoinAmount, + Rational? relCoinAmount, + }) : baseCoinAmount = baseCoinAmount ?? Rational.zero, + relCoinAmount = relCoinAmount ?? Rational.zero; final TradeCoinPairConfig config; final MyOrder? order; + // needed to show coin amounts instead of 0 in the order list table before + // the order is created + final Rational baseCoinAmount; + final Rational relCoinAmount; + MyOrder get orderPreview => MyOrder( base: config.baseCoinId, rel: config.relCoinId, diff --git a/lib/views/market_maker_bot/market_maker_bot_form.dart b/lib/views/market_maker_bot/market_maker_bot_form.dart index be6d6affa0..f14e48c4b1 100644 --- a/lib/views/market_maker_bot/market_maker_bot_form.dart +++ b/lib/views/market_maker_bot/market_maker_bot_form.dart @@ -49,7 +49,7 @@ class MarketMakerBotForm extends StatelessWidget { .read() .add(MarketMakerBotOrderUpdateRequested(tradePair)); - context.read().add(const TabChanged(1)); + context.read().add(const TabChanged(2)); marketMakerTradeFormBloc.add(const MarketMakerTradeFormClearRequested()); } diff --git a/lib/views/market_maker_bot/market_maker_bot_page.dart b/lib/views/market_maker_bot/market_maker_bot_page.dart index 97ce17b6b9..aca38331b4 100644 --- a/lib/views/market_maker_bot/market_maker_bot_page.dart +++ b/lib/views/market_maker_bot/market_maker_bot_page.dart @@ -61,6 +61,7 @@ class _MarketMakerBotPageState extends State { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), + coinsBloc, ), ), ), diff --git a/lib/views/market_maker_bot/market_maker_bot_tab_content_wrapper.dart b/lib/views/market_maker_bot/market_maker_bot_tab_content_wrapper.dart index e105726981..e837fad916 100644 --- a/lib/views/market_maker_bot/market_maker_bot_tab_content_wrapper.dart +++ b/lib/views/market_maker_bot/market_maker_bot_tab_content_wrapper.dart @@ -148,7 +148,7 @@ class _SelectedTabContent extends StatelessWidget { } void _onSwapItemClick(Swap swap) { - routingState.dexState.setDetailsAction(swap.uuid); + routingState.marketMakerState.setDetailsAction(swap.uuid); } } diff --git a/lib/views/market_maker_bot/market_maker_bot_tab_type.dart b/lib/views/market_maker_bot/market_maker_bot_tab_type.dart index 05f0b4f747..c959b3b657 100644 --- a/lib/views/market_maker_bot/market_maker_bot_tab_type.dart +++ b/lib/views/market_maker_bot/market_maker_bot_tab_type.dart @@ -6,8 +6,8 @@ import 'package:web_dex/views/market_maker_bot/tab_type_enum.dart'; enum MarketMakerBotTabType implements TabTypeEnum { marketMaker, - orders, inProgress, + orders, history; @override @@ -40,7 +40,7 @@ enum MarketMakerBotTabType implements TabTypeEnum { /// This is a temporary solution to avoid changing the entire DEX flow to add /// the market maker bot tab. - // TODO(Francois): separate the tab widget logic from the page logic + // TODO: separate the tab widget logic from the page logic DexListType toDexListType() { switch (this) { case marketMaker: diff --git a/lib/views/market_maker_bot/trade_pair_list_item.dart b/lib/views/market_maker_bot/trade_pair_list_item.dart index 948ffa9000..9d2614bcf5 100644 --- a/lib/views/market_maker_bot/trade_pair_list_item.dart +++ b/lib/views/market_maker_bot/trade_pair_list_item.dart @@ -31,9 +31,9 @@ class TradePairListItem extends StatelessWidget { final config = pair.config; final order = pair.order; final sellCoin = config.baseCoinId; - final sellAmount = order?.baseAmountAvailable ?? Rational.zero; + final sellAmount = order?.baseAmountAvailable ?? pair.baseCoinAmount; final buyCoin = config.relCoinId; - final buyAmount = order?.relAmountAvailable ?? Rational.zero; + final buyAmount = order?.relAmountAvailable ?? pair.relCoinAmount; final String date = order != null ? getFormattedDate(order.createdAt) : '-'; final double fillProgress = order != null ? tradingEntitiesBloc.getProgressFillSwap(pair.order!) From 1aa57889177318f473bbb34111f01eefc14ae677 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 25 Nov 2024 17:06:11 +0200 Subject: [PATCH 14/34] feat(system-time-check): add fallback world time apis (#182) * add system clock repository with fallback urls * add directly connected peers rpc as fallback * improve utc parsing and request exception handling * revert changes to build_config --- lib/bloc/app_bloc_root.dart | 3 +- .../system_clock_repository.dart | 117 ++++++++++++++++++ .../system_health/system_health_bloc.dart | 43 ++++++- lib/mm2/mm2_api/mm2_api.dart | 31 ++++- .../get_directly_connected_peers.dart | 18 +++ ...get_directly_connected_peers_response.dart | 37 ++++++ lib/shared/utils/utils.dart | 26 ---- 7 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 lib/bloc/system_health/system_clock_repository.dart create mode 100644 lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart create mode 100644 lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index c8e93c5ae6..59a065b390 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -40,6 +40,7 @@ import 'package:web_dex/bloc/nfts/nft_main_repo.dart'; import 'package:web_dex/bloc/runtime_coin_updates/coin_config_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; +import 'package:web_dex/bloc/system_health/system_clock_repository.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; @@ -227,7 +228,7 @@ class AppBlocRoot extends StatelessWidget { ), ), BlocProvider( - create: (_) => SystemHealthBloc(), + create: (_) => SystemHealthBloc(SystemClockRepository(), mm2Api), ), BlocProvider( lazy: false, diff --git a/lib/bloc/system_health/system_clock_repository.dart b/lib/bloc/system_health/system_clock_repository.dart new file mode 100644 index 0000000000..21cc0c6954 --- /dev/null +++ b/lib/bloc/system_health/system_clock_repository.dart @@ -0,0 +1,117 @@ +// lib/repositories/system_clock_repository.dart +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:web_dex/shared/utils/utils.dart'; + +class SystemClockRepository { + SystemClockRepository({ + http.Client? httpClient, + Duration? maxAllowedDifference, + Duration? apiTimeout, + }) : _httpClient = httpClient ?? http.Client(), + _maxAllowedDifference = + maxAllowedDifference ?? const Duration(seconds: 60), + _apiTimeout = apiTimeout ?? const Duration(seconds: 2); + + static const _utcWorldTimeApis = [ + 'https://worldtimeapi.org/api/timezone/UTC', + 'https://timeapi.io/api/time/current/zone?timeZone=UTC', + 'http://worldclockapi.com/api/json/utc/now', + ]; + + final Duration _maxAllowedDifference; + final Duration _apiTimeout; + final http.Client _httpClient; + + /// Queries the available 3rd party APIs to validate the system clock validity + /// returning true if the system clock is within allowed difference of the API + /// time, false otherwise. Uses the first successful response + Future isSystemClockValid({ + List timeApiUrls = _utcWorldTimeApis, + }) async { + try { + final futures = timeApiUrls.map((url) => _httpGet(url)); + + final responses = await Future.wait( + futures, + eagerError: false, + ); + + for (final response in responses) { + if (response.statusCode != 200) { + continue; + } + + final jsonResponse = json.decode(response.body) as Map; + final DateTime apiTime = _parseUtcDateTimeString(jsonResponse); + final localTime = DateTime.timestamp(); + final Duration difference = apiTime.difference(localTime).abs(); + + return difference < _maxAllowedDifference; + } + + // Log error if no successful responses + log('All time API requests failed').ignore(); + return true; + } catch (e) { + log('Failed to validate system clock: $e').ignore(); + return true; // Don't block usage + } + } + + Future _httpGet(String url) async { + try { + return await _httpClient.get(Uri.parse(url)).timeout(_apiTimeout); + } catch (e) { + return http.Response('Error: $e', HttpStatus.internalServerError); + } + } + + DateTime _parseUtcDateTimeString(Map jsonResponse) { + dynamic apiTimeStr = jsonResponse['datetime'] ?? // worldtimeapi.org + jsonResponse['dateTime'] ?? // worldclockapi.com + jsonResponse['currentDateTime']; // timeapi.io + + if (apiTimeStr == null) { + throw Exception('API response does not contain datetime field'); + } + + if (apiTimeStr is! String || apiTimeStr.isEmpty) { + throw const FormatException('API datetime field is not a string'); + } + + // Convert +00:00 format to Z format if needed + if (apiTimeStr.endsWith('+00:00')) { + apiTimeStr = apiTimeStr.replaceAll('+00:00', 'Z'); + } else if (!apiTimeStr.endsWith('Z')) { + apiTimeStr += 'Z'; // Add UTC timezone indicator if missing + } + + final apiTime = DateTime.parse(apiTimeStr); + if (!apiTime.isUtc) { + throw const FormatException('API time is not in UTC'); + } + return apiTime; + } + + /// Checks if there are enough active seeders to indicate valid system clock + Future hasActiveSeeders() async { + // TODO: Implement seeder check logic onur suggested - few seeders + // implies that the user's clock is invalid and being rejected by seeders + throw UnimplementedError('Not implemented yet'); + } + + /// Combines multiple clock validation methods + Future isClockValidWithAllChecks() async { + final apiCheck = await isSystemClockValid(); + final seederCheck = await hasActiveSeeders(); + + return apiCheck && seederCheck; + } + + void dispose() { + _httpClient.close(); + } +} diff --git a/lib/bloc/system_health/system_health_bloc.dart b/lib/bloc/system_health/system_health_bloc.dart index 6287e7e98d..9736932f3c 100644 --- a/lib/bloc/system_health/system_health_bloc.dart +++ b/lib/bloc/system_health/system_health_bloc.dart @@ -1,16 +1,22 @@ import 'dart:async'; + import 'package:bloc/bloc.dart'; -import 'system_health_event.dart'; -import 'system_health_state.dart'; -import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/bloc/system_health/system_clock_repository.dart'; +import 'package:web_dex/bloc/system_health/system_health_event.dart'; +import 'package:web_dex/bloc/system_health/system_health_state.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart'; class SystemHealthBloc extends Bloc { - SystemHealthBloc() : super(SystemHealthInitial()) { + SystemHealthBloc(this._systemClockRepository, this._api) + : super(SystemHealthInitial()) { on(_onCheckSystemClock); _startPeriodicCheck(); } Timer? _timer; + final SystemClockRepository _systemClockRepository; + final Mm2Api _api; void _startPeriodicCheck() { add(CheckSystemClock()); @@ -26,12 +32,39 @@ class SystemHealthBloc extends Bloc { ) async { emit(SystemHealthLoadInProgress()); try { - emit(SystemHealthLoadSuccess(await systemClockIsValid())); + final bool systemClockValid = + await _systemClockRepository.isSystemClockValid(); + final bool connectedPeersHealthy = await _arePeersConnected(); + final bool isSystemHealthy = systemClockValid || connectedPeersHealthy; + + emit(SystemHealthLoadSuccess(isSystemHealthy)); } catch (_) { emit(SystemHealthLoadFailure()); } } + Future _arePeersConnected() async { + try { + final directlyConnectedPeers = + await _api.getDirectlyConnectedPeers(GetDirectlyConnectedPeers()); + final connectedPeersHealthy = directlyConnectedPeers.peers.length >= 2; + return connectedPeersHealthy; + } on Exception catch (_) { + // TODO: remove once the breaking rpc name change is in main + try { + final directlyConnectedPeers = await _api.getDirectlyConnectedPeers( + GetDirectlyConnectedPeers(method: 'get_peers_info'), + ); + final connectedPeersHealthy = directlyConnectedPeers.peers.length >= 2; + return connectedPeersHealthy; + } catch (_) { + // fall through and return false + } + + return false; + } + } + @override Future close() { _timer?.cancel(); diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 5f3b8e8d2a..53f3649432 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -10,6 +10,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/cancel_order/cancel_order_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/electrum/electrum_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/enable/enable_req.dart'; @@ -19,9 +21,9 @@ import 'package:web_dex/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req. import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/kmd_rewards_info/kmd_rewards_info_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/market_maker_bot_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/market_maker_bot_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol.dart'; @@ -940,4 +942,31 @@ class Mm2Api { return null; } } + + Future getDirectlyConnectedPeers( + GetDirectlyConnectedPeers request, + ) async { + try { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { + log( + 'Error getting directly connected peers: ${json['error']}', + isError: true, + path: 'api => getDirectlyConnectedPeers', + ); + throw Exception('Failed to get directly connected peers'); + } + + return GetDirectlyConnectedPeersResponse.fromJson(json); + } catch (e, s) { + log( + 'Error getting directly connected peers', + path: 'api => getDirectlyConnectedPeers', + trace: s, + isError: true, + ); + rethrow; + } + } } diff --git a/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart new file mode 100644 index 0000000000..9cde2cb47d --- /dev/null +++ b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart @@ -0,0 +1,18 @@ +import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; + +class GetDirectlyConnectedPeers implements BaseRequest { + GetDirectlyConnectedPeers({this.method = 'get_directly_connected_peers'}); + + @override + final String method; + @override + late String userpass; + + @override + Map toJson() { + return { + 'method': method, + 'userpass': userpass, + }; + } +} diff --git a/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart new file mode 100644 index 0000000000..817f8e2e71 --- /dev/null +++ b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart @@ -0,0 +1,37 @@ +class GetDirectlyConnectedPeersResponse { + GetDirectlyConnectedPeersResponse({ + required this.peers, + }); + + factory GetDirectlyConnectedPeersResponse.fromJson( + Map json) { + final peersMap = json['result'] as Map? ?? {}; + final peersList = peersMap.keys + .map((key) => DirectlyConnectedPeer( + peerId: key, + peerAddresses: peersMap[key] as List? ?? [], + )) + .toList(); + + return GetDirectlyConnectedPeersResponse(peers: peersList); + } + + final List peers; +} + +class DirectlyConnectedPeer { + DirectlyConnectedPeer({ + required this.peerId, + required this.peerAddresses, + }); + + factory DirectlyConnectedPeer.fromJson(Map json) { + return DirectlyConnectedPeer( + peerId: json['id'] as String? ?? '', + peerAddresses: json['addresses'] as List? ?? [], + ); + } + + final String peerId; + final List peerAddresses; +} diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 7d72810dd6..67de08231b 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:app_theme/app_theme.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -18,34 +17,9 @@ import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/performance_analytics/performance_analytics.dart'; import 'package:web_dex/services/logger/get_logger.dart'; import 'package:web_dex/shared/constants.dart'; -import 'package:http/http.dart' as http; export 'package:web_dex/shared/utils/extensions/async_extensions.dart'; export 'package:web_dex/shared/utils/prominent_colors.dart'; -Future systemClockIsValid() async { - try { - final response = await http - .get(Uri.parse('https://worldtimeapi.org/api/timezone/UTC')) - .timeout(const Duration(seconds: 20)); - - if (response.statusCode == 200) { - final jsonResponse = json.decode(response.body); - final apiTimeStr = jsonResponse['datetime']; - final apiTime = DateTime.parse(apiTimeStr).toUtc(); - final localTime = DateTime.now().toUtc(); - final difference = apiTime.difference(localTime).abs().inSeconds; - - return difference < 60; - } else { - log('Failed to get time from API'); - return true; // Do not block the usage - } - } catch (e) { - log('Failed to validate system clock'); - return true; // Do not block the usage - } -} - void copyToClipBoard(BuildContext context, String str) { final themeData = Theme.of(context); try { From bbac902075137b9e645e962020afade1b7bc5a7d Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 26 Nov 2024 13:37:08 +0200 Subject: [PATCH 15/34] feature: kdf sdk integration part 1 - startup (#177) * add komodo_defi_sdk to dependencies update required dependencies, and fix build errors * WIP: replace mm2 classes with defi sdk * replace kdf rpc status checks with isSignedIn The new SDK abstracts away the KDF functionality behind an authentication class, so the previous status checks are not possible, and the isSignedIn is the closest alternative without removing the logic entirely (possibly breaking change) * bump kdf version * load coin assets from sdk package komodo_defi_framework package already downloads the configs and icons, so load them from the external package instead of redownloading and loading the same assets twice * ci: update paths in validate action * replace dynamic index.html with static version the drawbacks of bundling with webpack outweighed the negligible size reduction in the kdf wasm files, so it was dropped in favour of a static index.html in the sdk * fix failing unit tests caused by the `rational` and `decimal` package updates * update docs remove nodejs, api update script, and add fvm as flutter installation alternative * fix breaking changes to rpc method return types sdk returns different types which have to be accounted for, sometimes on a per-rpc basis * fix validation warnings * WIP: fix integration tests temporarily disable suspended assets test. The `coins_config.json` is no longer editable as it is obtained from an external package, so alternative means of invalidating the electrum URLs for an asset are required * re-enable suspended assets test on chrome * block electrum urls in chrome for suspended assets test * update logs location in ui-tests workflow * fix taker order integration test * change from sdk to komodo_defi_framework * fix debug utils & withdraw test * remove driver start step from ui test workflow browser driver startup is now handled by integration test runner * improve driver management & fix `test_withdraw` test add and refactor integration test utility functions * add pub get flag flutter drive runs `flutter pub get` before each test by default, which slows down the current implementation, which runs each set of tests independently * add profile mode step to ui-tests-on-pr * add verbose logging to integration tests steps some errors, like pumpAndSettle timeouts, do not produce useful stacktraces, so adding print statements is necessary unless we run the integration tests in debug mode (not recommended) * fix cex_prices integration test * fix theme switching on web * fix nft and trezor RPC call type errors type conversions missed during initial migration, but caught by integration tests * migrate web file_loader to js_interop and package:web * change file loader to conditional import structure js_interop causes builds to fail once again * fix seed file upload & add keep-running flag safaridriver logs do not include console logs, so we have to keep the browser window open to read console logs in the event of failure * fix intermittent test failures * bump sdk version & add debug statements * increase flutter drive timeout & remove tests.dart group structure is better suited to the new integration_test format rather than flutter_drive. `await app.main` fails after the second test when in the same group * fix merge error * fix validation warnings & flaky dex taker test * add port option & fix flaky dex ui test steps * tests: add longer wait after taker/maker confirmation button click * fix misc integration test errors when switching theme * fix trading bot order count in tab bar * fix analyze warnings and increase tab refresh rate * restore missing error check throw exception if error response is received from API. Market maker bot was failing to start/stop after this was removed as part of the sdk integration * fix trading bot & dex order list onClick events * fix merge issues & trade bot empty list check list null = empty * fix best orders and connected peers parsing v2 uses orderaddress object for address field instead of string --- .github/actions/generate-assets/action.yml | 3 + .github/actions/validate-build/action.yml | 35 +- .github/workflows/ui-tests-on-pr.yml | 101 +- .gitignore | 7 - README.md | 1 - app_build/.gitignore | 160 -- app_build/BUILD_CONFIG_README.md | 105 +- app_build/build_config.json | 60 - assets/config/.gitkeep | 0 docs/BUILD_CONFIG.md | 26 +- docs/INSTALL_FLUTTER.md | 32 +- docs/INTEGRATION_TESTING.md | 55 +- docs/MULTIPLE_FLUTTER_VERSIONS.md | 39 +- docs/PROJECT_SETUP.md | 8 +- docs/UPDATE_API_MODULE.md | 62 - ios/Podfile.lock | 19 + lib/app_config/app_config.dart | 1 + lib/app_config/coins_config_parser.dart | 8 +- lib/bloc/app_bloc_root.dart | 6 +- lib/bloc/auth_bloc/auth_bloc.dart | 23 +- lib/bloc/auth_bloc/auth_bloc_event.dart | 7 +- lib/bloc/auth_bloc/auth_repository.dart | 31 +- lib/bloc/bridge_form/bridge_validator.dart | 2 +- lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart | 98 +- lib/bloc/dex_tab_bar/dex_tab_bar_event.dart | 34 +- lib/bloc/dex_tab_bar/dex_tab_bar_state.dart | 47 +- .../market_maker_bot_bloc.dart | 33 +- .../market_maker_bot_repository.dart | 30 +- ...arket_maker_bot_order_list_repository.dart | 8 +- .../market_maker_trade_form_state.dart | 2 +- lib/bloc/taker_form/taker_validator.dart | 2 +- .../trezor_connection_bloc.dart | 3 +- .../trezor_init_bloc/trezor_init_bloc.dart | 17 +- lib/blocs/blocs.dart | 4 +- lib/blocs/startup_bloc.dart | 3 +- lib/mm2/mm2.dart | 198 +- lib/mm2/mm2_android.dart | 66 - lib/mm2/mm2_api/mm2_api.dart | 543 ++--- lib/mm2/mm2_api/mm2_api_nft.dart | 80 +- lib/mm2/mm2_api/mm2_api_trezor.dart | 73 +- .../mm2_api/rpc/best_orders/best_orders.dart | 108 +- ...get_directly_connected_peers_response.dart | 16 +- .../max_maker_vol/max_maker_vol_response.dart | 20 +- .../max_taker_vol/max_taker_vol_response.dart | 14 +- .../rpc/my_orders/my_orders_response.dart | 28 +- .../my_recent_swaps_response.dart | 31 +- lib/mm2/mm2_api/rpc/rpc_error.dart | 20 +- lib/mm2/mm2_ios.dart | 77 - lib/mm2/mm2_linux.dart | 102 - lib/mm2/mm2_macos.dart | 64 - lib/mm2/mm2_web.dart | 77 - lib/mm2/mm2_windows.dart | 106 - lib/model/dex_list_type.dart | 4 +- lib/platform/platform_native.dart | 4 - lib/platform/platform_web.dart | 3 - .../page_content_router_delegate.dart | 2 +- .../page_menu/page_menu_router_delegate.dart | 2 +- lib/services/file_loader/file_loader.dart | 6 + ...le_loader.dart => file_loader_native.dart} | 13 +- .../file_loader/file_loader_stub.dart | 5 + lib/services/file_loader/file_loader_web.dart | 115 +- lib/services/logger/universal_logger.dart | 3 +- lib/shared/utils/debug_utils.dart | 7 +- lib/shared/utils/formatters.dart | 44 +- lib/shared/utils/utils.dart | 20 +- lib/views/dex/dex_page.dart | 47 +- lib/views/dex/dex_tab_bar.dart | 43 +- .../form/exchange_info/exchange_rate.dart | 27 +- .../market_maker_bot_order_list.dart | 11 +- .../market_maker_bot_page.dart | 11 +- .../market_maker_bot_tab_bar.dart | 47 +- .../market_maker_bot_tab_type.dart | 6 +- lib/views/market_maker_bot/tab_type_enum.dart | 4 +- .../wallet_page/wallet_main/wallet_main.dart | 1 + .../widgets/iguana_wallets_manager.dart | 24 +- .../widgets/wallet_simple_import.dart | 12 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Podfile.lock | 19 + macos/Runner.xcodeproj/project.pbxproj | 6 + package-lock.json | 1905 ----------------- package.json | 30 - packages/komodo_cex_market_data/pubspec.lock | 435 ++++ packages/komodo_cex_market_data/pubspec.yaml | 5 +- packages/komodo_coin_updates/pubspec.yaml | 16 +- .../lib/src/images/coin_icon.dart | 3 +- pubspec.lock | 263 ++- pubspec.yaml | 100 +- run_integration_tests.dart | 391 ++-- test_integration/common/goto.dart | 28 +- test_integration/common/pump_and_settle.dart | 30 - test_integration/common/tester_utils.dart | 20 - .../widget_tester_action_extensions.dart | 53 + .../common/widget_tester_find_extension.dart | 8 + .../common/widget_tester_pump_extension.dart | 65 + test_integration/helpers/restore_wallet.dart | 49 +- .../integration_test_arguments.dart | 81 + test_integration/runners/app_data.dart | 72 + .../drivers/chrome_config_manager.dart | 126 ++ .../runners/drivers/chrome_driver.dart | 54 + test_integration/runners/drivers/find.dart | 78 + .../runners/drivers/firefox_driver.dart | 34 + .../runners/drivers/safari_driver.dart | 37 + .../runners/drivers/web_browser_driver.dart | 113 + .../drivers/web_driver_process_mixin.dart | 193 ++ .../runners/integration_test_runner.dart | 136 ++ .../tests/dex_tests/dex_tests.dart | 17 +- .../tests/dex_tests/maker_orders_test.dart | 117 +- .../tests/dex_tests/taker_orders_test.dart | 344 +-- .../fiat_onramp_tests/fiat_onramp_tests.dart | 13 +- .../tests/fiat_onramp_tests/form_tests.dart | 38 +- .../tests/misc_tests/feedback_tests.dart | 4 +- .../tests/misc_tests/menu_tests.dart | 5 - .../tests/misc_tests/misc_tests.dart | 19 +- .../tests/misc_tests/theme_test.dart | 23 +- .../tests/nfts_tests/nfts_tests.dart | 38 +- .../tests/no_login_tests/no_login_tests.dart | 40 +- .../tests/suspended_assets_test/runner.dart | 93 - .../suspended_assets_test.dart | 53 +- .../wallets_manager_create_test.dart | 42 +- .../wallets_manager_import_test.dart | 85 +- .../wallets_manager_tests.dart | 45 +- .../wallets_tests/test_activate_coins.dart | 38 +- .../test_bitrefill_integration.dart | 8 +- .../tests/wallets_tests/test_cex_prices.dart | 59 +- .../tests/wallets_tests/test_coin_assets.dart | 32 +- .../tests/wallets_tests/test_filters.dart | 29 +- .../tests/wallets_tests/test_withdraw.dart | 191 +- .../tests/wallets_tests/wallet_tools.dart | 49 +- .../tests/wallets_tests/wallets_tests.dart | 23 +- .../helpers/calculate_buy_amount_test.dart | 56 +- web/assets/template.html | 198 -- web/flutter_bootstrap.js | 24 - web/index.html | 156 +- web/index.js | 91 - web/kdf/res/kdf_wrapper.dart | 94 + web/kdf/res/kdflib_bootstrapper.js | 79 + web/services/zip/zip.js | 27 - web/services/zip/zip_worker.js | 19 - webpack.config.js | 33 - .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 3 + 143 files changed, 4412 insertions(+), 5295 deletions(-) delete mode 100644 app_build/.gitignore delete mode 100644 assets/config/.gitkeep delete mode 100644 docs/UPDATE_API_MODULE.md delete mode 100644 lib/mm2/mm2_android.dart delete mode 100644 lib/mm2/mm2_ios.dart delete mode 100644 lib/mm2/mm2_linux.dart delete mode 100644 lib/mm2/mm2_macos.dart delete mode 100644 lib/mm2/mm2_web.dart delete mode 100644 lib/mm2/mm2_windows.dart rename lib/services/file_loader/{get_file_loader.dart => file_loader_native.dart} (57%) create mode 100644 lib/services/file_loader/file_loader_stub.dart delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 packages/komodo_cex_market_data/pubspec.lock delete mode 100644 test_integration/common/pump_and_settle.dart delete mode 100644 test_integration/common/tester_utils.dart create mode 100644 test_integration/common/widget_tester_action_extensions.dart create mode 100644 test_integration/common/widget_tester_find_extension.dart create mode 100644 test_integration/common/widget_tester_pump_extension.dart create mode 100644 test_integration/integration_test_arguments.dart create mode 100644 test_integration/runners/app_data.dart create mode 100644 test_integration/runners/drivers/chrome_config_manager.dart create mode 100644 test_integration/runners/drivers/chrome_driver.dart create mode 100644 test_integration/runners/drivers/find.dart create mode 100644 test_integration/runners/drivers/firefox_driver.dart create mode 100644 test_integration/runners/drivers/safari_driver.dart create mode 100644 test_integration/runners/drivers/web_browser_driver.dart create mode 100644 test_integration/runners/drivers/web_driver_process_mixin.dart create mode 100644 test_integration/runners/integration_test_runner.dart delete mode 100644 test_integration/tests/suspended_assets_test/runner.dart delete mode 100644 web/assets/template.html delete mode 100644 web/flutter_bootstrap.js delete mode 100644 web/index.js create mode 100644 web/kdf/res/kdf_wrapper.dart create mode 100644 web/kdf/res/kdflib_bootstrapper.js delete mode 100644 web/services/zip/zip.js delete mode 100644 web/services/zip/zip_worker.js delete mode 100644 webpack.config.js diff --git a/.github/actions/generate-assets/action.yml b/.github/actions/generate-assets/action.yml index fa83013f8e..153eeec07d 100644 --- a/.github/actions/generate-assets/action.yml +++ b/.github/actions/generate-assets/action.yml @@ -22,6 +22,9 @@ runs: unset GITHUB_API_PUBLIC_READONLY_TOKEN fi + # Run flutter build once to download coin icons and config files. + # This step is expected to "fail", since flutter build has to run again + # after the assets are downloaded to register them in AssetManifest.bin flutter pub get > /dev/null 2>&1 flutter build web --release > /dev/null 2>&1 || true rm -rf build/* diff --git a/.github/actions/validate-build/action.yml b/.github/actions/validate-build/action.yml index ebf5b8b49a..2e228c0587 100644 --- a/.github/actions/validate-build/action.yml +++ b/.github/actions/validate-build/action.yml @@ -7,9 +7,11 @@ runs: continue-on-error: false shell: bash run: | - # Check that the web build folder contains a wasm file in the format build/web/dist/*.wasm - if [ ! -f build/web/dist/*.wasm ]; then - echo "Error: Web build failed. No wasm file found in build/web/dist/" + COIN_ASSETS_DIR=build/web/assets/packages/komodo_defi_framework/assets + + # 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 + 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 @@ -26,13 +28,6 @@ runs: exit 1 fi - # Check that the index.html has uncommitted changes to ensure that the placeholder was replaced with the generated content - git update-index --no-assume-unchanged web/index.html - if git diff --quiet web/index.html; then - echo "Error: Web build failed. index.html has no uncommitted changes which indicates an issue with the \`template.html\` to \`index.html\` generation" - exit 1 - fi - # Decode the AssetManifest.bin and check for the coin icon presence if [ ! -f build/web/assets/AssetManifest.bin ]; then echo "Error: AssetManifest.bin file not found." @@ -43,42 +38,42 @@ runs: echo "Output of case-invariant grep on build/web/assets/AssetManifest.bin" strings build/web/assets/AssetManifest.bin | grep -i "assets/coin_icons/png/kmd.png" echo "Listing kmd png files in assets/coin_icons/png" - ls -R assets/coin_icons/png | grep kmd.png + ls -R build/web/assets | grep kmd.png if ! strings build/web/assets/AssetManifest.bin | grep -qi "assets/coin_icons/png/kmd.png"; then echo "Error: Coin icon not found in AssetManifest.bin" exit 1 fi fi - # Check that build/web/assets/app_build/build_config.json is present, is valid json + # Check that app_build/build_config.json is present, is valid json # and does not contain "LAST_RUN" in the first line (invalid json format) - if [ ! -f build/web/assets/app_build/build_config.json ]; then + if [ ! -f app_build/build_config.json ]; then echo "Error: build_config.json file not found." exit 1 fi - if ! jq . build/web/assets/app_build/build_config.json > /dev/null; then + if ! jq . app_build/build_config.json > /dev/null; then echo "Error: build_config.json is not valid json" exit 1 fi - # Check that build/web/assets/assets/config/coins.json is present, is valid json + # Check that $COIN_ASSETS_DIR/config/coins.json is present, is valid json # and does not contain "LAST_RUN" in the first line (invalid json format) - if [ ! -f build/web/assets/assets/config/coins.json ]; then + if [ ! -f $COIN_ASSETS_DIR/config/coins.json ]; then echo "Error: coins.json file not found." exit 1 fi - if ! jq . build/web/assets/assets/config/coins.json > /dev/null; then + if ! jq . $COIN_ASSETS_DIR/config/coins.json > /dev/null; then echo "Error: coins.json is not valid json" exit 1 fi - # Check that build/web/assets/assets/config/coins_config.json is present, is valid json + # Check that $COIN_ASSETS_DIR/config/coins_config.json is present, is valid json # and does not contain "LAST_RUN" in the first line (invalid json format) - if [ ! -f build/web/assets/assets/config/coins_config.json ]; then + if [ ! -f $COIN_ASSETS_DIR/config/coins_config.json ]; then echo "Error: coins_config.json file not found." exit 1 fi - if ! jq . build/web/assets/assets/config/coins_config.json > /dev/null; then + if ! jq . $COIN_ASSETS_DIR/config/coins_config.json > /dev/null; then echo "Error: coins_config.json is not valid json" exit 1 fi diff --git a/.github/workflows/ui-tests-on-pr.yml b/.github/workflows/ui-tests-on-pr.yml index 880e3e5439..ca1148214c 100644 --- a/.github/workflows/ui-tests-on-pr.yml +++ b/.github/workflows/ui-tests-on-pr.yml @@ -10,27 +10,44 @@ jobs: ui_tests: name: Test ${{ matrix.name }} runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 45 strategy: fail-fast: false matrix: name: [ - web-app-linux, + web-app-linux-profile, web-app-macos, ] include: - - name: web-app-linux - os: ubuntu-latest + - name: web-app-linux-profile + os: [self-hosted, linux] + browser: chrome + display: 'headless' + resolution: '1600,1024' + mode: profile + # memory_profile.json should be generated in profile mode + driver_logs: | + ./*.log + ./memory_profile.json - name: web-app-macos - os: macos-14 + os: [self-hosted, macos] + browser: safari + display: 'headless' # has no affect with safaridriver + resolution: '1600,1024' # has no affect with safaridriver + mode: release + driver_logs: | + ./*.log + ~/Library/Logs/com.apple.WebDriver/**/*.log + ~/Library/Logs/com.apple.WebDriver/**/*.txt steps: - name: Setup GH Actions uses: actions/checkout@v4 + # Flutter integration test setup - name: Install Chrome and chromedriver - if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} + if: ${{ matrix.browser == 'chrome' }} uses: browser-actions/setup-chrome@v1 id: setup-chrome with: @@ -38,17 +55,9 @@ jobs: install-chromedriver: true install-dependencies: true - - name: Add chromedriver to PATH - if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} - run: | - export PATH="${{ steps.setup-chrome.outputs.chrome-path }}:${PATH}" - export PATH="${{ steps.setup-chrome.outputs.chromedriver-path }}:${PATH}" - - echo "Chrome version: ${{ steps.setup-chrome.outputs.chrome-version }}" - echo "chromedriver version: $(chromedriver --version)" - name: Enable safaridriver (sudo) (MacOS) - if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} + if: ${{ matrix.browser == 'safari' }} timeout-minutes: 1 run: | defaults write com.apple.Safari IncludeDevelopMenu YES @@ -62,64 +71,40 @@ jobs: uses: ./.github/actions/generate-assets with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: Validate build uses: ./.github/actions/validate-build - - name: Test air_dex chrome (unix) (Linux) - if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} - id: tests-chrome + # Run integration tests + - name: Test air_dex ${{ matrix.browser }} + id: integration-tests continue-on-error: true timeout-minutes: 35 env: GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - chromedriver --port=4444 --silent --enable-chrome-logs --log-path=chrome_console.log & - dart run_integration_tests.dart -d 'headless' -b '1600,1024' --browser-name=chrome - - - name: Run safaridriver in background (MacOS) - if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} - continue-on-error: true - run: | - safaridriver -p 4444 --diagnose & - - - name: Test air_dex safari (MacOS) - if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} - id: tests-safari - continue-on-error: true - timeout-minutes: 35 - env: - GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - flutter pub get - dart run_integration_tests.dart --browser-name=safari - - - name: Upload logs (Linux) - if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} + dart run_integration_tests.dart \ + -d ${{ matrix.display }} \ + -b ${{ matrix.resolution }} \ + -n ${{ matrix.browser }} \ + -m ${{ matrix.mode }} + + # Post-test steps (upload logs, coverage, and failure check) + - name: Upload driver logs uses: actions/upload-artifact@v4 with: - name: ${{ runner.os }}-chromedriver-logs.zip - path: ./chrome_console.log - - - name: Upload logs (macOS) - if: ${{ matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine' }} - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-safaridriver-logs.zip - path: ~/Library/Logs/com.apple.WebDriver/ + name: ${{ runner.os }}-${{ matrix.browser }}-logs + path: ${{ matrix.driver_logs }} + if-no-files-found: warn - name: Generate coverage report - if: ${{ matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon' }} - id: test-coverage-report + if: ${{ matrix.browser == 'chrome' }} + continue-on-error: true timeout-minutes: 35 uses: ./.github/actions/code-coverage with: test_file: 'test_integration' - - name: Fail workflow if tests failed (Linux) - if: ${{ (matrix.os == 'ubuntu-latest' || runner.name == 'ci-builder-radon') && steps.tests-chrome.outcome == 'failure' }} - run: exit 1 - - - name: Fail workflow if tests failed (MacOS) - if: ${{ (matrix.os == 'macos-latest' || runner.name == 'ci-builder-astatine') && steps.tests-safari.outcome == 'failure' }} + - name: Fail workflow if tests failed + if: ${{ steps.integration-tests.outcome == 'failure' }} run: exit 1 diff --git a/.gitignore b/.gitignore index ac512ae593..f647132e55 100644 --- a/.gitignore +++ b/.gitignore @@ -66,7 +66,6 @@ airdex-build.tar.gz # js node_modules -assets/config/test_wallet.json assets/**/debug_data.json contrib/coins_config.json @@ -84,12 +83,6 @@ macos/kdf # Android C++ files android/app/.cxx/ -# Coins asset files -assets/config/coins.json -assets/config/coins_config.json -assets/config/coins_ci.json -assets/coin_icons/ - # Python venv/ diff --git a/README.md b/README.md index 50f713273e..42c8523dd6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ Current production version is available here: https://app.komodoplatform.com - [Project setup](docs/PROJECT_SETUP.md) - [Firebase Setup](docs/FIREBASE_SETUP.md) - [Coins config, update](docs/COINS_CONFIG.md) -- [API module, update](docs/UPDATE_API_MODULE.md) - [App version, update](docs/UPDATE_APP_VERSION.md) - [Run the App](docs/BUILD_RUN_APP.md) - [Build release version of the App](docs/BUILD_RELEASE.md) diff --git a/app_build/.gitignore b/app_build/.gitignore deleted file mode 100644 index 6769e21d99..0000000000 --- a/app_build/.gitignore +++ /dev/null @@ -1,160 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file diff --git a/app_build/BUILD_CONFIG_README.md b/app_build/BUILD_CONFIG_README.md index df66436c3e..c9569c1af5 100644 --- a/app_build/BUILD_CONFIG_README.md +++ b/app_build/BUILD_CONFIG_README.md @@ -2,111 +2,7 @@ ## TL;DR -- **Configure API**: Ensure your `build_config.json` includes the correct API commit hash, update flags, source URLs, and platform-specific paths and targets. - **Configure Coins**: Set up the coins repository details, including commit hashes, URLs, branches, and runtime update settings. -- **Run Build Process**: The build steps are automatically executed as part of Flutter's build process. Ensure Node.js 18 is installed. - ---- - -## `build_config.json` Structure - -### Top-Level Keys - -- `api`: Configuration related to the DeFi API updates and supported platforms. -- `coins`: Configuration related to coin assets and updates. - -### Example Configuration - -```json -{ - "api": { - "api_commit_hash": "b0fd99e8406e67ea06435dd028991caa5f522b5c", - "branch": "main", - "fetch_at_build_enabled": true, - "source_urls": [ - "https://api.github.com/repos/KomodoPlatform/komodo-defi-framework", - "https://sdk.devbuilds.komodo.earth" - ], - "platforms": { - "web": { - "matching_keyword": "wasm", - "valid_zip_sha256_checksums": [ - "f4065f8cbfe2eb2c9671444402b79e1f94df61987b0cee6d503de567a2bc3ff0" - ], - "path": "web/src/mm2" - }, - "ios": { - "matching_keyword": "ios-aarch64", - "valid_zip_sha256_checksums": [ - "17156647a0bac0e630a33f9bdbcfd59c847443c9e88157835fff6a17738dcf0c" - ], - "path": "ios" - }, - "macos": { - "matching_keyword": "Darwin-Release", - "valid_zip_sha256_checksums": [ - "9472c37ae729bc634b02b64a13676e675b4ab1629d8e7c334bfb1c0360b6000a" - ], - "path": "macos" - }, - "windows": { - "matching_keyword": "Win64", - "valid_zip_sha256_checksums": [ - "f65075f3a04d27605d9ce7282ff6c8d5ed84692850fbc08de14ee41d036c4c5a" - ], - "path": "windows/runner/exe" - }, - "android-armv7": { - "matching_keyword": "android-armv7", - "valid_zip_sha256_checksums": [ - "bae9c33dca4fae3b9d10d25323df16b6f3976565aa242e5324e8f2643097b4c6" - ], - "path": "android/app/src/main/cpp/libs/armeabi-v7a" - }, - "android-aarch64": { - "matching_keyword": "android-aarch64", - "valid_zip_sha256_checksums": [ - "435c857c5cd4fe929238f490d2d3ba58c84cf9c601139c5cd23f63fbeb5befb6" - ], - "path": "android/app/src/main/cpp/libs/arm64-v8a" - }, - "linux": { - "matching_keyword": "Linux-Release", - "valid_zip_sha256_checksums": [ - "16f35c201e22db182ddc16ba9d356d324538d9f792d565833977bcbf870feaec" - ], - "path": "linux/mm2" - } - } - }, - "coins": { - "update_commit_on_build": true, - "bundled_coins_repo_commit": "6c33675ce5e5ec6a95708eb6046304ac4a5c3e70", - "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", - "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", - "coins_repo_branch": "master", - "runtime_updates_enabled": true, - "mapped_files": { - "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", - "assets/config/coins.json": "coins" - }, - "mapped_folders": { - "assets/coin_icons/png/": "icons" - } - } -} -``` - ---- - -## `api` Configuration - -### Parameters and Explanation - -- **api_commit_hash**: Specifies the commit hash of the API version currently in use. This ensures the API is pulled from a specific commit in the repository, providing consistency and stability by locking to a known state. -#### Platform Configuration - -Each platform configuration contains: --- @@ -116,6 +12,7 @@ Each platform configuration contains: - **update_commit_on_build**: A boolean flag indicating whether the commit hash should be updated on build. This ensures the coin configurations are in sync with the latest state of the repository. - **bundled_coins_repo_commit**: Specifies the commit hash of the bundled coins repository. This ensures the coin configurations are in sync with a specific state of the repository, providing consistency and stability. + --- ## Configuring and Running the Build Process diff --git a/app_build/build_config.json b/app_build/build_config.json index 4969bf516f..0aa3925d27 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -1,64 +1,4 @@ { - "api": { - "api_commit_hash": "35e92394928825c337f246f8e19fbfab1f65c4a8", - "branch": "main", - "fetch_at_build_enabled": true, - "source_urls": [ - "https://api.github.com/repos/KomodoPlatform/komodo-defi-framework", - "https://sdk.devbuilds.komodo.earth" - ], - "platforms": { - "web": { - "matching_keyword": "wasm", - "valid_zip_sha256_checksums": [ - "b29dd447cbc6a116c31cac8e222732e74b9cc28bbd43cb0e141ccced9c75492b" - ], - "path": "web/src/mm2" - }, - "ios": { - "matching_keyword": "ios-aarch64", - "valid_zip_sha256_checksums": [ - "e44e3dd81b35e739afce30628373ac4e9dc85e665954f2084725ca9a45678b77" - ], - "path": "ios" - }, - "macos": { - "matching_keyword": "Darwin-Release", - "valid_zip_sha256_checksums": [ - "71dd75505781d531fd1b4e2621b91aeee253ff6ac8501059773a9a82452a5b3f" - ], - "path": "macos" - }, - "windows": { - "matching_keyword": "Win64", - "valid_zip_sha256_checksums": [ - "27a4b10b4016d3ef04989b9b1ad8bb94db503efe50edac9e259482fff84e4213" - ], - "path": "windows/runner/exe" - }, - "android-armv7": { - "matching_keyword": "android-armv7", - "valid_zip_sha256_checksums": [ - "51c15be880abeddf24c34e5c802fd0a1ebacc7acde87b686f15bffabb1ae836d" - ], - "path": "android/app/src/main/cpp/libs/armeabi-v7a" - }, - "android-aarch64": { - "matching_keyword": "android-aarch64", - "valid_zip_sha256_checksums": [ - "37ef40e6b5d2a91c24d097ccbe4ac7e29e1dc65aa3940d93b5530fc54aac5577" - ], - "path": "android/app/src/main/cpp/libs/arm64-v8a" - }, - "linux": { - "matching_keyword": "Linux-Release", - "valid_zip_sha256_checksums": [ - "8993a0d31ef7a3554089be77b0c587bae3e7a1aa075bd1ca9ae6db6b96ae0c31" - ], - "path": "linux/mm2" - } - } - }, "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, diff --git a/assets/config/.gitkeep b/assets/config/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/BUILD_CONFIG.md b/docs/BUILD_CONFIG.md index effac03839..733738479c 100644 --- a/docs/BUILD_CONFIG.md +++ b/docs/BUILD_CONFIG.md @@ -4,38 +4,14 @@ Coin configs and asset files are automatically downloaded as part of the flutter build pipeline, based on the settings configured in [build_config.json](/app_build/build_config.json). -There are two sections of note in [build_config.json](/app_build/build_config.json), `api` and `coins`. +There are is one section of note in [build_config.json](/app_build/build_config.json), -- `api` contains the configuration for fetching the API binaries, and the checksums used to validate the downloaded files. - `coins` contains the configuration for fetching the coin assets, including where to download the files to and where to download the files from. The config is read by the build step for every invocation of `flutter run` or `flutter build`, so no further actions are required to update the coin assets or API binaries. NOTE: The build step will fail on the first run if the coin assets are not present in the specified folders. Run the same command again and the build should succeed. -## API - -The build step will use the settings in [build_config.json](/app_build/build_config.json) to download the API binaries from the specified URLs and validate the checksums. - -By default, the build step will - -- Download the API binaries from the commit in the branch of the API repository specified in `build_config.json`. -- Skip downloading the API binaries for a platform if the file already exists and the checksum is valid. -- Fail the build if no download `source_urls` returned a file with a valid checksum. - -### Configuration - -In [build_config.json](/app_build/build_config.json) update `api_commit_hash` and `branch` to the latest commit hash in the desired branch name. Use the `fetch_at_build_enabled` parameter to dictate whether the API binary downloads and checksum validation should run for every build. Example: - -```json - "api": { - "api_commit_hash": "f956070bc4c33723f753ed6ecaf2dc32a6f44972", - "branch": "master", - "fetch_at_build_enabled": true, - ... - } -``` - ## Coins The build step will check [build_config.json](/app_build/build_config.json) for the [coins](https://github.com/KomodoPlatform/coins) repository GitHub API URL, branch and commit hash, and it will then download the mapped files and folders from the repository to the specified local files and folders. diff --git a/docs/INSTALL_FLUTTER.md b/docs/INSTALL_FLUTTER.md index cc308acc73..498d3efc03 100644 --- a/docs/INSTALL_FLUTTER.md +++ b/docs/INSTALL_FLUTTER.md @@ -11,12 +11,13 @@ There are two main ways to get an older copy of Flutter. The first way is by cloning the official repository and then pinning to an older version. 1. Clone Flutter with - ``` + + ```bash cd ~ git clone https://github.com/flutter/flutter.git ``` -2. [Pin Flutter version](FLUTTER_VERSION.md#pin-flutter-version) +2. [Pin Flutter version](FLUTTER_VERSION.md#pin-flutter-version) The second way is via downloading the desired version from the SDK Archives. Here are [Windows](https://docs.flutter.dev/release/archive?tab=windows), [Mac](https://docs.flutter.dev/release/archive?tab=macos) @@ -30,22 +31,37 @@ If you opt for the SDK Archive, you easily change to use the [Pin Flutter versio Add the flutter binaries subfolder `flutter/bin` to your system PATH. This process differs for each OS: For macOS: - ``` + + ```bash nano ~/.zshrc export PATH="$PATH:$HOME/flutter/bin" ``` + For Linux: - ``` + + ```bash vim ~/.bashrc export PATH="$PATH:$HOME/flutter/bin" ``` + For Windows, follow the instructions below (from [flutter.dev](https://docs.flutter.dev/get-started/install/windows#update-your-path)):: - - From the Start search bar, enter `env` and select **Edit environment variables for your account**. - - Under **User variables** check if there is an entry called **Path**: - - If the entry exists, append the full path to flutter\bin using ; as a separator from existing values. - - If the entry doesn't exist, create a new user variable named Path with the full path to flutter\bin as its value. +- From the Start search bar, enter `env` and select **Edit environment variables for your account**. +- Under **User variables** check if there is an entry called **Path**: + - If the entry exists, append the full path to flutter\bin using ; as a separator from existing values. + - If the entry doesn't exist, create a new user variable named Path with the full path to flutter\bin as its value. You might need to logout and re-login (or source the shell configuration file, if applicable) to make changes apply. On macOS and Linux it should also be possible to confirm it's been added to the PATH correctly by running `which flutter`. + +## Alternative: FVM + +The recommended method of handling multiple Flutter versions is to use [FVM](https://fvm.app/documentation/getting-started/installation). + +```bash +curl -fsSL https://fvm.app/install.sh | bash + +# Configure the Flutter version to use in the current directory (e.g. ~/komodo-wallet) +fvm use stable +``` diff --git a/docs/INTEGRATION_TESTING.md b/docs/INTEGRATION_TESTING.md index bd94c8be66..6dbef38d8e 100644 --- a/docs/INTEGRATION_TESTING.md +++ b/docs/INTEGRATION_TESTING.md @@ -8,46 +8,62 @@ ## 2. How to run tests -### 2.1. Web/Chrome/Safari/Firefox +### 2.1. Download Driver for Web/Chrome/Safari/Firefox [https://github.com/flutter/flutter/wiki/Running-Flutter-Driver-tests-with-Web](https://github.com/flutter/flutter/wiki/Running-Flutter-Driver-tests-with-Web) #### 2.1.1. Download and unpack web drivers - ##### Chrome: + +##### Chrome + - ##### Safari: +##### Safari + Configure Safari to Enable WebDriver Support. Safari’s WebDriver support for developers is turned off by default. Run once: + ```bash safaridriver --enable ``` + Note: If you’re upgrading from a previous macOS release, you may need to use sudo. - ##### Firefox: - - Install and check the version of Firefox. +##### Firefox + +- Install and check the version of Firefox. - - Download the Gecko driver for that version from the releases +- Download the Gecko driver for that version from the releases Note that this section is experimental, at this point we don't have automated tests running on Firefox. #### 2.1.2. Launch the WebDriver - - for Google Chrome + +NOTE: `chromedriver` is now launched automatically by `run_integration_tests.dart` script, so +this step is optional if you plan to use Chrome. + +- for Google Chrome + ```bash chromedriver --port=4444 --silent --enable-chrome-logs --log-path=console.log ``` - - or Firefox + +- or Firefox + ```bash geckodriver --port=4444 ``` - - or Safari + +- or Safari + ```bash /usr/bin/safaridriver --port=4444 --diagnose ``` -#### 2.1.3. Run test. From the root of the project, run the following command: + +#### 2.1.3. Run test. From the root of the project, run the following command ```bash dart run_integration_tests.dart @@ -64,11 +80,12 @@ Or, to run single test: Change `/testname_test.dart` to actual test file, located in ./test_integration directory. Currently available test groups: - - `dex_tests/dex_tests.dart` - - `wallets_manager_tests/wallets_manager_tests.dart` - - `wallets_tests/wallets_tests.dart` - - `misc_tests/misc_tests.dart` - - `no_login_tests/no_login_tests.dart` + +- `dex_tests/dex_tests.dart` +- `wallets_manager_tests/wallets_manager_tests.dart` +- `wallets_tests/wallets_tests.dart` +- `misc_tests/misc_tests.dart` +- `no_login_tests/no_login_tests.dart` and run @@ -78,7 +95,7 @@ Currently available test groups: Each test in test groups can be run separately in exact same fashion. -#### 2.1.4. To simulate different screen dimensions, you can use the --browserDimension or -b argument, -d or --display argument to configure headless run: +#### 2.1.4. To simulate different screen dimensions, you can use the --browserDimension or -b argument, -d or --display argument to configure headless run ```bash dart run_integration_tests.dart -b '360,640' @@ -92,7 +109,7 @@ Currently available test groups: dart run_integration_tests.dart -b '1600,1040' -d 'headless' ``` -#### 2.1.5. To run tests in different browsers, you can specify the --browser-name or -n argument: +#### 2.1.5. To run tests in different browsers, you can specify the --browser-name or -n argument ```bash dart run_integration_tests.dart -n 'safari' @@ -100,6 +117,6 @@ Currently available test groups: ```bash dart run_integration_tests.dart --browser-name=firefox - ``` + ``` - By default, the Chrome browser is used to run tests \ No newline at end of file + By default, the Chrome browser is used to run tests diff --git a/docs/MULTIPLE_FLUTTER_VERSIONS.md b/docs/MULTIPLE_FLUTTER_VERSIONS.md index 16fcca984e..3f47be011b 100644 --- a/docs/MULTIPLE_FLUTTER_VERSIONS.md +++ b/docs/MULTIPLE_FLUTTER_VERSIONS.md @@ -1,42 +1,57 @@ # Handle multiple Flutter versions +## FVM + +The recommended method of handling multiple Flutter versions is to use [FVM](https://fvm.app/documentation/getting-started/installation). + +```bash +curl -fsSL https://fvm.app/install.sh | bash + +# Configure the Flutter version to use in the current directory (e.g. ~/komodo-wallet) +fvm use stable +``` + ## macOS -### 1. Clone new Flutter instance alongside with the existing one: +### 1. Clone new Flutter instance alongside with the existing one + ``` cd ~ git clone https://github.com/flutter/flutter.git flutter_web cd ./flutter_web -git checkout 3.3.9 +git checkout 3.23.4 ``` -### pen (or create) `.zshrc` file in your home directory: +### pen (or create) `.zshrc` file in your home directory + ``` nano ~/.zshrc ``` + Add line: + ``` alias flutter_web="$HOME/flutter_web/bin/flutter" ``` + Save and close. -### 3. Check if newly installed Flutter version is accessible from terminal: +### 3. Check if newly installed Flutter version is accessible from terminal + ``` cd ~ flutter_web doctor ``` +### 4. Add new Flutter version to VSCode -### 4. Add new Flutter version to VSCode: - - - Settings (⌘,) -> Extensions -> Dart -> SDK -> Flutter Sdk Paths -> Add Item -> `~/flutter_web` - - ⌘⇧P -> Developer: Reload window - - ⌘⇧P -> Flutter: Change SDK - +- Settings (⌘,) -> Extensions -> Dart -> SDK -> Flutter Sdk Paths -> Add Item -> `~/flutter_web` +- ⌘⇧P -> Developer: Reload window +- ⌘⇧P -> Flutter: Change SDK ### 5. Add to Android Studio - - Settings (⌘,) -> Languages & Frameworks -> Flutter -> SDK -> Flutter SDK Path -> `~/flutter_web` +- Settings (⌘,) -> Languages & Frameworks -> Flutter -> SDK -> Flutter SDK Path -> `~/flutter_web` ---- @@ -44,4 +59,4 @@ flutter_web doctor ---- -## Linux TBD \ No newline at end of file +## Linux TBD diff --git a/docs/PROJECT_SETUP.md b/docs/PROJECT_SETUP.md index cf3888cdef..9ca730bdd8 100644 --- a/docs/PROJECT_SETUP.md +++ b/docs/PROJECT_SETUP.md @@ -28,18 +28,14 @@ Komodo Wallet is a cross-platform application, meaning it can be built for multi 3. Run `flutter doctor` and make sure all checks (except version) pass 4. [Clone project repository](CLONE_REPOSITORY.md) - 5. Install [nodejs and npm](https://nodejs.org/en/download). Make sure `npm` is in your system PATH and you can run `npm run build` from the project root folder. Node LTS (v18, v20) is required. - - > In case of an error, try to run `npm i`. - - 6. Build and run the App for each target platform: + 5. Build and run the App for each target platform: - [Web](BUILD_RUN_APP.md#web) - [Android mobile](BUILD_RUN_APP.md#android) - [iOS mobile](BUILD_RUN_APP.md#ios) (macOS host only) - [macOS desktop](BUILD_RUN_APP.md#macos-desktop) (macOS host only) - [Windows desktop](BUILD_RUN_APP.md#windows-desktop) (Windows host only) - [Linux desktop](BUILD_RUN_APP.md#linux-desktop) (Linux host only) - 7. [Build release version](BUILD_RELEASE.md) + 6. [Build release version](BUILD_RELEASE.md) ## Dev Container setup (Web and Android builds only) diff --git a/docs/UPDATE_API_MODULE.md b/docs/UPDATE_API_MODULE.md deleted file mode 100644 index 24aacaec68..0000000000 --- a/docs/UPDATE_API_MODULE.md +++ /dev/null @@ -1,62 +0,0 @@ -# API module - -## Current version - -Current API module version is `b0fd99e` (`v2.0.0-beta`) - -### Prerequisites - -```bash -# Install Dart dependencies -dart pub get - -# Use Node version 18 -nvm use 18 -``` - -### Usage - -The script will check the `.api_last_updated_[PLATFORM_NAME]` file for every platform listed in `platforms`, and if the last updated version is different from the current API `version`, it will update the API module, `.api_last_updated_[PLATFORM_NAME]` file, and [documentation](#current-version). - -By default, the script will update the API module for all supported platforms to the version specified in [build_config.json](../app_build/build_config.json). - -### Configuration - -In [build_config.json](../app_build/build_config.json), update the API version to the latest commit hash from the [atomicDEX-API](https://github.com/KomodoPlatform/atomicDEX-API) repository. Example: - -```json - "api": { - "api_commit_hash": "fa74561", - ... - } -``` - -To add a new platform to the update script, add a new item to the `platforms` list in [build_config.json](../app_build/build_config.json). - -```json - "api": { - ... - "platforms": { - "linux": { - "keywords": ["linux", "x86_64"], - "path": "linux" - }, - ... - } - } -``` - -- `keywords` is a list of keywords that will be used to find the platform-specific API module zip file on the API CI upload server (`base_url`). -- `path` is the path to the API module directory in the project. - -### Error handling - -In case of errors, please check our [Project setup](PROJECT_SETUP.md) section and verify your environment. - -One possible solution is to run: - -```bash -npm i -``` - -By updating the documentation, users can now rely on the Dart-based build process for fetching and updating the API module, simplifying the workflow and removing the dependency on the Python script. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80e40ed665..0b009fc10e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -84,6 +84,8 @@ PODS: - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 6.0.3) + - flutter_secure_storage (6.0.0): + - Flutter - GoogleAppMeasurement (10.25.0): - GoogleAppMeasurement/AdIdSupport (= 10.25.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -151,6 +153,11 @@ PODS: - GTMSessionFetcher/Core (3.5.0) - integration_test (0.0.1): - Flutter + - komodo_defi_framework (0.0.1): + - Flutter + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS - MLImage (1.0.0-beta5) - MLKitBarcodeScanning (5.0.0): - MLKitCommon (~> 11.0) @@ -204,7 +211,10 @@ DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) + - komodo_defi_framework (from `.symlinks/plugins/komodo_defi_framework/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -250,8 +260,14 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" + komodo_defi_framework: + :path: ".symlinks/plugins/komodo_defi_framework/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" package_info_plus: @@ -280,6 +296,7 @@ SPEC CHECKSUMS: FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 @@ -288,6 +305,8 @@ SPEC CHECKSUMS: GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + komodo_defi_framework: 03bdd4759a7251e288dbf9f0aea9157d9db8c496 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 MLImage: 1824212150da33ef225fbd3dc49f184cf611046c MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 121ae76ff3..7c17621117 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -12,6 +12,7 @@ const String allWalletsStorageKey = 'all-wallets'; const String defaultDexCoin = 'KMD'; const List localeList = [Locale('en')]; const String assetsPath = 'assets'; +const String coinsAssetsPath = 'packages/komodo_defi_framework/assets'; // Temporary feature flag to allow merging of the PR // TODO: Remove this flag after the feature is finalized diff --git a/lib/app_config/coins_config_parser.dart b/lib/app_config/coins_config_parser.dart index eea3ec0c73..9fd428b240 100644 --- a/lib/app_config/coins_config_parser.dart +++ b/lib/app_config/coins_config_parser.dart @@ -20,7 +20,7 @@ class CoinConfigParser { Future> _readGlobalConfig() async { final String globalConfig = - await rootBundle.loadString('$assetsPath/config/coins.json'); + await rootBundle.loadString('$coinsAssetsPath/config/coins.json'); final List globalCoinsJson = jsonDecode(globalConfig); _globalConfigCache = globalCoinsJson; @@ -41,8 +41,8 @@ class CoinConfigParser { /// Checks if the local coin configs exist. /// Returns `true` if the local coin configs exist, otherwise `false`. Future hasLocalConfigs({ - String coinsPath = '$assetsPath/config/coins.json', - String coinsConfigPath = '$assetsPath/config/coins_config.json', + String coinsPath = '$coinsAssetsPath/config/coins.json', + String coinsConfigPath = '$coinsAssetsPath/config/coins_config.json', }) async { try { final bool coinsFileExists = await doesAssetExist(coinsPath); @@ -99,7 +99,7 @@ class CoinConfigParser { Future> _readLocalConfig() async { final String localConfig = - await rootBundle.loadString('$assetsPath/config/coins_config.json'); + await rootBundle.loadString('$coinsAssetsPath/config/coins_config.json'); final Map json = jsonDecode(localConfig); return json; diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 59a065b390..217c5bff16 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -321,8 +321,10 @@ class _MyAppViewState extends State<_MyAppView> { Future _precacheCoinIcons() async { if (_currentPrecacheOperation != null && !_currentPrecacheOperation!.isCompleted) { - _currentPrecacheOperation! - .completeError('New request to precache icons started.'); + // completeError throws an uncaught exception, which causes the UI + // tests to fail when switching between light and dark theme + log('New request to precache icons started.'); + _currentPrecacheOperation!.complete(); } _currentPrecacheOperation = Completer(); diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 8ccd4a7f00..53568d9e5b 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -23,6 +23,7 @@ class AuthBloc extends Bloc { add(AuthChangedEvent(mode: event)); }); } + late StreamSubscription _authorizationSubscription; final AuthRepository _authRepo; final AuthChecker _authChecker = getAuthChecker(); @@ -70,7 +71,16 @@ class AuthBloc extends Bloc { ); await _logOut(); - await _logIn(event.seed, event.wallet); + await _authRepo.logIn( + AuthorizeMode.logIn, + seed: event.seed, + walletName: event.wallet.name, + password: event.password, + ); + currentWalletBloc.wallet = event.wallet; + if (event.wallet.config.type == WalletType.iguana) { + _authChecker.addSession(event.wallet.config.seedPhrase); + } log( 're-logged in from a wallet', @@ -92,15 +102,4 @@ class AuthBloc extends Bloc { } currentWalletBloc.wallet = null; } - - Future _logIn( - String seed, - Wallet wallet, - ) async { - await _authRepo.logIn(AuthorizeMode.logIn, seed); - currentWalletBloc.wallet = wallet; - if (wallet.config.type == WalletType.iguana) { - _authChecker.addSession(wallet.config.seedPhrase); - } - } } diff --git a/lib/bloc/auth_bloc/auth_bloc_event.dart b/lib/bloc/auth_bloc/auth_bloc_event.dart index b1b8c64f39..f23d9eb64a 100644 --- a/lib/bloc/auth_bloc/auth_bloc_event.dart +++ b/lib/bloc/auth_bloc/auth_bloc_event.dart @@ -15,8 +15,13 @@ class AuthLogOutEvent extends AuthBlocEvent { } class AuthReLogInEvent extends AuthBlocEvent { - const AuthReLogInEvent({required this.seed, required this.wallet}); + const AuthReLogInEvent({ + required this.seed, + required this.password, + required this.wallet, + }); final String seed; + final String password; final Wallet wallet; } diff --git a/lib/bloc/auth_bloc/auth_repository.dart b/lib/bloc/auth_bloc/auth_repository.dart index cb62dfa571..abeeabcc58 100644 --- a/lib/bloc/auth_bloc/auth_repository.dart +++ b/lib/bloc/auth_bloc/auth_repository.dart @@ -9,9 +9,24 @@ class AuthRepository { final StreamController _authController = StreamController.broadcast(); Stream get authMode => _authController.stream; - Future logIn(AuthorizeMode mode, [String? seed]) async { - await _startMM2(seed); - await waitMM2StatusChange(MM2Status.rpcIsUp, mm2, waitingTime: 60000); + + Future logIn( + AuthorizeMode mode, { + String? seed, + String? password, + String? walletName, + }) async { + try { + await mm2.start( + passphrase: seed, + walletName: walletName, + walletPassword: password, + ); + } catch (e) { + log('mm2 start error: ${e.toString()}'); + rethrow; + } + setAuthMode(mode); } @@ -21,18 +36,8 @@ class AuthRepository { Future logOut() async { await mm2.stop(); - await waitMM2StatusChange(MM2Status.isNotRunningYet, mm2); setAuthMode(AuthorizeMode.noLogin); } - - Future _startMM2(String? seed) async { - try { - await mm2.start(seed); - } catch (e) { - log('mm2 start error: ${e.toString()}'); - rethrow; - } - } } final AuthRepository authRepo = AuthRepository(); diff --git a/lib/bloc/bridge_form/bridge_validator.dart b/lib/bloc/bridge_form/bridge_validator.dart index c57db4e245..22ca4fba2a 100644 --- a/lib/bloc/bridge_form/bridge_validator.dart +++ b/lib/bloc/bridge_form/bridge_validator.dart @@ -269,7 +269,7 @@ class BridgeValidator { final coin = _coinsRepo.getCoin(selectedOrder.coin); final ownAddress = coin?.address; - if (selectedOrderAddress == ownAddress) { + if (selectedOrderAddress.addressData == ownAddress) { _add(BridgeSetError(_tradingWithSelfError())); return true; } diff --git a/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart b/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart index ac228d5239..0e98c14b3f 100644 --- a/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart +++ b/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart @@ -3,8 +3,12 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; +import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/model/authorize_mode.dart'; +import 'package:web_dex/model/my_orders/my_order.dart'; +import 'package:web_dex/model/swap.dart'; import 'package:web_dex/model/trading_entities_filter.dart'; import 'package:web_dex/views/market_maker_bot/tab_type_enum.dart'; @@ -12,9 +16,18 @@ part 'dex_tab_bar_event.dart'; part 'dex_tab_bar_state.dart'; class DexTabBarBloc extends Bloc { - DexTabBarBloc(super.initialState, AuthRepository authRepo) { + DexTabBarBloc( + AuthRepository authRepo, + this._tradingEntitiesBloc, + this._tradingBotRepository, + ) : super(const DexTabBarState.initial()) { on(_onTabChanged); on(_onFilterChanged); + on(_onStartListening); + on(_onStopListening); + on(_onMyOrdersUpdated); + on(_onSwapsUpdated); + on(_onTradeBotOrdersUpdated); _authorizationSubscription = authRepo.authMode.listen((event) { if (event == AuthorizeMode.noLogin) { @@ -23,22 +36,55 @@ class DexTabBarBloc extends Bloc { }); } + final TradingEntitiesBloc _tradingEntitiesBloc; + final MarketMakerBotOrderListRepository _tradingBotRepository; + + StreamSubscription? _authorizationSubscription; + StreamSubscription>? _myOrdersSubscription; + StreamSubscription>? _swapsSubscription; + StreamSubscription>? _tradeBotOrdersSubscription; + @override - Future close() { - _authorizationSubscription.cancel(); + Future close() async { + await _authorizationSubscription?.cancel(); + await _myOrdersSubscription?.cancel(); + await _swapsSubscription?.cancel(); + await _tradeBotOrdersSubscription?.cancel(); return super.close(); } - late StreamSubscription _authorizationSubscription; - int get tabIndex => state.tabIndex; + Future _onStartListening( + StartListening event, + Emitter emit, + ) async { + _myOrdersSubscription = _tradingEntitiesBloc.outMyOrders.listen((myOrders) { + add(MyOrdersUpdated(myOrders)); + }); + + _swapsSubscription = _tradingEntitiesBloc.outSwaps.listen((swaps) { + add(SwapsUpdated(swaps)); + }); + + _tradeBotOrdersSubscription = Stream.periodic(const Duration(seconds: 3)) + .asyncMap((_) => _tradingBotRepository.getTradePairs()) + .listen((orders) { + add(TradeBotOrdersUpdated(orders)); + }); + } - int get ordersCount => tradingEntitiesBloc.myOrders.length; + Future _onStopListening( + StopListening event, + Emitter emit, + ) async { + await _myOrdersSubscription?.cancel(); + _myOrdersSubscription = null; - int get inProgressCount => - tradingEntitiesBloc.swaps.where((swap) => !swap.isCompleted).length; + await _swapsSubscription?.cancel(); + _swapsSubscription = null; - int get completedCount => - tradingEntitiesBloc.swaps.where((swap) => swap.isCompleted).length; + await _tradeBotOrdersSubscription?.cancel(); + _tradeBotOrdersSubscription = null; + } FutureOr _onTabChanged(TabChanged event, Emitter emit) { emit(state.copyWith(tabIndex: event.tabIndex)); @@ -49,9 +95,37 @@ class DexTabBarBloc extends Bloc { state.copyWith( filters: { ...state.filters, - event.tabType: event.filter, + event.tabType: event.filter!, }, ), ); } + + void _onMyOrdersUpdated(MyOrdersUpdated event, Emitter emit) { + final ordersCount = event.myOrders.length; + emit(state.copyWith(ordersCount: ordersCount)); + } + + void _onSwapsUpdated(SwapsUpdated event, Emitter emit) { + final inProgressCount = + event.swaps.where((swap) => !swap.isCompleted).length; + final completedCount = event.swaps.where((swap) => swap.isCompleted).length; + emit( + state.copyWith( + inProgressCount: inProgressCount, + completedCount: completedCount, + ), + ); + } + + void _onTradeBotOrdersUpdated( + TradeBotOrdersUpdated event, + Emitter emit, + ) { + emit( + state.copyWith( + tradeBotOrdersCount: event.tradeBotOrders.length, + ), + ); + } } diff --git a/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart b/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart index 56a7ec9154..85bf68b5c4 100644 --- a/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart +++ b/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart @@ -17,9 +17,41 @@ class TabChanged extends DexTabBarEvent { class FilterChanged extends DexTabBarEvent { const FilterChanged({required this.tabType, required this.filter}); - final TabTypeEnum tabType; + final ITabTypeEnum tabType; final TradingEntitiesFilter? filter; @override List get props => [tabType, filter]; } + +class StartListening extends DexTabBarEvent { + const StartListening(); +} + +class StopListening extends DexTabBarEvent { + const StopListening(); +} + +class MyOrdersUpdated extends DexTabBarEvent { + const MyOrdersUpdated(this.myOrders); + final List myOrders; + + @override + List get props => [myOrders]; +} + +class SwapsUpdated extends DexTabBarEvent { + const SwapsUpdated(this.swaps); + final List swaps; + + @override + List get props => [swaps]; +} + +class TradeBotOrdersUpdated extends DexTabBarEvent { + const TradeBotOrdersUpdated(this.tradeBotOrders); + final List tradeBotOrders; + + @override + List get props => [tradeBotOrders]; +} diff --git a/lib/bloc/dex_tab_bar/dex_tab_bar_state.dart b/lib/bloc/dex_tab_bar/dex_tab_bar_state.dart index 9cfd6caa0b..171855d6e1 100644 --- a/lib/bloc/dex_tab_bar/dex_tab_bar_state.dart +++ b/lib/bloc/dex_tab_bar/dex_tab_bar_state.dart @@ -1,22 +1,55 @@ part of 'dex_tab_bar_bloc.dart'; class DexTabBarState extends Equatable { - const DexTabBarState({required this.tabIndex, this.filters = const {}}); - factory DexTabBarState.initial() => const DexTabBarState(tabIndex: 0); + const DexTabBarState({ + required this.tabIndex, + required this.filters, + required this.ordersCount, + required this.inProgressCount, + required this.completedCount, + required this.tradeBotOrdersCount, + }); - final int tabIndex; - final Map filters; + const DexTabBarState.initial() + : tabIndex = 0, + filters = const {}, + ordersCount = 0, + inProgressCount = 0, + completedCount = 0, + tradeBotOrdersCount = 0; - @override - List get props => [tabIndex, filters]; + final int tabIndex; + final Map filters; + final int ordersCount; + final int inProgressCount; + final int completedCount; + final int tradeBotOrdersCount; DexTabBarState copyWith({ int? tabIndex, - Map? filters, + Map? filters, + int? ordersCount, + int? inProgressCount, + int? completedCount, + int? tradeBotOrdersCount, }) { return DexTabBarState( tabIndex: tabIndex ?? this.tabIndex, filters: filters ?? this.filters, + ordersCount: ordersCount ?? this.ordersCount, + inProgressCount: inProgressCount ?? this.inProgressCount, + completedCount: completedCount ?? this.completedCount, + tradeBotOrdersCount: tradeBotOrdersCount ?? this.tradeBotOrdersCount, ); } + + @override + List get props => [ + tabIndex, + filters, + ordersCount, + inProgressCount, + completedCount, + tradeBotOrdersCount, + ]; } diff --git a/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart b/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart index be04e62b2c..45f07d86d9 100644 --- a/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart +++ b/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_repository.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_status.dart'; @@ -24,16 +25,28 @@ class MarketMakerBotBloc ) : _botRepository = marketMaketBotRepository, _orderRepository = orderRepository, super(const MarketMakerBotState.initial()) { - on(_onStartRequested); - on(_onStopRequested); - on(_onOrderUpdateRequested); - on(_onOrderCancelRequested); + on( + _onStartRequested, + transformer: restartable(), + ); + on( + _onStopRequested, + transformer: restartable(), + ); + on( + _onOrderUpdateRequested, + transformer: sequential(), + ); + on( + _onOrderCancelRequested, + transformer: sequential(), + ); } final MarketMakerBotRepository _botRepository; final MarketMakerBotOrderListRepository _orderRepository; - void _onStartRequested( + Future _onStartRequested( MarketMakerBotStartRequested event, Emitter emit, ) async { @@ -56,7 +69,7 @@ class MarketMakerBotBloc } } - void _onStopRequested( + Future _onStopRequested( MarketMakerBotStopRequested event, Emitter emit, ) async { @@ -76,7 +89,7 @@ class MarketMakerBotBloc } } - void _onOrderUpdateRequested( + Future _onOrderUpdateRequested( MarketMakerBotOrderUpdateRequested event, Emitter emit, ) async { @@ -85,7 +98,7 @@ class MarketMakerBotBloc try { // Add the trade pair to stored settings immediately to provide feedback // and updates to the user. - _botRepository.addTradePairToStoredSettings(event.tradePair); + await _botRepository.addTradePairToStoredSettings(event.tradePair); // Cancel the order immediately to provide feedback to the user that // the bot is being updated, since the restart process may take some time. @@ -111,7 +124,7 @@ class MarketMakerBotBloc } } - void _onOrderCancelRequested( + Future _onOrderCancelRequested( MarketMakerBotOrderCancelRequested event, Emitter emit, ) async { @@ -166,7 +179,7 @@ class MarketMakerBotBloc } return; } - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); } } } diff --git a/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_repository.dart b/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_repository.dart index dad9b6bf81..a328a69bf8 100644 --- a/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_repository.dart +++ b/lib/bloc/market_maker_bot/market_maker_bot/market_maker_bot_repository.dart @@ -24,7 +24,7 @@ class MarketMakerBotRepository { Future start({ required int botId, int retries = 10, - int delay = 2000, + Duration delay = const Duration(milliseconds: 2000), }) async { final requestParams = await loadStoredConfig(); final request = MarketMakerBotRequest( @@ -33,7 +33,7 @@ class MarketMakerBotRepository { params: requestParams, ); - if (requestParams.tradeCoinPairs?.isEmpty == true) { + if (requestParams.tradeCoinPairs?.isEmpty ?? true) { throw ArgumentError('No trade pairs configured'); } @@ -49,10 +49,10 @@ class MarketMakerBotRepository { Future stop({ required int botId, int retries = 10, - int delay = 2000, + Duration delay = const Duration(milliseconds: 2000), }) async { try { - MarketMakerBotRequest request = MarketMakerBotRequest( + final MarketMakerBotRequest request = MarketMakerBotRequest( id: botId, method: MarketMakerBotMethod.stop.value, ); @@ -77,7 +77,7 @@ class MarketMakerBotRepository { TradeCoinPairConfig tradePair, { required int botId, int retries = 10, - int delay = 2000, + Duration delay = const Duration(milliseconds: 2000), }) async* { yield MarketMakerBotStatus.stopping; await stop(botId: botId, retries: retries, delay: delay); @@ -92,7 +92,7 @@ class MarketMakerBotRepository { ]); } - if (requestParams.tradeCoinPairs?.isEmpty == true) { + if (requestParams.tradeCoinPairs?.isEmpty ?? true) { yield MarketMakerBotStatus.stopped; } else { yield MarketMakerBotStatus.starting; @@ -116,7 +116,7 @@ class MarketMakerBotRepository { Iterable tradeCoinPairConfig, { required int botId, int retries = 10, - int delay = 2000, + Duration delay = const Duration(milliseconds: 2000), }) async* { yield MarketMakerBotStatus.stopping; await stop(botId: botId, retries: retries, delay: delay); @@ -129,7 +129,7 @@ class MarketMakerBotRepository { requestParams.tradeCoinPairs?.remove(tradePair.name); } - if (requestParams.tradeCoinPairs?.isEmpty == true) { + if (requestParams.tradeCoinPairs?.isEmpty ?? true) { yield MarketMakerBotStatus.stopped; } else { // yield MarketMakerBotStatus.starting; @@ -170,10 +170,10 @@ class MarketMakerBotRepository { Future _startStopBotWithExponentialBackoff( MarketMakerBotRequest request, { required int retries, - required int delay, + required Duration delay, }) async { final isStartRequest = request.method == MarketMakerBotMethod.start.value; - final isTradePairsEmpty = request.params?.tradeCoinPairs?.isEmpty == true; + final isTradePairsEmpty = request.params?.tradeCoinPairs?.isEmpty ?? true; if (isStartRequest && isTradePairsEmpty) { throw ArgumentError('No trade pairs configured'); } @@ -190,12 +190,12 @@ class MarketMakerBotRepository { if (e is RpcException) { if (request.method == MarketMakerBotMethod.start.value && e.error.errorType == RpcErrorType.alreadyStarted) { - log('Market maker bot already started', isError: true); + log('Market maker bot already started', isError: true).ignore(); return; } else if (request.method == MarketMakerBotMethod.stop.value && e.error.errorType == RpcErrorType.alreadyStopped || e.error.errorType == RpcErrorType.alreadyStopping) { - log('Market maker bot already stopped', isError: true); + log('Market maker bot already stopped', isError: true).ignore(); return; } } @@ -205,8 +205,8 @@ class MarketMakerBotRepository { isError: true, trace: s, path: 'MarketMakerBotBloc', - ); - await Future.delayed(Duration(milliseconds: delay)); + ).ignore(); + await Future.delayed(delay); retries--; delay *= 2; } @@ -244,7 +244,7 @@ class MarketMakerBotRepository { /// The settings are updated in the settings repository. /// Throws an [Exception] if the settings cannot be updated. /// - /// The [tradePair] to remove from the existing settings. + /// The [tradePairsToRemove] to remove from the existing settings. Future removeTradePairsFromStoredSettings( List tradePairsToRemove, ) async { diff --git a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart index 6767dc0bda..a1d93d47bd 100644 --- a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart +++ b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart @@ -7,16 +7,16 @@ import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/services/orders_service/my_orders_service.dart'; class MarketMakerBotOrderListRepository { - final MyOrdersService _ordersService; - final SettingsRepository _settingsRepository; final CoinsBloc _coinsRepository; - const MarketMakerBotOrderListRepository( this._ordersService, this._settingsRepository, this._coinsRepository, ); + final MyOrdersService _ordersService; + final SettingsRepository _settingsRepository; + Future cancelOrders(List tradePairs) async { final orders = await _ordersService.getOrders(); final ordersToCancel = orders @@ -31,7 +31,7 @@ class MarketMakerBotOrderListRepository { ) .toList(); - if (ordersToCancel?.isEmpty == true) { + if (ordersToCancel?.isEmpty ?? false) { return; } 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 473bbcf270..72233b3d56 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 @@ -39,7 +39,7 @@ class MarketMakerTradeFormState extends Equatable with FormzMixin { MarketMakerTradeFormState.initial() : sellCoin = const CoinSelectInput.pure(), buyCoin = const CoinSelectInput.pure(), - minimumTradeVolume = const TradeVolumeInput.pure(0.01), + minimumTradeVolume = const TradeVolumeInput.pure(0.1), maximumTradeVolume = const TradeVolumeInput.pure(0.9), sellAmount = const CoinTradeAmountInput.pure(), buyAmount = const CoinTradeAmountInput.pure(), diff --git a/lib/bloc/taker_form/taker_validator.dart b/lib/bloc/taker_form/taker_validator.dart index a69c41bb6c..62673392b5 100644 --- a/lib/bloc/taker_form/taker_validator.dart +++ b/lib/bloc/taker_form/taker_validator.dart @@ -134,7 +134,7 @@ class TakerValidator { final coin = _coinsRepo.getCoin(selectedOrder.coin); final ownAddress = coin?.address; - if (selectedOrderAddress == ownAddress) { + if (selectedOrderAddress.addressData == ownAddress) { add(TakerAddError(_tradingWithSelfError())); return true; } diff --git a/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart b/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart index 9a135ad81d..7706ed4fab 100644 --- a/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart +++ b/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart @@ -59,8 +59,7 @@ class TrezorConnectionBloc switch (status) { case TrezorConnectionStatus.unreachable: - final MM2Status mm2Status = await mm2.status(); - if (mm2Status == MM2Status.rpcIsUp) await authRepo.logOut(); + if (await mm2.isSignedIn()) await authRepo.logOut(); await _authRepo.logIn(AuthorizeMode.noLogin); return; case TrezorConnectionStatus.unknown: diff --git a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart index 1dfbb7430f..0883c8907d 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart @@ -224,22 +224,19 @@ class TrezorInitBloc extends Bloc { } Future _loginToHiddenMode() async { - final MM2Status mm2Status = await mm2.status(); + final bool mm2SignedIn = await mm2.isSignedIn(); + if (state.authMode == AuthorizeMode.hiddenLogin && mm2SignedIn) return; - if (state.authMode == AuthorizeMode.hiddenLogin && - mm2Status == MM2Status.rpcIsUp) return; - - if (mm2Status == MM2Status.rpcIsUp) await _authRepo.logOut(); - await _authRepo.logIn(AuthorizeMode.hiddenLogin, seedForHiddenLogin); + if (mm2SignedIn) await _authRepo.logOut(); + await _authRepo.logIn(AuthorizeMode.hiddenLogin, seed: seedForHiddenLogin); } Future _logoutFromHiddenMode() async { - final MM2Status mm2Status = await mm2.status(); + final bool mm2SignedIn = await mm2.isSignedIn(); - if (state.authMode != AuthorizeMode.hiddenLogin && - mm2Status == MM2Status.rpcIsUp) return; + if (state.authMode != AuthorizeMode.hiddenLogin && mm2SignedIn) return; - if (mm2Status == MM2Status.rpcIsUp) await _authRepo.logOut(); + if (mm2SignedIn) await _authRepo.logOut(); await _authRepo.logIn(AuthorizeMode.noLogin); } diff --git a/lib/blocs/blocs.dart b/lib/blocs/blocs.dart index 6fe3a54411..14455eb207 100644 --- a/lib/blocs/blocs.dart +++ b/lib/blocs/blocs.dart @@ -10,7 +10,7 @@ import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/blocs/wallets_bloc.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/services/cex_service/cex_service.dart'; -import 'package:web_dex/services/file_loader/get_file_loader.dart'; +import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/shared/utils/encryption_tool.dart'; // todo(yurii): recommended bloc arch refactoring order: @@ -29,7 +29,7 @@ WalletsBloc walletsBloc = WalletsBloc( ); // 4) CurrentWalletBloc currentWalletBloc = CurrentWalletBloc( - fileLoader: fileLoader, + fileLoader: FileLoader.fromPlatform(), authRepo: authRepo, walletsRepo: walletsRepo, encryptionTool: EncryptionTool(), diff --git a/lib/blocs/startup_bloc.dart b/lib/blocs/startup_bloc.dart index a2e84dd645..58c26e0de2 100644 --- a/lib/blocs/startup_bloc.dart +++ b/lib/blocs/startup_bloc.dart @@ -32,8 +32,7 @@ class StartUpBloc implements BlocBase { } Future run() async { - if (mm2 is MM2WithInit) await (mm2 as MM2WithInit).init(); - + await mm2.init(); final wasAlreadyRunning = running; authRepo.authMode.listen((event) { diff --git a/lib/mm2/mm2.dart b/lib/mm2/mm2.dart index bcf35ce63e..2339603b6c 100644 --- a/lib/mm2/mm2.dart +++ b/lib/mm2/mm2.dart @@ -1,34 +1,73 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/bloc/settings/settings_repository.dart'; -import 'package:web_dex/mm2/mm2_android.dart'; +import 'package:komodo_defi_framework/komodo_defi_framework.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/mm2/mm2_api/rpc/get_my_peer_id/get_my_peer_id_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_response.dart'; -import 'package:web_dex/mm2/mm2_ios.dart'; -import 'package:web_dex/mm2/mm2_linux.dart'; -import 'package:web_dex/mm2/mm2_macos.dart'; -import 'package:web_dex/mm2/mm2_web.dart'; -import 'package:web_dex/mm2/mm2_windows.dart'; import 'package:web_dex/shared/utils/password.dart'; import 'package:web_dex/shared/utils/utils.dart'; -final MM2 mm2 = _createMM2(); +final MM2 mm2 = MM2(); + +final class MM2 { + MM2() { + final String newRpcPassword = generatePassword(); + + if (!validateRPCPassword(newRpcPassword)) { + log( + "If you're seeing this, there's a bug in the rpcPassword generation code.", + path: 'auth_bloc => _startMM2', + ); + throw Exception('invalid rpc password'); + } + _rpcPassword = newRpcPassword; + } + late final String _rpcPassword; + late final KomodoDefiFramework _kdf; -abstract class MM2 { - const MM2(); - static late String _rpcPassword; + Future isSignedIn() => _kdf.isRunning(); - Future start(String? passphrase); + Future init() async { + final hostConfig = LocalConfig(rpcPassword: _rpcPassword, https: false); + final startupConfig = await KdfStartupConfig.noAuthStartup( + rpcPassword: _rpcPassword, + ); - Future stop(); + _kdf = KomodoDefiFramework.create(hostConfig: hostConfig); + _kdf.startKdf(startupConfig); + } + + Future start({ + String? passphrase, + String? walletName, + String? walletPassword, + }) async { + if (passphrase == null) { + log('Passpharse is null, and SDK is already initialised, ' + 'so skipping KDF start call') + .ignore(); + return; + } + + if (await _kdf.isRunning()) { + await _kdf.kdfStop(); + } + + final startupConfig = await KdfStartupConfig.generateWithDefaults( + walletName: walletName ?? '', + walletPassword: walletPassword ?? '', + enableHd: false, + rpcPassword: _rpcPassword, + seed: passphrase, + ); + await _kdf.startKdf(startupConfig); + } + + Future stop() async { + await _kdf.kdfStop(); + } Future version() async { - final dynamic responseStr = await call(VersionRequest()); - final Map responseJson = jsonDecode(responseStr); + final JsonMap responseJson = await call(VersionRequest()); final VersionResponse response = VersionResponse.fromJson(responseJson); return response.result; @@ -36,83 +75,68 @@ abstract class MM2 { Future isLive() async { try { - final String response = await call(GetMyPeerIdRequest()); - final Map responseJson = jsonDecode(response); - - return responseJson['result']?.isNotEmpty ?? false; + final JsonMap response = await call(GetMyPeerIdRequest()); + return (response['result'] as String?)?.isNotEmpty ?? false; } catch (e, s) { log( - 'Get my peer id error: ${e.toString()}', + 'Get my peer id error: $e', path: 'mm2 => isLive', trace: s, isError: true, - ); + ).ignore(); return false; } } - Future status(); - - Future call(dynamic reqStr); - - static String prepareRequest(dynamic req) { - final String reqStr = jsonEncode(_assertPass(req)); - return reqStr; + Future call(dynamic request) async { + final dynamic requestWithUserpass = _assertPass(request); + final JsonMap jsonRequest = requestWithUserpass is Map + ? JsonMap.from(requestWithUserpass) + // ignore: avoid_dynamic_calls + : (requestWithUserpass?.toJson != null + // ignore: avoid_dynamic_calls + ? requestWithUserpass.toJson() as JsonMap + : requestWithUserpass as JsonMap); + + final response = await _kdf.client.executeRpc(jsonRequest); + return _deepConvertMap(response as Map); } - static Future> generateStartParams({ - required String gui, - required String? passphrase, - required String? userHome, - required String? dbDir, - }) async { - String newRpcPassword = generatePassword(); - - if (!validateRPCPassword(newRpcPassword)) { - log( - 'If you\'re seeing this, there\'s a bug in the rpcPassword generation code.', - path: 'auth_bloc => _startMM2', - ); - throw Exception('invalid rpc password'); - } - _rpcPassword = newRpcPassword; + /// Recursively converts the provided map to JsonMap. This is required, as + /// many of the responses received from the sdk are + /// LinkedHashMap + Map _deepConvertMap(Map map) { + return map.map((key, value) { + if (value is Map) return MapEntry(key.toString(), _deepConvertMap(value)); + if (value is List) { + return MapEntry(key.toString(), _deepConvertList(value)); + } + return MapEntry(key.toString(), value); + }); + } - // Use the repository to load the known global coins, so that we can load - // from the bundled configs OR the storage provider after updates are - // downloaded from GitHub. - final List coins = (await coinsRepo.getKnownGlobalCoins()) - .map((e) => e.toJson() as dynamic) - .toList(); - - // Load the stored settings to get the message service config. - final storedSettings = await SettingsRepository.loadStoredSettings(); - final messageServiceConfig = - storedSettings.marketMakerBotSettings.messageServiceConfig; - - return { - 'mm2': 1, - 'allow_weak_password': false, - 'rpc_password': _rpcPassword, - 'netid': 8762, - 'coins': coins, - 'gui': gui, - if (dbDir != null) 'dbdir': dbDir, - if (userHome != null) 'userhome': userHome, - if (passphrase != null) 'passphrase': passphrase, - if (messageServiceConfig != null) - 'message_service_cfg': messageServiceConfig.toJson(), - }; + List _deepConvertList(List list) { + return list.map((value) { + if (value is Map) return _deepConvertMap(value); + if (value is List) return _deepConvertList(value); + return value; + }).toList(); } - static dynamic _assertPass(dynamic req) { + // this is a necessary evil for now becuase of the RPC models that override + // or use the `late String? userpass` field, which would require refactoring + // most of the RPC models and directly affected code. + dynamic _assertPass(dynamic req) { if (req is List) { - for (dynamic element in req) { + for (final dynamic element in req) { + // ignore: avoid_dynamic_calls element.userpass = _rpcPassword; } } else { if (req is Map) { req['userpass'] = _rpcPassword; } else { + // ignore: avoid_dynamic_calls req.userpass = _rpcPassword; } } @@ -121,24 +145,6 @@ abstract class MM2 { } } -MM2 _createMM2() { - if (kIsWeb) { - return MM2Web(); - } else if (Platform.isMacOS) { - return MM2MacOs(); - } else if (Platform.isIOS) { - return MM2iOS(); - } else if (Platform.isWindows) { - return MM2Windows(); - } else if (Platform.isLinux) { - return MM2Linux(); - } else if (Platform.isAndroid) { - return MM2Android(); - } - - throw UnimplementedError(); -} - // 0 - MM2 is not running yet. // 1 - MM2 is running, but no context yet. // 2 - MM2 is running, but no RPC yet. @@ -164,7 +170,3 @@ enum MM2Status { } } } - -abstract class MM2WithInit { - Future init(); -} diff --git a/lib/mm2/mm2_android.dart b/lib/mm2/mm2_android.dart deleted file mode 100644 index ef6a57cb5a..0000000000 --- a/lib/mm2/mm2_android.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/rpc.dart'; -import 'package:web_dex/mm2/rpc_native.dart'; -import 'package:web_dex/services/logger/get_logger.dart'; -import 'package:web_dex/services/native_channel.dart'; - -class MM2Android extends MM2 implements MM2WithInit { - final RPC _rpc = RPCNative(); - - @override - Future start(String? passphrase) async { - await stop(); - final Directory dir = await getApplicationDocumentsDirectory(); - final String filesPath = '${dir.path}/'; - final Map params = await MM2.generateStartParams( - passphrase: passphrase, - gui: 'web_dex Android', - userHome: filesPath, - dbDir: filesPath, - ); - - final int errorCode = await nativeChannel.invokeMethod( - 'start', {'params': jsonEncode(params)}); - - if (kDebugMode) { - print('MM2 start response:$errorCode'); - } - // todo: handle 'already running' case - } - - @override - Future stop() async { - // todo: consider using FFI instead of RPC here - await mm2Api.stop(); - } - - @override - Future status() async { - return MM2Status.fromInt( - await nativeChannel.invokeMethod('status')); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } - - @override - Future init() async { - await _subscribeOnLogs(); - } - - Future _subscribeOnLogs() async { - nativeEventChannel.receiveBroadcastStream().listen((log) async { - if (log is String) { - await logger.write(log); - } - }); - } -} diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 53f3649432..7a9268b8ee 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; @@ -65,8 +66,8 @@ class Mm2Api { Mm2Api({ required MM2 mm2, }) : _mm2 = mm2 { - trezor = Mm2ApiTrezor(_call); - nft = Mm2ApiNft(_call); + trezor = Mm2ApiTrezor(_mm2.call); + nft = Mm2ApiNft(_mm2.call); } final MM2 _mm2; @@ -75,40 +76,40 @@ class Mm2Api { VersionResponse? _versionResponse; Future?> getEnabledCoins(List knownCoins) async { - dynamic response; + JsonMap response; try { - response = await _call(GetEnabledCoinsReq()); + response = await _mm2.call(GetEnabledCoinsReq()); } catch (e) { log( - 'Error getting enabled coins: ${e.toString()}', + 'Error getting enabled coins: $e', path: 'api => getEnabledCoins => _call', isError: true, - ); + ).ignore(); return null; } dynamic resultJson; try { - resultJson = jsonDecode(response)['result']; + resultJson = response['result']; } catch (e, s) { log( - 'Error parsing of enabled coins response: ${e.toString()}', + 'Error parsing of enabled coins response: $e', path: 'api => getEnabledCoins => jsonDecode', trace: s, isError: true, - ); + ).ignore(); return null; } final List list = []; if (resultJson is List) { - for (dynamic item in resultJson) { + for (final dynamic item in resultJson) { final Coin? coin = knownCoins.firstWhereOrNull( (Coin known) => known.abbr == item['ticker'], ); if (coin != null) { - coin.address = item['address']; + coin.address = item['address'] as String?; list.add(coin); } } @@ -152,135 +153,57 @@ class Mm2Api { Future _enableEthWithTokensCoins( List coinRequests, ) async { - dynamic response; - try { - response = await _call(coinRequests); - log( - response, - path: 'api => _enableEthWithTokensCoins', - ); - } catch (e, s) { - log( - 'Error enabling coins: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => _call', - trace: s, - isError: true, - ); - return; - } - - dynamic json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of enable coins response: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => jsonDecode', - trace: s, - isError: true, - ); - return; - } - - if (json is List) { - for (var item in json) { - if (item['error'] != null) { + return _callMany( + coinRequests, + (dynamic request) async { + final JsonMap json = await _mm2.call(request); + if (json['error'] != null) { log( - item['error'], + json['error'].toString(), path: 'api => _enableEthWithTokensCoins:', isError: true, - ); + ).ignore(); + return; } - } - - return; - } else if (json is Map && json['error'] != null) { - log( - json['error'], - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ); - return; - } + }, + logPath: 'api => _enableEthWithTokensCoins => _call', + ); } Future _enableErc20Coins(List coinRequests) async { - dynamic response; - try { - response = await _call(coinRequests); - log( - response, - path: 'api => _enableErc20Coins', - ); - } catch (e, s) { - log( - 'Error enabling coins: ${e.toString()}', - path: 'api => _enableErc20Coins => _call', - trace: s, - isError: true, - ); - return; - } + return _callMany( + coinRequests, + (dynamic request) async { + final JsonMap json = await _mm2.call(request); - List json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of enable coins response: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => jsonDecode', - trace: s, - isError: true, - ); - return; - } - for (dynamic item in json) { - if (item['error'] != null) { - log( - item['error'], - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ); - } - } + if (json['error'] != null) { + log( + json['error'].toString(), + path: 'api => _enableEthWithTokensCoins:', + isError: true, + ).ignore(); + } + }, + logPath: 'api => _enableErc20Coins => _call', + ); } Future _enableElectrumCoins(List electrumRequests) async { - try { - final dynamic response = await _call(electrumRequests); - log( - response, - path: 'api => _enableElectrumCoins => _call', - ); - } catch (e, s) { - log( - 'Error enabling electrum coins: ${e.toString()}', - path: 'api => _enableElectrumCoins => _call', - trace: s, - isError: true, - ); - return; - } + await _callMany( + electrumRequests, + _mm2.call, + logPath: 'api => _enableElectrumCoins => _call', + ); } Future _enableTendermintWithAssets( - List request, + List tendermintRequests, ) async { - try { - final dynamic response = await _call(request); - log( - response, - path: 'api => _enableTendermintWithAssets => _call', - ); - } catch (e, s) { - log( - 'Error enabling tendermint coins: ${e.toString()}', - path: 'api => _enableTendermintWithAssets => _call', - trace: s, - isError: true, - ); - return; - } + return _callMany( + tendermintRequests, + _mm2.call, + logPath: 'api => _enableTendermintWithAssets => _call', + ); } Future _enableTendermintTokens( @@ -289,20 +212,20 @@ class Mm2Api { ) async { try { if (tendermintWithAssetsRequest != null) { - await _call(tendermintWithAssetsRequest); + await _mm2.call(tendermintWithAssetsRequest); } - final dynamic response = await _call(request); - log( - response, - path: 'api => _enableTendermintToken => _call', + return _callMany( + request, + _mm2.call, + logPath: 'api => _enableTendermintToken => _call', ); } catch (e, s) { log( - 'Error enabling tendermint tokens: ${e.toString()}', + 'Error enabling tendermint tokens: $e', path: 'api => _enableTendermintToken => _call', trace: s, isError: true, - ); + ).ignore(); return; } } @@ -310,110 +233,78 @@ class Mm2Api { Future _enableSlpTokens( List requests, ) async { - try { - final dynamic response = await _call(requests); - log( - response, - path: 'api => _enableSlpTokens => _call', - ); - } catch (e, s) { - log( - 'Error enabling bch coins: ${e.toString()}', - path: 'api => _enableSlpTokens => _call', - trace: s, - isError: true, - ); - return; - } + return _callMany( + requests, + _mm2.call, + logPath: 'api => _enableSlpTokens => _call', + ); } Future _enableBchWithTokens( List requests, ) async { - try { - final dynamic response = await _call(requests); - log( - response, - path: 'api => _enableBchWithTokens => _call', - ); - } catch (e, s) { - log( - 'Error enabling bch coins: ${e.toString()}', - path: 'api => _enableBchWithTokens => _call', - trace: s, - isError: true, - ); - return; - } + return _callMany( + requests, + _mm2.call, + logPath: 'api => _enableBchWithTokens => _call', + ); } Future disableCoin(String coin) async { try { - await _call(DisableCoinReq(coin: coin)); + await _mm2.call(DisableCoinReq(coin: coin)); } catch (e, s) { log( - 'Error disabling $coin: ${e.toString()}', + 'Error disabling $coin: $e', path: 'api=> disableCoin => _call', trace: s, isError: true, - ); + ).ignore(); return; } } Future getMaxMakerVol(String abbr) async { - dynamic response; + JsonMap? response; try { - response = await _call(MaxMakerVolRequest(coin: abbr)); + response = await _mm2.call(MaxMakerVolRequest(coin: abbr)); } catch (e, s) { log( - 'Error getting max maker vol $abbr: ${e.toString()}', + 'Error getting max maker vol $abbr: $e', path: 'api => getMaxMakerVol => _call', trace: s, isError: true, - ); + ).ignore(); return null; } - Map json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of max maker vol $abbr response: ${e.toString()}', - path: 'api => getMaxMakerVol => jsonDecode', - trace: s, - isError: true, - ); - return null; - } - - final error = json['error']; + final error = response['error']; if (error != null) { log( - 'Error parsing of max maker vol $abbr response: ${error.toString()}', + 'Error parsing of max maker vol $abbr response: $error', path: 'api => getMaxMakerVol => error', isError: true, - ); + ).ignore(); return null; } - return MaxMakerVolResponse.fromJson(json['result']); + return MaxMakerVolResponse.fromJson( + Map.from(response['result'] as Map? ?? {}), + ); } Future?> getActiveSwaps( ActiveSwapsRequest request, ) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting active swaps: ${e.toString()}', + 'Error getting active swaps: $e', path: 'api => getActiveSwaps', trace: s, isError: true, - ); + ).ignore(); return {'error': 'something went wrong'}; } } @@ -423,36 +314,30 @@ class Mm2Api { String address, ) async { try { - final dynamic response = await _call( + return await _mm2.call( ValidateAddressRequest(coin: coinAbbr, address: address), ); - final Map json = jsonDecode(response); - - return json; } catch (e, s) { log( - 'Error validating address $coinAbbr: ${e.toString()}', + 'Error validating address $coinAbbr: $e', path: 'api => validateAddress', trace: s, isError: true, - ); + ).ignore(); return null; } } Future?> withdraw(WithdrawRequest request) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error withdrawing ${request.params.coin}: ${e.toString()}', + 'Error withdrawing ${request.params.coin}: $e', path: 'api => withdraw', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -461,17 +346,14 @@ class Mm2Api { SendRawTransactionRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.coin}: $e', path: 'api => sendRawTransaction', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -480,17 +362,14 @@ class Mm2Api { MyTxHistoryRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.coin}: $e', path: 'api => getTransactions', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -499,17 +378,14 @@ class Mm2Api { MyTxHistoryV2Request request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error sending raw transaction ${request.params.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.params.coin}: $e', path: 'api => getTransactions', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -518,92 +394,84 @@ class Mm2Api { KmdRewardsInfoRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting rewards info: ${e.toString()}', + 'Error getting rewards info: $e', path: 'api => getRewardsInfo', trace: s, isError: true, - ); + ).ignore(); return null; } } Future?> getBestOrders(BestOrdersRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting best orders ${request.coin}: ${e.toString()}', + 'Error getting best orders ${request.coin}: $e', path: 'api => getBestOrders', trace: s, isError: true, - ); - return {'error': e}; + ).ignore(); + return {'error': e.toString()}; } } Future> sell(SellRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error sell ${request.base}/${request.rel}: ${e.toString()}', + 'Error sell ${request.base}/${request.rel}: $e', path: 'api => sell', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future?> setprice(SetPriceRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error setprice ${request.base}/${request.rel}: ${e.toString()}', + 'Error setprice ${request.base}/${request.rel}: $e', path: 'api => setprice', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future> cancelOrder(CancelOrderRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error cancelOrder ${request.uuid}: ${e.toString()}', + 'Error cancelOrder ${request.uuid}: $e', path: 'api => cancelOrder', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future> getSwapStatus(MySwapStatusReq request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error sell getting swap status ${request.uuid}: ${e.toString()}', + 'Error sell getting swap status ${request.uuid}: $e', path: 'api => getSwapStatus', trace: s, isError: true, - ); + ).ignore(); return {'error': 'something went wrong'}; } } @@ -611,44 +479,42 @@ class Mm2Api { Future getMyOrders() async { try { final MyOrdersRequest request = MyOrdersRequest(); - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return MyOrdersResponse.fromJson(json); + return MyOrdersResponse.fromJson(response); } catch (e, s) { log( - 'Error getting my orders: ${e.toString()}', + 'Error getting my orders: $e', path: 'api => getMyOrders', trace: s, isError: true, - ); + ).ignore(); return null; } } Future getRawSwapData(MyRecentSwapsRequest request) async { - return await _call(request); + return jsonEncode(await _mm2.call(request)); } Future getMyRecentSwaps( MyRecentSwapsRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return MyRecentSwapsResponse.fromJson(json); + return MyRecentSwapsResponse.fromJson(response); } catch (e, s) { log( - 'Error getting my recent swaps: ${e.toString()}', + 'Error getting my recent swaps: $e', path: 'api => getMyRecentSwaps', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -656,38 +522,36 @@ class Mm2Api { Future getOrderStatus(String uuid) async { try { final OrderStatusRequest request = OrderStatusRequest(uuid: uuid); - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return OrderStatusResponse.fromJson(json); + return OrderStatusResponse.fromJson(response); } catch (e, s) { log( - 'Error getting order status $uuid: ${e.toString()}', + 'Error getting order status $uuid: $e', path: 'api => getOrderStatus', trace: s, isError: true, - ); + ).ignore(); return null; } } Future importSwaps(ImportSwapsRequest request) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final JsonMap response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return ImportSwapsResponse.fromJson(json); + return ImportSwapsResponse.fromJson(response); } catch (e, s) { log( - 'Error import swaps : ${e.toString()}', + 'Error import swaps : $e', path: 'api => importSwaps', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -696,24 +560,23 @@ class Mm2Api { RecoverFundsOfSwapRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { log( 'Error recovering funds of swap ${request.uuid}: ${json['error']}', path: 'api => recoverFundsOfSwap', isError: true, - ); + ).ignore(); return null; } return RecoverFundsOfSwapResponse.fromJson(json); } catch (e, s) { log( - 'Error recovering funds of swap ${request.uuid}: ${e.toString()}', + 'Error recovering funds of swap ${request.uuid}: $e', path: 'api => recoverFundsOfSwap', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -722,19 +585,18 @@ class Mm2Api { MaxTakerVolRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } return MaxTakerVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting max taker volume ${request.coin}: ${e.toString()}', + 'Error getting max taker volume ${request.coin}: $e', path: 'api => getMaxTakerVolume', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -743,32 +605,30 @@ class Mm2Api { MinTradingVolRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } return MinTradingVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting min trading volume ${request.coin}: ${e.toString()}', + 'Error getting min trading volume ${request.coin}: $e', path: 'api => getMinTradingVol', trace: s, isError: true, - ); + ).ignore(); return null; } } Future getOrderbook(OrderbookRequest request) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return OrderbookResponse( request: request, - error: json['error'], + error: json['error'] as String?, ); } @@ -778,11 +638,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting orderbook ${request.base}/${request.rel}: ${e.toString()}', + 'Error getting orderbook ${request.base}/${request.rel}: $e', path: 'api => getOrderbook', trace: s, isError: true, - ); + ).ignore(); return OrderbookResponse( request: request, @@ -796,18 +656,17 @@ class Mm2Api { ) async { final request = OrderBookDepthReq(pairs: pairs); try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } return OrderBookDepthResponse.fromJson(json); } catch (e, s) { log( - 'Error getting orderbook depth $request: ${e.toString()}', + 'Error getting orderbook depth $request: $e', path: 'api => getOrderBookDepth', trace: s, - ); + ).ignore(); } return null; } @@ -818,8 +677,7 @@ class Mm2Api { TradePreimageRequest request, ) async { try { - final String response = await _call(request); - final Map responseJson = await jsonDecode(response); + final JsonMap responseJson = await _mm2.call(request); if (responseJson['error'] != null) { return ApiResponse(request: request, error: responseJson); } @@ -829,11 +687,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting trade preimage ${request.base}/${request.rel}: ${e.toString()}', + 'Error getting trade preimage ${request.base}/${request.rel}: $e', path: 'api => getTradePreimage', trace: s, isError: true, - ); + ).ignore(); return ApiResponse( request: request, ); @@ -853,25 +711,22 @@ class Mm2Api { MarketMakerBotRequest marketMakerBotRequest, ) async { try { - final dynamic response = await _call(marketMakerBotRequest.toJson()); + final JsonMap response = await _mm2.call(marketMakerBotRequest.toJson()); log( - response, + response.toString(), path: 'api => ${marketMakerBotRequest.method} => _call', - ); + ).ignore(); - if (response is String) { - final Map responseJson = jsonDecode(response); - if (responseJson['error'] != null) { - throw RpcException(RpcError.fromJson(responseJson)); - } + if (response['error'] != null) { + throw RpcException(RpcError.fromJson(response)); } } catch (e, s) { log( - 'Error starting or stopping simple market maker bot: ${e.toString()}', + 'Error starting or stopping simple market maker bot: $e', path: 'api => start_simple_market_maker_bot => _call', trace: s, isError: true, - ); + ).ignore(); rethrow; } } @@ -893,41 +748,72 @@ class Mm2Api { Future convertLegacyAddress(ConvertAddressRequest request) async { try { - final String response = await _call(request); - final Map responseJson = jsonDecode(response); - return responseJson['result']?['address']; + final JsonMap responseJson = await _mm2.call(request); + return responseJson['result']?['address'] as String?; } catch (e, s) { log( - 'Convert address error: ${e.toString()}', + 'Convert address error: $e', path: 'api => convertLegacyAddress', trace: s, isError: true, - ); + ).ignore(); return null; } } Future stop() async { - await _call(StopReq()); + await _mm2.call(StopReq()); } - Future _call(dynamic req) async { - final MM2Status mm2Status = await _mm2.status(); - if (mm2Status != MM2Status.rpcIsUp) { - return '{"error": "Error, mm2 status: $mm2Status"}'; + Future _callMany( + List requests, + Future Function(T request) processor, { + required String logPath, + bool concurrent = false, + bool enableLogging = true, + }) async { + try { + if (concurrent) { + await Future.wait( + requests.map((request) async { + final dynamic response = await processor(request); + if (enableLogging) { + log( + response.toString(), + path: logPath, + ).ignore(); + } + }), + ); + } else { + for (final request in requests) { + final dynamic response = await processor(request); + if (enableLogging) { + log( + response.toString(), + path: logPath, + ).ignore(); + } + } + } + } catch (e, s) { + if (enableLogging) { + log( + 'Error processing requests: $e', + path: logPath, + trace: s, + isError: true, + ).ignore(); + } + return; } - - final dynamic response = await _mm2.call(req); - - return response; } Future showPrivKey( ShowPrivKeyRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } @@ -938,7 +824,7 @@ class Mm2Api { path: 'api => showPrivKey', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -947,14 +833,13 @@ class Mm2Api { GetDirectlyConnectedPeers request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { log( 'Error getting directly connected peers: ${json['error']}', isError: true, path: 'api => getDirectlyConnectedPeers', - ); + ).ignore(); throw Exception('Failed to get directly connected peers'); } @@ -965,7 +850,7 @@ class Mm2Api { path: 'api => getDirectlyConnectedPeers', trace: s, isError: true, - ); + ).ignore(); rethrow; } } diff --git a/lib/mm2/mm2_api/mm2_api_nft.dart b/lib/mm2/mm2_api/mm2_api_nft.dart index 4bf166ab77..71a5096683 100644 --- a/lib/mm2/mm2_api/mm2_api_nft.dart +++ b/lib/mm2/mm2_api/mm2_api_nft.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/get_nft_list/get_nft_list_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/update_nft/update_nft_req.dart'; @@ -16,7 +17,7 @@ import 'package:web_dex/shared/utils/utils.dart'; class Mm2ApiNft { Mm2ApiNft(this.call); - final Future Function(dynamic) call; + final Future Function(dynamic) call; Future> updateNftList( List chains) async { @@ -24,22 +25,22 @@ class Mm2ApiNft { final List nftChains = await getActiveNftChains(chains); if (nftChains.isEmpty) { return { - "error": - "Please ensure an NFT chain is activated and patiently await while your NFTs are loaded." + 'error': + 'Please ensure an NFT chain is activated and patiently await ' + 'while your NFTs are loaded.' }; } final UpdateNftRequest request = UpdateNftRequest(chains: nftChains); - final dynamic rawResponse = await call(request); - final Map json = jsonDecode(rawResponse); + final JsonMap json = await call(request); log( request.toJson().toString(), path: 'UpdateNftRequest', - ); + ).ignore(); log( - rawResponse, + json.toJsonString(), path: 'UpdateNftResponse', - ); + ).ignore(); return json; } catch (e, s) { log( @@ -47,7 +48,7 @@ class Mm2ApiNft { path: 'UpdateNftResponse', trace: s, isError: true, - ); + ).ignore(); throw TransportError(message: e.toString()); } } @@ -59,14 +60,10 @@ class Mm2ApiNft { try { final RefreshNftMetadataRequest request = RefreshNftMetadataRequest( chain: chain, tokenAddress: tokenAddress, tokenId: tokenId); - final dynamic rawResponse = await call(request); - - final Map json = jsonDecode(rawResponse); - - return json; + return await call(request); } catch (e) { log(e.toString(), - path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true); + path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true).ignore(); throw TransportError(message: e.toString()); } } @@ -76,40 +73,36 @@ class Mm2ApiNft { final List nftChains = await getActiveNftChains(chains); if (nftChains.isEmpty) { return { - "error": - "Please ensure the NFT chain is activated and patiently await " - "while your NFTs are loaded." + 'error': + 'Please ensure the NFT chain is activated and patiently await ' + 'while your NFTs are loaded.' }; } final GetNftListRequest request = GetNftListRequest(chains: nftChains); - final dynamic rawResponse = await call(request); - final Map json = jsonDecode(rawResponse); + final JsonMap json = await call(request); log( request.toJson().toString(), path: 'getActiveNftChains', - ); + ).ignore(); log( - rawResponse, + json.toJsonString(), path: 'UpdateNftResponse', - ); + ).ignore(); return json; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftList', isError: true); + log(e.toString(), path: 'Mm2ApiNft => getNftList', isError: true).ignore(); throw TransportError(message: e.toString()); } } Future> withdraw(WithdrawNftRequest request) async { try { - final dynamic rawResponse = await call(request); - final Map json = jsonDecode(rawResponse); - - return json; + return await call(request); } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => withdraw', isError: true); + log(e.toString(), path: 'Mm2ApiNft => withdraw', isError: true).ignore(); throw TransportError(message: e.toString()); } } @@ -117,15 +110,14 @@ class Mm2ApiNft { Future> getNftTxs( NftTransactionsRequest request, bool withAdditionalInfo) async { try { - final String rawResponse = await call(request); - final Map json = jsonDecode(rawResponse); + final JsonMap json = await call(request); if (withAdditionalInfo) { final jsonUpdated = await const ProxyApiNft().addDetailsToTx(json); return jsonUpdated; } return json; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftTransactions', isError: true); + log(e.toString(), path: 'Mm2ApiNft => getNftTransactions', isError: true).ignore(); throw TransportError(message: e.toString()); } } @@ -137,7 +129,7 @@ class Mm2ApiNft { .getTxDetailsByHash(request.chain, request.txHash); return additionalTxInfo; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftTxDetails', isError: true); + log(e.toString(), path: 'Mm2ApiNft => getNftTxDetails', isError: true).ignore(); throw TransportError(message: e.toString()); } } @@ -149,19 +141,19 @@ class Mm2ApiNft { // log(apiCoins.toString(), path: 'Mm2ApiNft => apiCoins', isError: true); final List enabledCoins = apiCoins.map((c) => c.abbr).toList(); log(enabledCoins.toString(), - path: 'Mm2ApiNft => enabledCoins', isError: true); + path: 'Mm2ApiNft => enabledCoins', isError: true).ignore(); final List nftCoins = chains.map((c) => c.coinAbbr()).toList(); - log(nftCoins.toString(), path: 'Mm2ApiNft => nftCoins', isError: true); + log(nftCoins.toString(), path: 'Mm2ApiNft => nftCoins', isError: true).ignore(); final List activeChains = chains .map((c) => c) .toList() .where((c) => enabledCoins.contains(c.coinAbbr())) .toList(); log(activeChains.toString(), - path: 'Mm2ApiNft => activeChains', isError: true); + path: 'Mm2ApiNft => activeChains', isError: true).ignore(); final List nftChains = activeChains.map((c) => c.toApiRequest()).toList(); - log(nftChains.toString(), path: 'Mm2ApiNft => nftChains', isError: true); + log(nftChains.toString(), path: 'Mm2ApiNft => nftChains', isError: true).ignore(); return nftChains; } } @@ -170,10 +162,10 @@ class ProxyApiNft { static const _errorBaseMessage = 'ProxyApiNft API: '; const ProxyApiNft(); Future> addDetailsToTx(Map json) async { - final transactions = List.from(json['result']['transfer_history']); + final transactions = List.from(json['result']['transfer_history'] as List? ?? []); final listOfAdditionalData = transactions .map((tx) => { - 'blockchain': convertChainForProxy(tx['chain']), + 'blockchain': convertChainForProxy(tx['chain'] as String), 'tx_hash': tx['transaction_hash'], }) .toList(); @@ -182,10 +174,10 @@ class ProxyApiNft { Uri.parse(txByHashUrl), body: jsonEncode(listOfAdditionalData), ); - final Map jsonBody = jsonDecode(response.body); + final jsonBody = jsonDecode(response.body) as JsonMap; json['result']['transfer_history'] = transactions.map((element) { - final String? txHash = element['transaction_hash']; - final tx = jsonBody[txHash]; + final txHash = element['transaction_hash'] as String?; + final tx = jsonBody[txHash] as JsonMap?; if (tx != null) { element['confirmations'] = tx['confirmations']; element['fee_details'] = tx['fee_details']; @@ -212,8 +204,8 @@ class ProxyApiNft { Uri.parse(txByHashUrl), body: body, ); - final Map jsonBody = jsonDecode(response.body); - return jsonBody[txHash]; + final jsonBody = jsonDecode(response.body) as JsonMap; + return jsonBody[txHash] as JsonMap; } catch (e) { throw Exception(_errorBaseMessage + e.toString()); } diff --git a/lib/mm2/mm2_api/mm2_api_trezor.dart b/lib/mm2/mm2_api/mm2_api_trezor.dart index 9073fb2915..3a6673f95b 100644 --- a/lib/mm2/mm2_api/mm2_api_trezor.dart +++ b/lib/mm2/mm2_api/mm2_api_trezor.dart @@ -1,5 +1,4 @@ -import 'dart:convert'; - +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_status/trezor_balance_status_request.dart'; @@ -29,12 +28,11 @@ import 'package:web_dex/shared/utils/utils.dart'; class Mm2ApiTrezor { Mm2ApiTrezor(this.call); - final Future Function(dynamic) call; + final Future Function(dynamic) call; Future init(InitTrezorReq request) async { try { - final String response = await call(request); - return InitTrezorRes.fromJson(jsonDecode(response)); + return InitTrezorRes.fromJson(await call(request)); } catch (e) { return InitTrezorRes( error: e.toString(), @@ -44,8 +42,7 @@ class Mm2ApiTrezor { Future initStatus(InitTrezorStatusReq request) async { try { - final String response = await call(request); - return InitTrezorStatusRes.fromJson(jsonDecode(response)); + return InitTrezorStatusRes.fromJson(await call(request)); } catch (e) { return InitTrezorStatusRes(error: e.toString()); } @@ -55,7 +52,8 @@ class Mm2ApiTrezor { try { await call(request); } catch (e) { - log(e.toString(), path: 'api => initTrezorCancel', isError: true); + log(e.toString(), path: 'api => initTrezorCancel', isError: true) + .ignore(); } } @@ -63,7 +61,7 @@ class Mm2ApiTrezor { try { await call(request); } catch (e) { - log(e.toString(), path: 'api => trezorPin', isError: true); + log(e.toString(), path: 'api => trezorPin', isError: true).ignore(); } } @@ -71,45 +69,46 @@ class Mm2ApiTrezor { try { await call(request); } catch (e) { - log(e.toString(), path: 'api => trezorPassphrase', isError: true); + log(e.toString(), path: 'api => trezorPassphrase', isError: true) + .ignore(); } } Future enableUtxo( - TrezorEnableUtxoReq request) async { + TrezorEnableUtxoReq request, + ) async { try { - final String response = await call(request); - return TrezorEnableUtxoResponse.fromJson(jsonDecode(response)); + return TrezorEnableUtxoResponse.fromJson(await call(request)); } catch (e) { return TrezorEnableUtxoResponse(error: e.toString()); } } Future enableUtxoStatus( - TrezorEnableUtxoStatusReq request) async { + TrezorEnableUtxoStatusReq request, + ) async { try { - final String response = await call(request); - return TrezorEnableUtxoStatusResponse.fromJson(jsonDecode(response)); + return TrezorEnableUtxoStatusResponse.fromJson(await call(request)); } catch (e) { return TrezorEnableUtxoStatusResponse(error: e.toString()); } } Future balanceInit( - TrezorBalanceInitRequest request) async { + TrezorBalanceInitRequest request, + ) async { try { - final String response = await call(request); - return TrezorBalanceInitResponse.fromJson(jsonDecode(response)); + return TrezorBalanceInitResponse.fromJson(await call(request)); } catch (e) { return TrezorBalanceInitResponse(error: e.toString()); } } Future balanceStatus( - TrezorBalanceStatusRequest request) async { + TrezorBalanceStatusRequest request, + ) async { try { - final String response = await call(request); - return TrezorBalanceStatusResponse.fromJson(jsonDecode(response)); + return TrezorBalanceStatusResponse.fromJson(await call(request)); } catch (e) { return TrezorBalanceStatusResponse(error: e.toString()); } @@ -117,9 +116,9 @@ class Mm2ApiTrezor { Future initNewAddress(String coin) async { try { - final String response = + final JsonMap response = await call(TrezorGetNewAddressInitReq(coin: coin)); - return TrezorGetNewAddressInitResponse.fromJson(jsonDecode(response)); + return TrezorGetNewAddressInitResponse.fromJson(response); } catch (e) { return TrezorGetNewAddressInitResponse(error: e.toString()); } @@ -127,9 +126,9 @@ class Mm2ApiTrezor { Future getNewAddressStatus(int taskId) async { try { - final String response = + final JsonMap response = await call(TrezorGetNewAddressStatusReq(taskId: taskId)); - return GetNewAddressResponse.fromJson(jsonDecode(response)); + return GetNewAddressResponse.fromJson(response); } catch (e) { return GetNewAddressResponse(error: e.toString()); } @@ -139,24 +138,23 @@ class Mm2ApiTrezor { try { await call(TrezorGetNewAddressCancelReq(taskId: taskId)); } catch (e) { - log(e.toString(), path: 'api_trezor => getNewAddressCancel'); + log(e.toString(), path: 'api_trezor => getNewAddressCancel').ignore(); } } Future withdraw(TrezorWithdrawRequest request) async { try { - final String response = await call(request); - return TrezorWithdrawResponse.fromJson(jsonDecode(response)); + return TrezorWithdrawResponse.fromJson(await call(request)); } catch (e) { return TrezorWithdrawResponse(error: e.toString()); } } Future withdrawStatus( - TrezorWithdrawStatusRequest request) async { + TrezorWithdrawStatusRequest request, + ) async { try { - final String response = await call(request); - return TrezorWithdrawStatusResponse.fromJson(jsonDecode(response)); + return TrezorWithdrawStatusResponse.fromJson(await call(request)); } catch (e) { return TrezorWithdrawStatusResponse(error: e.toString()); } @@ -166,25 +164,24 @@ class Mm2ApiTrezor { try { await call(request); } catch (e) { - log(e.toString(), path: 'api => withdrawCancel', isError: true); + log(e.toString(), path: 'api => withdrawCancel', isError: true).ignore(); } } Future getConnectionStatus(String pubKey) async { try { - final String response = + final JsonMap responseJson = await call(TrezorConnectionStatusRequest(pubKey: pubKey)); - final Map responseJson = jsonDecode(response); - final String? status = responseJson['result']?['status']; + final String? status = responseJson['result']?['status'] as String?; if (status == null) return TrezorConnectionStatus.unknown; return TrezorConnectionStatus.fromString(status); } catch (e, s) { log( - 'Error getting trezor status: ${e.toString()}', + 'Error getting trezor status: $e', path: 'api => trezorConnectionStatus', trace: s, isError: true, - ); + ).ignore(); return TrezorConnectionStatus.unknown; } } diff --git a/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart b/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart index cade27c162..3c19a1da4a 100644 --- a/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart +++ b/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart @@ -1,17 +1,30 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/orderbook/order.dart'; import 'package:web_dex/shared/utils/utils.dart'; +// Define the AddressType enum +enum AddressType { + transparent, + shielded, +} + class BestOrders { BestOrders({this.result, this.error}); factory BestOrders.fromJson(Map json) { - final Map> ordersMap = >{}; - for (var key in json['result']['orders'].keys) { - final List bestOrders = []; - for (var result in json['result']['orders'][key]) { - bestOrders.add(BestOrder.fromJson(result)); + final ordersMap = >{}; + final orders = json['result']['orders'] as JsonMap? ?? {}; + for (final String key in orders.keys) { + final bestOrders = []; + final bestOrdersJson = orders[key] as List; + for (final dynamic result in bestOrdersJson) { + bestOrders + .add(BestOrder.fromJson(result as Map? ?? {})); } ordersMap.putIfAbsent(key, () => bestOrders); } @@ -38,22 +51,29 @@ class BestOrder { maxVolume: order.maxVolume, minVolume: order.minVolume ?? Rational.zero, coin: coin ?? order.base, - address: order.address ?? '', + address: OrderAddress( + addressType: AddressType.transparent, // Assuming transparent as default + addressData: order.address ?? '', + ), uuid: order.uuid ?? '', ); } factory BestOrder.fromJson(Map json) { return BestOrder( - price: fract2rat(json['price']['fraction']) ?? - Rational.parse(json['price']['decimal']), - maxVolume: fract2rat(json['base_max_volume']['fraction']) ?? - Rational.parse(json['base_max_volume']['decimal']), - minVolume: fract2rat(json['base_min_volume']['fraction']) ?? - Rational.parse(json['base_min_volume']['decimal']), - coin: json['coin'], - address: json['address']['address_data'], - uuid: json['uuid'], + price: fract2rat(json['price']['fraction'] as Map?) ?? + Rational.parse(json['price']['decimal'] as String? ?? ''), + maxVolume: fract2rat( + json['base_max_volume']['fraction'] as Map?, + ) ?? + Rational.parse(json['base_max_volume']['decimal'] as String? ?? ''), + minVolume: fract2rat( + json['base_min_volume']['fraction'] as Map?, + ) ?? + Rational.parse(json['base_min_volume']['decimal'] as String? ?? ''), + coin: json['coin'] as String? ?? '', + address: OrderAddress.fromJson(json['address'] as Map), + uuid: json['uuid'] as String? ?? '', ); } @@ -61,12 +81,66 @@ class BestOrder { final Rational maxVolume; final Rational minVolume; final String coin; - final String address; + final OrderAddress address; + final String uuid; @override String toString() { return 'BestOrder($coin, $price)'; } +} - final String uuid; +class OrderAddress extends Equatable { + const OrderAddress({ + required this.addressType, + required this.addressData, + }); + + const OrderAddress.transparent(String? addressData) + : this( + addressType: AddressType.transparent, + addressData: addressData, + ); + + const OrderAddress.shielded() + : this( + addressType: AddressType.shielded, + addressData: null, + ); + + factory OrderAddress.fromJson(Map json) { + // Only [addressType] is required, since shielded addresses don't have + // [addressData] + if (json['address_type'] == null) { + throw Exception('Invalid address'); + } + + // Parse the addressType string into the AddressType enum + final typeString = json['address_type'] as String; + final AddressType addressType; + switch (typeString.toLowerCase()) { + case 'transparent': + addressType = AddressType.transparent; + case 'shielded': + addressType = AddressType.shielded; + default: + throw Exception('Unknown address type: $typeString'); + } + + return OrderAddress( + addressType: addressType, + addressData: json['address_data'] as String?, + ); + } + + final AddressType addressType; + final String? addressData; + + @override + List get props => [addressType, addressData]; + + @override + String toString() { + return 'OrderAddress($addressType, $addressData)'; + } } diff --git a/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart index 817f8e2e71..3f08c50e66 100644 --- a/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart +++ b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart @@ -4,14 +4,16 @@ class GetDirectlyConnectedPeersResponse { }); factory GetDirectlyConnectedPeersResponse.fromJson( - Map json) { + Map json, + ) { final peersMap = json['result'] as Map? ?? {}; - final peersList = peersMap.keys - .map((key) => DirectlyConnectedPeer( - peerId: key, - peerAddresses: peersMap[key] as List? ?? [], - )) - .toList(); + final peersList = peersMap.keys.map((String key) { + final peers = peersMap[key] as List? ?? []; + return DirectlyConnectedPeer( + peerId: key, + peerAddresses: peers.map((dynamic peer) => peer.toString()).toList(), + ); + }).toList(); return GetDirectlyConnectedPeersResponse(peers: peersList); } diff --git a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart index 463e8db80e..1d6e685c70 100644 --- a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart +++ b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart @@ -4,14 +4,18 @@ class MaxMakerVolResponse { required this.balance, }); - final MaxMakerVolResponseValue volume; - final MaxMakerVolResponseValue balance; - factory MaxMakerVolResponse.fromJson(Map json) => MaxMakerVolResponse( - volume: MaxMakerVolResponseValue.fromJson(json['volume']), - balance: MaxMakerVolResponseValue.fromJson(json['balance']), + volume: MaxMakerVolResponseValue.fromJson( + Map.from(json['volume'] as Map? ?? {}), + ), + balance: MaxMakerVolResponseValue.fromJson( + Map.from(json['balance'] as Map? ?? {}), + ), ); + + final MaxMakerVolResponseValue volume; + final MaxMakerVolResponseValue balance; } class MaxMakerVolResponseValue { @@ -19,10 +23,10 @@ class MaxMakerVolResponseValue { required this.decimal, }); - final String decimal; - factory MaxMakerVolResponseValue.fromJson(Map json) => - MaxMakerVolResponseValue(decimal: json['decimal']); + MaxMakerVolResponseValue(decimal: json['decimal'] as String); + + final String decimal; Map toJson() => { 'decimal': decimal, diff --git a/lib/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart b/lib/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart index b8185d8e49..4f336b902f 100644 --- a/lib/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart +++ b/lib/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart @@ -6,8 +6,13 @@ class MaxTakerVolResponse { factory MaxTakerVolResponse.fromJson(Map json) => MaxTakerVolResponse( - coin: json['coin'] ?? '', - result: MaxTakerVolumeResponseResult.fromJson(json['result'])); + coin: json['coin'] as String? ?? '', + result: MaxTakerVolumeResponseResult.fromJson( + Map.from( + json['result'] as Map? ?? {}, + ), + ), + ); final String coin; final MaxTakerVolumeResponseResult result; } @@ -18,7 +23,10 @@ class MaxTakerVolumeResponseResult { required this.denom, }); factory MaxTakerVolumeResponseResult.fromJson(Map json) => - MaxTakerVolumeResponseResult(denom: json['denom'], numer: json['numer']); + MaxTakerVolumeResponseResult( + denom: json['denom'] as String, + numer: json['numer'] as String, + ); final String denom; final String numer; diff --git a/lib/mm2/mm2_api/rpc/my_orders/my_orders_response.dart b/lib/mm2/mm2_api/rpc/my_orders/my_orders_response.dart index 92594fc4b2..c54d3b562d 100644 --- a/lib/mm2/mm2_api/rpc/my_orders/my_orders_response.dart +++ b/lib/mm2/mm2_api/rpc/my_orders/my_orders_response.dart @@ -8,7 +8,9 @@ class MyOrdersResponse { factory MyOrdersResponse.fromJson(Map json) => MyOrdersResponse( - result: MyOrdersResponseResult.fromJson(json['result']), + result: MyOrdersResponseResult.fromJson( + Map.from(json['result'] as Map? ?? {}), + ), ); MyOrdersResponseResult result; @@ -27,22 +29,26 @@ class MyOrdersResponseResult { factory MyOrdersResponseResult.fromJson(Map json) => MyOrdersResponseResult( makerOrders: Map.from(json['maker_orders']).map( - (dynamic k, dynamic v) => - MapEntry(k, MakerOrder.fromJson(v))), + (dynamic k, dynamic v) => + MapEntry(k, MakerOrder.fromJson(v)), + ), takerOrders: Map.from(json['taker_orders']).map( - (dynamic k, dynamic v) => - MapEntry(k, TakerOrder.fromJson(v))), + (dynamic k, dynamic v) => + MapEntry(k, TakerOrder.fromJson(v)), + ), ); Map makerOrders; Map takerOrders; Map toJson() => { - 'maker_orders': Map.from(makerOrders) - .map((dynamic k, dynamic v) => - MapEntry(k, v.toJson())), - 'taker_orders': Map.from(takerOrders) - .map((dynamic k, dynamic v) => - MapEntry(k, v.toJson())), + 'maker_orders': + Map.from(makerOrders).map( + (dynamic k, dynamic v) => MapEntry(k, v.toJson()), + ), + 'taker_orders': + Map.from(takerOrders).map( + (dynamic k, dynamic v) => MapEntry(k, v.toJson()), + ), }; } diff --git a/lib/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart b/lib/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart index 019d229622..444cbd1452 100644 --- a/lib/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart +++ b/lib/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart @@ -7,7 +7,9 @@ class MyRecentSwapsResponse { factory MyRecentSwapsResponse.fromJson(Map json) => MyRecentSwapsResponse( - result: MyRecentSwapsResponseResult.fromJson(json['result']), + result: MyRecentSwapsResponseResult.fromJson( + Map.from(json['result'] as Map? ?? {}), + ), ); MyRecentSwapsResponseResult result; @@ -31,16 +33,20 @@ class MyRecentSwapsResponseResult { factory MyRecentSwapsResponseResult.fromJson(Map json) => MyRecentSwapsResponseResult( - fromUuid: json['from_uuid'], - limit: json['limit'] ?? 0, - skipped: json['skipped'] ?? 0, - swaps: List.from((json['swaps'] ?? []) - .where((dynamic x) => x != null) - .map((dynamic x) => Swap.fromJson(x))), - total: json['total'] ?? 0, - foundRecords: json['found_records'] ?? 0, - pageNumber: json['page_number'] ?? 0, - totalPages: json['total_pages'] ?? 0, + fromUuid: json['from_uuid'] as String?, + limit: json['limit'] as int? ?? 0, + skipped: json['skipped'] as int? ?? 0, + swaps: List.from( + (json['swaps'] as List? ?? []) + .where((dynamic x) => x != null) + .map( + (dynamic x) => Swap.fromJson(x as Map? ?? {}), + ), + ), + total: json['total'] as int? ?? 0, + foundRecords: json['found_records'] as int? ?? 0, + pageNumber: json['page_number'] as int? ?? 0, + totalPages: json['total_pages'] as int? ?? 0, ); String? fromUuid; @@ -57,7 +63,8 @@ class MyRecentSwapsResponseResult { 'limit': limit, 'skipped': skipped, 'swaps': List.from( - swaps.map>((Swap x) => x.toJson())), + swaps.map>((Swap x) => x.toJson()), + ), 'total': total, }; } diff --git a/lib/mm2/mm2_api/rpc/rpc_error.dart b/lib/mm2/mm2_api/rpc/rpc_error.dart index 043ee8aaad..27d6eb95b7 100644 --- a/lib/mm2/mm2_api/rpc/rpc_error.dart +++ b/lib/mm2/mm2_api/rpc/rpc_error.dart @@ -2,10 +2,10 @@ import 'package:equatable/equatable.dart'; import 'package:web_dex/mm2/mm2_api/rpc/rpc_error_type.dart'; class RpcException implements Exception { - final RpcError error; - const RpcException(this.error); + final RpcError error; + @override String toString() { return 'RpcException: ${error.error}'; @@ -13,14 +13,6 @@ class RpcException implements Exception { } class RpcError extends Equatable { - final String? mmrpc; - final String? error; - final String? errorPath; - final String? errorTrace; - final RpcErrorType? errorType; - final String? errorData; - final int? id; - const RpcError({ this.mmrpc, this.error, @@ -41,6 +33,14 @@ class RpcError extends Equatable { id: json['id'] as int?, ); + final String? mmrpc; + final String? error; + final String? errorPath; + final String? errorTrace; + final RpcErrorType? errorType; + final String? errorData; + final int? id; + Map toJson() => { 'mmrpc': mmrpc, 'error': error, diff --git a/lib/mm2/mm2_ios.dart b/lib/mm2/mm2_ios.dart deleted file mode 100644 index 73039a43a2..0000000000 --- a/lib/mm2/mm2_ios.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:path_provider/path_provider.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/rpc.dart'; -import 'package:web_dex/mm2/rpc_native.dart'; -import 'package:web_dex/services/logger/get_logger.dart'; -import 'package:web_dex/services/native_channel.dart'; - -class MM2iOS extends MM2 implements MM2WithInit { - final RPC _rpc = RPCNative(); - - @override - Future start(String? passphrase) async { - final Directory dir = await getApplicationDocumentsDirectory(); - final String filesPath = '${dir.path}/'; - final Map params = await MM2.generateStartParams( - passphrase: passphrase, - gui: 'web_dex iOs', - userHome: filesPath, - dbDir: filesPath, - ); - - final int errorCode = await nativeChannel.invokeMethod( - 'start', {'params': jsonEncode(params)}); - - await logger.write('MM2 start response: $errorCode'); - } - - @override - Future stop() async { - final int errorCode = await nativeChannel.invokeMethod('stop'); - - await logger.write('MM2 sop response: $errorCode'); - } - - @override - Future status() async { - return MM2Status.fromInt( - await nativeChannel.invokeMethod('status')); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } - - @override - Future init() async { - await _subscribeOnEvents(); - } - - Future _subscribeOnEvents() async { - nativeEventChannel.receiveBroadcastStream().listen((event) async { - Map eventJson; - try { - eventJson = jsonDecode(event); - } catch (e) { - logger.write('Error decoding MM2 event: $e'); - return; - } - - if (eventJson['type'] == 'log') { - await logger.write(eventJson['message']); - } else if (eventJson['type'] == 'app_did_become_active') { - if (!await isLive()) await _restartMM2AndCoins(); - } - }); - } - - Future _restartMM2AndCoins() async { - await nativeChannel.invokeMethod('restart'); - await coinsBloc.reactivateAll(); - } -} diff --git a/lib/mm2/mm2_linux.dart b/lib/mm2/mm2_linux.dart deleted file mode 100644 index c038150e51..0000000000 --- a/lib/mm2/mm2_linux.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/rpc.dart'; -import 'package:web_dex/mm2/rpc_native.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class MM2Linux extends MM2 { - final RPC _rpc = RPCNative(); - Process? _mm2Process; - - @override - Future start(String? passphrase) async { - await _killMM2Process(); - - final String mainPath = _mainPath; - final Map params = await MM2.generateStartParams( - passphrase: passphrase, - gui: 'web_dex linux', - userHome: mainPath, - dbDir: '$mainPath/DB', - ); - - final mm2Config = await _createMM2ConfigFile(mainPath, params); - - if (mm2Config == null) { - return; - } - - _mm2Process = await Process.start( - _exePath, - [], - environment: { - 'MM_CONF_PATH': mm2Config.path, - }, - ); - - await waitMM2StatusChange(MM2Status.rpcIsUp, mm2, waitingTime: 60000); - mm2Config.deleteSync(recursive: true); - - _mm2Process?.stdout.transform(utf8.decoder).listen((data) async { - log( - data, - path: 'mm2 log', - isError: false, - ); - }); - _mm2Process?.stderr.transform(utf8.decoder).listen((event) async { - log( - event, - path: 'mm2 error log', - isError: true, - ); - }); - } - - @override - Future stop() async { - await mm2Api.stop(); - await _killMM2Process(); - } - - @override - Future status() async { - try { - final status = await version(); - return status.isNotEmpty ? MM2Status.rpcIsUp : MM2Status.isNotRunningYet; - } catch (_) { - return MM2Status.isNotRunningYet; - } - } - - Future _createMM2ConfigFile( - String dir, Map params) async { - try { - final File mm2Config = File('$dir/MM2.json')..createSync(recursive: true); - await mm2Config.writeAsString(jsonEncode(params)); - return mm2Config; - } catch (e) { - log('mm2 linux error mm2 config file not created: ${e.toString()}'); - return null; - } - } - - String get _mainPath => Platform.resolvedExecutable - .substring(0, Platform.resolvedExecutable.lastIndexOf("/")); - - String get _exePath => path.join(_mainPath, 'mm2'); - - Future _killMM2Process() async { - _mm2Process?.kill(); - await Process.run('pkill', ['-f', 'mm2']); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } -} diff --git a/lib/mm2/mm2_macos.dart b/lib/mm2/mm2_macos.dart deleted file mode 100644 index cd6873b1f5..0000000000 --- a/lib/mm2/mm2_macos.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/rpc.dart'; -import 'package:web_dex/mm2/rpc_native.dart'; -import 'package:web_dex/services/logger/get_logger.dart'; -import 'package:web_dex/services/native_channel.dart'; - -class MM2MacOs extends MM2 implements MM2WithInit { - final RPC _rpc = RPCNative(); - - @override - Future start(String? passphrase) async { - final Directory dir = await getApplicationDocumentsDirectory(); - final String filesPath = '${dir.path}/'; - final Map params = await MM2.generateStartParams( - passphrase: passphrase, - gui: 'web_dex macOs', - userHome: filesPath, - dbDir: filesPath, - ); - - final int errorCode = await nativeChannel.invokeMethod( - 'start', {'params': jsonEncode(params)}); - - if (kDebugMode) { - print('MM2 start response:$errorCode'); - } - } - - @override - Future stop() async { - final int errorCode = await nativeChannel.invokeMethod('stop'); - - await logger.write('MM2 sop response: $errorCode'); - } - - @override - Future status() async { - return MM2Status.fromInt( - await nativeChannel.invokeMethod('status')); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } - - @override - Future init() async { - await _subscribeOnLogs(); - } - - Future _subscribeOnLogs() async { - nativeEventChannel.receiveBroadcastStream().listen((log) async { - if (log is String) { - await logger.write(log); - } - }); - } -} diff --git a/lib/mm2/mm2_web.dart b/lib/mm2/mm2_web.dart deleted file mode 100644 index a6753981ad..0000000000 --- a/lib/mm2/mm2_web.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:convert'; - -// ignore: unnecessary_import -import 'package:universal_html/js.dart'; -import 'package:universal_html/js_util.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/rpc_web.dart'; -import 'package:web_dex/platform/platform.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class MM2Web extends MM2 implements MM2WithInit { - final RPCWeb _rpc = const RPCWeb(); - - @override - Future init() async { - // TODO! Test for breaking changes to mm2 initialisation accross reloads - while (isBusyPreloading == true) { - await Future.delayed(const Duration(milliseconds: 10)); - // TODO: Safe guard for max retries - } - - if (isPreloaded == false) { - await promiseToFuture(initWasm()); - } - } - - /// TODO: Document - bool? get isBusyPreloading => context['is_mm2_preload_busy'] as bool?; - - /// TODO: Document - bool? get isPreloaded => context['is_mm2_preloaded'] as bool?; - - @override - Future start(String? passphrase) async { - final Map params = await MM2.generateStartParams( - passphrase: passphrase, - gui: 'web_dex web', - dbDir: null, - userHome: null, - ); - - await promiseToFuture( - wasmRunMm2( - jsonEncode(params), - allowInterop Function(int level, String message)>( - _handleLog, - ), - ), - ); - } - - @override - Future stop() async { - // todo: consider using FFI instead of RPC here - await mm2Api.stop(); - } - - @override - Future version() async { - return wasmVersion(); - } - - @override - Future status() async { - return MM2Status.fromInt(wasmMm2Status()); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } - - Future _handleLog(int level, String message) async { - log(message, path: 'mm2 log'); - } -} diff --git a/lib/mm2/mm2_windows.dart b/lib/mm2/mm2_windows.dart deleted file mode 100644 index 3cd0e6e4a4..0000000000 --- a/lib/mm2/mm2_windows.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/rpc.dart'; -import 'package:web_dex/mm2/rpc_native.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class MM2Windows extends MM2 { - final RPC _rpc = RPCNative(); - Process? _mm2Process; - - @override - Future start(String? passphrase) async { - await _killMM2Process(); - - final String mainPath = _mainPath; - final Map params = await MM2.generateStartParams( - gui: 'web_dex windows', - passphrase: passphrase, - userHome: mainPath, - dbDir: '$mainPath/DB', - ); - - final mm2Config = await _createMM2ConfigFile(mainPath, params); - - if (mm2Config == null) { - return; - } - - _mm2Process = await Process.start( - _exePath, - [], - environment: { - 'MM_CONF_PATH': mm2Config.path, - }, - ); - - _mm2Process?.stdout.transform(utf8.decoder).listen((data) async { - log( - data, - path: 'mm2 log', - isError: false, - ); - }); - _mm2Process?.stderr.transform(utf8.decoder).listen((event) async { - log( - event, - path: 'mm2 error log', - isError: true, - ); - }); - _mm2Process?.exitCode.then((exitCode) async { - log('mm2 exit code: $exitCode'); - }); - - await waitMM2StatusChange(MM2Status.rpcIsUp, mm2, waitingTime: 60000); - mm2Config.deleteSync(recursive: true); - } - - @override - Future status() async { - try { - final status = await version(); - return status.isNotEmpty ? MM2Status.rpcIsUp : MM2Status.isNotRunningYet; - } catch (_) { - return MM2Status.isNotRunningYet; - } - } - - @override - Future stop() async { - await mm2Api.stop(); - await _killMM2Process(); - } - - Future _createMM2ConfigFile( - String dir, Map params) async { - try { - final File mm2Config = File('$dir/MM2.json')..createSync(recursive: true); - await mm2Config.writeAsString(jsonEncode(params)); - return mm2Config; - } catch (e) { - log('mm2 windows error mm2 config file not created: ${e.toString()}'); - return null; - } - } - - String get _mainPath => Platform.resolvedExecutable - .substring(0, Platform.resolvedExecutable.lastIndexOf("\\")); - - String get _exePath => - path.join(path.dirname(Platform.resolvedExecutable), 'mm2.exe'); - - Future _killMM2Process() async { - _mm2Process?.kill(); - await Process.run('taskkill', ['/F', '/IM', 'mm2.exe', '/T']); - } - - @override - Future call(dynamic reqStr) async { - return await _rpc.call(MM2.prepareRequest(reqStr)); - } -} diff --git a/lib/model/dex_list_type.dart b/lib/model/dex_list_type.dart index 6d10eef5bb..95f88fd85f 100644 --- a/lib/model/dex_list_type.dart +++ b/lib/model/dex_list_type.dart @@ -7,14 +7,14 @@ import 'package:web_dex/views/market_maker_bot/tab_type_enum.dart'; /// The order in this enum is important. /// When you rearrange the elements, the order of the tabs must change. /// Remember to change the initial tab -enum DexListType implements TabTypeEnum { +enum DexListType implements ITabTypeEnum { swap, inProgress, orders, history; @override - String name(DexTabBarBloc bloc) { + String name(DexTabBarState bloc) { switch (this) { case swap: return LocaleKeys.swap.tr(); diff --git a/lib/platform/platform_native.dart b/lib/platform/platform_native.dart index cb75f3ea45..85d80df98c 100644 --- a/lib/platform/platform_native.dart +++ b/lib/platform/platform_native.dart @@ -15,7 +15,3 @@ String wasmVersion() => ''; void changeTheme(int themeModeIndex) {} void changeHtmlTheme(int themeIndex) {} - -Future zipEncode(String fileName, String fileContent) async { - return null; -} diff --git a/lib/platform/platform_web.dart b/lib/platform/platform_web.dart index 332dd37f77..6cfdf22c79 100644 --- a/lib/platform/platform_web.dart +++ b/lib/platform/platform_web.dart @@ -26,6 +26,3 @@ external void reloadPage(); @JS('changeTheme') external void changeHtmlTheme(int themeIndex); - -@JS('zip_encode') -external Future zipEncode(String fileName, String fileContent); diff --git a/lib/router/navigators/page_content/page_content_router_delegate.dart b/lib/router/navigators/page_content/page_content_router_delegate.dart index 50e3151e32..6de6a7306b 100644 --- a/lib/router/navigators/page_content/page_content_router_delegate.dart +++ b/lib/router/navigators/page_content/page_content_router_delegate.dart @@ -27,7 +27,7 @@ class PageContentRouterDelegate extends RouterDelegate case MainMenuValue.fiat: return const FiatPage(); case MainMenuValue.dex: - return DexPage(); + return const DexPage(); case MainMenuValue.bridge: return const BridgePage(); case MainMenuValue.marketMakerBot: diff --git a/lib/router/navigators/page_menu/page_menu_router_delegate.dart b/lib/router/navigators/page_menu/page_menu_router_delegate.dart index dfadebf338..1fa084b33e 100644 --- a/lib/router/navigators/page_menu/page_menu_router_delegate.dart +++ b/lib/router/navigators/page_menu/page_menu_router_delegate.dart @@ -29,7 +29,7 @@ class PageMenuRouterDelegate extends RouterDelegate case MainMenuValue.fiat: return isMobile ? const FiatPage() : empty; case MainMenuValue.dex: - return isMobile ? DexPage() : empty; + return isMobile ? const DexPage() : empty; case MainMenuValue.bridge: return isMobile ? const BridgePage() : empty; case MainMenuValue.marketMakerBot: diff --git a/lib/services/file_loader/file_loader.dart b/lib/services/file_loader/file_loader.dart index 02af467e21..6e3fb0fa23 100644 --- a/lib/services/file_loader/file_loader.dart +++ b/lib/services/file_loader/file_loader.dart @@ -1,7 +1,13 @@ import 'package:file_picker/file_picker.dart'; +import 'package:web_dex/services/file_loader/file_loader_stub.dart' + if (dart.library.io) 'package:web_dex/services/file_loader/file_loader_native.dart' + if (dart.library.html) 'package:web_dex/services/file_loader/file_loader_web.dart'; abstract class FileLoader { const FileLoader(); + + factory FileLoader.fromPlatform() => createFileLoader(); + Future save({ required String fileName, required String data, diff --git a/lib/services/file_loader/get_file_loader.dart b/lib/services/file_loader/file_loader_native.dart similarity index 57% rename from lib/services/file_loader/get_file_loader.dart rename to lib/services/file_loader/file_loader_native.dart index 1adae1161f..694299d6cb 100644 --- a/lib/services/file_loader/get_file_loader.dart +++ b/lib/services/file_loader/file_loader_native.dart @@ -1,18 +1,11 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/services/file_loader/file_loader_native_desktop.dart'; -import 'package:web_dex/services/file_loader/file_loader_web.dart'; +import 'package:web_dex/services/file_loader/mobile/file_loader_native_android.dart'; +import 'package:web_dex/services/file_loader/mobile/file_loader_native_ios.dart'; -import 'mobile/file_loader_native_android.dart'; -import 'mobile/file_loader_native_ios.dart'; - -final FileLoader fileLoader = _getFileLoader(); -FileLoader _getFileLoader() { - if (kIsWeb) { - return const FileLoaderWeb(); - } +FileLoader createFileLoader() { if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { return const FileLoaderNativeDesktop(); } diff --git a/lib/services/file_loader/file_loader_stub.dart b/lib/services/file_loader/file_loader_stub.dart new file mode 100644 index 0000000000..2cd5fcec12 --- /dev/null +++ b/lib/services/file_loader/file_loader_stub.dart @@ -0,0 +1,5 @@ +import 'package:web_dex/services/file_loader/file_loader.dart'; + +FileLoader createFileLoader() => throw UnsupportedError( + 'Cannot create file loader without platform implementation', + ); diff --git a/lib/services/file_loader/file_loader_web.dart b/lib/services/file_loader/file_loader_web.dart index 1316d92182..65ed4a874c 100644 --- a/lib/services/file_loader/file_loader_web.dart +++ b/lib/services/file_loader/file_loader_web.dart @@ -1,12 +1,14 @@ -import 'dart:convert'; +import 'dart:js_interop'; -import 'package:universal_html/html.dart'; -import 'package:universal_html/js_util.dart'; -import 'package:web_dex/platform/platform.dart'; +import 'package:web/web.dart' as web; import 'package:web_dex/services/file_loader/file_loader.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +FileLoader createFileLoader() => const FileLoaderWeb(); class FileLoaderWeb implements FileLoader { const FileLoaderWeb(); + @override Future save({ required String fileName, @@ -25,62 +27,107 @@ class FileLoaderWeb implements FileLoader { required String filename, required String data, }) async { - final AnchorElement anchor = AnchorElement(); - anchor.href = - '${Uri.dataFromString(data, mimeType: 'text/plain', encoding: utf8)}'; - anchor.download = filename; - anchor.style.display = 'none'; - anchor.click(); + final dataArray = web.TextEncoder().encode(data); + final blob = + web.Blob([dataArray].toJS, web.BlobPropertyBag(type: 'text/plain')); + + final url = web.URL.createObjectURL(blob); + + try { + // Create an anchor element and set the attributes + final anchor = web.HTMLAnchorElement() + ..href = url + ..download = filename + ..style.display = 'none'; + + // Append to the DOM and trigger click + web.document.body?.append(anchor); + anchor + ..click() + ..remove(); + } finally { + // Revoke the object URL + web.URL.revokeObjectURL(url); + } } Future _saveAsCompressedFile({ required String fileName, required String data, }) async { - final String? compressedData = - await promiseToFuture(zipEncode('$fileName.txt', data)); + try { + // add the extension of the contained file to the filename, so that the + // extracted file is simply the filename excluding '.zip' + final fileNameWithExt = '$fileName.txt'; + + final encoder = web.TextEncoder(); + final dataArray = encoder.encode(data); + final blob = + web.Blob([dataArray].toJS, web.BlobPropertyBag(type: 'text/plain')); + + final response = web.Response(blob); + final compressedResponse = web.Response( + response.body!.pipeThrough( + web.CompressionStream('gzip') as web.ReadableWritablePair, + ), + ); - if (compressedData == null) return; + final compressedBlob = await compressedResponse.blob().toDart; + final url = web.URL.createObjectURL(compressedBlob); - final anchor = AnchorElement(); - anchor.href = 'data:application/zip;base64,$compressedData'; - anchor.download = '$fileName.zip'; - anchor.click(); + final anchor = web.HTMLAnchorElement() + ..href = url + ..download = '$fileNameWithExt.zip' + ..style.display = 'none'; + + web.document.body?.append(anchor); + anchor + ..click() + ..remove(); + + web.URL.revokeObjectURL(url); + } catch (e) { + log('Error compressing and saving file: $e').ignore(); + } } @override Future upload({ - required Function(String name, String? content) onUpload, - required Function(String) onError, + required void Function(String name, String? content) onUpload, + required void Function(String) onError, LoadFileType? fileType, }) async { - final FileUploadInputElement uploadInput = FileUploadInputElement(); + final uploadInput = web.HTMLInputElement()..type = 'file'; + if (fileType != null) { - uploadInput.setAttribute('accept', _getMimeType(fileType)); + uploadInput.accept = _getMimeType(fileType); } + uploadInput.click(); - uploadInput.onChange.listen((Event event) { - final List? files = uploadInput.files; + uploadInput.onChange.listen((event) { + final web.FileList? files = uploadInput.files; if (files == null) { return; } - if (files.length == 1) { - final file = files[0]; - final FileReader reader = FileReader(); + if (files.length == 1) { + final web.File? file = files.item(0); + final reader = web.FileReader(); - reader.onLoadEnd.listen((_) { + reader.onLoadEnd.listen((event) { final result = reader.result; - if (result is String) { - onUpload(file.name, result); + if (result case final String content) { + onUpload(file!.name, content); } }); - reader.onError.listen( - (ProgressEvent _) {}, - onError: (Object error) => onError(error.toString()), - ); - reader.readAsText(file); + reader + ..onerror = (JSAny event) { + if (event is web.ErrorEvent) { + onError(event.message); + } + }.toJS + ..readAsText(file! as web.Blob); } }); } diff --git a/lib/services/logger/universal_logger.dart b/lib/services/logger/universal_logger.dart index 541866eeb9..e75464c2cd 100644 --- a/lib/services/logger/universal_logger.dart +++ b/lib/services/logger/universal_logger.dart @@ -4,7 +4,6 @@ import 'package:dragon_logs/dragon_logs.dart'; import 'package:intl/intl.dart'; import 'package:web_dex/app_config/package_information.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; -import 'package:web_dex/services/file_loader/get_file_loader.dart'; import 'package:web_dex/services/logger/log_message.dart'; import 'package:web_dex/services/logger/logger.dart'; import 'package:web_dex/services/logger/logger_metadata_mixin.dart'; @@ -87,7 +86,7 @@ class UniversalLogger with LoggerMetadataMixin implements LoggerInterface { DateFormat('dd.MM.yyyy_HH-mm-ss').format(DateTime.now()); final String filename = 'komodo_wallet_log_$date'; - await fileLoader.save( + await FileLoader.fromPlatform().save( fileName: filename, data: await DragonLogs.exportLogsString(), type: LoadFileType.compressed, diff --git a/lib/shared/utils/debug_utils.dart b/lib/shared/utils/debug_utils.dart index 6625728a6b..3d38c45a4b 100644 --- a/lib/shared/utils/debug_utils.dart +++ b/lib/shared/utils/debug_utils.dart @@ -29,8 +29,11 @@ Future initDebugData(AuthBloc authBloc) async { return; } if (newWalletJson['automateLogin'] == true) { - authBloc.add( - AuthReLogInEvent(seed: newWalletJson['seed'], wallet: debugWallet)); + authBloc.add(AuthReLogInEvent( + seed: newWalletJson['seed'], + wallet: debugWallet, + password: newWalletJson['password'], + )); } } catch (e) { return; diff --git a/lib/shared/utils/formatters.dart b/lib/shared/utils/formatters.dart index 26ce9e4212..aaa2e618b4 100644 --- a/lib/shared/utils/formatters.dart +++ b/lib/shared/utils/formatters.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; +import 'package:decimal/decimal.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,14 +9,10 @@ import 'package:web_dex/shared/constants.dart'; final List currencyInputFormatters = [ DecimalTextInputFormatter(decimalRange: decimalRange), - FilteringTextInputFormatter.allow(numberRegExp) + FilteringTextInputFormatter.allow(numberRegExp), ]; class DurationLocalization { - final String milliseconds; - final String seconds; - final String minutes; - final String hours; DurationLocalization({ required this.milliseconds, @@ -23,11 +20,15 @@ class DurationLocalization { required this.minutes, required this.hours, }); + final String milliseconds; + final String seconds; + final String minutes; + final String hours; } /// unit test: [testDurationFormat] String durationFormat( - Duration duration, DurationLocalization durationLocalization) { + Duration duration, DurationLocalization durationLocalization,) { final int hh = duration.inHours; final int mm = duration.inMinutes.remainder(60); final int ss = duration.inSeconds.remainder(60); @@ -35,7 +36,7 @@ String durationFormat( if (ms < 1000) return '$ms${durationLocalization.milliseconds}'; - StringBuffer output = StringBuffer(); + final StringBuffer output = StringBuffer(); if (hh > 0) { output.write('$hh${durationLocalization.hours} '); } @@ -50,7 +51,9 @@ String durationFormat( /// unit test: [testNumberWithoutExponent] String getNumberWithoutExponent(String value) { try { - return Rational.parse(value).toDecimalString(); + return Rational.parse(value) + .toDecimal(scaleOnInfinitePrecision: 10) + .toString(); } catch (_) { return value; } @@ -125,7 +128,6 @@ class DecimalTextInputFormatter extends TextInputFormatter { return TextEditingValue( text: truncated, selection: newSelection, - composing: TextRange.empty, ); } } @@ -171,8 +173,10 @@ String formatDexAmt(dynamic amount) { switch (amount.runtimeType) { case double: + return cutTrailingZeros((amount as double).toStringAsFixed(8)); case Rational: - return cutTrailingZeros(amount.toStringAsFixed(8) ?? ''); + return cutTrailingZeros( + (amount as Rational).toDecimal(scaleOnInfinitePrecision: 12).toStringAsFixed(8),); case String: return cutTrailingZeros(double.parse(amount).toStringAsFixed(8)); case int: @@ -241,8 +245,8 @@ const one = 1; final hugeFormatter = NumberFormat.compactLong(); final billionFormatter = NumberFormat.decimalPattern(); -final thousandFormatter = NumberFormat("###,###,###,###", "en_US"); -final oneFormatter = NumberFormat("###,###,###,###.00", "en_US"); +final thousandFormatter = NumberFormat('###,###,###,###', 'en_US'); +final oneFormatter = NumberFormat('###,###,###,###.00', 'en_US'); /// unit tests: [testToStringAmount] /// Main idea is to keep length of value less then 13 symbols @@ -266,11 +270,11 @@ String toStringAmount(double amount, [int? digits]) { case >= one: return oneFormatter.format(amount); case >= lowAmount: - String pattern = "0.00######"; + String pattern = '0.00######'; if (digits != null) { pattern = "0.00${List.filled(digits - 2, "#").join()}"; } - return NumberFormat(pattern, "en_US").format(amount); + return NumberFormat(pattern, 'en_US').format(amount); } return amount.toStringAsPrecision(4); } @@ -296,12 +300,14 @@ void formatAmountInput(TextEditingController controller, Rational? value) { final String currentText = controller.text; if (currentText.isNotEmpty && Rational.parse(currentText) == value) return; - final newText = - value == null ? '' : cutTrailingZeros(value.toStringAsFixed(8)); + final newText = value == null + ? '' + : cutTrailingZeros( + value.toDecimal(scaleOnInfinitePrecision: 12).toStringAsFixed(8), + ); controller.value = TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: newText.length), - composing: TextRange.empty, ); } @@ -327,8 +333,8 @@ void formatAmountInput(TextEditingController controller, Rational? value) { /// ``` /// unit tests: [testTruncateHash] String truncateMiddleSymbols(String text, - [int? startSymbolsCount, int endCount = 7]) { - int startCount = startSymbolsCount ?? (text.startsWith('0x') ? 6 : 4); + [int? startSymbolsCount, int endCount = 7,]) { + final int startCount = startSymbolsCount ?? (text.startsWith('0x') ? 6 : 4); if (text.length <= startCount + endCount + 3) return text; final String firstPart = text.substring(0, startCount); final String secondPart = text.substring(text.length - endCount, text.length); diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 67de08231b..8b1005f1ac 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:app_theme/app_theme.dart'; import 'package:bip39/bip39.dart' as bip39; +import 'package:decimal/decimal.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -10,7 +11,6 @@ import 'package:rational/rational.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/wallet.dart'; @@ -60,8 +60,12 @@ void copyToClipBoard(BuildContext context, String str) { /// unit tests: [testCustomDoubleToString] String doubleToString(double dv, [int fractions = 8]) { final Rational r = Rational.parse(dv.toString()); - if (r.isInteger) return r.toStringAsFixed(0); - String sv = r.toStringAsFixed(fractions > 20 ? 20 : fractions); + if (r.isInteger) { + return r.toDecimal(scaleOnInfinitePrecision: 24).toStringAsFixed(0); + } + String sv = r + .toDecimal(scaleOnInfinitePrecision: 24) + .toStringAsFixed(fractions > 20 ? 20 : fractions); final dot = sv.indexOf('.'); // Looks like we already have [cutTrailingZeros] sv = sv.replaceFirst(RegExp(r'0+$'), '', dot); @@ -640,16 +644,6 @@ Future pauseWhile( } } -Future waitMM2StatusChange(MM2Status status, MM2 mm2, - {int waitingTime = 3000}) async { - final int start = DateTime.now().millisecondsSinceEpoch; - - while (await mm2.status() != status && - DateTime.now().millisecondsSinceEpoch - start < waitingTime) { - await Future.delayed(const Duration(milliseconds: 100)); - } -} - enum HashExplorerType { address, tx, diff --git a/lib/views/dex/dex_page.dart b/lib/views/dex/dex_page.dart index 5695c3d4d3..912e0f8fd1 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -3,9 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; +import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; +import 'package:web_dex/bloc/settings/settings_repository.dart'; +import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/router/state/routing_state.dart'; +import 'package:web_dex/services/orders_service/my_orders_service.dart'; import 'package:web_dex/shared/ui/clock_warning_banner.dart'; import 'package:web_dex/shared/widgets/hidden_without_wallet.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; @@ -13,7 +17,28 @@ import 'package:web_dex/views/dex/dex_tab_bar.dart'; import 'package:web_dex/views/dex/entities_list/dex_list_wrapper.dart'; import 'package:web_dex/views/dex/entity_details/trading_details.dart'; -class DexPage extends StatelessWidget { +class DexPage extends StatefulWidget { + const DexPage({super.key}); + + @override + State createState() => _DexPageState(); +} + +class _DexPageState extends State { + bool isTradingDetails = false; + + @override + void initState() { + routingState.dexState.addListener(_onRouteChange); + super.initState(); + } + + @override + void dispose() { + routingState.dexState.removeListener(_onRouteChange); + super.dispose(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -21,16 +46,27 @@ class DexPage extends StatelessWidget { BlocProvider( key: const Key('dex-page'), create: (BuildContext context) => DexTabBarBloc( - DexTabBarState.initial(), authRepo, - ), + tradingEntitiesBloc, + MarketMakerBotOrderListRepository( + myOrdersService, + SettingsRepository(), + coinsBloc, + ), + )..add(const StartListening()), ), ], - child: routingState.dexState.isTradingDetails + child: isTradingDetails ? TradingDetails(uuid: routingState.dexState.uuid) : _DexContent(), ); } + + void _onRouteChange() { + setState( + () => isTradingDetails = routingState.dexState.isTradingDetails, + ); + } } class _DexContent extends StatefulWidget { @@ -54,7 +90,6 @@ class _DexContentState extends State<_DexContent> { ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const HiddenWithoutWallet( child: Padding( @@ -89,6 +124,6 @@ class _DexContentState extends State<_DexContent> { } bool shouldShowTabContent(int tabIndex) { - return (DexListType.values.length > tabIndex); + return DexListType.values.length > tabIndex; } } diff --git a/lib/views/dex/dex_tab_bar.dart b/lib/views/dex/dex_tab_bar.dart index 1b1f554b6f..bab393df49 100644 --- a/lib/views/dex/dex_tab_bar.dart +++ b/lib/views/dex/dex_tab_bar.dart @@ -2,48 +2,35 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/dex_list_type.dart'; -import 'package:web_dex/model/my_orders/my_order.dart'; -import 'package:web_dex/model/swap.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab_bar.dart'; class DexTabBar extends StatelessWidget { - const DexTabBar({Key? key}) : super(key: key); + const DexTabBar({super.key}); @override Widget build(BuildContext context) { + const values = DexListType.values; return BlocBuilder( builder: (context, state) { final DexTabBarBloc bloc = context.read(); - return StreamBuilder>( - stream: tradingEntitiesBloc.outMyOrders, - builder: (context, _) => StreamBuilder>( - stream: tradingEntitiesBloc.outSwaps, - builder: (context, _) => ConstrainedBox( - constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), - child: UiTabBar( - currentTabIndex: bloc.tabIndex, - tabs: _buildTabs(bloc), - ), - ), + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), + child: UiTabBar( + currentTabIndex: state.tabIndex, + tabs: List.generate(values.length, (index) { + final tab = values[index]; + return UiTab( + key: Key(tab.key), + text: tab.name(state), + isSelected: state.tabIndex == index, + onClick: () => bloc.add(TabChanged(index)), + ); + }), ), ); }, ); } - - List _buildTabs(DexTabBarBloc bloc) { - const values = DexListType.values; - return List.generate(values.length, (index) { - final tab = values[index]; - return UiTab( - key: Key(tab.key), - text: tab.name(bloc), - isSelected: bloc.state.tabIndex == index, - onClick: () => bloc.add(TabChanged(index)), - ); - }); - } } diff --git a/lib/views/dex/simple/form/exchange_info/exchange_rate.dart b/lib/views/dex/simple/form/exchange_info/exchange_rate.dart index b80ad2e047..e01dd332b9 100644 --- a/lib/views/dex/simple/form/exchange_info/exchange_rate.dart +++ b/lib/views/dex/simple/form/exchange_info/exchange_rate.dart @@ -10,12 +10,12 @@ import 'package:web_dex/views/dex/dex_helpers.dart'; class ExchangeRate extends StatelessWidget { const ExchangeRate({ - Key? key, required this.base, required this.rel, required this.rate, + super.key, this.showDetails = true, - }) : super(key: key); + }); final String? base; final String? rel; @@ -28,22 +28,22 @@ class ExchangeRate extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, children: [ Text( '${LocaleKeys.rate.tr()}:', style: theme.custom.tradingFormDetailsLabel, ), - isEmptyData - ? Text('0.00', style: theme.custom.tradingFormDetailsContent) - : Flexible( - child: _Rates( - base: base, - rel: rel, - rate: rate, - showDetails: showDetails, - ), - ), + if (isEmptyData) + Text('0.00', style: theme.custom.tradingFormDetailsContent) + else + Flexible( + child: _Rates( + base: base, + rel: rel, + rate: rate, + showDetails: showDetails, + ), + ), ], ); } @@ -69,7 +69,6 @@ class _Rates extends StatelessWidget { children: [ Row( mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.max, children: [ Text( ' 1 ${Coin.normalizeAbbr(base ?? '')} = ', diff --git a/lib/views/market_maker_bot/market_maker_bot_order_list.dart b/lib/views/market_maker_bot/market_maker_bot_order_list.dart index 4615984304..5198d63377 100644 --- a/lib/views/market_maker_bot/market_maker_bot_order_list.dart +++ b/lib/views/market_maker_bot/market_maker_bot_order_list.dart @@ -16,17 +16,17 @@ import 'package:web_dex/views/market_maker_bot/trade_pair_list_item.dart'; class MarketMakerBotOrdersList extends StatefulWidget { const MarketMakerBotOrdersList({ - super.key, required this.entitiesFilterData, + super.key, this.onEdit, this.onCancel, this.onCancelAll, }); final TradingEntitiesFilter? entitiesFilterData; - final Function(TradePair)? onEdit; - final Function(TradePair)? onCancel; - final Function(List)? onCancelAll; + final void Function(TradePair)? onEdit; + final void Function(TradePair)? onCancel; + final void Function(List)? onCancelAll; @override State createState() => @@ -70,7 +70,6 @@ class _MarketMakerBotOrdersListState extends State { return BlocBuilder( builder: (context, botState) => Column( - mainAxisSize: MainAxisSize.max, children: [ if (!isMobile) Column( @@ -152,7 +151,6 @@ class _MarketMakerBotOrdersListState extends State { backgroundColor: Colors.transparent, border: Border.all( color: const Color.fromRGBO(234, 234, 234, 1), - width: 1.0, ), textStyle: const TextStyle(fontSize: 12), onPressed: botState.isUpdating @@ -166,7 +164,6 @@ class _MarketMakerBotOrdersListState extends State { backgroundColor: Colors.transparent, border: Border.all( color: const Color.fromRGBO(234, 234, 234, 1), - width: 1.0, ), textStyle: const TextStyle(fontSize: 12), onPressed: botState.isUpdating diff --git a/lib/views/market_maker_bot/market_maker_bot_page.dart b/lib/views/market_maker_bot/market_maker_bot_page.dart index aca38331b4..812ccbd9ee 100644 --- a/lib/views/market_maker_bot/market_maker_bot_page.dart +++ b/lib/views/market_maker_bot/market_maker_bot_page.dart @@ -42,13 +42,20 @@ class _MarketMakerBotPageState extends State { @override Widget build(BuildContext context) { + final orderListRepository = MarketMakerBotOrderListRepository( + myOrdersService, + SettingsRepository(), + coinsBloc, + ); + return MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => DexTabBarBloc( - DexTabBarState.initial(), authRepo, - ), + tradingEntitiesBloc, + orderListRepository, + )..add(const StartListening()), ), BlocProvider( create: (BuildContext context) => MarketMakerTradeFormBloc( diff --git a/lib/views/market_maker_bot/market_maker_bot_tab_bar.dart b/lib/views/market_maker_bot/market_maker_bot_tab_bar.dart index df9409fb01..318c800c3a 100644 --- a/lib/views/market_maker_bot/market_maker_bot_tab_bar.dart +++ b/lib/views/market_maker_bot/market_maker_bot_tab_bar.dart @@ -2,48 +2,39 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/model/my_orders/my_order.dart'; -import 'package:web_dex/model/swap.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab_bar.dart'; import 'package:web_dex/views/market_maker_bot/market_maker_bot_tab_type.dart'; class MarketMakerBotTabBar extends StatelessWidget { - const MarketMakerBotTabBar({Key? key}) : super(key: key); + const MarketMakerBotTabBar({super.key}); @override Widget build(BuildContext context) { + const tabBarEntries = MarketMakerBotTabType.values; + return BlocBuilder( builder: (context, state) { - final DexTabBarBloc bloc = context.read(); - return StreamBuilder>( - stream: tradingEntitiesBloc.outMyOrders, - builder: (context, _) => StreamBuilder>( - stream: tradingEntitiesBloc.outSwaps, - builder: (context, _) => ConstrainedBox( - constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), - child: UiTabBar( - currentTabIndex: bloc.tabIndex, - tabs: _buidTabs(bloc), - ), + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), + child: UiTabBar( + currentTabIndex: state.tabIndex, + tabs: List.generate( + tabBarEntries.length, + (index) { + final tab = tabBarEntries[index]; + return UiTab( + key: Key(tab.key), + text: tab.name(state), + isSelected: state.tabIndex == index, + onClick: () => + context.read().add(TabChanged(index)), + ); + }, ), ), ); }, ); } - - List _buidTabs(DexTabBarBloc bloc) { - const values = MarketMakerBotTabType.values; - return List.generate(values.length, (index) { - final tab = values[index]; - return UiTab( - key: Key(tab.key), - text: tab.name(bloc), - isSelected: bloc.state.tabIndex == index, - onClick: () => bloc.add(TabChanged(index)), - ); - }); - } } diff --git a/lib/views/market_maker_bot/market_maker_bot_tab_type.dart b/lib/views/market_maker_bot/market_maker_bot_tab_type.dart index c959b3b657..2404120b5f 100644 --- a/lib/views/market_maker_bot/market_maker_bot_tab_type.dart +++ b/lib/views/market_maker_bot/market_maker_bot_tab_type.dart @@ -4,19 +4,19 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/views/market_maker_bot/tab_type_enum.dart'; -enum MarketMakerBotTabType implements TabTypeEnum { +enum MarketMakerBotTabType implements ITabTypeEnum { marketMaker, inProgress, orders, history; @override - String name(DexTabBarBloc bloc) { + String name(DexTabBarState bloc) { switch (this) { case marketMaker: return LocaleKeys.makeMarket.tr(); case orders: - return '${LocaleKeys.orders.tr()} (${bloc.ordersCount})'; + return '${LocaleKeys.orders.tr()} (${bloc.tradeBotOrdersCount})'; case inProgress: return '${LocaleKeys.inProgress.tr()} (${bloc.inProgressCount})'; case history: diff --git a/lib/views/market_maker_bot/tab_type_enum.dart b/lib/views/market_maker_bot/tab_type_enum.dart index b29cf668d9..00eee3243e 100644 --- a/lib/views/market_maker_bot/tab_type_enum.dart +++ b/lib/views/market_maker_bot/tab_type_enum.dart @@ -1,6 +1,6 @@ import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; -abstract class TabTypeEnum { +abstract class ITabTypeEnum { String get key; - String name(DexTabBarBloc bloc); + String name(DexTabBarState bloc); } diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart index 18e4451a81..c3b0696464 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -90,6 +90,7 @@ class _WalletMainState extends State header: isMobile ? PageHeader(title: LocaleKeys.wallet.tr()) : null, content: Expanded( child: CustomScrollView( + key: const Key('wallet-page-scroll-view'), slivers: [ SliverToBoxAdapter( child: Column( diff --git a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart index bf308d11cc..b08a04b37d 100644 --- a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart +++ b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart @@ -213,6 +213,7 @@ class _IguanaWalletsManagerState extends State { newWallet, walletsManagerEventsFactory.createEvent( widget.eventType, WalletsManagerEventMethod.create), + password, ); } @@ -240,10 +241,12 @@ class _IguanaWalletsManagerState extends State { } await _reLogin( - walletConfig.seedPhrase, - newWallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.import)); + walletConfig.seedPhrase, + newWallet, + walletsManagerEventsFactory.createEvent( + widget.eventType, WalletsManagerEventMethod.import), + password, + ); } Future _logInToWallet(String password, Wallet wallet) async { @@ -266,6 +269,7 @@ class _IguanaWalletsManagerState extends State { wallet, walletsManagerEventsFactory.createEvent( widget.eventType, WalletsManagerEventMethod.loginExisting), + password, ); } @@ -284,12 +288,20 @@ class _IguanaWalletsManagerState extends State { } Future _reLogin( - String seed, Wallet wallet, AnalyticsEventData analyticsEventData) async { + String seed, + Wallet wallet, + AnalyticsEventData analyticsEventData, + String password, + ) async { final AnalyticsBloc analyticsBloc = context.read(); final AuthBloc authBloc = context.read(); if (await authBloc.isLoginAllowed(wallet)) { analyticsBloc.add(AnalyticsSendDataEvent(analyticsEventData)); - authBloc.add(AuthReLogInEvent(seed: seed, wallet: wallet)); + authBloc.add(AuthReLogInEvent( + seed: seed, + wallet: wallet, + password: password, + )); } if (mounted) { setState(() { diff --git a/lib/views/wallets_manager/widgets/wallet_simple_import.dart b/lib/views/wallets_manager/widgets/wallet_simple_import.dart index 569d8bfab1..c3d39047f6 100644 --- a/lib/views/wallets_manager/widgets/wallet_simple_import.dart +++ b/lib/views/wallets_manager/widgets/wallet_simple_import.dart @@ -2,13 +2,12 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; -import 'package:web_dex/services/file_loader/get_file_loader.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/disclaimer/eula_tos_checkboxes.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; @@ -17,11 +16,11 @@ import 'package:web_dex/views/wallets_manager/widgets/custom_seed_dialog.dart'; class WalletSimpleImport extends StatefulWidget { const WalletSimpleImport({ - Key? key, required this.onImport, required this.onUploadFiles, required this.onCancel, - }) : super(key: key); + super.key, + }); final void Function({ required String name, @@ -160,7 +159,7 @@ class _WalletImportWrapperState extends State { return UploadButton( buttonText: LocaleKeys.walletCreationUploadFile.tr(), uploadFile: () async { - await fileLoader.upload( + await FileLoader.fromPlatform().upload( onUpload: (fileName, fileData) => widget.onUploadFiles( fileData: fileData ?? '', fileName: fileName, @@ -214,12 +213,10 @@ class _WalletImportWrapperState extends State { autofocus: true, autocorrect: false, textInputAction: TextInputAction.next, - enableInteractiveSelection: true, validator: (String? name) => _inProgress ? null : walletsBloc.validateWalletName(name ?? ''), inputFormatters: [LengthLimitingTextInputFormatter(40)], hintText: LocaleKeys.walletCreationNameHint.tr(), - validationMode: InputValidationMode.eager, ); } @@ -232,7 +229,6 @@ class _WalletImportWrapperState extends State { textInputAction: TextInputAction.done, autocorrect: false, obscureText: _isSeedHidden, - enableInteractiveSelection: true, maxLines: _isSeedHidden ? 1 : null, errorMaxLines: 4, style: Theme.of(context).textTheme.bodyMedium, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 6adbd2c46b..906a05dda2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f32b910ed9..5731285cba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,11 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux url_launcher_linux window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + komodo_defi_framework ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0256bc5cab..e2b064fd0d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,8 @@ import Foundation import firebase_analytics import firebase_core import flutter_inappwebview_macos +import flutter_secure_storage_macos +import local_auth_darwin import mobile_scanner import package_info_plus import path_provider_foundation @@ -21,6 +23,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index de0fa55b64..9e65ebe852 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -45,6 +45,8 @@ PODS: - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - OrderedSet (~> 6.0.3) + - flutter_secure_storage_macos (6.1.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - GoogleAppMeasurement (10.25.0): - GoogleAppMeasurement/AdIdSupport (= 10.25.0) @@ -94,6 +96,11 @@ PODS: - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - komodo_defi_framework (0.0.1): + - FlutterMacOS + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS - mobile_scanner (5.2.3): - FlutterMacOS - nanopb (2.30910.0): @@ -125,7 +132,10 @@ DEPENDENCIES: - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - komodo_defi_framework (from `Flutter/ephemeral/.symlinks/plugins/komodo_defi_framework/macos`) + - local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -155,8 +165,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos flutter_inappwebview_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral + komodo_defi_framework: + :path: Flutter/ephemeral/.symlinks/plugins/komodo_defi_framework/macos + local_auth_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin mobile_scanner: :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos package_info_plus: @@ -183,9 +199,12 @@ SPEC CHECKSUMS: FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + komodo_defi_framework: f716eeef2f8d5cd3a97efe7bb103e8e18285032c + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 mobile_scanner: 0a05256215b047af27b9495db3b77640055e8824 nanopb: 438bc412db1928dac798aa6fd75726007be04262 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index c001eee039..34ca498fed 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -362,6 +362,9 @@ "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_macos/flutter_inappwebview_macos.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework", + "${BUILT_PRODUCTS_DIR}/komodo_defi_framework/komodo_defi_framework.framework", + "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/mobile_scanner/mobile_scanner.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", @@ -381,6 +384,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/komodo_defi_framework.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mobile_scanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ab845d760b..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,1905 +0,0 @@ -{ - "name": "web_dex", - "version": "0.2.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web_dex", - "version": "0.2.0", - "license": "ISC", - "dependencies": { - "jszip": "^3.10.1" - }, - "devDependencies": { - "clean-webpack-plugin": "^4.0.0", - "html-webpack-plugin": "^5.5.0", - "webpack": "^5.88.2", - "webpack-cli": "^4.10.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@types/eslint": { - "version": "8.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "17.0.23", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, - "dependencies": { - "envinfo": "^7.7.3" - }, - "peerDependencies": { - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, - "peerDependencies": { - "webpack-cli": "4.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.20.2", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/camel-case": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001325", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clean-css": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/colorette": { - "version": "2.0.16", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/del": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.105", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "dev": true, - "license": "MIT" - }, - "node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.0", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globby": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "8.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "webpack": "^5.20.0" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/import-local": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/interpret": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-core-module": { - "version": "2.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loader-runner": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "dev": true, - "license": "MIT" - }, - "node_modules/lower-case": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mime-db": { - "version": "1.52.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-releases": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.0.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/pify": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/rechoir": { - "version": "0.7.1", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/resolve": { - "version": "1.22.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.17.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", - "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", - "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.3.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utila": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "cross-spawn": "^7.0.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 020e5aa553..0000000000 --- a/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "web_dex", - "version": "0.2.0", - "description": "Developer guide.", - "main": "web/index.js", - "directories": { - "lib": "lib", - "test": "test" - }, - "scripts": { - "test": "echo \"Error: no test specified\" ; exit 1", - "build": "git update-index --assume-unchanged web/index.html ; npx webpack --config=webpack.config.js --mode=production", - "build:dev": "git update-index --assume-unchanged web/index.html ; npx webpack --config=webpack.config.js --mode=development" - }, - "repository": { - "type": "git", - "url": "''" - }, - "author": "", - "license": "ISC", - "devDependencies": { - "clean-webpack-plugin": "^4.0.0", - "html-webpack-plugin": "^5.5.0", - "webpack": "^5.88.2", - "webpack-cli": "^4.10.0" - }, - "dependencies": { - "jszip": "^3.10.1" - } -} \ No newline at end of file diff --git a/packages/komodo_cex_market_data/pubspec.lock b/packages/komodo_cex_market_data/pubspec.lock new file mode 100644 index 0000000000..631f00c1eb --- /dev/null +++ b/packages/komodo_cex_market_data/pubspec.lock @@ -0,0 +1,435 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hive: + dependency: "direct main" + description: + path: hive + ref: "470473ffc1ba39f6c90f31ababe0ee63b76b69fe" + resolved-ref: "470473ffc1ba39f6c90f31ababe0ee63b76b69fe" + url: "https://github.com/KomodoPlatform/hive.git" + source: git + version: "2.2.3" + http: + dependency: "direct main" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: d11b55850c68c1f6c0cf00eabded4e66c4043feaf6c0d7ce4a36785137df6331 + url: "https://pub.dev" + source: hosted + version: "1.25.5" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + url: "https://pub.dev" + source: hosted + version: "0.7.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + url: "https://pub.dev" + source: hosted + version: "0.6.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + url: "https://pub.dev" + source: hosted + version: "14.2.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" + url: "https://pub.dev" + source: hosted + version: "0.1.5" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.4.0-256.0.dev <4.0.0" diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index f61bfb7985..890a72ccd2 100644 --- a/packages/komodo_cex_market_data/pubspec.yaml +++ b/packages/komodo_cex_market_data/pubspec.yaml @@ -10,10 +10,7 @@ environment: dependencies: http: 1.2.2 # dart.dev - equatable: - git: - url: https://github.com/KomodoPlatform/equatable.git - ref: 2117551ff3054f8edb1a58f63ffe1832a8d25623 #2.0.5 + equatable: 2.0.5 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index 3d1cb6a681..c8e1319832 100644 --- a/packages/komodo_coin_updates/pubspec.yaml +++ b/packages/komodo_coin_updates/pubspec.yaml @@ -9,24 +9,14 @@ environment: # Add regular dependencies here. dependencies: http: 1.2.2 # dart.dev - path_provider: 2.1.1 # flutter.dev + path_provider: 2.1.4 # flutter.dev path: 1.9.0 # dart.dev - transitive to direct komodo_persistence_layer: path: ../komodo_persistence_layer/ - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - flutter_bloc: - git: - url: https://github.com/KomodoPlatform/bloc.git - path: packages/flutter_bloc/ - ref: 32d5002fb8b8a1e548fe8021d8468327680875ff # 8.1.1 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - equatable: - git: - url: https://github.com/KomodoPlatform/equatable.git - ref: 2117551ff3054f8edb1a58f63ffe1832a8d25623 #2.0.5 + flutter_bloc: 8.1.6 + equatable: 2.0.5 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 57bcb448e3..4d729332ad 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,7 +2,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -const coinImagesFolder = 'coin_icons/png/'; +const coinImagesFolder = 'packages/komodo_defi_framework/assets/coin_icons/png/'; +// NB: ENSURE IT STAYS IN SYNC WITH MAIN PROJECT in `lib/src/utils/utils.dart`. const mediaCdnUrl = 'https://komodoplatform.github.io/coins/icons/'; final Map _assetExistenceCache = {}; diff --git a/pubspec.lock b/pubspec.lock index e44519a0fa..0b1211a301 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -56,6 +56,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + aws_client: + dependency: transitive + description: + name: aws_client + sha256: "433b3a5ca80eff10744d2403102477662a040b0204c5b31a1d1a1398f91494f0" + url: "https://pub.dev" + source: hosted + version: "0.6.0" badges: dependency: "direct main" description: @@ -170,6 +178,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.3" + decimal: + dependency: "direct main" + description: + name: decimal + sha256: "4140a688f9e443e2f4de3a1162387bf25e1ac6d51e24c9da263f245210f41440" + url: "https://pub.dev" + source: hosted + version: "3.0.2" dragon_charts_flutter: dependency: "direct main" description: @@ -214,11 +230,10 @@ packages: equatable: dependency: "direct main" description: - path: "." - ref: "2117551ff3054f8edb1a58f63ffe1832a8d25623" - resolved-ref: "2117551ff3054f8edb1a58f63ffe1832a8d25623" - url: "https://github.com/KomodoPlatform/equatable.git" - source: git + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted version: "2.0.5" fake_async: dependency: transitive @@ -308,6 +323,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.18.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -316,12 +339,11 @@ packages: flutter_bloc: dependency: "direct main" description: - path: "packages/flutter_bloc" - ref: "32d5002fb8b8a1e548fe8021d8468327680875ff" - resolved-ref: "32d5002fb8b8a1e548fe8021d8468327680875ff" - url: "https://github.com/KomodoPlatform/bloc.git" - source: git - version: "8.1.1" + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_driver: dependency: transitive description: flutter @@ -420,6 +442,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.19" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_slidable: dependency: "direct main" description: @@ -577,6 +647,60 @@ packages: relative: true source: path version: "1.0.0" + komodo_coins: + dependency: transitive + description: + path: "packages/komodo_coins" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.0.1" + komodo_defi_framework: + dependency: transitive + description: + path: "packages/komodo_defi_framework" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.1.0" + komodo_defi_local_auth: + dependency: transitive + description: + path: "packages/komodo_defi_local_auth" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.1.0+1" + komodo_defi_rpc_methods: + dependency: transitive + description: + path: "packages/komodo_defi_rpc_methods" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.1.0+1" + komodo_defi_sdk: + dependency: "direct main" + description: + path: "packages/komodo_defi_sdk" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.1.0+1" + komodo_defi_types: + dependency: "direct main" + description: + path: "packages/komodo_defi_types" + ref: dev + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.0.1" komodo_persistence_layer: dependency: "direct main" description: @@ -592,11 +716,11 @@ packages: source: path version: "0.0.0" komodo_wallet_build_transformer: - dependency: "direct dev" + dependency: transitive description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: aed015fbec529f806ef40f59c884130d3b9b5806 + resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -632,6 +756,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + local_auth: + dependency: transitive + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" + url: "https://pub.dev" + source: hosted + version: "1.0.46" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" logging: dependency: transitive description: @@ -688,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.3" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" nested: dependency: transitive description: @@ -749,10 +921,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" path_provider_android: dependency: transitive description: @@ -877,12 +1049,11 @@ packages: rational: dependency: "direct main" description: - path: "." - ref: "84d8fe00e33560405c6d72b22a6e9c5c468db058" - resolved-ref: "84d8fe00e33560405c6d72b22a6e9c5c468db058" - url: "https://github.com/KomodoPlatform/dart-rational.git" - source: git - version: "1.2.1" + name: rational + sha256: "2b64ca15530cb6faae04bb309bdf834e5ef61b5db0b4d76779ff3b1618c0f9be" + url: "https://pub.dev" + source: hosted + version: "2.0.0" share_plus: dependency: "direct main" description: @@ -895,66 +1066,66 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -1016,6 +1187,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -1181,10 +1360,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "2469694ad079893e3b434a627970c33f2fa5adc46dfe03c9617546969a9a8afc" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1277,10 +1456,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -1347,5 +1526,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.5.3 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 57188d2daf..8a62cff237 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,10 +57,8 @@ dependencies: http: 1.2.2 # dart.dev intl: 0.19.0 # dart.dev js: 0.6.7 # dart.dev - shared_preferences: 2.1.1 # flutter.dev url_launcher: 6.3.0 # flutter.dev crypto: 3.0.3 # dart.dev - path_provider: 2.1.1 # flutter.dev cross_file: 0.3.4+2 # flutter.dev video_player: 2.7.0 # flutter.dev @@ -82,11 +80,8 @@ dependencies: ## ---- Fluttercommunity.dev - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - equatable: - git: - url: https://github.com/KomodoPlatform/equatable.git - ref: 2117551ff3054f8edb1a58f63ffe1832a8d25623 #2.0.5 + # does not require review, since hosted and git versions are the same + equatable: 2.0.5 # sdk depends on hosted version, and not from git # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 package_info_plus: @@ -130,37 +125,15 @@ dependencies: url: https://github.com/KomodoPlatform/qr.flutter.git ref: e3f8d3d4bbe8661f6c941acde8c9815a876756a3 #4.0.0 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - uuid: - 3.0.6 - # git: - # url: https://github.com/KomodoPlatform/dart-uuid.git - # ref: 832f38af9e4a676d1f47c302785e8a00d3fc72a9 #3.0.6 - - # Pending approval + # Pending approval easy_localization: 3.0.7 # last reviewed 3.0.2 via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - flutter_bloc: - git: - url: https://github.com/KomodoPlatform/bloc.git - path: packages/flutter_bloc/ - ref: 32d5002fb8b8a1e548fe8021d8468327680875ff # 8.1.1 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - rational: - git: - url: https://github.com/KomodoPlatform/dart-rational.git - ref: 84d8fe00e33560405c6d72b22a6e9c5c468db058 #1.2.1 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 universal_html: git: url: https://github.com/KomodoPlatform/universal_html.git ref: 6a1bc7d9e6ed735ab9f7b319f9eedb138ce8b0e5 #2.2.2 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: git: @@ -196,7 +169,7 @@ dependencies: # MRC: At least 3.3.0 is needed for AGP 8.0+ compatibility on Android mobile_scanner: ^5.1.0 - # Newly added, not yet reviewed + # Newly added, not yet reviewed formz: 0.7.0 # TODO: review required @@ -204,17 +177,30 @@ dependencies: bloc_concurrency: ^0.2.5 file_picker: ^8.1.2 # Updated from git ref @5.1.4 -dev_dependencies: - integration_test: # SDK - sdk: flutter - test: ^1.24.1 # dart.dev + # TODO: review required - SDK integration + path_provider: 2.1.4 # flutter.dev + shared_preferences: 2.3.2 # flutter.dev + decimal: 3.0.2 # transitive dependency that is required to fix breaking changes in rational package + rational: 2.0.0 # sdk depends on decimal ^3.0.2, which depends on rational ^2.0.0 + uuid: 4.4.2 # sdk depends on this version + flutter_bloc: 8.1.6 # sdk depends on this version, and hosted instead of git reference + komodo_defi_sdk: # TODO: change to pub.dev version? + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_defi_sdk + ref: dev - komodo_wallet_build_transformer: + komodo_defi_types: git: url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git - path: packages/komodo_wallet_build_transformer + path: packages/komodo_defi_types ref: dev +dev_dependencies: + integration_test: # SDK + sdk: flutter + test: ^1.24.1 # dart.dev + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your @@ -236,10 +222,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/ - - assets/config/ - - assets/coin_icons/ - assets/custom_icons/16px/ - - assets/coin_icons/png/ - assets/logo/ - assets/fonts/ - assets/flags/ @@ -255,40 +238,10 @@ flutter: - assets/fiat/fiat_icons_square/ - assets/fiat/providers/ - assets/packages/flutter_inappwebview_web/assets/web/ + - app_build/build_config.json + - packages/komodo_defi_framework/assets/config/ + - packages/komodo_defi_framework/assets/coin_icons/png/ - # Komodo Wallet build transformer configuration. This handles all build-time - # dependencies, such as fetching coin assets, platform-specific assets, and - # more. This replaces the complicated build process that was previously - # required running multiple scripts and manual steps. - - path: app_build/build_config.json - transformers: - - package: komodo_wallet_build_transformer - args: [ - # Uncomment any of the following options to disable specific build - # steps. They are executed in the order listed in `_build_steps` - # in `packages/komodo_wallet_build_transformer/bin/komodo_wallet_build_transformer.dart` - # Configure fetch_defi_api in `build_config.json` - --fetch_defi_api, - # Configure `fetch_coin_assets` in `build_config.json` - --fetch_coin_assets, - --copy_platform_assets, - # Uncomment the following option to enable concurrent build step - # execution. This is useful for reducing build time in development, - # but is not recommended for production builds. - # - --concurrent, - - #! NB: There may be complications if we want to publish the - # sub-packages as separate packages. However, Flutter's upcoming - # support for native/web build hooks may simplify this process. - # This package must be listed in the `package_config.json` of the - # root Flutter app. - --artifact_output_package=web_dex, - - # The path to the build config file relative to the root of the - # artifact output package. - --config_output_path=app_build/build_config.json, - ] - # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -323,7 +276,6 @@ flutter: - asset: assets/fallback_fonts/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf weight: 400 - # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf diff --git a/run_integration_tests.dart b/run_integration_tests.dart index bb3b62d6f8..9b9d01a2b9 100644 --- a/run_integration_tests.dart +++ b/run_integration_tests.dart @@ -1,85 +1,102 @@ -// ignore_for_file: avoid_print, prefer_interpolation_to_compose_strings +// ignore_for_file: avoid_print -import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; -// omit './test_integration/tests/' part of path to testfile -final List testsList = [ - 'suspended_assets_test/suspended_assets_test.dart', - 'wallets_tests/wallets_tests.dart', - 'wallets_manager_tests/wallets_manager_tests.dart', - 'dex_tests/dex_tests.dart', - 'misc_tests/misc_tests.dart', - 'fiat_onramp_tests/fiat_onramp_tests.dart', -]; - -//app data path for mac and linux -const String macAppData = '/Library/Containers/com.komodo.komodowallet'; -const String linuxAppData = '/.local/share/com.komodo.KomodoWallet'; -const String windowsAppData = r'\AppData\Roaming\com.komodo'; - -// TODO: convert to class & include args as class members -const String suspendedCoin = 'KMD'; -File? _configFile; -bool verbose = false; +import 'test_integration/integration_test_arguments.dart'; +import 'test_integration/runners/drivers/web_browser_driver.dart'; +import 'test_integration/runners/integration_test_runner.dart'; Future main(List args) async { final ArgParser parser = _configureArgParser(); - final ArgResults runArguments = parser.parse(args); - - final bool runHelp = runArguments['help'] as bool; - verbose = runArguments['verbose'] as bool; - final String testToRunArg = runArguments['testToRun'] as String; - final String browserDimensionArg = runArguments['browserDimension'] as String; - final String displayArg = runArguments['displayMode'] as String; - final String deviceArg = runArguments['device'] as String; - final String runModeArg = runArguments['runMode'] as String; - final String browserNameArg = runArguments['browser-name'] as String; + IntegrationTestArguments testArgs = + IntegrationTestArguments.fromArgs(parser.parse(args)); + final Set testsList = + getTestsList(testArgs.isWeb && testArgs.isChrome); + bool didTestFail = false; + WebBrowserDriver? driver; + const testsWithUrlBlocking = [ + 'suspended_assets_test/suspended_assets_test.dart', + ]; - // Show help message and exit - if (runHelp) { + if (testArgs.runHelp) { print(parser.usage); exit(0); } - // Coins config setup for suspended_assets_test - final Map originalConfig; - _configFile = await _findCoinsConfigFile(); - originalConfig = _readConfig(); + if (testArgs.testToRun.isNotEmpty) { + testsList + ..clear() + ..add(testArgs.testToRun); + } + + driver = testArgs.isWeb + ? createWebBrowserDriver( + browser: WebBrowser.fromName(testArgs.browserName), + port: testArgs.driverPort, + ) + : null; + _registerProcessSignalHandlers(driver); + + // Block electrum servers for the suspended assets test to force an + // activation error for coins relient on the domain + final bool isUrlBlockedTest = + testsWithUrlBlocking.any((test) => testsList.contains(test)); + if (testArgs.isWeb && testArgs.isChrome && isUrlBlockedTest) { + await driver?.blockUrl('*.cipig.net'); + // `flutter pub get` is required between tests, since blocking domains + // modifies the flutter_tools package, which needs to be rebuilt + testArgs = testArgs.copyWith(pub: true, concurrent: false); + } - // Run tests - if (testToRunArg.isNotEmpty) { - await _runTest( - testToRunArg, - browserDimensionArg, - displayArg, - deviceArg, - runModeArg, - browserNameArg, - originalConfig, + try { + final testRunner = IntegrationTestRunner( + testArgs, + testsDirectory: 'test_integration/tests', ); - } else { - for (final String test in testsList) { - try { - await _runTest( - test, - browserDimensionArg, - displayArg, - deviceArg, - runModeArg, - browserNameArg, - originalConfig, - ); - } catch (e, s) { - print(s); - throw Exception('Caught error executing _runTest: ' + e.toString()); + await driver?.start(); + + final testFutures = testsList.map((test) async { + await testRunner.runTest(test); + await driver?.reset(); // reset configuration changes + }); + + if (testArgs.concurrent) { + await Future.wait(testFutures); + } else { + for (final testFuture in testFutures) { + await testFuture; } } + } on ProcessException catch (e, s) { + print('TEST FAILED: ${e.executable} ${e.arguments.join(' ')}'); + print('$e: \n$s'); + didTestFail = true; + } catch (e, s) { + print('$e: \n$s'); + didTestFail = true; + } finally { + await driver?.stop(); } + + exit(didTestFail ? 1 : 0); } +void _registerProcessSignalHandlers(WebBrowserDriver? driver) { + ProcessSignal.sigint.watch().listen((_) { + print('Caught SIGINT, shutting down...'); + if (driver != null) cleanup(driver); + }); + + ProcessSignal.sigterm.watch().listen((_) { + print('Caught SIGTERM, shutting down...'); + if (driver != null) cleanup(driver); + }); +} + +// leaving the args here for now so that the available options and default +// values are easy to find and modify ArgParser _configureArgParser() { final parser = ArgParser() ..addFlag( @@ -141,218 +158,52 @@ ArgParser _configureArgParser() { abbr: 'n', defaultsTo: 'chrome', help: 'Set browser to run tests on', - allowed: ['chrome', 'safari', 'firefox', 'edge'], + allowed: ['chrome', 'safari', 'firefox'], + ) + ..addOption( + 'driver-port', + abbr: 'p', + defaultsTo: '4444', + help: 'Port to use to start and communicate with the web browser driver', + ) + ..addFlag( + 'pub', + negatable: false, + help: 'Run pub get before running each test group', + ) + ..addFlag( + 'concurrent', + abbr: 'c', + help: 'Run tests concurrently. This is not recommended with the current ' + 'flutter build steps and transformers.', + ) + ..addFlag( + 'keep-running', + abbr: 'k', ); return parser; } -Future _runTest( - String test, - String browserDimentionFromArg, - String displayStateFromArg, - String deviceFromArg, - String runModeFromArg, - String browserNameArg, - Map originalConfigPassed, -) async { - print('Running test ' + test); - - if (test == 'suspended_assets_test/suspended_assets_test.dart') { - _breakConfig(originalConfigPassed); - } - - print('Starting process for test: ' + test); - - ProcessResult result; - try { - if (deviceFromArg == 'web-server') { - if (verbose) { - print( - "RUNNING: 'flutter drive --dart-define=testing_mode=true " - '--driver=test_driver/integration_test.dart ' - '--target=test_integration/tests/$test -d $deviceFromArg ' - '--browser-dimension $browserDimentionFromArg ' - '--$displayStateFromArg ' - '--$runModeFromArg ' - "--browser-name $browserNameArg'", - ); - } - result = await Process.run( - 'flutter', - [ - 'drive', - '--dart-define=testing_mode=true', - '--driver=test_driver/integration_test.dart', - '--target=test_integration/tests/' + test, - if (verbose) '-v', - '-d', - deviceFromArg, - '--browser-dimension', - browserDimentionFromArg, - '--' + displayStateFromArg, - '--' + runModeFromArg, - '--browser-name', - browserNameArg, - '--web-renderer', - 'canvaskit', - ], - runInShell: true, - ); - } else { - //Clear app data before tests for Desktop native app - await _clearNativeAppsData(); - - // Run integration tests for native apps - // E.g. Linux, MacOS, Windows, iOS, Android - result = await Process.run( - 'flutter', - [ - 'drive', - '--dart-define=testing_mode=true', - '--driver=test_driver/integration_test.dart', - '--target=test_integration/tests/' + test, - if (verbose) '-v', - '-d', - deviceFromArg, - '--' + runModeFromArg, - ], - runInShell: true, - ); - } - } catch (e, s) { - if (test == 'suspended_assets_test/suspended_assets_test.dart') { - _restoreConfig(originalConfigPassed); - print('Restored original coins configuration file.'); - } - print(s); - throw Exception('Error running flutter drive Process: ' + e.toString()); - } - - stdout.write(result.stdout); - if (test == 'suspended_assets_test/suspended_assets_test.dart') { - _restoreConfig(originalConfigPassed); - print('Restored original coins configuration file.'); - } - // Flutter drive can return failed test results just as stdout message, - // we need to parse this message and detect test failure manually - if (_didAnyTestFail(result)) { - throw ProcessException( - 'flutter', - ['test ' + test], - 'Failure details are in $browserNameArg driver output.\n', - -1, - ); - } - print('\n---\n'); -} - -bool _didAnyTestFail(ProcessResult result) { - final caseInvariantConsoleOutput = result.stdout.toString().toLowerCase() + - result.stderr.toString().toLowerCase(); - - return caseInvariantConsoleOutput.contains('failure details') || - caseInvariantConsoleOutput.contains('test failed') || - !caseInvariantConsoleOutput.contains('all tests passed'); -} - -Map _readConfig() { - Map json; - - try { - final String jsonStr = _configFile!.readAsStringSync(); - json = jsonDecode(jsonStr) as Map; - } catch (e) { - print('Unable to load json from ${_configFile!.path}:\n$e'); - rethrow; - } - - return json; -} - -void _breakConfig(Map config) { - if (_configFile == null) { - throw Exception('Coins config file not found'); - } else { - print('Temporarily breaking $suspendedCoin electrum config' - " in '${_configFile!.path}' to test suspended state."); - } - - final broken = Map.from(config); - // ignore: avoid_dynamic_calls - broken[suspendedCoin]['electrum'] = [ - { - 'url': 'broken.e1ectrum.net:10063', - 'ws_url': 'broken.e1ectrum.net:30063', - } - ]; - - _writeConfig(broken); -} - -void _restoreConfig(Map originalConfig) { - _writeConfig(originalConfig); -} - -void _writeConfig(Map config) { - final String spaces = ' ' * 4; - final JsonEncoder encoder = JsonEncoder.withIndent(spaces); - - _configFile!.writeAsStringSync(encoder.convert(config)); -} - -Future _findCoinsConfigFile() async { - final config = File('assets/config/coins_config.json'); - - if (!config.existsSync()) { - throw Exception('Coins config file not found at ${config.path}'); - } - - return config; +// ignore: avoid_positional_boolean_parameters ? there's only one parameter +Set getTestsList(bool runSuspendedAssetsTest) { + // omit './test_integration/tests/' part of path to testfile + return { + // Suspended assets tests rely on blocking network requests to electrum + // servers, which is only supported on web platforms at this time. + // The previous approach was to modify coin_config.json, but this is no + // longer possible with it being managed by an external package. Any changes + // to the file in the `build/` directory will be overwritten. + if (runSuspendedAssetsTest) + 'suspended_assets_test/suspended_assets_test.dart', + 'wallets_tests/wallets_tests.dart', + 'wallets_manager_tests/wallets_manager_tests.dart', + 'dex_tests/dex_tests.dart', + 'misc_tests/misc_tests.dart', + 'fiat_onramp_tests/fiat_onramp_tests.dart', + }; } -Future _clearNativeAppsData() async { - ProcessResult deleteResult; - if (Platform.isWindows) { - final homeDir = Platform.environment['UserProfile']; - if (Directory('$homeDir$windowsAppData').existsSync()) { - deleteResult = await Process.run( - 'rmdir', - ['/s', '/q', '$homeDir$windowsAppData'], - runInShell: true, - ); - if (deleteResult.exitCode == 0) { - print('Windows App data removed successfully.'); - } else { - print( - 'Failed to remove Windows app data. Error: ${deleteResult.stderr}', - ); - } - } else { - print('No need clean windows app data'); - } - } else if (Platform.isLinux) { - final homeDir = Platform.environment['HOME']; - deleteResult = await Process.run( - 'rm', - ['-rf', '$homeDir$linuxAppData'], - runInShell: true, - ); - if (deleteResult.exitCode == 0) { - print('Linux App data removed successfully.'); - } else { - print('Failed to remove Linux app data. Error: ${deleteResult.stderr}'); - } - } else if (Platform.isMacOS) { - final homeDir = Platform.environment['HOME']; - deleteResult = await Process.run( - 'rm', - ['-rf', '$homeDir$macAppData'], - runInShell: true, - ); - if (deleteResult.exitCode == 0) { - print('MacOS App data removed successfully.'); - } else { - print('Failed to remove MacOS app data. Error: ${deleteResult.stderr}'); - } - } +Future cleanup(WebBrowserDriver driver) async { + await driver.stop(); + exit(0); } diff --git a/test_integration/common/goto.dart b/test_integration/common/goto.dart index 2ad08b8daa..01586dc955 100644 --- a/test_integration/common/goto.dart +++ b/test_integration/common/goto.dart @@ -1,35 +1,41 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web_dex/common/screen_type.dart'; -import 'tester_utils.dart'; +import 'widget_tester_action_extensions.dart'; +import 'widget_tester_find_extension.dart'; +import 'widget_tester_pump_extension.dart'; Future walletPage(WidgetTester tester, {ScreenType? type}) async { - return await _go(find.byKey(const Key('main-menu-wallet')), tester); + return await _go('main-menu-wallet', tester); } Future dexPage(WidgetTester tester, {ScreenType? type}) async { - return await _go(find.byKey(const Key('main-menu-dex')), tester); + return await _go('main-menu-dex', tester); } Future bridgePage(WidgetTester tester, {ScreenType? type}) async { - return await _go(find.byKey(const Key('main-menu-bridge')), tester); + return await _go('main-menu-bridge', tester); } Future nftsPage(WidgetTester tester, {ScreenType? type}) async { - return await _go(find.byKey(const Key('main-menu-nft')), tester); + return await _go('main-menu-nft', tester); } Future settingsPage(WidgetTester tester, {ScreenType? type}) async { - await _go(find.byKey(const Key('main-menu-settings')), tester); + await _go('main-menu-settings', tester); } Future supportPage(WidgetTester tester, {ScreenType? type}) async { - return await _go(find.byKey(const Key('main-menu-support')), tester); + return await _go('main-menu-support', tester); } -Future _go(Finder finder, WidgetTester tester) async { +Future _go(String key, WidgetTester tester, {int nFrames = 60}) async { + // ignore: avoid_print + print('🔍 GOTO: navigating to $key'); + final Finder finder = find.byKeyName(key); expect(finder, findsOneWidget, reason: 'goto.dart _go($finder)'); - await testerTap(tester, finder); - await tester.pumpAndSettle(); + await tester.tapAndPump(finder); + await tester.pumpNFrames(nFrames); + // ignore: avoid_print + print('🔍 GOTO: finished navigating to to $key'); } diff --git a/test_integration/common/pump_and_settle.dart b/test_integration/common/pump_and_settle.dart deleted file mode 100644 index 9c63a2c307..0000000000 --- a/test_integration/common/pump_and_settle.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; - -Future pumpUntilDisappear( - WidgetTester tester, - Finder finder, { - Duration timeout = const Duration(seconds: 30), -}) async { - bool timerDone = false; - final timer = - Timer(timeout, () => throw TimeoutException('Pump until has timed out')); - while (timerDone != true) { - await tester.pumpAndSettle(); - - final found = tester.any(finder); - if (!found) { - timerDone = true; - } - } - timer.cancel(); -} - -extension WidgetTesterPumpExtension on WidgetTester { - Future pumpNFrames(int frames) async { - for (int i = 0; i < frames; i++) { - await pump(); - } - } -} diff --git a/test_integration/common/tester_utils.dart b/test_integration/common/tester_utils.dart deleted file mode 100644 index 129754cb93..0000000000 --- a/test_integration/common/tester_utils.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'pause.dart'; -import 'pump_and_settle.dart'; - -Future testerTap(WidgetTester tester, Finder finder) async { - await tester.tap(finder); - await tester.pumpNFrames(10); - await pause(); -} - -Future isWidgetVisible(WidgetTester tester, Finder finder) async { - try { - await tester.pumpAndSettle(); - expect(finder, findsOneWidget); - return true; - } catch (e) { - return false; - } -} diff --git a/test_integration/common/widget_tester_action_extensions.dart b/test_integration/common/widget_tester_action_extensions.dart new file mode 100644 index 0000000000..d5d73f70df --- /dev/null +++ b/test_integration/common/widget_tester_action_extensions.dart @@ -0,0 +1,53 @@ +// ignore_for_file: avoid_print + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; + +import 'pause.dart'; +import 'widget_tester_pump_extension.dart'; + +extension WidgetTesterActionExtensions on WidgetTester { + Future tapAndPump( + Finder finder, { + int nFrames = 30, + }) async { + await ensureVisible(finder); + await tap(finder); + await pause(); + await pumpNFrames(nFrames); + } + + Future waitForButtonEnabled( + Finder buttonFinder, { + Duration timeout = const Duration(seconds: 10), + Duration interval = const Duration(milliseconds: 100), + }) async { + final stopwatch = Stopwatch()..start(); + + while (stopwatch.elapsed < timeout) { + // TODO: change to more generic type + final button = widget(buttonFinder); + if (button.onPressed != null) { + print('🔍 Button became enabled after ' + '${stopwatch.elapsed.inSeconds} seconds'); + return; + } + await pump(interval); + } + + throw TimeoutException('Button did not become enabled ' + 'within ${timeout.inSeconds} seconds'); + } + + Future isWidgetVisible(Finder finder) async { + try { + await pumpAndSettle(); + expect(finder, findsOneWidget); + return true; + } catch (e) { + return false; + } + } +} diff --git a/test_integration/common/widget_tester_find_extension.dart b/test_integration/common/widget_tester_find_extension.dart new file mode 100644 index 0000000000..9f3b769b34 --- /dev/null +++ b/test_integration/common/widget_tester_find_extension.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension WidgetTesterFindExtension on CommonFinders { + Finder byKeyName(String key) { + return find.byKey(Key(key)); + } +} diff --git a/test_integration/common/widget_tester_pump_extension.dart b/test_integration/common/widget_tester_pump_extension.dart new file mode 100644 index 0000000000..9053d27686 --- /dev/null +++ b/test_integration/common/widget_tester_pump_extension.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension WidgetTesterPumpExtension on WidgetTester { + Future pumpNFrames( + int frames, { + Duration delay = const Duration(milliseconds: 10), + }) async { + for (int i = 0; i < frames; i++) { + await pump(); + await Future.delayed(delay); + } + } + + Future pumpUntilVisible( + Finder finder, { + Duration timeout = const Duration(seconds: 60), + bool throwOnError = true, + }) async { + final endTime = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(endTime)) { + await pumpAndSettle(); + + if (any(finder)) { + return; + } + } + + if (!throwOnError) { + return; + } + + String finderDescription = ''; + try { + finderDescription = 'Finder: $finder'; + final Widget finderWidget = widget(finder); + finderDescription += ', Widget: $finderWidget'; + } catch (e) { + finderDescription += ', unable to retrieve widget information'; + } + + throw TimeoutException('pumpUntilVisible timed out: $finderDescription'); + } + + Future pumpUntilDisappear( + Finder finder, { + Duration timeout = const Duration(seconds: 30), + }) async { + bool timerDone = false; + final timer = Timer( + timeout, () => throw TimeoutException('Pump until has timed out')); + while (timerDone != true) { + await pumpAndSettle(); + + final found = any(finder); + if (!found) { + timerDone = true; + } + } + timer.cancel(); + } +} diff --git a/test_integration/helpers/restore_wallet.dart b/test_integration/helpers/restore_wallet.dart index 94c037b87f..c7852f208f 100644 --- a/test_integration/helpers/restore_wallet.dart +++ b/test_integration/helpers/restore_wallet.dart @@ -1,14 +1,19 @@ +// ignore_for_file: avoid_print + import 'package:bip39/bip39.dart' as bip39; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/wallet.dart'; -import '../common/pump_and_settle.dart'; -import '../helpers/get_funded_wif.dart'; +import '../common/widget_tester_action_extensions.dart'; +import '../common/widget_tester_pump_extension.dart'; import 'connect_wallet.dart'; +import 'get_funded_wif.dart'; Future restoreWalletToTest(WidgetTester tester) async { + print('🔍 RESTORE WALLET: Starting wallet restoration test'); + // Restores wallet to be used in following tests final String testSeed = getFundedWif(); const String walletName = 'my-wallet'; @@ -35,43 +40,44 @@ Future restoreWalletToTest(WidgetTester tester) async { const String confirmCustomSeedText = 'I Understand'; await tester.pumpAndSettle(); + print('🔍 RESTORE WALLET: Connecting wallet'); isMobile ? await tapOnMobileConnectWallet(tester, WalletType.iguana) : await tapOnAppBarConnectWallet(tester, WalletType.iguana); + + print('🔍 RESTORE WALLET: Tapping import wallet button'); await tester.ensureVisible(importWalletButton); await tester.tap(importWalletButton); await tester.pumpAndSettle(); - await tester.tap(nameField); + print('🔍 RESTORE WALLET: Entering wallet details'); + await tester.tapAndPump(nameField); await tester.enterText(nameField, walletName); await tester.enterText(importSeedField, testSeed); await tester.pump(); - // Change focus from the text field to allow the text error to appear - // TODO: look into more aggressive input validation - await tester.tap(eulaCheckBox); - await tester.pump(); - await tester.tap(tocCheckBox); - await tester.pumpNFrames(5); + print('🔍 RESTORE WALLET: Accepting terms'); + await tester.tapAndPump(eulaCheckBox); + await tester.tapAndPump(tocCheckBox); if (!bip39.validateMnemonic(testSeed)) { - await tester.tap(allowCustomSeedCheckbox); - await tester.pumpNFrames(5); + print('🔍 RESTORE WALLET: Handling custom seed input'); + await tester.tapAndPump(allowCustomSeedCheckbox); await tester.enterText(customSeedDialogInput, confirmCustomSeedText); await tester.pumpNFrames(5); - await tester.tap(customSeedDialogOkButton); - // Works on safari with 5 frames, but requires more on chrome? - await tester.pumpNFrames(10); + await tester.tapAndPump(customSeedDialogOkButton); } + print('🔍 RESTORE WALLET: Confirming wallet creation'); await tester.dragUntilVisible( importConfirmButton, walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpNFrames(5); - await tester.tap(importConfirmButton); - await tester.pumpNFrames(5); + await tester.pumpNFrames(10); + await tester.tapAndPump(importConfirmButton); + + print('🔍 RESTORE WALLET: Setting up password'); await tester.enterText(passwordField, password); await tester.enterText(passwordConfirmField, password); await tester.dragUntilVisible( @@ -79,7 +85,10 @@ Future restoreWalletToTest(WidgetTester tester) async { walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpNFrames(5); - await tester.tap(importConfirmButton); - await pumpUntilDisappear(tester, walletsManagerWrapper); + await tester.pumpNFrames(10); + await tester.tapAndPump(importConfirmButton); + + print('🔍 RESTORE WALLET: Waiting for completion'); + await tester.pumpUntilDisappear(walletsManagerWrapper); + print('🔍 RESTORE WALLET: Wallet restoration completed'); } diff --git a/test_integration/integration_test_arguments.dart b/test_integration/integration_test_arguments.dart new file mode 100644 index 0000000000..cc92883d23 --- /dev/null +++ b/test_integration/integration_test_arguments.dart @@ -0,0 +1,81 @@ +import 'package:args/args.dart'; + +class IntegrationTestArguments { + const IntegrationTestArguments({ + required this.runHelp, + required this.verbose, + required this.pub, + required this.testToRun, + required this.browserDimension, + required this.displayMode, + required this.device, + required this.runMode, + required this.browserName, + required this.concurrent, + required this.keepRunning, + required this.driverPort, + }); + + factory IntegrationTestArguments.fromArgs(ArgResults results) { + return IntegrationTestArguments( + runHelp: results['help'] as bool, + verbose: results['verbose'] as bool? ?? false, + testToRun: results['testToRun'] as String, + browserDimension: results['browserDimension'] as String, + displayMode: results['displayMode'] as String, + device: results['device'] as String, + runMode: results['runMode'] as String, + browserName: results['browser-name'] as String, + pub: results['pub'] as bool? ?? false, + concurrent: results['concurrent'] as bool? ?? false, + keepRunning: results['keep-running'] as bool? ?? false, + driverPort: int.tryParse(results['driver-port'] as String? ?? '') ?? 4444, + ); + } + + final bool runHelp; + final bool verbose; + final bool pub; + final String testToRun; + final String browserDimension; + final String displayMode; + final String device; + final String runMode; + final String browserName; + final bool concurrent; + final bool keepRunning; + final int driverPort; + + bool get isChrome => browserName == 'chrome'; + bool get isWeb => device == 'web-server'; + + IntegrationTestArguments copyWith({ + bool? runHelp, + bool? verbose, + bool? pub, + String? testToRun, + String? browserDimension, + String? displayMode, + String? device, + String? runMode, + String? browserName, + bool? concurrent, + bool? keepRunning, + int? driverPort, + }) { + return IntegrationTestArguments( + runHelp: runHelp ?? this.runHelp, + verbose: verbose ?? this.verbose, + pub: pub ?? this.pub, + testToRun: testToRun ?? this.testToRun, + browserDimension: browserDimension ?? this.browserDimension, + displayMode: displayMode ?? this.displayMode, + device: device ?? this.device, + runMode: runMode ?? this.runMode, + browserName: browserName ?? this.browserName, + concurrent: concurrent ?? this.concurrent, + keepRunning: keepRunning ?? this.keepRunning, + driverPort: driverPort ?? this.driverPort, + ); + } +} diff --git a/test_integration/runners/app_data.dart b/test_integration/runners/app_data.dart new file mode 100644 index 0000000000..8b5cae157f --- /dev/null +++ b/test_integration/runners/app_data.dart @@ -0,0 +1,72 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +//app data path for mac and linux +const String macAppData = '/Library/Containers/com.komodo.komodowallet'; +const String linuxAppData = '/.local/share/com.komodo.KomodoWallet'; +const String windowsAppData = r'\AppData\Roaming\com.komodo'; + +Future clearNativeAppsData() async { + if (Platform.isWindows) { + await _clearAppDataWindows(); + } else if (Platform.isLinux) { + await _clearAppDataLinux(); + } else if (Platform.isMacOS) { + await _clearAppDataMacos(); + } +} + +Future _clearAppDataMacos() async { + ProcessResult deleteResult = ProcessResult(-1, 0, null, null); + final homeDir = Platform.environment['HOME']; + deleteResult = await Process.run( + 'rm', + ['-rf', '$homeDir$macAppData'], + runInShell: true, + ); + if (deleteResult.exitCode == 0) { + print('MacOS App data removed successfully.'); + } else { + print('Failed to remove MacOS app data. Error: ${deleteResult.stderr}'); + } + return deleteResult; +} + +Future _clearAppDataLinux() async { + ProcessResult deleteResult = ProcessResult(-1, 0, null, null); + final homeDir = Platform.environment['HOME']; + deleteResult = await Process.run( + 'rm', + ['-rf', '$homeDir$linuxAppData'], + runInShell: true, + ); + if (deleteResult.exitCode == 0) { + print('Linux App data removed successfully.'); + } else { + print('Failed to remove Linux app data. Error: ${deleteResult.stderr}'); + } + return deleteResult; +} + +Future _clearAppDataWindows() async { + ProcessResult deleteResult = ProcessResult(-1, 0, null, null); + final homeDir = Platform.environment['UserProfile']; + if (Directory('$homeDir$windowsAppData').existsSync()) { + deleteResult = await Process.run( + 'rmdir', + ['/s', '/q', '$homeDir$windowsAppData'], + runInShell: true, + ); + if (deleteResult.exitCode == 0) { + print('Windows App data removed successfully.'); + } else { + print( + 'Failed to remove Windows app data. Error: ${deleteResult.stderr}', + ); + } + } else { + print('No need clean windows app data'); + } + return deleteResult; +} diff --git a/test_integration/runners/drivers/chrome_config_manager.dart b/test_integration/runners/drivers/chrome_config_manager.dart new file mode 100644 index 0000000000..a34c99d761 --- /dev/null +++ b/test_integration/runners/drivers/chrome_config_manager.dart @@ -0,0 +1,126 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +/// Chrome configuration backup and restore functionality +class ChromeConfigManager { + ChromeConfigManager(this.flutterRoot) { + _chromeConfigPaths = [ + '$flutterRoot/packages/flutter_tools/lib/src/web/chrome.dart', + '$flutterRoot/packages/flutter_tools/lib/src/drive/web_driver_service.dart', + ]; + _backupPaths = _chromeConfigPaths.map((path) => '$path.backup').toList(); + } + + final String flutterRoot; + late final List _chromeConfigPaths; + late final List _backupPaths; + final List _argsAppended = []; + + bool get isConfigured => _argsAppended.isNotEmpty; + + /// Create backup and append the provided arguments to the ChromeConfiguration + /// in the flutter_tools package. + /// + /// Throws an [Exception] if something goes wrong with finding flutter + /// or modifying the file. + void appendArgsToChromeConfiguration(List args) { + _deleteFlutterToolsStampFile(throwOnFailure: false); + + for (var i = 0; i < _chromeConfigPaths.length; i++) { + final configPath = _chromeConfigPaths[i]; + // Clear existing arguments from the file to fix a bug & in case of CTRL+C + // Do this before creating a backup, since the backup is used to replace + // the modified config after `flutter drive` is done. + _clearArgumentsFromFile(configPath, args); + + final backupPath = _backupPaths[i]; + print('Creating backup of chrome configuration at $backupPath'); + final file = File(configPath)..copySync(backupPath); + + print('Modifying the chrome configuration in $configPath'); + final contents = file.readAsStringSync(); + final newContents = contents.replaceFirst( + '--disable-extensions', + "--disable-extensions','${args.join("','")}", + ); + + file.writeAsStringSync(newContents); + print('ChromeConfiguration updated with args: $args at $configPath'); + } + + _argsAppended.addAll(args); + } + + void _deleteFlutterToolsStampFile({bool throwOnFailure = true}) { + try { + final stamp = '$flutterRoot/bin/cache/flutter_tools.stamp'; + final snapshot = '$flutterRoot/bin/cache/flutter_tools.snapshot'; + final stampFile = File(stamp); + final snapshotFile = File(snapshot); + if (!stampFile.existsSync() || !snapshotFile.existsSync()) { + throw Exception(''' + Flutter tools stamp file not found. Please run `flutter pub get` first. + $stampFile + '''); + } + stampFile.deleteSync(); + print('Deleted flutter tools stamp file at $stamp'); + snapshotFile.deleteSync(); + print('Deleted flutter tools snapshot file at $snapshot'); + } catch (e) { + if (throwOnFailure) { + rethrow; + } + } + } + + /// Restore the original ChromeConfiguration from backup. + /// + /// Throws an [Exception] if the backup file cannot be found + /// or if there's an error restoring it. + void restoreChromeConfiguration() { + if (!isConfigured) { + print('ChromeConfiguration is not configured. Nothing to restore.'); + return; + } + + for (var i = 0; i < _chromeConfigPaths.length; i++) { + final configPath = _chromeConfigPaths[i]; + final backupPath = _backupPaths[i]; + + final backupFile = File(backupPath); + if (backupFile.existsSync()) { + print('Restoring chrome configuration from backup at $configPath'); + backupFile + ..copySync(configPath) + ..deleteSync(); + } else { + print( + 'No backup file found at $backupPath. Attempting to clean configurations.'); + _clearArgumentsFromFile(configPath, _argsAppended); + } + } + + _deleteFlutterToolsStampFile(); + print('ChromeConfiguration restored or cleaned successfully'); + _argsAppended.clear(); + } + + void _clearArgumentsFromFile(String configPath, List args) { + final configFile = File(configPath); + if (!configFile.existsSync()) { + throw Exception('Configuration file not found at $configPath'); + } + + print('Cleaning $configPath of existing args'); + final contents = configFile.readAsStringSync(); + var cleanedContents = contents; + for (var arg in args) { + print("Removing all instances of ,'$arg'"); + cleanedContents = cleanedContents.replaceAll(",'$arg'", ''); + } + + configFile.writeAsStringSync(cleanedContents); + } +} diff --git a/test_integration/runners/drivers/chrome_driver.dart b/test_integration/runners/drivers/chrome_driver.dart new file mode 100644 index 0000000000..f2af690159 --- /dev/null +++ b/test_integration/runners/drivers/chrome_driver.dart @@ -0,0 +1,54 @@ +import 'chrome_config_manager.dart'; +import 'find.dart'; +import 'web_browser_driver.dart'; +import 'web_driver_process_mixin.dart'; + +class ChromeDriver extends WebBrowserDriver with WebDriverProcessMixin { + ChromeDriver({ + this.port = 4444, + this.silent = true, + this.enableChromeLogs = true, + this.logFilePath = 'chromedriver.log', + }) { + chromeConfigManager = ChromeConfigManager(findFlutterRoot()); + } + + @override + final int port; + final bool silent; + final bool enableChromeLogs; + final String logFilePath; + @override + String get driverName => 'ChromeDriver'; + late final ChromeConfigManager chromeConfigManager; + + @override + Future start() async { + final args = [ + '--port=$port', + '--log-path=$logFilePath', + if (silent) '--silent', + if (enableChromeLogs) '--enable-chrome-logs', + ]; + + await startDriver('chromedriver', args); + } + + @override + Future stop() async { + chromeConfigManager.restoreChromeConfiguration(); + await stopDriver(); + } + + @override + Future reset() async { + chromeConfigManager.restoreChromeConfiguration(); + } + + @override + Future blockUrl(String url, {String redirectUrl = '127.0.0.1'}) async { + chromeConfigManager.appendArgsToChromeConfiguration([ + '--host-resolver-rules=MAP $url $redirectUrl', + ]); + } +} diff --git a/test_integration/runners/drivers/find.dart b/test_integration/runners/drivers/find.dart new file mode 100644 index 0000000000..8e19b3044b --- /dev/null +++ b/test_integration/runners/drivers/find.dart @@ -0,0 +1,78 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +/// Attempt multiple methods of finding the current flutter executable root +/// from PATH, environment variables, and where. +/// +/// Throws an [Exception] if the flutter executable cannot be found. +/// Returns the path to the flutter executable if found. +String findFlutterRoot() { + // Check FLUTTER_ROOT environment variable first + final flutterRoot = Platform.environment['FLUTTER_ROOT']; + if (flutterRoot != null && Directory(flutterRoot).existsSync()) { + return flutterRoot; + } + + // Common installation paths by platform + final commonPaths = [ + if (Platform.isMacOS) ...[ + '/usr/local/flutter', + '${Platform.environment['HOME']}/flutter', + '${Platform.environment['HOME']}/development/flutter', + ], + if (Platform.isLinux) ...[ + '/usr/local/flutter', + '${Platform.environment['HOME']}/flutter', + '${Platform.environment['HOME']}/development/flutter', + '/opt/flutter', + ], + if (Platform.isWindows) ...[ + r'C:\flutter', + r'C:\src\flutter', + '${Platform.environment['LOCALAPPDATA']}\\flutter', + '${Platform.environment['USERPROFILE']}\\flutter', + ], + ]; + + // Check common paths + for (final path in commonPaths) { + if (Directory(path).existsSync()) { + return path; + } + } + + // Check PATH environment variable + final pathEnv = Platform.environment['PATH']; + if (pathEnv != null) { + for (final path in pathEnv.split(Platform.pathSeparator)) { + // Look for flutter executable in PATH + final flutterExe = Platform.isWindows + ? '$path${Platform.pathSeparator}flutter.bat' + : '$path${Platform.pathSeparator}flutter'; + + if (File(flutterExe).existsSync()) { + // Return parent directory of bin folder + return Directory(path).parent.path; + } + } + } + + // Try using where/which command + final command = Platform.isWindows ? 'where' : 'which'; + final result = Process.runSync(command, ['flutter']); + + if (result.exitCode == 0) { + final executablePath = result.stdout.toString().trim(); + // Return parent directory of bin folder + return Directory(File(executablePath).parent.path).parent.path; + } + + throw Exception(''' +Flutter SDK not found. Please ensure Flutter is installed and either: +- FLUTTER_ROOT environment variable is set +- Flutter is installed in a common location +- Flutter binary is available in PATH +'''); +} + diff --git a/test_integration/runners/drivers/firefox_driver.dart b/test_integration/runners/drivers/firefox_driver.dart new file mode 100644 index 0000000000..078d48118b --- /dev/null +++ b/test_integration/runners/drivers/firefox_driver.dart @@ -0,0 +1,34 @@ +import 'web_browser_driver.dart'; +import 'web_driver_process_mixin.dart'; + +class FirefoxDriver extends WebBrowserDriver with WebDriverProcessMixin { + FirefoxDriver({ + this.port = 4444, + this.verbose = true, + }); + + @override + final int port; + final bool verbose; + @override + String get driverName => 'GeckoDriver'; + + @override + Future start() async { + final args = [ + '-p', + port.toString(), + if (verbose) '--quiet', + ]; + + await startDriver('geckodriver', args); + } + + @override + Future stop() => stopDriver(); + + @override + Future blockUrl(String url) async { + // not supported + } +} diff --git a/test_integration/runners/drivers/safari_driver.dart b/test_integration/runners/drivers/safari_driver.dart new file mode 100644 index 0000000000..64100d4cf7 --- /dev/null +++ b/test_integration/runners/drivers/safari_driver.dart @@ -0,0 +1,37 @@ +// ignore_for_file: avoid_print + +import 'web_browser_driver.dart'; +import 'web_driver_process_mixin.dart'; + +class SafariDriver extends WebBrowserDriver with WebDriverProcessMixin { + SafariDriver({ + this.port = 4444, + this.verbose = true, + }); + + @override + final int port; + final bool verbose; + + @override + String get driverName => 'SafariDriver'; + + @override + Future start() async { + final args = [ + '-p', + port.toString(), + if (verbose) '--diagnose', + ]; + + await startDriver('safaridriver', args); + } + + @override + Future stop() => stopDriver(); + + @override + Future blockUrl(String url) async { + // not supported + } +} diff --git a/test_integration/runners/drivers/web_browser_driver.dart b/test_integration/runners/drivers/web_browser_driver.dart new file mode 100644 index 0000000000..f711b757a4 --- /dev/null +++ b/test_integration/runners/drivers/web_browser_driver.dart @@ -0,0 +1,113 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'chrome_driver.dart'; +import 'firefox_driver.dart'; +import 'safari_driver.dart'; + +abstract class WebBrowserDriver { + Future start(); + Future stop(); + Future blockUrl(String url); + Future reset() async {} + + static String findDriverExecutable(String driverName) { + if (File(driverName).existsSync()) { + return './$driverName'; + } + + if (Platform.environment['PATH'] != null) { + for (final path + in Platform.environment['PATH']!.split(Platform.pathSeparator)) { + if (File('$path/$driverName').existsSync()) { + return '$path/$driverName'; + } + } + } + + final whichResult = Process.runSync('which', [driverName]); + if (whichResult.exitCode == 0) { + return whichResult.stdout.toString().trim(); + } + + final whereResult = Process.runSync('where', [driverName]); + if (whereResult.exitCode == 0) { + return whereResult.stdout.toString().trim(); + } + + throw Exception('$driverName not found. Please install it and add it to ' + 'PATH or the current directory.'); + } +} + +WebBrowserDriver? createWebBrowserDriver({ + required WebBrowser browser, + int port = 4444, + bool silent = true, + bool enableChromeLogs = true, + String logFilePath = '', +}) { + if (logFilePath.isEmpty) { + // ignore: parameter_assignments + logFilePath = '${browser.driverName}.log'; + } + + switch (browser) { + case WebBrowser.chrome: + return ChromeDriver( + port: port, + silent: silent, + enableChromeLogs: enableChromeLogs, + logFilePath: logFilePath, + ); + case WebBrowser.safari: + return SafariDriver( + port: port, + verbose: true, + ); + case WebBrowser.firefox: + return FirefoxDriver( + port: port, + verbose: !silent, + ); + // ignore: no_default_cases + default: + return null; + } +} + +enum WebBrowser { + chrome, + firefox, + edge, + safari; + + factory WebBrowser.fromName(String browserName) { + switch (browserName.toLowerCase()) { + case 'chrome': + return WebBrowser.chrome; + case 'firefox': + return WebBrowser.firefox; + case 'edge': + return WebBrowser.edge; + case 'safari': + return WebBrowser.safari; + default: + throw ArgumentError('Invalid browser name: $browserName'); + } + } + + String get driverName { + switch (this) { + case WebBrowser.chrome: + return 'chromedriver'; + case WebBrowser.firefox: + return 'geckodriver'; + case WebBrowser.edge: + return 'msedgedriver'; + case WebBrowser.safari: + return 'safaridriver'; + } + } +} diff --git a/test_integration/runners/drivers/web_driver_process_mixin.dart b/test_integration/runners/drivers/web_driver_process_mixin.dart new file mode 100644 index 0000000000..a886d57025 --- /dev/null +++ b/test_integration/runners/drivers/web_driver_process_mixin.dart @@ -0,0 +1,193 @@ +// ignore_for_file: avoid_print + +import 'dart:convert'; +import 'dart:io'; + +import 'web_browser_driver.dart'; + +mixin WebDriverProcessMixin { + int? _processId; + String get driverName; + int get port; + + bool get isRunning => _processId != null; + + Future startDriver( + String executableName, + List args, { + ProcessStartMode mode = ProcessStartMode.detachedWithStdio, + }) async { + if (await isPortInUse(port)) { + print('Port $port is already in use. Please stop the running $driverName' + ' or use a different port.'); + print('Continuing tests with the prcoess currently using the port'); + return; + } + + final isProcessActive = + await _isProcessRunningByName(executableName) && _processId != null; + if (isRunning || isProcessActive) { + print('Attempting to stop running $driverName with PID $_processId'); + await stopDriver(); + if (isRunning || await _isProcessRunningByName(executableName)) { + print('Failed to stop running $driverName. Please try closing it with ' + 'Task Manager (Windows) or Activity Monitor (macOS)'); + return; + } + } + + final driverPath = WebBrowserDriver.findDriverExecutable(executableName); + print('Running: $driverPath ${args.join(' ')}'); + final process = await Process.start(driverPath, args, mode: mode); + _processId = process.pid; + + _captureStream(process.stdout, 'stdout').ignore(); + _captureStream(process.stderr, 'stderr').ignore(); + + await Future.delayed(const Duration(seconds: 2)); + // check if process is still running and did not exit with an exit code + final isProcessRunning = await _isProcessRunning(_processId ?? -1, port); + if (!isProcessRunning) { + _processId = null; + throw Exception( + 'Failed to start $driverName. Process $_processId not running'); + } + + print('$driverName started on port $port (PID: $_processId)'); + } + + Future stopDriver() async { + if (_processId == null) { + print('Cannot stop $driverName: no pid - likely not running or started'); + return; + } + + try { + print('Attempting to stop $driverName with PID $_processId'); + if (Platform.isWindows) { + await _killWindowsProcess(_processId!); + } else { + await _killUnixProcess(_processId!); + } + _processId = null; + } catch (e) { + print('Error stopping $driverName: $e'); + } + + print('$driverName stopped'); + } + + Future _killUnixProcess(int pid) async { + final result = await Process.run('kill', ['-9', pid.toString()]); + if (result.exitCode != 0) { + print( + 'Warning: Failed to kill $driverName process: ${result.stderr}', + ); + } + } + + Future _killWindowsProcess(int pid) async { + final result = await Process.run( + 'taskkill', + ['/F', '/PID', pid.toString()], + ); + if (result.exitCode != 0) { + print( + 'Warning: Failed to kill $driverName process: ${result.stderr}', + ); + } + } +} + +Future _isProcessRunning(int pid, int port) async { + if (pid <= 0) { + return false; + } + + bool isRunning = true; + if (Platform.isWindows) { + isRunning = await _isWindowsProcessRunning(pid); + } else { + isRunning = await _isUnixProcessRunning(pid); + } + + if (!isRunning) { + return false; + } + + try { + final socket = await Socket.connect('127.0.0.1', port); + await socket.close(); + } on SocketException catch (_) { + return false; + } + + return true; +} + +Future _isUnixProcessRunning(int pid) async { + final result = await Process.run( + 'ps', + ['-p', pid.toString()], + ); + if (result.exitCode != 0) { + return false; + } + + return true; +} + +Future _isWindowsProcessRunning(int pid) async { + final result = await Process.run( + 'tasklist', + ['/FI', 'PID eq $pid'], + ); + if (result.exitCode != 0) { + return false; + } + + return true; +} + +Future _isProcessRunningByName(String processName) async { + bool isRunning; + if (Platform.isWindows) { + isRunning = await _isWindowsProcessRunningByName(processName); + } else { + isRunning = await _isUnixProcessRunningByName(processName); + } + return isRunning; +} + +Future _isUnixProcessRunningByName(String processName) async { + final result = await Process.run('ps', ['-A']); + if (result.exitCode != 0) { + return false; + } + return result.stdout.contains(processName); +} + +Future _isWindowsProcessRunningByName(String processName) async { + final result = await Process.run('tasklist', []); + if (result.exitCode != 0) { + return false; + } + return result.stdout.contains(processName); +} + +Future isPortInUse(int port) async { + try { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, port); + await server.close(); + return false; + } on SocketException { + return true; + } +} + +Future _captureStream(Stream> stream, String streamName) async { + await for (final line + in stream.transform(utf8.decoder).transform(const LineSplitter())) { + print('[$streamName] $line'); + } +} diff --git a/test_integration/runners/integration_test_runner.dart b/test_integration/runners/integration_test_runner.dart new file mode 100644 index 0000000000..e9a49569b4 --- /dev/null +++ b/test_integration/runners/integration_test_runner.dart @@ -0,0 +1,136 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import '../integration_test_arguments.dart'; +import 'app_data.dart'; + +/// Runs integration tests for web or native apps using the `flutter drive` +/// command. +class IntegrationTestRunner { + /// Runs integration tests for web or native apps using the `flutter drive` + /// command. + /// + /// [_args] is the arguments for the integration test. + /// [testsDirectory] is the path to the directory containing the integration + /// tests. Defaults to `integration_test/tests/`. + /// + /// Throws a [ProcessException] if the test fails. + IntegrationTestRunner( + this._args, { + this.testsDirectory = 'integration_test/tests/', + }); + + final IntegrationTestArguments _args; + final String testsDirectory; + + bool get isWeb => _args.device == 'web-server'; + + Future runTest(String test) async { + ProcessResult result; + + print('Running test $test'); + + try { + if (isWeb) { + result = await _runWebServerTest(test); + } else { + //Clear app data before tests for Desktop native app + await clearNativeAppsData(); + + // Run integration tests for native apps + // E.g. Linux, MacOS, Windows, iOS, Android + result = await _runNativeTest(test); + } + } catch (e, s) { + print(s); + throw Exception('Error running flutter drive Process: $e'); + } + + printProcessOutput(result); + + // Flutter drive can return failed test results just as stdout message, + // we need to parse this message and detect test failure manually + if (_didAnyTestFail(result)) { + throw ProcessException( + 'flutter', + ['test $test'], + 'Failure details are in ${_args.browserName} driver output.\n', + -1, + ); + } + print('Finished Running $test\n---\n'); + } + + void printProcessOutput(ProcessResult result) { + print('===== Process Output Start ====='); + stdout.write(result.stdout); + + if (_didAnyTestFail(result)) { + print('----- STDERR -----'); + stderr.write(result.stderr); + print('----- End of STDERR -----'); + } + + print('===== Process Output End ====='); + } + + Future _runNativeTest(String test) async { + return Process.run( + 'flutter', + [ + 'drive', + '--dart-define=testing_mode=true', + '--driver=test_driver/integration_test.dart', + '--target=$testsDirectory/$test', + if (_args.verbose) '-v', + '-d', + _args.device, + '--${_args.runMode}', + if (_args.runMode == 'profile') '--profile-memory=memory_profile.json', + '--${_args.pub ? '' : 'no-'}pub', + '--${_args.keepRunning ? '' : 'no-'}keep-app-running', + '--timeout=600', + ], + runInShell: true, + ); + } + + Future _runWebServerTest(String test) async { + return Process.run( + 'flutter', + [ + 'drive', + '--dart-define=testing_mode=true', + '--driver=test_driver/integration_test.dart', + '--target=$testsDirectory/$test', + if (_args.verbose) '-v', + '-d', + _args.device, + '--browser-dimension', + _args.browserDimension, + '--${_args.displayMode}', + '--${_args.runMode}', + if (_args.runMode == 'profile') '--profile-memory=memory_profile.json', + '--browser-name', + _args.browserName, + '--web-renderer', + 'canvaskit', + '--${_args.pub ? '' : 'no-'}pub', + '--${_args.keepRunning ? '' : 'no-'}keep-app-running', + '--driver-port=${_args.driverPort}', + '--timeout=600', + ], + runInShell: true, + ); + } + + bool _didAnyTestFail(ProcessResult result) { + final caseInvariantConsoleOutput = result.stdout.toString().toLowerCase() + + result.stderr.toString().toLowerCase(); + + return caseInvariantConsoleOutput.contains('failure details') || + caseInvariantConsoleOutput.contains('test failed') || + !caseInvariantConsoleOutput.contains('all tests passed'); + } +} diff --git a/test_integration/tests/dex_tests/dex_tests.dart b/test_integration/tests/dex_tests/dex_tests.dart index 7247518282..de22662608 100644 --- a/test_integration/tests/dex_tests/dex_tests.dart +++ b/test_integration/tests/dex_tests/dex_tests.dart @@ -6,12 +6,20 @@ import 'package:web_dex/main.dart' as app; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; -import './maker_orders_test.dart'; -import './taker_orders_test.dart'; +import 'maker_orders_test.dart'; +import 'taker_orders_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets( + dexWidgetTests(); +} + +void dexWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 30), +}) { + return testWidgets( 'Run DEX tests:', (WidgetTester tester) async { tester.testTextInput.register(); @@ -27,5 +35,8 @@ void main() { print('END DEX TESTS'); }, semanticsEnabled: false, + skip: skip, + retry: retryLimit, + timeout: Timeout(timeout), ); } diff --git a/test_integration/tests/dex_tests/maker_orders_test.dart b/test_integration/tests/dex_tests/maker_orders_test.dart index 60974c800a..901c322ef2 100644 --- a/test_integration/tests/dex_tests/maker_orders_test.dart +++ b/test_integration/tests/dex_tests/maker_orders_test.dart @@ -8,10 +8,14 @@ import 'package:web_dex/shared/widgets/focusable_widget.dart'; import 'package:web_dex/views/dex/entities_list/orders/order_item.dart'; import '../../common/pause.dart'; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; +import '../wallets_tests/wallet_tools.dart'; Future testMakerOrder(WidgetTester tester) async { + print('🔍 MAKER ORDER: Starting maker order test'); + const String sellCoin = 'DOC'; const String sellAmount = '0.012345'; const String buyCoin = 'MARTY'; @@ -46,30 +50,37 @@ Future testMakerOrder(WidgetTester tester) async { final Finder orderUuidWidget = find.byKey(const Key('maker-order-uuid')); // Open maker order form - await tester.tap(dexSectionButton); - await tester.pumpAndSettle(); - await tester.tap(makeOrderTab); - await tester.pumpAndSettle(); + await tester.tapAndPump(dexSectionButton); + print('🔍 MAKER ORDER: Tapped DEX section button'); + + await tester.tapAndPump(makeOrderTab); + print('🔍 MAKER ORDER: Opened make order tab'); // Select sell coin, enter sell amount - await tester.tap(sellCoinSelectButton); - await tester.pumpAndSettle(); - await tester.enterText(sellCoinSearchField, sellCoin); - await tester.pumpAndSettle(); - await tester.tap(sellCoinItem); - await tester.pumpAndSettle(); - await tester.enterText(sellAmountField, sellAmount); - await tester.pumpAndSettle(); + await tester.tapAndPump(sellCoinSelectButton); + print('🔍 MAKER ORDER: Opening sell coin selector'); + + await enterText(tester, finder: sellCoinSearchField, text: sellCoin); + print('🔍 MAKER ORDER: Searching for sell coin: $sellCoin'); + + await tester.tapAndPump(sellCoinItem); + print('🔍 MAKER ORDER: Selected sell coin'); + + await enterText(tester, finder: sellAmountField, text: sellAmount); + print('🔍 MAKER ORDER: Entered sell amount: $sellAmount'); // Select buy coin, enter buy amount - await tester.tap(buyCoinSelectButton); - await tester.pumpAndSettle(); - await tester.enterText(buyCoinSearchField, buyCoin); - await tester.pumpAndSettle(); - await tester.tap(buyCoinItem); - await tester.pumpAndSettle(); - await tester.enterText(buyAmountField, buyAmount); - await tester.pumpAndSettle(); + await tester.tapAndPump(buyCoinSelectButton); + print('🔍 MAKER ORDER: Opening buy coin selector'); + + await enterText(tester, finder: buyCoinSearchField, text: buyCoin); + print('🔍 MAKER ORDER: Searching for buy coin: $buyCoin'); + + await tester.tapAndPump(buyCoinItem); + print('🔍 MAKER ORDER: Selected buy coin'); + + await enterText(tester, finder: buyAmountField, text: buyAmount); + print('🔍 MAKER ORDER: Entered buy amount: $buyAmount'); // Create order await tester.dragUntilVisible( @@ -77,42 +88,76 @@ Future testMakerOrder(WidgetTester tester) async { find.byKey(const Key('maker-form-layout-scroll')), const Offset(0, -100), ); - await tester.tap(makeOrderButton); - await tester.pumpAndSettle(); + print('🔍 MAKER ORDER: Scrolled to make order button'); + await tester.waitForButtonEnabled( + makeOrderButton, + // system health check runs on a 30-second timer, so allow for multiple + // checks until the button is visible + timeout: const Duration(seconds: 90), + ); + await tester.tapAndPump(makeOrderButton, nFrames: 90); + print('🔍 MAKER ORDER: Tapped make order button'); await tester.dragUntilVisible( makeOrderConfirmButton, find.byKey(const Key('maker-order-conformation-scroll')), const Offset(0, -100), ); - await tester.tap(makeOrderConfirmButton); + print('🔍 MAKER ORDER: Scrolled to confirm button'); + + await tester.waitForButtonEnabled( + makeOrderConfirmButton, + // system health check runs on a 30-second timer, so allow for multiple + // checks until the button is visible + timeout: const Duration(seconds: 90), + ); + print('🔍 MAKER ORDER: Confirm button is now enabled'); + await tester.tapAndPump(makeOrderConfirmButton); + // wait for confirm button loader and switch to new page - 30 frames is not + // always enough, and would rather wait for settle to prevent random failures await tester.pumpAndSettle(); + print('🔍 MAKER ORDER: Confirmed order creation'); await pause(sec: 5); // Open order details page expect(orderListItem, findsOneWidget); - await tester.tap(find.descendant( - of: orderListItem, matching: find.byType(FocusableWidget))); + await tester.tap( + find.descendant( + of: orderListItem, + matching: find.byType(FocusableWidget), + ), + ); + print('🔍 MAKER ORDER: Opened order details'); await tester.pumpAndSettle(); // Find order UUID on maker order details page expect(orderUuidWidget, findsOneWidget); truncatedUuid = (orderUuidWidget.evaluate().single.widget as Text).data; + print('🔍 MAKER ORDER: Found order UUID: $truncatedUuid'); expect(truncatedUuid != null, isTrue); expect(truncatedUuid?.isNotEmpty, isTrue); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run maker order tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testMakerOrder(tester); - - print('END MAKER ORDER TESTS'); - }, semanticsEnabled: false); + testWidgets( + 'Run maker order tests:', + (WidgetTester tester) async { + print('🔍 MAIN: Starting maker order test suite'); + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + + print('🔍 MAIN: Accepting alpha warning'); + await acceptAlphaWarning(tester); + + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await tester.pumpAndSettle(); + + await testMakerOrder(tester); + print('🔍 MAIN: Maker order test completed successfully'); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/dex_tests/taker_orders_test.dart b/test_integration/tests/dex_tests/taker_orders_test.dart index 47abd0124f..6841fe8b3b 100644 --- a/test_integration/tests/dex_tests/taker_orders_test.dart +++ b/test_integration/tests/dex_tests/taker_orders_test.dart @@ -10,43 +10,50 @@ import 'package:web_dex/shared/widgets/copied_text.dart'; import 'package:web_dex/views/dex/entities_list/history/history_item.dart'; import '../../common/pause.dart'; +import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_pump_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; Future testTakerOrder(WidgetTester tester) async { + print('🔍 TAKER ORDER: Starting taker order test'); + final String sellCoin = Random().nextDouble() > 0.5 ? 'DOC' : 'MARTY'; const String sellAmount = '0.01'; final String buyCoin = sellCoin == 'DOC' ? 'MARTY' : 'DOC'; + print('🔍 TAKER ORDER: Selected sell coin: $sellCoin, buy coin: $buyCoin'); - final Finder dexSectionButton = find.byKey(const Key('main-menu-dex')); - final Finder dexSectionSwapTab = find.byKey(const Key('dex-swap-tab')); - final Finder sellCoinSelectButton = find.byKey( - const Key('taker-form-sell-switcher'), - ); - final Finder sellCoinSearchField = find.descendant( - of: find.byKey(const Key('taker-sell-coins-table')), - matching: find.byKey(const Key('search-field')), - ); - final Finder sellCoinItem = find.byKey(Key('Coin-table-item-$sellCoin')); - final Finder sellAmountField = find.descendant( - of: find.byKey(const Key('taker-sell-amount')), - matching: find.byKey(const Key('amount-input')), - ); - final Finder buyCoinSelectButton = - find.byKey(const Key('taker-form-buy-switcher')); - final Finder buyCoinSearchField = find.descendant( - of: find.byKey(const Key('taker-orders-table')), - matching: find.byKey(const Key('search-field')), + await _openTakerOrderForm(tester); + await _selectSellCoin(tester, sellAmount: sellAmount, sellCoin: sellCoin); + await _selectBuyCoin(tester, buyCoin: buyCoin); + await _createTakerOrder(tester); + print('🔍 TAKER ORDER: Form completed and order submitted'); + + print('🔍 TAKER ORDER: Waiting for swap completion (max 15 minutes)'); + await tester.pumpAndSettle().timeout( + const Duration(minutes: 15), + onTimeout: () { + print('❌ TAKER ORDER: Swap timeout - exceeded 15 minutes'); + throw Exception( + 'Test error: DOC->MARTY taker Swap took more than 15 minutes'); + }, ); - final Finder buyCoinItem = find.byKey(Key('BestOrder-table-item-$buyCoin')); + await _expectSwapSuccess(tester); + print('🔍 TAKER ORDER: Swap completed successfully'); + + await _testSwapHistoryTable(tester); + print('🔍 TAKER ORDER: History verification completed'); +} + +Finder _infiniteBidFinder() { + print('🔍 INFINITE BID: Searching for infinite bid volume'); const String infiniteBidVolume = '2.00'; final bidsTable = find.byKey(const Key('orderbook-bids-list')); bool infiniteBidPredicate(Widget widget) { if (widget is Text) { return widget.data?.contains(infiniteBidVolume) ?? false; } - return false; } @@ -54,10 +61,34 @@ Future testTakerOrder(WidgetTester tester) async { of: bidsTable, matching: find.byWidgetPredicate(infiniteBidPredicate), ); + print('🔍 INFINITE BID: Bid search completed'); + return infiniteBids; +} + +Future _testSwapHistoryTable( + WidgetTester tester, { + Duration timeout = const Duration(milliseconds: 5000), +}) async { + print('🔍 HISTORY CHECK: Starting history table verification'); + + final Finder backButton = find.byKey(const Key('return-button')); + final Finder historyTab = find.byKey(const Key('dex-history-tab')); + + await tester.tapAndPump(backButton); + print('🔍 HISTORY CHECK: Returned to previous screen'); + + await tester.tapAndPump(historyTab); + print('🔍 HISTORY CHECK: Opened history tab'); + + await tester.pump(timeout); + expect(find.byType(HistoryItem), findsOneWidget, + reason: 'Test error: Swap history item not found'); + print('🔍 HISTORY CHECK: Found history item successfully'); +} + +Future _expectSwapSuccess(WidgetTester tester) async { + print('🔍 SWAP VERIFY: Starting swap verification process'); - final Finder takeOrderButton = find.byKey(const Key('take-order-button')); - final Finder takeOrderConfirmButton = - find.byKey(const Key('take-order-confirm-button')); final Finder tradingDetailsScrollable = find.byType(Scrollable); final Finder takerFeeSentEventStep = find.byKey(const Key('swap-details-step-TakerFeeSent')); @@ -71,159 +102,178 @@ Future testTakerOrder(WidgetTester tester) async { find.byKey(const Key('swap-details-step-MakerPaymentSpent')); final Finder swapSuccess = find.byKey(const Key('swap-status-success')); final Finder backButton = find.byKey(const Key('return-button')); - final Finder historyTab = find.byKey(const Key('dex-history-tab')); - - // Open taker order form - await tester.tap(dexSectionButton); - await tester.pumpAndSettle(); - await tester.tap(dexSectionSwapTab); - await tester.pumpAndSettle(); - // Select sell coin, enter sell amount - await tester.tap(sellCoinSelectButton); - await tester.pumpAndSettle(); - await tester.enterText(sellCoinSearchField, sellCoin); - await tester.pumpAndSettle(); - await tester.tap(sellCoinItem); - await tester.pumpAndSettle(); - await tester.enterText(sellAmountField, sellAmount); - await tester.pumpAndSettle(); - - // Select buy coin - await tester.tap(buyCoinSelectButton); - await tester.pumpAndSettle(); - await tester.enterText(buyCoinSearchField, buyCoin); - await tester.pumpAndSettle(); - await tester.tap(buyCoinItem); - await tester.pumpAndSettle(); + expect(swapSuccess, findsOneWidget); + print('🔍 SWAP VERIFY: Found success status'); - await pause(); - - // Select infinite bid if it exists - if (infiniteBids.evaluate().isNotEmpty) { - await tester.tap(infiniteBids.first); - await tester.pumpAndSettle(); - } - - // Create order - await tester.dragUntilVisible( - takeOrderButton, - find.byKey(const Key('taker-form-layout-scroll')), - const Offset(0, -150), - ); - await tester.tap(takeOrderButton); - await tester.pumpAndSettle(); - - await tester.dragUntilVisible( - takeOrderConfirmButton, - find.byKey(const Key('taker-order-confirmation-scroll')), - const Offset(0, -150), - ); - await tester.tap(takeOrderConfirmButton); - await tester.pumpAndSettle().timeout( - const Duration(minutes: 10), - onTimeout: () { - throw 'Test error: DOC->MARTY taker Swap took more than 10 minutes'; - }, - ); + expect( + find.descendant( + of: takerFeeSentEventStep, matching: find.byType(CopiedText)), + findsOneWidget); + print('🔍 SWAP VERIFY: Taker fee sent verified'); expect( - swapSuccess, - findsOneWidget, - reason: 'Test error: Taker Swap was not successful (probably failed)', - ); + find.descendant( + of: makerPaymentReceivedEventStep, matching: find.byType(CopiedText)), + findsOneWidget); + print('🔍 SWAP VERIFY: Maker payment received verified'); + await tester.dragUntilVisible(takerPaymentSentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); + print('🔍 SWAP VERIFY: Scrolled to taker payment sent'); expect( find.descendant( - of: takerFeeSentEventStep, - matching: find.byType(CopiedText), - ), + of: takerPaymentSentEventStep, matching: find.byType(CopiedText)), findsOneWidget, - reason: "Test error: 'takerFeeSent' event tx copied text not found", ); + + await tester.dragUntilVisible(takerPaymentSpentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); expect( find.descendant( - of: makerPaymentReceivedEventStep, - matching: find.byType(CopiedText), - ), + of: takerPaymentSpentEventStep, matching: find.byType(CopiedText)), findsOneWidget, - reason: "Test error: 'makerPaymentReceived' event tx copied text not found", ); - await tester.dragUntilVisible( - takerPaymentSentEventStep, - tradingDetailsScrollable, - const Offset(0, -10), - ); + await tester.dragUntilVisible(makerPaymentSpentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); expect( find.descendant( - of: takerPaymentSentEventStep, - matching: find.byType(CopiedText), - ), + of: makerPaymentSpentEventStep, matching: find.byType(CopiedText)), findsOneWidget, - reason: "Test error: 'takerPaymentSent' event tx copied text not found", ); await tester.dragUntilVisible( - takerPaymentSpentEventStep, - tradingDetailsScrollable, - const Offset(0, -10), - ); - expect( - find.descendant( - of: takerPaymentSpentEventStep, - matching: find.byType(CopiedText), - ), - findsOneWidget, - reason: "Test error: 'takerPaymentSpent' event tx copied text not found", + backButton, tradingDetailsScrollable, const Offset(0, 10)); + print('🔍 SWAP VERIFY: All swap steps verified successfully'); +} + +Future _createTakerOrder(WidgetTester tester) async { + print('🔍 CREATE ORDER: Starting order creation'); + + final Finder takeOrderButton = find.byKey(const Key('take-order-button')); + final Finder takeOrderConfirmButton = + find.byKey(const Key('take-order-confirm-button')); + + await tester.dragUntilVisible(takeOrderButton, + find.byKey(const Key('taker-form-layout-scroll')), const Offset(0, -150)); + print('🔍 CREATE ORDER: Scrolled to take order button'); + await tester.waitForButtonEnabled( + takeOrderButton, + // system health check runs on a 30-second timer, so allow for multiple + // checks until the button is visible + timeout: const Duration(seconds: 90), ); + await tester.tapAndPump(takeOrderButton); + print('🔍 CREATE ORDER: Tapped take order button'); + // wait for confirm button loader and page switch + await tester.pumpAndSettle(); + await pause(sec: 2); await tester.dragUntilVisible( - makerPaymentSpentEventStep, - tradingDetailsScrollable, - const Offset(0, -10), + takeOrderConfirmButton, + find.byKey(const Key('taker-order-confirmation-scroll')), + const Offset(0, -150)); + print('🔍 CREATE ORDER: Scrolled to confirm button'); + await tester.tapAndPump(takeOrderConfirmButton); + print('🔍 CREATE ORDER: Order confirmed'); +} + +Future _openTakerOrderForm(WidgetTester tester) async { + print('🔍 OPEN FORM: Navigating to taker order form'); + + final Finder dexSectionButton = find.byKey(const Key('main-menu-dex')); + final Finder dexSectionSwapTab = find.byKey(const Key('dex-swap-tab')); + + await tester.tap(dexSectionButton); + print('🔍 OPEN FORM: Opened DEX section'); + await tester.pumpAndSettle(); + + await tester.tap(dexSectionSwapTab); + print('🔍 OPEN FORM: Opened swap tab'); + await tester.pumpAndSettle(); +} + +Future _selectSellCoin( + WidgetTester tester, { + required String sellCoin, + required String sellAmount, +}) async { + print('🔍 SELL CONFIG: Setting up sell parameters'); + final Finder sellCoinSelectButton = + find.byKey(const Key('taker-form-sell-switcher')); + final Finder sellCoinSearchField = find.descendant( + of: find.byKey(const Key('taker-sell-coins-table')), + matching: find.byKey(const Key('search-field')), ); - expect( - find.descendant( - of: makerPaymentSpentEventStep, - matching: find.byType(CopiedText), - ), - findsOneWidget, - reason: "Test error: 'makerPaymentSpent' event tx copied text not found", + final Finder sellCoinItem = find.byKey(Key('Coin-table-item-$sellCoin')); + final Finder sellAmountField = find.descendant( + of: find.byKey(const Key('taker-sell-amount')), + matching: find.byKey(const Key('amount-input')), ); - await tester.dragUntilVisible( - backButton, - tradingDetailsScrollable, - const Offset(0, 10), - ); + await tester.tapAndPump(sellCoinSelectButton); + print('🔍 SELL CONFIG: Opened coin selector'); - await tester.tap(backButton); - await tester.pumpAndSettle(); - await tester.tap(historyTab); - await tester.pump(const Duration(milliseconds: 1000)); - expect( - find.byType(HistoryItem), - findsOneWidget, - reason: 'Test error: Swap history item not found', + await tester.enterText(sellCoinSearchField, sellCoin); + print('🔍 SELL CONFIG: Entered search text: $sellCoin'); + await tester.pumpNFrames(10); + + await tester.tapAndPump(sellCoinItem); + print('🔍 SELL CONFIG: Selected coin'); + + await tester.enterText(sellAmountField, sellAmount); + print('🔍 SELL CONFIG: Entered amount: $sellAmount'); + await tester.pumpNFrames(10); +} + +Future _selectBuyCoin(WidgetTester tester, + {required String buyCoin}) async { + print('🔍 BUY CONFIG: Setting up buy parameters'); + + final Finder buyCoinSelectButton = + find.byKey(const Key('taker-form-buy-switcher')); + final Finder buyCoinSearchField = find.descendant( + of: find.byKey(const Key('taker-orders-table')), + matching: find.byKey(const Key('search-field')), ); + final Finder buyCoinItem = find.byKey(Key('BestOrder-table-item-$buyCoin')); + final Finder infiniteBids = _infiniteBidFinder(); + + await tester.tapAndPump(buyCoinSelectButton); + print('🔍 BUY CONFIG: Opened coin selector'); + + await tester.enterText(buyCoinSearchField, buyCoin); + print('🔍 BUY CONFIG: Entered search text: $buyCoin'); + await tester.pumpNFrames(10); + + await tester.tapAndPump(buyCoinItem); + print('🔍 BUY CONFIG: Selected coin'); + + await pause(); + + if (infiniteBids.evaluate().isNotEmpty) { + print('🔍 BUY CONFIG: Found infinite bid, selecting it'); + await tester.tapAndPump(infiniteBids.first); + } } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets( - 'Run taker order tests:', - (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testTakerOrder(tester); - - print('END TAKER ORDER TESTS'); - }, - semanticsEnabled: false, - ); + testWidgets('Run taker order tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting taker order test suite'); + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + + print('🔍 MAIN: Accepting alpha warning'); + await acceptAlphaWarning(tester); + + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await tester.pumpAndSettle(); + + await testTakerOrder(tester); + print('🔍 MAIN: Taker order test completed successfully'); + }, semanticsEnabled: false); } diff --git a/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart b/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart index a3e9d226d9..d0ed7338d2 100644 --- a/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart +++ b/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart @@ -10,7 +10,15 @@ import 'form_tests.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets( + fiatOnRampWidgetTests(); +} + +void fiatOnRampWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( 'Run Fiat On-Ramp tests:', (WidgetTester tester) async { tester.testTextInput.register(); @@ -23,5 +31,8 @@ void main() { print('END Fiat On-Ramp TESTS'); }, semanticsEnabled: false, + skip: skip, + retry: retryLimit, + timeout: Timeout(timeout), ); } diff --git a/test_integration/tests/fiat_onramp_tests/form_tests.dart b/test_integration/tests/fiat_onramp_tests/form_tests.dart index 5d774eff83..cde1105510 100644 --- a/test_integration/tests/fiat_onramp_tests/form_tests.dart +++ b/test_integration/tests/fiat_onramp_tests/form_tests.dart @@ -5,14 +5,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; -import '../../common/pump_and_settle.dart'; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; Future testFiatFormInputs(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Starting fiat form test'); final Finder fiatFinder = find.byKey(const Key('main-menu-fiat')); await tester.tap(fiatFinder); + print('🔍 FIAT FORM TEST: Tapped fiat menu item'); // wait for fiat form to load fiat currencies, coin list, and payment methods await tester.pumpAndSettle(); @@ -24,23 +26,27 @@ Future testFiatFormInputs(WidgetTester tester) async { } Future _textSubmit(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Testing form submission'); final Finder submitFinder = find.byKey(const Key('fiat-onramp-submit-button')); final Finder webviewFinder = find.byKey(const Key('flutter-in-app-webview')); expect(submitFinder, findsOneWidget, reason: 'Submit button not found'); await tester.tap(submitFinder); + print('🔍 FIAT FORM TEST: Tapped submit button'); await tester.pumpAndSettle(); expect(webviewFinder, findsOneWidget, reason: 'Webview not found'); + print('🔍 FIAT FORM TEST: Verified webview loaded'); } Future _testFiatAmountField(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Testing fiat amount field'); final Finder fiatAmountFinder = find.byKey(const Key('fiat-amount-form-field')); - await tester.tap(fiatAmountFinder); - await tester.pump(); + await tester.tapAndPump(fiatAmountFinder); await tester.enterText(fiatAmountFinder, '50'); + print('🔍 FIAT FORM TEST: Entered fiat amount: 50'); await tester.pump(); await tester.pumpAndSettle(); // wait for payment methods to populate @@ -48,6 +54,7 @@ Future _testFiatAmountField(WidgetTester tester) async { } Future _testFiatSelection(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Testing fiat currency selection'); final Finder fiatDropdownFinder = find.byKey(const Key('fiat-onramp-fiat-dropdown')); final Finder usdIconFinder = @@ -55,16 +62,17 @@ Future _testFiatSelection(WidgetTester tester) async { final Finder eurIconFinder = find.byKey(const Key('fiat-onramp-currency-item-EUR')); - await tester.tap(fiatDropdownFinder); - await tester.pumpNFrames(10); + await tester.tapAndPump(fiatDropdownFinder); expect(usdIconFinder, findsOneWidget, reason: 'USD icon not found'); expect(eurIconFinder, findsOneWidget, reason: 'EUR icon not found'); - await tester.tap(eurIconFinder); - await tester.pump(); + print('🔍 FIAT FORM TEST: Verified USD and EUR options'); + await tester.tapAndPump(eurIconFinder); + print('🔍 FIAT FORM TEST: Selected EUR'); await tester.pumpAndSettle(); // wait for payment methods to populate } Future _testCoinSelection(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Testing coin selection'); final Finder coinDropdownFinder = find.byKey(const Key('fiat-onramp-coin-dropdown')); final Finder btcIconFinder = @@ -72,17 +80,18 @@ Future _testCoinSelection(WidgetTester tester) async { final Finder maticIconFinder = find.byKey(const Key('fiat-onramp-currency-item-LTC')); - await tester.tap(coinDropdownFinder); - await tester.pumpAndSettle(); + await tester.tapAndPump(coinDropdownFinder); expect(btcIconFinder, findsOneWidget, reason: 'BTC icon not found'); + print('🔍 FIAT FORM TEST: Verified BTC option'); await _tapCurrencyItem(tester, maticIconFinder); - await tester.pump(); + print('🔍 FIAT FORM TEST: Selected LTC'); await tester.pumpAndSettle(); // wait for payment methods to populate await _testPaymentMethodSelection(tester); } Future _tapCurrencyItem(WidgetTester tester, Finder asset) async { + print('🔍 FIAT FORM TEST: Tapping currency item'); final Finder list = find.byKey(const Key('fiat-onramp-currency-list')); final Finder dialog = find.byKey(const Key('fiat-onramp-currency-dialog')); @@ -92,12 +101,14 @@ Future _tapCurrencyItem(WidgetTester tester, Finder asset) async { reason: 'Fiat onramp currency dialog not found', ); expect(list, findsOneWidget, reason: 'Fiat onramp currency list not found'); + print('🔍 FIAT FORM TEST: Verified currency dialog and list'); await tester.dragUntilVisible(asset, list, const Offset(0, -50)); await tester.pumpAndSettle(); - await tester.tap(asset); + await tester.tapAndPump(asset); } Future _testPaymentMethodSelection(WidgetTester tester) async { + print('🔍 FIAT FORM TEST: Testing payment method selection'); final Finder rampPaymentMethodFinder = find.byKey(const Key('fiat-payment-method-ramp-0')); final Finder banxaPaymentMethodFinder = @@ -113,9 +124,10 @@ Future _testPaymentMethodSelection(WidgetTester tester) async { findsOneWidget, reason: 'Banxa payment method not found', ); + print('🔍 FIAT FORM TEST: Verified Ramp and Banxa payment methods'); - await tester.tap(rampPaymentMethodFinder); - await tester.pump(); + await tester.tapAndPump(rampPaymentMethodFinder); + print('🔍 FIAT FORM TEST: Selected Ramp payment method'); } void main() { diff --git a/test_integration/tests/misc_tests/feedback_tests.dart b/test_integration/tests/misc_tests/feedback_tests.dart index e276000bdd..5b89d9183f 100644 --- a/test_integration/tests/misc_tests/feedback_tests.dart +++ b/test_integration/tests/misc_tests/feedback_tests.dart @@ -7,13 +7,13 @@ import 'package:web_dex/main.dart' as app; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; Future testFeedbackForm(WidgetTester tester) async { await goto.settingsPage(tester); await tester.pumpAndSettle(); - await testerTap(tester, find.byKey(const Key('settings-menu-item-feedback'))); + await tester.tapAndPump(find.byKey(const Key('settings-menu-item-feedback'))); await tester.pumpAndSettle(); tester.ensureVisible(find.byKey(const Key('feedback-email-field'))); tester.ensureVisible(find.byKey(const Key('feedback-message-field'))); diff --git a/test_integration/tests/misc_tests/menu_tests.dart b/test_integration/tests/misc_tests/menu_tests.dart index f5d0ed2fb4..36e69d1635 100644 --- a/test_integration/tests/misc_tests/menu_tests.dart +++ b/test_integration/tests/misc_tests/menu_tests.dart @@ -22,15 +22,12 @@ Future testMainMenu(WidgetTester tester) async { ); await goto.walletPage(tester); - await tester.pumpAndSettle(); expect(find.byKey(const Key('wallet-page-coins-list')), findsOneWidget); await goto.dexPage(tester); - await tester.pumpAndSettle(); expect(find.byKey(const Key('dex-page')), findsOneWidget); await goto.bridgePage(tester); - await tester.pumpAndSettle(); expect( find.byKey(const Key('bridge-page')), findsOneWidget, @@ -38,11 +35,9 @@ Future testMainMenu(WidgetTester tester) async { ); await goto.nftsPage(tester); - await tester.pumpAndSettle(); expect(find.byKey(const Key('nft-page')), findsOneWidget); await goto.settingsPage(tester); - await tester.pumpAndSettle(); expect(general, findsOneWidget); expect(security, findsOneWidget); expect(feedback, findsOneWidget); diff --git a/test_integration/tests/misc_tests/misc_tests.dart b/test_integration/tests/misc_tests/misc_tests.dart index f4d9bf21ba..7615a6ab45 100644 --- a/test_integration/tests/misc_tests/misc_tests.dart +++ b/test_integration/tests/misc_tests/misc_tests.dart @@ -6,13 +6,21 @@ import 'package:web_dex/main.dart' as app; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; -import './feedback_tests.dart'; -import './menu_tests.dart'; -import './theme_test.dart'; +import 'feedback_tests.dart'; +import 'menu_tests.dart'; +import 'theme_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets( + miscWidgetTests(); +} + +void miscWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( 'Run misc tests:', (WidgetTester tester) async { tester.testTextInput.register(); @@ -30,5 +38,8 @@ void main() { print('END MISC TESTS'); }, semanticsEnabled: false, + skip: skip, + retry: retryLimit, + timeout: Timeout(timeout), ); } diff --git a/test_integration/tests/misc_tests/theme_test.dart b/test_integration/tests/misc_tests/theme_test.dart index 0fb214ee7b..8bb1e92eab 100644 --- a/test_integration/tests/misc_tests/theme_test.dart +++ b/test_integration/tests/misc_tests/theme_test.dart @@ -6,25 +6,28 @@ import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; import '../../common/goto.dart' as goto; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; Future testThemeSwitcher(WidgetTester tester) async { + print('🔍 THEME TEST: Starting theme switcher test'); + final themeSwitcherFinder = find.byKey(const Key('theme-switcher')); final themeSettingsSwitcherLight = find.byKey(const Key('theme-settings-switcher-Light')); final themeSettingsSwitcherDark = find.byKey(const Key('theme-settings-switcher-Dark')); + final currentBrightness = + Theme.of(tester.element(themeSwitcherFinder)).brightness; print( - 'brightness: ${Theme.of(tester.element(themeSwitcherFinder)).brightness}, ' - 'expected: ${Brightness.dark}', - ); - // Check default theme (dark) + '🔍 THEME TEST: Initial brightness: $currentBrightness, expected: ${Brightness.dark}'); expect( Theme.of(tester.element(themeSwitcherFinder)).brightness, equals(Brightness.dark), reason: 'Default theme should be dark theme', ); + print('🔍 THEME TEST: Verified default dark theme'); // await tester.tap(themeSwitcherFinder); // await tester.pumpAndSettle(); @@ -35,22 +38,24 @@ Future testThemeSwitcher(WidgetTester tester) async { // ); await goto.settingsPage(tester); - await tester.tap(themeSettingsSwitcherDark); - await tester.pumpAndSettle(); + + await tester.tapAndPump(themeSettingsSwitcherDark); + print('🔍 THEME TEST: Tapped dark theme switcher'); expect( Theme.of(tester.element(themeSwitcherFinder)).brightness, equals(Brightness.dark), reason: 'Current theme should be dark theme', ); + print('🔍 THEME TEST: Verified dark theme selection'); - await tester.pumpAndSettle(); - await tester.tap(themeSettingsSwitcherLight); - await tester.pumpAndSettle(); + await tester.tapAndPump(themeSettingsSwitcherLight); + print('🔍 THEME TEST: Tapped light theme switcher'); expect( Theme.of(tester.element(themeSwitcherFinder)).brightness, equals(Brightness.light), reason: 'Current theme should be light theme', ); + print('🔍 THEME TEST: Verified light theme selection'); } void main() { diff --git a/test_integration/tests/nfts_tests/nfts_tests.dart b/test_integration/tests/nfts_tests/nfts_tests.dart index 16ce45a686..628151059f 100644 --- a/test_integration/tests/nfts_tests/nfts_tests.dart +++ b/test_integration/tests/nfts_tests/nfts_tests.dart @@ -4,22 +4,36 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; -import './nft_networks.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; +import 'nft_networks.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run NFT tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - await testNftNetworks(tester); - await tester.pumpAndSettle(); + nftsWidgetTests(); +} + +void nftsWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( + 'Run NFT tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testNftNetworks(tester); - print('END NFT TESTS'); - }, semanticsEnabled: false); + print('END NFT TESTS'); + }, + semanticsEnabled: false, + skip: skip, + retry: retryLimit, + timeout: Timeout(timeout), + ); } diff --git a/test_integration/tests/no_login_tests/no_login_tests.dart b/test_integration/tests/no_login_tests/no_login_tests.dart index 45eaf216d6..2e2448f177 100644 --- a/test_integration/tests/no_login_tests/no_login_tests.dart +++ b/test_integration/tests/no_login_tests/no_login_tests.dart @@ -12,20 +12,32 @@ import 'no_login_wallet_access_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run no login mode tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); - await acceptAlphaWarning(tester); + noLoginWidgetTests(); +} + +void noLoginWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( + 'Run no login mode tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); - await pause(msg: 'START NO LOGIN MODE TESTS'); - await testNoLoginWalletAccess(tester); - // No Login taker form test should be always ran last here - await testNoLoginTakerForm(tester); + await acceptAlphaWarning(tester); + await pause(msg: 'START NO LOGIN MODE TESTS'); + await testNoLoginWalletAccess(tester); + // No Login taker form test should be always ran last here + await testNoLoginTakerForm(tester); - await pause(sec: 5, msg: 'END NO LOGIN MODE TESTS'); - await Future.delayed(const Duration(seconds: 5)); - await tester.pumpAndSettle(); - }, semanticsEnabled: false); + await pause(sec: 5, msg: 'END NO LOGIN MODE TESTS'); + }, + semanticsEnabled: false, + retry: retryLimit, + timeout: Timeout(timeout), + skip: skip, + ); } diff --git a/test_integration/tests/suspended_assets_test/runner.dart b/test_integration/tests/suspended_assets_test/runner.dart deleted file mode 100644 index 333b35cda5..0000000000 --- a/test_integration/tests/suspended_assets_test/runner.dart +++ /dev/null @@ -1,93 +0,0 @@ -// ignore_for_file: avoid_print, prefer_interpolation_to_compose_strings - -import 'dart:convert'; -import 'dart:io'; - -import '../../../run_integration_tests.dart'; - -File? _configFile; - -void main() async { - _configFile = await _findCoinsConfigFile(); - if (_configFile == null) { - throw 'Coins config file not found'; - } else { - print('Temporarily breaking $suspendedCoin electrum config' - ' in \'${_configFile!.path}\' to test suspended state.'); - } - - final Map originalConfig = _readConfig(); - _breakConfig(originalConfig); - - Process.run( - 'flutter', - [ - 'drive', - '--driver=test_driver/integration_test.dart', - '--target=test_integration/tests/suspended_assets_test/suspended_assets_test.dart', - '-d', - 'chrome', - '--profile' - ], - runInShell: true, - ).then((result) { - stdout.write(result.stdout); - _restoreConfig(originalConfig); - }).catchError((dynamic e) { - stdout.write(e); - _restoreConfig(originalConfig); - throw e; - }); -} - -Map _readConfig() { - Map json; - - try { - final String jsonStr = _configFile!.readAsStringSync(); - json = jsonDecode(jsonStr); - } catch (e) { - print('Unable to load json from ${_configFile!.path}:\n$e'); - rethrow; - } - - return json; -} - -void _writeConfig(Map config) { - final String spaces = ' ' * 4; - final JsonEncoder encoder = JsonEncoder.withIndent(spaces); - - _configFile!.writeAsStringSync(encoder.convert(config)); -} - -void _breakConfig(Map config) { - final Map broken = jsonDecode(jsonEncode(config)); - broken[suspendedCoin]['electrum'] = [ - { - 'url': 'broken.e1ectrum.net:10063', - 'ws_url': 'broken.e1ectrum.net:30063', - } - ]; - - _writeConfig(broken); -} - -void _restoreConfig(Map originalConfig) { - _writeConfig(originalConfig); -} - -// coins_config.json path contains version number, so can't be constant -Future _findCoinsConfigFile() async { - final List assets = - await Directory('assets').list().toList(); - - for (FileSystemEntity entity in assets) { - if (entity is! Directory) continue; - - final config = File(entity.path + '/config/coins_config.json'); - if (config.existsSync()) return config; - } - - return null; -} diff --git a/test_integration/tests/suspended_assets_test/suspended_assets_test.dart b/test_integration/tests/suspended_assets_test/suspended_assets_test.dart index 9b58f1e3d4..824891d05a 100644 --- a/test_integration/tests/suspended_assets_test/suspended_assets_test.dart +++ b/test_integration/tests/suspended_assets_test/suspended_assets_test.dart @@ -6,7 +6,6 @@ import 'package:integration_test/integration_test.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/main.dart' as app; -import '../../../run_integration_tests.dart'; import '../../common/goto.dart' as goto; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; @@ -14,31 +13,35 @@ import '../../helpers/restore_wallet.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run suspended asset tests:', (WidgetTester tester) async { - const String suspendedAsset = 'KMD'; - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); + testWidgets( + 'Run suspended asset tests:', + (WidgetTester tester) async { + const String suspendedAsset = 'KMD'; + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); + await acceptAlphaWarning(tester); - print('RESTORE WALLET TO TEST'); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); + print('RESTORE WALLET TO TEST'); + await restoreWalletToTest(tester); + await tester.pumpAndSettle(); - await goto.walletPage(tester); - final Finder searchCoinsField = - find.byKey(const Key('wallet-page-search-field')); - await tester.enterText(searchCoinsField, suspendedAsset); - await tester.pumpAndSettle(); - final Finder suspendedCoinLabel = isMobile - ? find.byKey(const Key('retry-suspended-asset-$suspendedCoin')) - : find.byKey(const Key('suspended-asset-message-$suspendedCoin')); - expect( - suspendedCoinLabel, - findsOneWidget, - reason: 'Test error: $suspendedCoin should be suspended,' - ' but corresponding label was not found.', - ); - }, semanticsEnabled: false); + await goto.walletPage(tester); + final Finder searchCoinsField = + find.byKey(const Key('wallet-page-search-field')); + await tester.enterText(searchCoinsField, suspendedAsset); + await tester.pumpAndSettle(); + final Finder suspendedCoinLabel = isMobile + ? find.byKey(const Key('retry-suspended-asset-$suspendedAsset')) + : find.byKey(const Key('suspended-asset-message-$suspendedAsset')); + expect( + suspendedCoinLabel, + findsOneWidget, + reason: 'Test error: $suspendedAsset should be suspended,' + ' but corresponding label was not found.', + ); + }, + semanticsEnabled: false, + ); } diff --git a/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart index 6c2900d70c..7ff193822d 100644 --- a/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart +++ b/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart @@ -8,11 +8,14 @@ import 'package:web_dex/main.dart' as app; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/header/actions/account_switcher.dart'; -import '../../common/pump_and_settle.dart'; +import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_pump_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/connect_wallet.dart'; Future testCreateWallet(WidgetTester tester) async { + print('🔍 CREATE WALLET: Starting wallet creation test'); + const String walletName = 'my-wallet-name'; const String password = 'pppaaasssDDD555444@@@'; final Finder createWalletButton = @@ -29,30 +32,39 @@ Future testCreateWallet(WidgetTester tester) async { final Finder walletsManagerWrapper = find.byKey(const Key('wallets-manager-wrapper')); - await tester.pumpAndSettle(); + print('🔍 CREATE WALLET: Connecting wallet via mobile interface'); await tapOnMobileConnectWallet(tester, WalletType.iguana); // New wallet test + print('🔍 CREATE WALLET: Verifying and tapping create wallet button'); expect(createWalletButton, findsOneWidget); - await tester.tap(createWalletButton); + await tester.tapAndPump(createWalletButton); await tester.pumpAndSettle(); // Wallet creation step + print('🔍 CREATE WALLET: Starting wallet creation form process'); expect(find.byKey(const Key('wallet-creation')), findsOneWidget); - await tester.tap(nameField); + + print('🔍 CREATE WALLET: Entering wallet details'); + await tester.tapAndPump(nameField); await tester.enterText(nameField, walletName); await tester.enterText(passwordField, password); await tester.enterText(passwordConfirmField, password); - await tester.pumpNFrames(10); - await tester.tap(eulaCheckBox); - await tester.pumpNFrames(10); - await tester.tap(tocCheckBox); - await tester.pumpNFrames(10); - await tester.tap(confirmButton); - await pumpUntilDisappear(tester, walletsManagerWrapper); + await tester.pumpNFrames(30); + + print('🔍 CREATE WALLET: Accepting terms and conditions'); + await tester.tapAndPump(eulaCheckBox); + await tester.tapAndPump(tocCheckBox); + + print('🔍 CREATE WALLET: Confirming wallet creation'); + await tester.tapAndPump(confirmButton); + await tester.pumpUntilDisappear(walletsManagerWrapper); + if (!isMobile) { + print('🔍 CREATE WALLET: Verifying wallet creation on desktop'); expect(authorizedWalletButton, findsOneWidget); } + print('🔍 CREATE WALLET: Wallet creation completed'); } void main() { @@ -60,15 +72,19 @@ void main() { testWidgets( 'Run Wallet Creation tests:', (WidgetTester tester) async { + print('🔍 WALLET TESTS: Starting wallet creation test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 WALLET TESTS: Accepting alpha warning'); await acceptAlphaWarning(tester); + + print('🔍 WALLET TESTS: Running wallet creation test'); await testCreateWallet(tester); await tester.pumpAndSettle(); - print('END WALLET CREATION TESTS'); + print('🔍 WALLET TESTS: All wallet creation tests completed'); }, semanticsEnabled: false, ); diff --git a/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart index a9952d2501..f9d1db9356 100644 --- a/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart +++ b/test_integration/tests/wallets_manager_tests/wallets_manager_import_test.dart @@ -8,7 +8,8 @@ import 'package:web_dex/main.dart' as app; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/header/actions/account_switcher.dart'; -import '../../common/pump_and_settle.dart'; +import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_pump_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/connect_wallet.dart'; @@ -18,13 +19,51 @@ Future testImportWallet(WidgetTester tester) async { const String customSeed = 'my-custom-seed'; final Finder importWalletButton = find.byKey(const Key('import-wallet-button')); - final Finder nameField = find.byKey(const Key('name-wallet-field')); + + await tapOnMobileConnectWallet(tester, WalletType.iguana); + + // New wallet test + expect(importWalletButton, findsOneWidget); + await tester.tapAndPump(importWalletButton); + await tester.pumpAndSettle(); + + await _createWallet(tester, customSeed: customSeed, walletName: walletName); + await _enterPassword(tester, walletName: walletName, password: password); +} + +Future _enterPassword( + WidgetTester tester, { + required String password, + required String walletName, +}) async { final Finder passwordField = find.byKey(const Key('create-password-field')); final Finder passwordConfirmField = find.byKey(const Key('create-password-field-confirm')); - final Finder importSeedField = find.byKey(const Key('import-seed-field')); final Finder importConfirmButton = find.byKey(const Key('confirm-seed-button')); + + final Finder authorizedWalletButton = + find.widgetWithText(AccountSwitcher, walletName); + final Finder walletsManagerWrapper = + find.byKey(const Key('wallets-manager-wrapper')); + + await tester.enterText(passwordField, password); + await tester.pumpNFrames(10); + await tester.enterText(passwordConfirmField, password); + await tester.tapAndPump(importConfirmButton); + await tester.pumpUntilDisappear(walletsManagerWrapper); + if (!isMobile) { + expect(authorizedWalletButton, findsOneWidget); + } +} + +Future _createWallet( + WidgetTester tester, { + required String walletName, + required String customSeed, +}) async { + final Finder nameField = find.byKey(const Key('name-wallet-field')); + final Finder importSeedField = find.byKey(const Key('import-seed-field')); final Finder allowCustomSeedCheckbox = find.byKey(const Key('checkbox-custom-seed')); final Finder customSeedDialogInput = @@ -34,45 +73,21 @@ Future testImportWallet(WidgetTester tester) async { const String confirmCustomSeedText = 'I Understand'; final Finder eulaCheckbox = find.byKey(const Key('checkbox-eula')); final Finder tocCheckbox = find.byKey(const Key('checkbox-toc')); - final Finder authorizedWalletButton = - find.widgetWithText(AccountSwitcher, walletName); - final Finder walletsManagerWrapper = - find.byKey(const Key('wallets-manager-wrapper')); - - await tester.pumpAndSettle(); - await tapOnMobileConnectWallet(tester, WalletType.iguana); - - // New wallet test - expect(importWalletButton, findsOneWidget); - await tester.tap(importWalletButton); - await tester.pumpAndSettle(); + final Finder importConfirmButton = + find.byKey(const Key('confirm-seed-button')); - // Wallet creation step - await tester.tap(nameField); + await tester.tapAndPump(nameField); await tester.enterText(nameField, walletName); await tester.enterText(importSeedField, customSeed); await tester.pumpNFrames(10); - await tester.tap(eulaCheckbox); - await tester.pumpNFrames(10); - await tester.tap(tocCheckbox); - await tester.pumpNFrames(10); - await tester.tap(allowCustomSeedCheckbox); - await tester.pumpNFrames(10); + await tester.tapAndPump(eulaCheckbox); + await tester.tapAndPump(tocCheckbox); + await tester.tapAndPump(allowCustomSeedCheckbox); await tester.enterText(customSeedDialogInput, confirmCustomSeedText); await tester.pumpNFrames(10); - await tester.tap(customSeedDialogOkButton); - await tester.pumpNFrames(20); - await tester.tap(importConfirmButton); + await tester.tapAndPump(customSeedDialogOkButton); + await tester.tapAndPump(importConfirmButton); await tester.pumpAndSettle(); - - // Enter password step - await tester.enterText(passwordField, password); - await tester.enterText(passwordConfirmField, password); - await tester.tap(importConfirmButton); - await pumpUntilDisappear(tester, walletsManagerWrapper); - if (!isMobile) { - expect(authorizedWalletButton, findsOneWidget); - } } void main() { diff --git a/test_integration/tests/wallets_manager_tests/wallets_manager_tests.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_tests.dart index 97829e8e2d..52fa2d05bf 100644 --- a/test_integration/tests/wallets_manager_tests/wallets_manager_tests.dart +++ b/test_integration/tests/wallets_manager_tests/wallets_manager_tests.dart @@ -4,25 +4,40 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; -import './wallets_manager_create_test.dart'; -import './wallets_manager_import_test.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/log_out.dart'; +import 'wallets_manager_create_test.dart'; +import 'wallets_manager_import_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run wallet manager tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - await acceptAlphaWarning(tester); - await tester.pumpAndSettle(); - await testCreateWallet(tester); - await tester.pumpAndSettle(); - await logOut(tester); - await tester.pumpAndSettle(); - await testImportWallet(tester); + walletsManagerWidgetTests(); +} + +void walletsManagerWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( + 'Run wallet manager tests:', + (WidgetTester tester) async { + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await tester.pumpAndSettle(); + await testCreateWallet(tester); + await tester.pumpAndSettle(); + await logOut(tester); + await tester.pumpAndSettle(); + await testImportWallet(tester); - print('END WALLET MANAGER TESTS'); - }, semanticsEnabled: false); + print('END WALLET MANAGER TESTS'); + }, + semanticsEnabled: false, + skip: skip, + retry: retryLimit, + timeout: Timeout(timeout), + ); } diff --git a/test_integration/tests/wallets_tests/test_activate_coins.dart b/test_integration/tests/wallets_tests/test_activate_coins.dart index d2ef49ca24..13f0d717c0 100644 --- a/test_integration/tests/wallets_tests/test_activate_coins.dart +++ b/test_integration/tests/wallets_tests/test_activate_coins.dart @@ -7,7 +7,7 @@ import 'package:web_dex/main.dart' as app; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; import 'wallet_tools.dart'; @@ -15,6 +15,7 @@ import 'wallet_tools.dart'; Future testActivateCoins(WidgetTester tester) async { await pause(sec: 2, msg: 'TEST COINS ACTIVATION'); + await pause(sec: 2, msg: '🔍 ACTIVATE COINS: Starting coins activation test'); const String ethByTicker = 'ETH'; const String dogeByName = 'gecoi'; const String kmdBep20ByTicker = 'KMD'; @@ -33,21 +34,40 @@ Future testActivateCoins(WidgetTester tester) async { ); await goto.walletPage(tester); + print('🔍 ACTIVATE COINS: Navigated to wallet page'); expect(totalAmount, findsOneWidget); await _testNoneExistCoin(tester); + print('🔍 ACTIVATE COINS: Completed non-existent coin test'); + await addAsset(tester, asset: dogeCoinItem, search: dogeByName); + print('🔍 ACTIVATE COINS: Added DOGE asset'); + await addAsset(tester, asset: kmdBep20CoinItem, search: kmdBep20ByTicker); + print('🔍 ACTIVATE COINS: Added KMD-BEP20 asset'); + await removeAsset(tester, asset: ethCoinItem, search: ethByTicker); + print('🔍 ACTIVATE COINS: Removed ETH asset'); + await removeAsset(tester, asset: dogeCoinItem, search: dogeByName); + print('🔍 ACTIVATE COINS: Removed DOGE asset'); + await removeAsset(tester, asset: kmdBep20CoinItem, search: kmdBep20ByTicker); + print('🔍 ACTIVATE COINS: Removed KMD-BEP20 asset'); + await goto.dexPage(tester); + print('🔍 ACTIVATE COINS: Navigated to DEX page'); + await goto.walletPage(tester); await pause(msg: 'END TEST COINS ACTIVATION'); + print('🔍 ACTIVATE COINS: Returned to wallet page'); + await pause(msg: '🔍 ACTIVATE COINS: Test completed'); } // Try to find non-existent coin Future _testNoneExistCoin(WidgetTester tester) async { + print('🔍 NON-EXISTENT COIN: Starting test'); + final Finder addAssetsButton = find.byKey( const Key('add-assets-button'), ); @@ -59,25 +79,35 @@ Future _testNoneExistCoin(WidgetTester tester) async { ); await goto.walletPage(tester); - await testerTap(tester, addAssetsButton); + print('🔍 NON-EXISTENT COIN: Navigated to wallet page'); + + await tester.tapAndPump(addAssetsButton); + print('🔍 NON-EXISTENT COIN: Tapped add assets button'); expect(searchCoinsField, findsOneWidget); await enterText(tester, finder: searchCoinsField, text: 'NOSUCHCOINEVER'); + print('🔍 NON-EXISTENT COIN: Searched for non-existent coin'); expect(ethCoinItem, findsNothing); + print('🔍 NON-EXISTENT COIN: Verified coin not found'); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Run coins activation tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting coins activation test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 MAIN: Accepting alpha warning'); await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await testActivateCoins(tester); await tester.pumpAndSettle(); - print('END COINS ACTIVATION TESTS'); + print('🔍 MAIN: Coins activation tests completed successfully'); }, semanticsEnabled: false); } diff --git a/test_integration/tests/wallets_tests/test_bitrefill_integration.dart b/test_integration/tests/wallets_tests/test_bitrefill_integration.dart index aa49679239..7fcf56920e 100644 --- a/test_integration/tests/wallets_tests/test_bitrefill_integration.dart +++ b/test_integration/tests/wallets_tests/test_bitrefill_integration.dart @@ -7,7 +7,7 @@ import 'package:web_dex/main.dart' as app; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_action_extensions.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; import 'wallet_tools.dart'; @@ -33,7 +33,7 @@ Future testBitrefillIntegration(WidgetTester tester) async { await goto.walletPage(tester); expect(totalAmount, findsOneWidget); - final bool isLtcVisible = await isWidgetVisible(tester, ltcActiveCoinItem); + final bool isLtcVisible = await tester.isWidgetVisible(ltcActiveCoinItem); if (!isLtcVisible) { await addAsset(tester, asset: ltcCoinSearchItem, search: ltcSearchTerm); await goto.dexPage(tester); @@ -42,11 +42,11 @@ Future testBitrefillIntegration(WidgetTester tester) async { await tester.pumpAndSettle(); expect(ltcActiveCoinItem, findsOneWidget); - await testerTap(tester, ltcActiveCoinItem); + await tester.tapAndPump(ltcActiveCoinItem); await tester.pumpAndSettle(); expect(bitrefillButton, findsOneWidget); - await testerTap(tester, bitrefillButton); + await tester.tapAndPump(bitrefillButton); await pause(msg: 'END TEST BITREFILL INTEGRATION'); } diff --git a/test_integration/tests/wallets_tests/test_cex_prices.dart b/test_integration/tests/wallets_tests/test_cex_prices.dart index 4f1da4b932..cdcc78dd9a 100644 --- a/test_integration/tests/wallets_tests/test_cex_prices.dart +++ b/test_integration/tests/wallets_tests/test_cex_prices.dart @@ -7,23 +7,24 @@ import 'package:web_dex/main.dart' as app; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_find_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; import 'wallet_tools.dart'; Future testCexPrices(WidgetTester tester) async { - print('TEST CEX PRICES'); - + print('🔍 CEX PRICES: Starting CEX prices test suite'); const String docByTicker = 'DOC'; const String kmdBep20ByTicker = 'KMD'; final Finder totalAmount = find.byKey( const Key('overview-total-balance'), ); - final Finder coinDetailsReturnButton = find.byKey( - const Key('back-button'), - ); + + // re-enable with coin details click + // final Finder coinDetailsReturnButton = find.byKey( + // const Key('back-button'), + // ); final Finder kmdBep20CoinActive = find.byKey( const Key('active-coin-item-kmd-bep20'), ); @@ -45,49 +46,74 @@ Future testCexPrices(WidgetTester tester) async { final Finder searchCoinsField = find.byKey( const Key('wallet-page-search-field'), ); + final Finder coinsList = find.byKeyName('wallet-page-scroll-view'); + + WidgetController.hitTestWarningShouldBeFatal = true; await goto.bridgePage(tester); - // Enter Wallet View + print('🔍 CEX PRICES: Navigated to bridge page'); await goto.walletPage(tester); + print('🔍 CEX PRICES: Navigated to wallet page'); expect(page, findsOneWidget); expect(totalAmount, findsOneWidget); await addAsset(tester, asset: docItem, search: docByTicker); + print('🔍 CEX PRICES: Added DOC asset'); + await addAsset(tester, asset: kmdBep20Item, search: kmdBep20ByTicker); + print('🔍 CEX PRICES: Added KMD-BEP20 asset'); try { expect(list, findsOneWidget); } on TestFailure { + print('🔍 CEX PRICES: List not found'); print('**Error** testCexPrices() list: $list'); } - // Check KMD-BEP20 cex price + print('🔍 CEX PRICES: Starting KMD-BEP20 price check'); final hasKmdBep20 = await filterAsset( tester, + assetScrollView: coinsList, asset: kmdBep20CoinActive, text: kmdBep20ByTicker, searchField: searchCoinsField, ); if (hasKmdBep20) { - await testerTap(tester, kmdBep20CoinActive); + await tester.dragUntilVisible( + kmdBep20CoinActive, + coinsList, + const Offset(0, -50), + ); + + // TODO: re-enable. Widget is found, but not tappable, despite being visible + // await tester.tapAndPump(kmdBep20CoinActive); + final Text text = kmdBep20Price.evaluate().single.widget as Text; final String? priceStr = text.data; final double? priceDouble = double.tryParse(priceStr ?? ''); + print('🔍 CEX PRICES: KMD-BEP20 price found: $priceStr'); expect(priceDouble != null && priceDouble > 0, true); - await testerTap(tester, coinDetailsReturnButton); + + // re-enable along with the coin tap above + // await tester.tapAndPump(coinDetailsReturnButton); + } else { + print('🔍 CEX PRICES: KMD-BEP20 not found in list'); } // Check DOC cex price (does not exist) // TODO: re-enable this after the doc/marty changes have been decided on - // await testerTap(tester, docCoinActive); + // await tester.tapAndPump(tester, docCoinActive); // expect(docPrice, findsNothing); await goto.walletPage(tester); await removeAsset(tester, asset: docItem, search: docByTicker); + print('🔍 CEX PRICES: Removed DOC asset'); + await removeAsset(tester, asset: kmdBep20Item, search: kmdBep20ByTicker); - await pause(msg: 'END TEST CEX PRICES'); + print('🔍 CEX PRICES: Removed KMD-BEP20 asset'); + await pause(msg: '🔍 CEX PRICES: Test completed'); } void main() { @@ -95,16 +121,21 @@ void main() { testWidgets( 'Run cex prices tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting CEX prices test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 MAIN: Accepting alpha warning'); await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await testCexPrices(tester); await tester.pumpAndSettle(); - print('END CEX PRICES TESTS'); + print('🔍 MAIN: CEX prices tests completed successfully'); }, semanticsEnabled: false, ); diff --git a/test_integration/tests/wallets_tests/test_coin_assets.dart b/test_integration/tests/wallets_tests/test_coin_assets.dart index f721a7b84d..2f1556900a 100644 --- a/test_integration/tests/wallets_tests/test_coin_assets.dart +++ b/test_integration/tests/wallets_tests/test_coin_assets.dart @@ -13,20 +13,30 @@ import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; Future testCoinIcons(WidgetTester tester) async { + print('🔍 COIN ICONS: Starting coin icons test'); + final Finder walletTab = find.byKey(const Key('main-menu-wallet')); final Finder addAssetsButton = find.byKey(const Key('add-assets-button')); await tester.tap(walletTab); + print('🔍 COIN ICONS: Tapped wallet tab'); await tester.pumpAndSettle(); + await tester.tap(addAssetsButton); + print('🔍 COIN ICONS: Tapped add assets button'); await tester.pumpAndSettle(); final listFinder = find.byKey(const Key('coins-manager-list')); - // Get the size of the list bool keepScrolling = true; + print('🔍 COIN ICONS: Starting icon verification loop'); + + int pageCount = 0; // Scroll down the list until we reach the end while (keepScrolling) { + pageCount++; + print('🔍 COIN ICONS: Checking page $pageCount'); + // Check the icons before scrolling final coinIcons = find .descendant(of: listFinder, matching: find.byType(CoinIcon)) @@ -35,13 +45,15 @@ Future testCoinIcons(WidgetTester tester) async { for (final coinIcon in coinIcons) { final coinAbr = abbr2Ticker(coinIcon.coinAbbr).toLowerCase(); - final assetPath = '$assetsPath/coin_icons/png/$coinAbr.png'; + final assetPath = '$coinsAssetsPath/coin_icons/png/$coinAbr.png'; final assetExists = await canLoadAsset(assetPath); - expect(assetExists, true, reason: 'Asset $assetPath does not exist'); + print('🔍 COIN ICONS: Checking asset for $coinAbr: ${assetExists ? "✓" : "✗"}'); + expect(assetExists, true, reason: 'Asset $coinsAssetsPath does not exist'); } - // Scoll the list + // Scroll the list await tester.drag(listFinder, const Offset(0, -500)); + print('🔍 COIN ICONS: Scrolled to next page'); await tester.pumpAndSettle(); // Check if we reached the end of the list @@ -50,6 +62,8 @@ Future testCoinIcons(WidgetTester tester) async { final maxScrollExtent = scrollable.controller!.position.maxScrollExtent; keepScrolling = currentPosition < maxScrollExtent; } + + print('🔍 COIN ICONS: Completed verification of all coin icons'); } Future canLoadAsset(String assetPath) async { @@ -57,6 +71,7 @@ Future canLoadAsset(String assetPath) async { try { final _ = await rootBundle.load(assetPath); } catch (e) { + print('🔍 ASSET CHECK: Failed to load asset: $assetPath'); assetExists = false; } return assetExists; @@ -65,15 +80,20 @@ Future canLoadAsset(String assetPath) async { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Run coin icons tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting coin icons test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 MAIN: Accepting alpha warning'); await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await testCoinIcons(tester); await tester.pumpAndSettle(); - print('END COINS ICONS TESTS'); + print('🔍 MAIN: Coin icons tests completed successfully'); }, semanticsEnabled: false); } diff --git a/test_integration/tests/wallets_tests/test_filters.dart b/test_integration/tests/wallets_tests/test_filters.dart index 8d0d69a612..3bf728ab37 100644 --- a/test_integration/tests/wallets_tests/test_filters.dart +++ b/test_integration/tests/wallets_tests/test_filters.dart @@ -9,6 +9,8 @@ import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; Future testFilters(WidgetTester tester) async { + print('🔍 FILTERS: Starting filters test'); + final Finder walletTab = find.byKey(const Key('main-menu-wallet')); final Finder addAssetsButton = find.byKey(const Key('add-assets-button')); final coinsManagerList = find.byKey(const Key('coins-manager-list')); @@ -23,36 +25,55 @@ Future testFilters(WidgetTester tester) async { find.descendant(of: coinsManagerList, matching: find.text('ERC-20')); await tester.tap(walletTab); + print('🔍 FILTERS: Tapped wallet tab'); await tester.pumpAndSettle(); + await tester.tap(addAssetsButton); + print('🔍 FILTERS: Tapped add assets button'); await tester.pumpAndSettle(); + await tester.tap(filtersButton); + print('🔍 FILTERS: Opened filters dropdown'); await tester.pumpAndSettle(); + await tester.tap(utxoFilterItem); + print('🔍 FILTERS: Applied UTXO filter'); await tester.pumpAndSettle(); + expect(bep20Items, findsNothing); expect(erc20Items, findsNothing); expect(utxoItems, findsWidgets); + print('🔍 FILTERS: Verified UTXO filter results'); + await tester.tap(utxoFilterItem); - await tester.tap(erc20FilterItem); + print('🔍 FILTERS: Removed UTXO filter'); + await tester.tap(erc20FilterItem); + print('🔍 FILTERS: Applied ERC20 filter'); await tester.pumpAndSettle(); + expect(bep20Items, findsNothing); expect(utxoItems, findsNothing); expect(erc20Items, findsWidgets); + print('🔍 FILTERS: Verified ERC20 filter results'); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run fliters tests:', (WidgetTester tester) async { + testWidgets('Run filters tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting filters test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 MAIN: Accepting alpha warning'); await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await testFilters(tester); await tester.pumpAndSettle(); - print('END FILTERS TESTS'); + print('🔍 MAIN: Filters tests completed successfully'); }, semanticsEnabled: false); } diff --git a/test_integration/tests/wallets_tests/test_withdraw.dart b/test_integration/tests/wallets_tests/test_withdraw.dart index 3ad3dd85e9..72be8b2c64 100644 --- a/test_integration/tests/wallets_tests/test_withdraw.dart +++ b/test_integration/tests/wallets_tests/test_withdraw.dart @@ -6,53 +6,78 @@ import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; -import '../../common/pause.dart'; -import '../../common/pump_and_settle.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_find_extension.dart'; +import '../../common/widget_tester_pump_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/get_funded_wif.dart'; import '../../helpers/restore_wallet.dart'; import 'wallet_tools.dart'; Future testWithdraw(WidgetTester tester) async { - print('TEST WITHDRAW'); + try { + print('🔍 WITHDRAW TEST: Starting withdraw test suite'); + Finder martyCoinItem = await _activateMarty(tester); + print('🔍 WITHDRAW TEST: Marty coin activated'); + + await _testCopyAddressButton(tester); + print('🔍 WITHDRAW TEST: Copy address button test completed'); + + await _sendAmountToAddress(tester, address: getRandomAddress()); + print('🔍 WITHDRAW TEST: Amount sent to address'); + + await _confirmSendAmountToAddress(tester); + print('🔍 WITHDRAW TEST: Send amount confirmed'); + + await removeAsset(tester, asset: martyCoinItem, search: 'marty'); + print('🔍 WITHDRAW TEST: Asset removed'); + + print('🔍 WITHDRAW TEST: All tests completed successfully'); + } catch (e, s) { + print('❌ WITHDRAW TEST: Error occurred during testing'); + print(e); + print(s); + rethrow; + } +} - final Finder martyCoinItem = find.byKey( - const Key('coins-manager-list-item-marty'), - ); - final Finder martyCoinActive = find.byKey( - const Key('active-coin-item-marty'), +Future _activateMarty(WidgetTester tester) async { + print('🔍 ACTIVATE MARTY: Starting activation process'); + + final Finder coinsList = find.byKeyName('wallet-page-scroll-view'); + final Finder martyCoinItem = find.byKeyName('coins-manager-list-item-marty'); + final Finder martyCoinActive = find.byKeyName('active-coin-item-marty'); + final Finder coinBalance = find.byKeyName('coin-details-balance'); + + await addAsset(tester, asset: martyCoinItem, search: 'marty'); + print('🔍 ACTIVATE MARTY: Asset added'); + + await tester.pumpUntilVisible( + martyCoinActive, + timeout: const Duration(seconds: 30), + throwOnError: false, ); + print('🔍 ACTIVATE MARTY: Waited for coin to become visible'); + + await tester.dragUntilVisible( + martyCoinActive, coinsList, const Offset(0, -50)); + print('🔍 ACTIVATE MARTY: Scrolled to coin'); + + await tester.tapAndPump(martyCoinActive); + print('🔍 ACTIVATE MARTY: Tapped on coin'); + + await tester.pumpAndSettle(); + expect(coinBalance, findsOneWidget); + print('🔍 ACTIVATE MARTY: Activation completed'); + return martyCoinItem; +} + +Future _testCopyAddressButton(WidgetTester tester) async { + print('🔍 COPY ADDRESS: Starting copy address test'); + final Finder coinBalance = find.byKey( const Key('coin-details-balance'), ); - final Finder sendButton = find.byKey( - const Key('coin-details-send-button'), - ); - final Finder addressInput = find.byKey( - const Key('withdraw-recipient-address-input'), - ); - final Finder amountInput = find.byKey( - const Key('enter-form-amount-input'), - ); - final Finder sendEnterButton = find.byKey( - const Key('send-enter-button'), - ); - final Finder confirmBackButton = find.byKey( - const Key('confirm-back-button'), - ); - final Finder confirmAgreeButton = find.byKey( - const Key('confirm-agree-button'), - ); - final Finder completeButtons = find.byKey( - const Key('complete-buttons'), - ); - final Finder viewOnExplorerButton = find.byKey( - const Key('send-complete-view-on-explorer'), - ); - final Finder doneButton = find.byKey( - const Key('send-complete-done'), - ); final Finder exitButton = find.byKey( const Key('back-button'), ); @@ -63,15 +88,6 @@ Future testWithdraw(WidgetTester tester) async { const Key('coin-details-address-field'), ); - await addAsset(tester, asset: martyCoinItem, search: 'marty'); - await pause(sec: 5); - await tester.pumpNFrames(20); - - expect(martyCoinActive, findsOneWidget); - await testerTap(tester, martyCoinActive); - - expect(coinBalance, findsOneWidget); - final AutoScrollText text = coinBalance.evaluate().single.widget as AutoScrollText; @@ -79,44 +95,74 @@ Future testWithdraw(WidgetTester tester) async { final double? priceDouble = double.tryParse(priceStr); expect(priceDouble != null && priceDouble > 0, true); expect(receiveButton, findsOneWidget); + await tester.tapAndPump(receiveButton); + print('🔍 COPY ADDRESS: Tapped receive button'); - await testerTap(tester, receiveButton); expect(copyAddressButton, findsOneWidget); - expect(copyAddressButton, findsOneWidget); - - await testerTap(tester, exitButton); - expect(sendButton, findsOneWidget); + await tester.tapAndPump(exitButton); + print('🔍 COPY ADDRESS: Copy address test completed'); +} - await testerTap(tester, sendButton); - expect(addressInput, findsOneWidget); - expect(amountInput, findsOneWidget); - expect(sendEnterButton, findsOneWidget); +Future _confirmSendAmountToAddress(WidgetTester tester) async { + print('🔍 CONFIRM SEND: Starting send confirmation'); - await testerTap(tester, addressInput); - await enterText(tester, finder: addressInput, text: getRandomAddress()); - await testerTap(tester, amountInput); - await enterText(tester, finder: amountInput, text: '0.01'); - await testerTap(tester, sendEnterButton); - await tester.pumpAndSettle(); // skip all loading & transition frames + final confirmBackButton = find.byKeyName('confirm-back-button'); + final confirmAgreeButton = find.byKeyName('confirm-agree-button'); + final completeButtons = find.byKeyName('complete-buttons'); + final viewOnExplorerButton = find.byKeyName('send-complete-view-on-explorer'); + final doneButton = find.byKeyName('send-complete-done'); + final exitButton = find.byKeyName('back-button'); expect(confirmBackButton, findsOneWidget); expect(confirmAgreeButton, findsOneWidget); - await testerTap(tester, confirmAgreeButton); - await tester.pumpAndSettle(); // skip all loading & transition frames + await tester.tapAndPump(confirmAgreeButton); + print('🔍 CONFIRM SEND: Agreed to confirmation'); + await tester.pumpAndSettle(); expect(completeButtons, findsOneWidget); expect(viewOnExplorerButton, findsOneWidget); expect(doneButton, findsOneWidget); - await testerTap(tester, doneButton); - await tester.pumpAndSettle(); // skip all loading & transition frames + await tester.tapAndPump(doneButton); + print('🔍 CONFIRM SEND: Tapped done button'); + await tester.pumpAndSettle(); expect(exitButton, findsOneWidget); - await testerTap(tester, exitButton); - await tester.pumpAndSettle(); // skip all loading & transition frames + await tester.tapAndPump(exitButton); + print('🔍 CONFIRM SEND: Confirmation completed'); + await tester.pumpAndSettle(); +} + +Future _sendAmountToAddress( + WidgetTester tester, { + String amount = '0.01', + required String address, +}) async { + print('🔍 SEND AMOUNT: Starting send amount process'); - await removeAsset(tester, asset: martyCoinItem, search: 'marty'); + final sendButton = find.byKeyName('coin-details-send-button'); + final addressInput = find.byKeyName('withdraw-recipient-address-input'); + final amountInput = find.byKeyName('enter-form-amount-input'); + final sendEnterButton = find.byKeyName('send-enter-button'); - print('END TEST WITHDRAW'); + expect(sendButton, findsOneWidget); + await tester.tapAndPump(sendButton); + print('🔍 SEND AMOUNT: Tapped send button'); + + expect(addressInput, findsOneWidget); + expect(amountInput, findsOneWidget); + expect(sendEnterButton, findsOneWidget); + + await tester.tapAndPump(addressInput); + await enterText(tester, finder: addressInput, text: address); + print('🔍 SEND AMOUNT: Entered address: $address'); + + await tester.tapAndPump(amountInput); + await enterText(tester, finder: amountInput, text: amount); + print('🔍 SEND AMOUNT: Entered amount: $amount'); + + await tester.tapAndPump(sendEnterButton); + print('🔍 SEND AMOUNT: Send process completed'); + await tester.pumpAndSettle(); } void main() { @@ -124,16 +170,21 @@ void main() { testWidgets( 'Run withdraw tests:', (WidgetTester tester) async { + print('🔍 MAIN: Starting withdraw test suite'); tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('ACCEPT ALPHA WARNING'); + + print('🔍 MAIN: Accepting alpha warning'); await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + print('🔍 MAIN: Wallet restored'); + await testWithdraw(tester); await tester.pumpAndSettle(); - print('END WITHDARW TESTS'); + print('🔍 MAIN: Withdraw tests completed successfully'); }, semanticsEnabled: false, ); diff --git a/test_integration/tests/wallets_tests/wallet_tools.dart b/test_integration/tests/wallets_tests/wallet_tools.dart index b0688b5ec7..894026e0e8 100644 --- a/test_integration/tests/wallets_tests/wallet_tools.dart +++ b/test_integration/tests/wallets_tests/wallet_tools.dart @@ -5,14 +5,16 @@ import 'package:flutter_test/flutter_test.dart'; import '../../common/goto.dart' as goto; import '../../common/pause.dart'; -import '../../common/pump_and_settle.dart'; -import '../../common/tester_utils.dart'; +import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_pump_extension.dart'; Future removeAsset( WidgetTester tester, { required Finder asset, required String search, }) async { + print('🔍 REMOVE ASSET: Starting remove asset flow'); + final Finder removeAssetsButton = find.byKey( const Key('remove-assets-button'), ); @@ -27,9 +29,10 @@ Future removeAsset( ); await goto.walletPage(tester); + print('🔍 REMOVE ASSET: Navigated to wallet page'); - await tester.tap(removeAssetsButton); - await tester.pumpNFrames(10); + await tester.tapAndPump(removeAssetsButton); + print('🔍 REMOVE ASSET: Tapped remove assets button'); expect(list, findsOneWidget); try { @@ -39,23 +42,28 @@ Future removeAsset( } await enterText(tester, finder: searchCoinsField, text: search); + print('🔍 REMOVE ASSET: Entered search text: $search'); try { expect(asset, findsOneWidget); } on TestFailure { + print('🔍 REMOVE ASSET: Asset not found initially, attempting to scroll'); print('**Error** removeAsset([$asset])'); await tester.dragUntilVisible(asset, list, const Offset(0, -5)); await tester.pumpAndSettle(); } - await testerTap(tester, asset); + await tester.tapAndPump(asset); + print('🔍 REMOVE ASSET: Tapped on asset'); try { expect(switchButton, findsOneWidget); } on TestFailure { + print('🔍 REMOVE ASSET: Switch button not found'); print('**Error** removeAsset(): switchButton: $switchButton'); } - await testerTap(tester, switchButton); + await tester.tapAndPump(switchButton); + print('🔍 REMOVE ASSET: Tapped switch button'); await pause(sec: 5); } @@ -64,6 +72,8 @@ Future addAsset( required Finder asset, required String search, }) async { + print('🔍 ADD ASSET: Starting add asset flow'); + final Finder list = find.byKey( const Key('coins-manager-list'), ); @@ -78,15 +88,19 @@ Future addAsset( ); await goto.walletPage(tester); + print('🔍 ADD ASSET: Navigated to wallet page'); try { expect(asset, findsNothing); } on TestFailure { + print('🔍 ADD ASSET: Asset already exists, skipping add'); // asset already created return; } - await testerTap(tester, addAssetsButton); + await tester.tapAndPump(addAssetsButton); + print('🔍 ADD ASSET: Tapped add assets button'); + try { expect(searchCoinsField, findsOneWidget); } on TestFailure { @@ -94,40 +108,50 @@ Future addAsset( } await enterText(tester, finder: searchCoinsField, text: search); + print('🔍 ADD ASSET: Entered search text: $search'); await tester.dragUntilVisible( asset, list, const Offset(-250, 0), ); - await tester.pumpAndSettle(); - await tester.tap(asset); + print('🔍 ADD ASSET: Scrolled to make asset visible'); + await tester.tapAndPump(asset); + print('🔍 ADD ASSET: Tapped on asset'); try { expect(switchButton, findsOneWidget); } on TestFailure { + print('🔍 ADD ASSET: Switch button not found'); print('**Error** addAsset(): switchButton: $switchButton'); } - await tester.tap(switchButton); - await tester.pumpAndSettle(); + await tester.tapAndPump(switchButton); + print('🔍 ADD ASSET: Tapped switch button'); } Future filterAsset( WidgetTester tester, { required Finder asset, + required Finder assetScrollView, required String text, required Finder searchField, }) async { + print('🔍 FILTER ASSET: Starting filter with text: $text'); + await enterText(tester, finder: searchField, text: text); + print('🔍 FILTER ASSET: Entered filter text'); await tester.pumpAndSettle(); try { + await tester.dragUntilVisible(asset, assetScrollView, const Offset(0, -50)); expect(asset, findsOneWidget); } on TestFailure { + print('🔍 FILTER ASSET: Asset not found after filtering'); await pause(msg: '**Error** filterAsset([$asset, $text])'); return false; } + print('🔍 FILTER ASSET: Successfully filtered asset'); return true; } @@ -135,8 +159,9 @@ Future enterText( WidgetTester tester, { required Finder finder, required String text, + int frames = 60, }) async { await tester.enterText(finder, text); - await tester.pumpNFrames(10); + await tester.pumpNFrames(frames); await pause(); } diff --git a/test_integration/tests/wallets_tests/wallets_tests.dart b/test_integration/tests/wallets_tests/wallets_tests.dart index 577ae94aed..701bfccf7c 100644 --- a/test_integration/tests/wallets_tests/wallets_tests.dart +++ b/test_integration/tests/wallets_tests/wallets_tests.dart @@ -15,29 +15,36 @@ import 'test_withdraw.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets( + walletsWidgetTests(); +} + +void walletsWidgetTests({ + bool skip = false, + int retryLimit = 0, + Duration timeout = const Duration(minutes: 10), +}) { + return testWidgets( 'Run wallet tests:', (WidgetTester tester) async { tester.testTextInput.register(); await app.main(); await tester.pumpAndSettle(); - print('RESTORE WALLET TO TEST'); await acceptAlphaWarning(tester); await restoreWalletToTest(tester); - await tester.pumpAndSettle(); await testCoinIcons(tester); - await tester.pumpAndSettle(); await testActivateCoins(tester); - await tester.pumpAndSettle(); await testCexPrices(tester); - await tester.pumpAndSettle(); await testWithdraw(tester); - await tester.pumpAndSettle(); await testFilters(tester); - print('END WALLET TESTS'); + // Disabled until the bitrefill feature is re-enabled + // await tester.pumpAndSettle(); + // await testBitrefillIntegration(tester); }, semanticsEnabled: false, + timeout: Timeout(timeout), + retry: retryLimit, + skip: skip, ); } diff --git a/test_units/tests/helpers/calculate_buy_amount_test.dart b/test_units/tests/helpers/calculate_buy_amount_test.dart index 79a62a7d86..db85ac8546 100644 --- a/test_units/tests/helpers/calculate_buy_amount_test.dart +++ b/test_units/tests/helpers/calculate_buy_amount_test.dart @@ -8,7 +8,7 @@ void testCalculateBuyAmount() { final BestOrder bestOrder = BestOrder( price: Rational.fromInt(2), maxVolume: Rational.fromInt(3), - address: '', + address: const OrderAddress.transparent(''), coin: 'KMD', minVolume: Rational.fromInt(1), uuid: '', @@ -16,62 +16,75 @@ void testCalculateBuyAmount() { expect( calculateBuyAmount( - sellAmount: Rational.fromInt(2), selectedOrder: bestOrder), + sellAmount: Rational.fromInt(2), + selectedOrder: bestOrder, + ), Rational.fromInt(4), ); expect( calculateBuyAmount( - sellAmount: Rational.parse('0.1'), selectedOrder: bestOrder), + sellAmount: Rational.parse('0.1'), + selectedOrder: bestOrder, + ), Rational.parse('0.2'), ); expect( calculateBuyAmount( - sellAmount: Rational.parse('1e-30'), selectedOrder: bestOrder), + sellAmount: Rational.parse('1e-30'), + selectedOrder: bestOrder, + ), Rational.parse('2e-30'), ); final BestOrder bestOrder2 = BestOrder( price: Rational.parse('1e-30'), maxVolume: Rational.fromInt(100), - address: '', + address: const OrderAddress.transparent(''), coin: 'KMD', minVolume: Rational.fromInt(1), uuid: '', ); expect( calculateBuyAmount( - sellAmount: Rational.parse('1e-30'), selectedOrder: bestOrder2), + sellAmount: Rational.parse('1e-30'), + selectedOrder: bestOrder2, + ), Rational.parse('1e-60'), ); expect( calculateBuyAmount( - sellAmount: Rational.parse('1e70'), selectedOrder: bestOrder2), + sellAmount: Rational.parse('1e70'), + selectedOrder: bestOrder2, + ), Rational.parse('1e40'), ); expect( calculateBuyAmount( - sellAmount: Rational.parse('123456789012345678901234567890'), - selectedOrder: bestOrder2), + sellAmount: Rational.parse('123456789012345678901234567890'), + selectedOrder: bestOrder2, + ), Rational.parse('0.123456789012345678901234567890'), ); final BestOrder bestOrder3 = BestOrder( price: Rational.parse('1e10'), maxVolume: Rational.fromInt(100), - address: '', + address: const OrderAddress.transparent(''), coin: 'KMD', minVolume: Rational.fromInt(1), uuid: '', ); expect( calculateBuyAmount( - sellAmount: Rational.parse('12345678901234567890123456789'), - selectedOrder: bestOrder3), + sellAmount: Rational.parse('12345678901234567890123456789'), + selectedOrder: bestOrder3, + ), Rational.parse('12345678901234567890123456789e10'), ); expect( calculateBuyAmount( - sellAmount: Rational.parse('12345678901234567890123456789e20'), - selectedOrder: bestOrder3), + sellAmount: Rational.parse('12345678901234567890123456789e20'), + selectedOrder: bestOrder3, + ), Rational.parse('12345678901234567890123456789e30'), ); }); @@ -79,17 +92,22 @@ void testCalculateBuyAmount() { final BestOrder bestOrder = BestOrder( price: Rational.fromInt(2), maxVolume: Rational.fromInt(3), - address: '', + address: const OrderAddress.transparent(''), coin: 'KMD', minVolume: Rational.fromInt(1), uuid: '', ); expect(calculateBuyAmount(sellAmount: null, selectedOrder: null), isNull); expect( - calculateBuyAmount( - sellAmount: Rational.fromInt(2), selectedOrder: null), - isNull); + calculateBuyAmount( + sellAmount: Rational.fromInt(2), + selectedOrder: null, + ), + isNull, + ); expect( - calculateBuyAmount(sellAmount: null, selectedOrder: bestOrder), isNull); + calculateBuyAmount(sellAmount: null, selectedOrder: bestOrder), + isNull, + ); }); } diff --git a/web/assets/template.html b/web/assets/template.html deleted file mode 100644 index a9ef9f74b1..0000000000 --- a/web/assets/template.html +++ /dev/null @@ -1,198 +0,0 @@ -<%= htmlWebpackPlugin.options.warning %> - - - - - - - - - - Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Komodo Wallet is starting... Please wait. -
- - - - - - - - \ No newline at end of file diff --git a/web/flutter_bootstrap.js b/web/flutter_bootstrap.js deleted file mode 100644 index fc8ba6f6d3..0000000000 --- a/web/flutter_bootstrap.js +++ /dev/null @@ -1,24 +0,0 @@ -{{flutter_js}} -{{flutter_build_config}} - -_flutter.loader.load({ - serviceWorkerSettings: { - serviceWorkerVersion: {{flutter_service_worker_version}}, - }, - config: { - 'hostElement': document.querySelector('#main-content'), - canvasKitBaseUrl: "/canvaskit/", - fontFallbackBaseUrl: "/assets/fallback_fonts/", - }, - onEntrypointLoaded: async function (engineInitializer) { - console.log('Flutter entrypoint loaded'); - const appRunner = await engineInitializer.initializeEngine(); - document.querySelector('#loading')?.classList.add('main_done'); - - return appRunner.runApp(); - - // NB: The code to remove the loading spinner is in the Flutter app. - // This allows the Flutter app to control the timing of the spinner removal. - - } -}); diff --git a/web/index.html b/web/index.html index 7aa020ed53..e487afc40e 100644 --- a/web/index.html +++ b/web/index.html @@ -1,12 +1,150 @@ -NB! This file is generated automatically as part of the build process in `./packages/komodo_wallet_build_transformer`. -Do not edit it manually. + + -If you need to manually rebuild the file, you can run the following command: + + + + + + + Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -```bash -npm install && npm run build -``` + + + + + +
+ Komodo Wallet is starting... Please wait. +
+ + + + + + \ No newline at end of file diff --git a/web/index.js b/web/index.js deleted file mode 100644 index ac80dfcb17..0000000000 --- a/web/index.js +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-check -// Use ES module import syntax to import functionality from the module -// that we have compiled. -// -// Note that the `default` import is an initialization function which -// will "boot" the module and make it ready to use. Currently browsers -// don't support natively imported WebAssembly as an ES module, but -// eventually the manual initialization won't be required! -import init, { LogLevel, Mm2MainErr, Mm2RpcErr, mm2_main, mm2_main_status, mm2_rpc, mm2_version } from "./src/mm2/kdflib.js"; -import './services/theme_checker/theme_checker.js'; -import zip from './services/zip/zip.js'; - -const LOG_LEVEL = LogLevel.Info; - -// Loads the wasm file, so we use the -// default export to inform it where the wasm file is located on the -// server, and then we wait on the returned promise to wait for the -// wasm to be loaded. -// @ts-ignore -window.init_wasm = async function () { - await init(); -} - -// @ts-ignore -window.run_mm2 = async function (params, handle_log) { - let config = { - conf: JSON.parse(params), - log_level: LOG_LEVEL, - } - - // run an MM2 instance - try { - mm2_main(config, handle_log); - } catch (e) { - switch (e) { - case Mm2MainErr.AlreadyRuns: - alert("MM2 already runs, please wait..."); - break; - case Mm2MainErr.InvalidParams: - alert("Invalid config"); - break; - case Mm2MainErr.NoCoinsInConf: - alert("No 'coins' field in config"); - break; - default: - alert(`Oops: ${e}`); - break; - } - handle_log(LogLevel.Error, JSON.stringify(e)) - } -} -// @ts-ignore -window.rpc_request = async function (request_js) { - try { - let reqJson = JSON.parse(request_js); - const response = await mm2_rpc(reqJson); - return JSON.stringify(response); - } catch (e) { - switch (e) { - case Mm2RpcErr.NotRunning: - alert("MM2 is not running yet"); - break; - case Mm2RpcErr.InvalidPayload: - alert(`Invalid payload: ${request_js}`); - break; - case Mm2RpcErr.InternalError: - alert(`An MM2 internal error`); - break; - default: - alert(`Unexpected error: ${e}`); - break; - } - throw (e); - } -} - - -// @ts-ignore -window.mm2_version = () => mm2_version().result; - -// @ts-ignore -window.mm2_status = function () { - return mm2_main_status(); -} -// @ts-ignore -window.reload_page = function () { - window.location.reload(); -} - -// @ts-ignore -window.zip_encode = zip.encode; \ No newline at end of file diff --git a/web/kdf/res/kdf_wrapper.dart b/web/kdf/res/kdf_wrapper.dart new file mode 100644 index 0000000000..1acf13e7b5 --- /dev/null +++ b/web/kdf/res/kdf_wrapper.dart @@ -0,0 +1,94 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:async'; +// This is a web-specific file, so it's safe to ignore this warning +// ignore: avoid_web_libraries_in_flutter +import 'dart:js' as js; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:js/js_util.dart'; + +class KdfPlugin { + static void registerWith(Registrar registrar) { + final plugin = KdfPlugin(); + // ignore: unused_local_variable + final channel = MethodChannel( + 'komodo_defi_framework/kdf', + const StandardMethodCodec(), + registrar, + )..setMethodCallHandler(plugin.handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'ensureLoaded': + return _ensureLoaded(); + case 'mm2Main': + final args = call.arguments as Map; + return _mm2Main( + args['conf'] as String, + args['logCallback'] as Function, + ); + case 'mm2MainStatus': + return _mm2MainStatus(); + case 'mm2Stop': + return _mm2Stop(); + default: + throw PlatformException( + code: 'Unimplemented', + details: 'Method ${call.method} not implemented', + ); + } + } + + bool _libraryLoaded = false; + Future? _loadPromise; + + Future _ensureLoaded() async { + if (_loadPromise != null) return _loadPromise; + + _loadPromise = _loadLibrary(); + await _loadPromise; + } + + Future _loadLibrary() async { + if (_libraryLoaded) return; + + final completer = Completer(); + + final script = + js.context['document'].callMethod('createElement', ['script']); + script['src'] = 'kdf/kdflib.js'; + script['onload'] = js.allowInterop(() { + _libraryLoaded = true; + completer.complete(); + }); + script['onerror'] = js.allowInterop((event) { + completer.completeError('Failed to load kdflib.js'); + }); + + js.context['document']['head'].callMethod('appendChild', [script]); + + return completer.future; + } + + Future _mm2Main(String conf, Function logCallback) async { + await _ensureLoaded(); + return dartify( + js.context.callMethod('mm2_main', [conf, js.allowInterop(logCallback)]), + )! as int; + } + + int _mm2MainStatus() { + if (!_libraryLoaded) { + throw StateError('KDF library not loaded. Call ensureLoaded() first.'); + } + return js.context.callMethod('mm2_main_status') as int; + } + + Future _mm2Stop() async { + await _ensureLoaded(); + return js.context.callMethod('mm2_stop') as int; + } +} diff --git a/web/kdf/res/kdflib_bootstrapper.js b/web/kdf/res/kdflib_bootstrapper.js new file mode 100644 index 0000000000..9c92413cfd --- /dev/null +++ b/web/kdf/res/kdflib_bootstrapper.js @@ -0,0 +1,79 @@ +// @ts-check +import init, { LogLevel } from "../kdf/bin/kdflib.js"; +import * as kdflib from "../kdf/bin/kdflib.js"; + +const LOG_LEVEL = LogLevel.Info; + +// Create a global 'kdf' object +const kdf = {}; + +// Initialization state +kdf._initPromise = null; +kdf._isInitializing = false; +kdf.isInitialized = false; + +// Loads the wasm file, so we use the +// default export to inform it where the wasm file is located on the +// server, and then we wait on the returned promise to wait for the +// wasm to be loaded. +// @ts-ignore +kdf.init_wasm = async function () { + if (kdf.isInitialized) { + // If already initialized, return immediately + return; + } + + if (kdf._initPromise) { + // If already initializing, await the existing promise + return await kdf._initPromise; + } + if (kdf._isInitializing) { + // If already initializing (but no promise yet), return a pending promise + return new Promise((resolve, reject) => { + const checkInitialization = () => { + if (kdf._initPromise) { + kdf._initPromise.then(resolve).catch(reject); + } else { + setTimeout(checkInitialization, 50); + } + }; + checkInitialization(); + }); + } + + kdf._isInitializing = true; + kdf._initPromise = init() + .then(() => { + kdf._isInitializing = false; + kdf._initPromise = null; + kdf.isInitialized = true; + }) + .catch((error) => { + kdf._isInitializing = false; + kdf._initPromise = null; + throw error; + }); + + return await kdf._initPromise; +} + + + +// @ts-ignore +kdf.reload_page = function () { + window.location.reload(); +} + +// @ts-ignore +// kdf.zip_encode = zip.encode; + + +Object.assign(kdf, kdflib); + +kdf.init_wasm().catch(console.error); + +// @ts-ignore +window.kdf = kdf; + +export default kdf; +export { kdf }; diff --git a/web/services/zip/zip.js b/web/services/zip/zip.js deleted file mode 100644 index c5053fc815..0000000000 --- a/web/services/zip/zip.js +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-check -class Zip { - constructor() { - this.encode = this.encode.bind(this); - this.worker = new Worker(new URL('./zip_worker.js', import.meta.url)); - } - - /** - * @param {String} fileName - * @param {String} fileContent - * @returns {Promise} - */ - async encode(fileName, fileContent) { - /** @type {Worker} */ - return new Promise((resolve, reject) => { - this.worker.postMessage({ fileContent, fileName }); - this.worker.onmessage = (event) => resolve(event.data); - this.worker.onerror = (e) => reject(e); - }).catch((e) => { - return null; - }); - } -} -/** @type {Zip} */ -const zip = new Zip(); - -export default zip; \ No newline at end of file diff --git a/web/services/zip/zip_worker.js b/web/services/zip/zip_worker.js deleted file mode 100644 index acb6868c4e..0000000000 --- a/web/services/zip/zip_worker.js +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -import 'jszip'; -import JSZip from 'jszip'; -/** @param {MessageEvent<{fileContent: String, fileName: String}>} event */ -onmessage = async (event) => { - const zip = new JSZip(); - const textContent = event.data.fileContent; - const fileName = event.data.fileName; - - zip.file(fileName, textContent); - const compressed = await zip.generateAsync({ - type: "base64", - compression: "DEFLATE", - compressionOptions: { - level: 9 - }, - }); - postMessage(compressed); -}; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index c71791fdda..0000000000 --- a/webpack.config.js +++ /dev/null @@ -1,33 +0,0 @@ -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); - -module.exports = (env, argv) => { - return { - entry: './web/index.js', - output: { - path: path.resolve(__dirname, 'web/dist'), - filename: 'script.[contenthash].js', - }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.resolve(__dirname, 'web/assets/template.html'), - filename: '../index.html', - warning: '\n\n', - - minify: { - collapseWhitespace: true, - keepClosingSlash: true, - removeRedundantAttributes: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributes: true, - useShortDoctype: true, - minifyCSS: true, - minifyJS: true, - }, - }), - new CleanWebpackPlugin(), - ], - devtool: argv.mode == 'development' ? 'source-map' : false, - } -}; \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 91ae331f0e..da8127b416 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -17,6 +19,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c759be6653..1117ebfecf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,12 +5,15 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core flutter_inappwebview_windows + flutter_secure_storage_windows + local_auth_windows share_plus url_launcher_windows window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + komodo_defi_framework ) set(PLUGIN_BUNDLED_LIBRARIES) From 5524c62cc679910d72a6e6a9d8d59d220e4414e1 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 9 Dec 2024 10:32:40 +0200 Subject: [PATCH 16/34] bump sdk version (#190) --- pubspec.lock | 14 ++++----- web/kdf/res/kdf_wrapper.dart | 56 ++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 0b1211a301..758717de1d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -652,7 +652,7 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -661,7 +661,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0" @@ -670,7 +670,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -679,7 +679,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -688,7 +688,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -697,7 +697,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -720,7 +720,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "53103feb7b14c18a1e375e3f98d020f363886801" + resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" diff --git a/web/kdf/res/kdf_wrapper.dart b/web/kdf/res/kdf_wrapper.dart index 1acf13e7b5..b6793c4a31 100644 --- a/web/kdf/res/kdf_wrapper.dart +++ b/web/kdf/res/kdf_wrapper.dart @@ -3,11 +3,12 @@ import 'dart:async'; // This is a web-specific file, so it's safe to ignore this warning // ignore: avoid_web_libraries_in_flutter -import 'dart:js' as js; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart'; class KdfPlugin { static void registerWith(Registrar registrar) { @@ -57,38 +58,57 @@ class KdfPlugin { final completer = Completer(); - final script = - js.context['document'].callMethod('createElement', ['script']); - script['src'] = 'kdf/kdflib.js'; - script['onload'] = js.allowInterop(() { - _libraryLoaded = true; - completer.complete(); - }); - script['onerror'] = js.allowInterop((event) { - completer.completeError('Failed to load kdflib.js'); - }); + final script = (document.createElement('script') as HTMLScriptElement) + ..src = 'kdf/kdflib.js' + ..onload = () { + _libraryLoaded = true; + completer.complete(); + }.toJS + ..onerror = (event) { + completer.completeError('Failed to load kdflib.js'); + }.toJS; - js.context['document']['head'].callMethod('appendChild', [script]); + document.head!.appendChild(script); return completer.future; } Future _mm2Main(String conf, Function logCallback) async { await _ensureLoaded(); - return dartify( - js.context.callMethod('mm2_main', [conf, js.allowInterop(logCallback)]), - )! as int; + + try { + final jsCallback = logCallback.toJS; + final jsResponse = globalContext.callMethod( + 'mm2_main'.toJS, + [conf.toJS, jsCallback].toJS, + ); + if (jsResponse == null) { + throw Exception('mm2_main call returned null'); + } + + final dynamic dartResponse = (jsResponse as JSAny?).dartify(); + if (dartResponse == null) { + throw Exception('Failed to convert mm2_main response to Dart'); + } + + return dartResponse as int; + } catch (e) { + throw Exception('Error in mm2_main: $e\nConfig: $conf'); + } } int _mm2MainStatus() { if (!_libraryLoaded) { throw StateError('KDF library not loaded. Call ensureLoaded() first.'); } - return js.context.callMethod('mm2_main_status') as int; + + final jsResult = globalContext.callMethod('mm2_main_status'.toJS); + return jsResult.dartify()! as int; } Future _mm2Stop() async { await _ensureLoaded(); - return js.context.callMethod('mm2_stop') as int; + final jsResult = globalContext.callMethod('mm2_stop'.toJS); + return jsResult.dartify()! as int; } } From 708e1652dd0f8c6b052e5317ff61b1484c7593dd Mon Sep 17 00:00:00 2001 From: smk762 <35845239+smk762@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:33:00 +0800 Subject: [PATCH 17/34] update kdf hashes in build_config (#189) --- app_build/build_config.json | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/app_build/build_config.json b/app_build/build_config.json index 0aa3925d27..8d75d8471f 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -1,4 +1,64 @@ { + "api": { + "api_commit_hash": "bb749a99344fb61e92410cb64b035dba1de35779", + "branch": "main", + "fetch_at_build_enabled": true, + "source_urls": [ + "https://api.github.com/repos/KomodoPlatform/komodo-defi-framework", + "https://sdk.devbuilds.komodo.earth" + ], + "platforms": { + "web": { + "matching_keyword": "wasm", + "valid_zip_sha256_checksums": [ + "877d42c9051c060609b969aa3dfa550d73ae24f53153dea08143d820e7d317c7" + ], + "path": "web/src/mm2" + }, + "ios": { + "matching_keyword": "ios-aarch64", + "valid_zip_sha256_checksums": [ + "545e657f6d15a1bd552e59f6006811f84876ae39837b6b68a39b1eec3094f8dc" + ], + "path": "ios" + }, + "macos": { + "matching_keyword": "Darwin-Release", + "valid_zip_sha256_checksums": [ + "62c3ef36cb3cd4fc166d381d7efce6b67d3b5dc62de4080bfb39462c0c4efb48" + ], + "path": "macos" + }, + "windows": { + "matching_keyword": "Win64", + "valid_zip_sha256_checksums": [ + "88a723d584ef10e528b11a16327a6bcdf25867cdb6c7e6afadc77a62c8bed7dc" + ], + "path": "windows/runner/exe" + }, + "android-armv7": { + "matching_keyword": "android-armv7", + "valid_zip_sha256_checksums": [ + "bfc294ece8481aabedf21b2d3e785eedc9719f066b396da91811427fa857f2b3" + ], + "path": "android/app/src/main/cpp/libs/armeabi-v7a" + }, + "android-aarch64": { + "matching_keyword": "android-aarch64", + "valid_zip_sha256_checksums": [ + "bdead61715e5f95e5f1d842691b7ee19b8dc9ef780bc13f1207589214ff0fc24" + ], + "path": "android/app/src/main/cpp/libs/arm64-v8a" + }, + "linux": { + "matching_keyword": "Linux-Release", + "valid_zip_sha256_checksums": [ + "e8c4cc2b6fbddc229820e7868591b757e223a47553a66755131330345a624093" + ], + "path": "linux/mm2" + } + } + }, "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, From 01bed5c9607698b1d693097c1d546e3248a40a6e Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:26:42 +0100 Subject: [PATCH 18/34] Merge coin icon fallback Add coin icon fallback for other widgets referencing the local image icons. Now all coin icons in the app share the same widget. Fallback to remote images CDN for missing coin icons Further coin icon fallback bug fixes Prevent flicker for fallback coin icons Prevent flicker for fallback coin icons by caching the status of the existence on the CDN. --- devtools_options.yaml | 3 +++ packages/komodo_ui_kit/lib/src/images/coin_icon.dart | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000000..fa0b357c4f --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 4d729332ad..57bcb448e3 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,8 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -const coinImagesFolder = 'packages/komodo_defi_framework/assets/coin_icons/png/'; -// NB: ENSURE IT STAYS IN SYNC WITH MAIN PROJECT in `lib/src/utils/utils.dart`. +const coinImagesFolder = 'coin_icons/png/'; const mediaCdnUrl = 'https://komodoplatform.github.io/coins/icons/'; final Map _assetExistenceCache = {}; From fb0c0486e4e127f18d92086f462ac3105af130b6 Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Tue, 17 Dec 2024 19:56:18 +0300 Subject: [PATCH 19/34] Fix zero balance issue of tokens without parent coin gas (#186) * fix zero balance issue by fallback to my_balance * update kdf wasm version * Remove undefined KDF build configs * deploy preview on PR to main * max_taker_vol fallback to my_balance * ci branches add release/* * max_taker_vol balance parse rational --------- Co-authored-by: CharlVS <77973576+CharlVS@users.noreply.github.com> Co-authored-by: Francois --- .../firebase-hosting-pull-request.yml | 11 +- app_build/build_config.json | 49 +- lib/mm2/mm2_api/mm2_api.dart | 665 +++++++++++------- .../rpc/my_balance/my_balance_req.dart | 17 + 4 files changed, 427 insertions(+), 315 deletions(-) create mode 100644 lib/mm2/mm2_api/rpc/my_balance/my_balance_req.dart diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 9c9e81b350..6e5e08b33c 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -6,6 +6,8 @@ on: pull_request: branches: - dev + - main + - release/* workflow_dispatch: jobs: @@ -62,12 +64,3 @@ jobs: channelId: dev-preview target: walletrc projectId: komodo-wallet-official - - - name: Deploy Komodo Wallet Web Production (`master` branch) - if: github.ref == 'refs/heads/master' && github.event_name != 'workflow_dispatch' - uses: FirebaseExtended/action-hosting-deploy@v0.7.1 - with: - repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_KOMODO_WALLET_OFFICIAL }}" - channelId: live - projectId: komodo-wallet-official diff --git a/app_build/build_config.json b/app_build/build_config.json index 8d75d8471f..5afc8db6e4 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -11,60 +11,17 @@ "web": { "matching_keyword": "wasm", "valid_zip_sha256_checksums": [ - "877d42c9051c060609b969aa3dfa550d73ae24f53153dea08143d820e7d317c7" + "91ac9f0e880ec49aedab21c5df130ffc9475cdbf0b6428cf6862842142176293" ], "path": "web/src/mm2" - }, - "ios": { - "matching_keyword": "ios-aarch64", - "valid_zip_sha256_checksums": [ - "545e657f6d15a1bd552e59f6006811f84876ae39837b6b68a39b1eec3094f8dc" - ], - "path": "ios" - }, - "macos": { - "matching_keyword": "Darwin-Release", - "valid_zip_sha256_checksums": [ - "62c3ef36cb3cd4fc166d381d7efce6b67d3b5dc62de4080bfb39462c0c4efb48" - ], - "path": "macos" - }, - "windows": { - "matching_keyword": "Win64", - "valid_zip_sha256_checksums": [ - "88a723d584ef10e528b11a16327a6bcdf25867cdb6c7e6afadc77a62c8bed7dc" - ], - "path": "windows/runner/exe" - }, - "android-armv7": { - "matching_keyword": "android-armv7", - "valid_zip_sha256_checksums": [ - "bfc294ece8481aabedf21b2d3e785eedc9719f066b396da91811427fa857f2b3" - ], - "path": "android/app/src/main/cpp/libs/armeabi-v7a" - }, - "android-aarch64": { - "matching_keyword": "android-aarch64", - "valid_zip_sha256_checksums": [ - "bdead61715e5f95e5f1d842691b7ee19b8dc9ef780bc13f1207589214ff0fc24" - ], - "path": "android/app/src/main/cpp/libs/arm64-v8a" - }, - "linux": { - "matching_keyword": "Linux-Release", - "valid_zip_sha256_checksums": [ - "e8c4cc2b6fbddc229820e7868591b757e223a47553a66755131330345a624093" - ], - "path": "linux/mm2" } } }, "coins": { - "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "7d2ca04e250b59362641da69f752aec8cbd83838", + "bundled_coins_repo_commit": "5e12a42ea95f8f50667291c7adae881eef513bf0", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", - "coins_repo_content_url": "https://komodoplatform.github.io/coins", + "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", "coins_repo_branch": "master", "runtime_updates_enabled": true, "mapped_files": { diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 7a9268b8ee..2c93834309 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:rational/rational.dart'; import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; @@ -11,8 +11,6 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/cancel_order/cancel_order_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/electrum/electrum_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/enable/enable_req.dart'; @@ -22,13 +20,14 @@ import 'package:web_dex/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req. import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/kmd_rewards_info/kmd_rewards_info_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/market_maker_bot_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/market_maker_bot_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol_response.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/my_balance/my_balance_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_orders/my_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_orders/my_orders_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request.dart'; @@ -48,8 +47,6 @@ import 'package:web_dex/mm2/mm2_api/rpc/rpc_error.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/stop/stop_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart'; @@ -66,8 +63,8 @@ class Mm2Api { Mm2Api({ required MM2 mm2, }) : _mm2 = mm2 { - trezor = Mm2ApiTrezor(_mm2.call); - nft = Mm2ApiNft(_mm2.call); + trezor = Mm2ApiTrezor(_call); + nft = Mm2ApiNft(_call); } final MM2 _mm2; @@ -76,40 +73,40 @@ class Mm2Api { VersionResponse? _versionResponse; Future?> getEnabledCoins(List knownCoins) async { - JsonMap response; + dynamic response; try { - response = await _mm2.call(GetEnabledCoinsReq()); + response = await _call(GetEnabledCoinsReq()); } catch (e) { log( - 'Error getting enabled coins: $e', + 'Error getting enabled coins: ${e.toString()}', path: 'api => getEnabledCoins => _call', isError: true, - ).ignore(); + ); return null; } dynamic resultJson; try { - resultJson = response['result']; + resultJson = jsonDecode(response)['result']; } catch (e, s) { log( - 'Error parsing of enabled coins response: $e', + 'Error parsing of enabled coins response: ${e.toString()}', path: 'api => getEnabledCoins => jsonDecode', trace: s, isError: true, - ).ignore(); + ); return null; } final List list = []; if (resultJson is List) { - for (final dynamic item in resultJson) { + for (dynamic item in resultJson) { final Coin? coin = knownCoins.firstWhereOrNull( (Coin known) => known.abbr == item['ticker'], ); if (coin != null) { - coin.address = item['address'] as String?; + coin.address = item['address']; list.add(coin); } } @@ -153,57 +150,135 @@ class Mm2Api { Future _enableEthWithTokensCoins( List coinRequests, ) async { - return _callMany( - coinRequests, - (dynamic request) async { - final JsonMap json = await _mm2.call(request); - if (json['error'] != null) { + dynamic response; + try { + response = await _call(coinRequests); + log( + response, + path: 'api => _enableEthWithTokensCoins', + ); + } catch (e, s) { + log( + 'Error enabling coins: ${e.toString()}', + path: 'api => _enableEthWithTokensCoins => _call', + trace: s, + isError: true, + ); + return; + } + + dynamic json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of enable coins response: ${e.toString()}', + path: 'api => _enableEthWithTokensCoins => jsonDecode', + trace: s, + isError: true, + ); + return; + } + + if (json is List) { + for (var item in json) { + if (item['error'] != null) { log( - json['error'].toString(), + item['error'], path: 'api => _enableEthWithTokensCoins:', isError: true, - ).ignore(); - return; + ); } - }, - logPath: 'api => _enableEthWithTokensCoins => _call', - ); + } + + return; + } else if (json is Map && json['error'] != null) { + log( + json['error'], + path: 'api => _enableEthWithTokensCoins:', + isError: true, + ); + return; + } } Future _enableErc20Coins(List coinRequests) async { - return _callMany( - coinRequests, - (dynamic request) async { - final JsonMap json = await _mm2.call(request); + dynamic response; + try { + response = await _call(coinRequests); + log( + response, + path: 'api => _enableErc20Coins', + ); + } catch (e, s) { + log( + 'Error enabling coins: ${e.toString()}', + path: 'api => _enableErc20Coins => _call', + trace: s, + isError: true, + ); + return; + } - if (json['error'] != null) { - log( - json['error'].toString(), - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ).ignore(); - } - }, - logPath: 'api => _enableErc20Coins => _call', - ); + List json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of enable coins response: ${e.toString()}', + path: 'api => _enableEthWithTokensCoins => jsonDecode', + trace: s, + isError: true, + ); + return; + } + for (dynamic item in json) { + if (item['error'] != null) { + log( + item['error'], + path: 'api => _enableEthWithTokensCoins:', + isError: true, + ); + } + } } Future _enableElectrumCoins(List electrumRequests) async { - await _callMany( - electrumRequests, - _mm2.call, - logPath: 'api => _enableElectrumCoins => _call', - ); + try { + final dynamic response = await _call(electrumRequests); + log( + response, + path: 'api => _enableElectrumCoins => _call', + ); + } catch (e, s) { + log( + 'Error enabling electrum coins: ${e.toString()}', + path: 'api => _enableElectrumCoins => _call', + trace: s, + isError: true, + ); + return; + } } Future _enableTendermintWithAssets( - List tendermintRequests, + List request, ) async { - return _callMany( - tendermintRequests, - _mm2.call, - logPath: 'api => _enableTendermintWithAssets => _call', - ); + try { + final dynamic response = await _call(request); + log( + response, + path: 'api => _enableTendermintWithAssets => _call', + ); + } catch (e, s) { + log( + 'Error enabling tendermint coins: ${e.toString()}', + path: 'api => _enableTendermintWithAssets => _call', + trace: s, + isError: true, + ); + return; + } } Future _enableTendermintTokens( @@ -212,20 +287,20 @@ class Mm2Api { ) async { try { if (tendermintWithAssetsRequest != null) { - await _mm2.call(tendermintWithAssetsRequest); + await _call(tendermintWithAssetsRequest); } - return _callMany( - request, - _mm2.call, - logPath: 'api => _enableTendermintToken => _call', + final dynamic response = await _call(request); + log( + response, + path: 'api => _enableTendermintToken => _call', ); } catch (e, s) { log( - 'Error enabling tendermint tokens: $e', + 'Error enabling tendermint tokens: ${e.toString()}', path: 'api => _enableTendermintToken => _call', trace: s, isError: true, - ).ignore(); + ); return; } } @@ -233,63 +308,174 @@ class Mm2Api { Future _enableSlpTokens( List requests, ) async { - return _callMany( - requests, - _mm2.call, - logPath: 'api => _enableSlpTokens => _call', - ); + try { + final dynamic response = await _call(requests); + log( + response, + path: 'api => _enableSlpTokens => _call', + ); + } catch (e, s) { + log( + 'Error enabling bch coins: ${e.toString()}', + path: 'api => _enableSlpTokens => _call', + trace: s, + isError: true, + ); + return; + } } Future _enableBchWithTokens( List requests, ) async { - return _callMany( - requests, - _mm2.call, - logPath: 'api => _enableBchWithTokens => _call', - ); + try { + final dynamic response = await _call(requests); + log( + response, + path: 'api => _enableBchWithTokens => _call', + ); + } catch (e, s) { + log( + 'Error enabling bch coins: ${e.toString()}', + path: 'api => _enableBchWithTokens => _call', + trace: s, + isError: true, + ); + return; + } } Future disableCoin(String coin) async { try { - await _mm2.call(DisableCoinReq(coin: coin)); + await _call(DisableCoinReq(coin: coin)); } catch (e, s) { log( - 'Error disabling $coin: $e', + 'Error disabling $coin: ${e.toString()}', path: 'api=> disableCoin => _call', trace: s, isError: true, - ).ignore(); + ); return; } } + Future getBalance(String abbr) async { + dynamic response; + try { + response = await _call(MyBalanceReq(coin: abbr)); + } catch (e, s) { + log( + 'Error getting balance $abbr: ${e.toString()}', + path: 'api => getBalance => _call', + trace: s, + isError: true, + ); + return null; + } + + Map json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of get balance $abbr response: ${e.toString()}', + path: 'api => getBalance => jsonDecode', + trace: s, + isError: true, + ); + return null; + } + + return json['balance']; + } + Future getMaxMakerVol(String abbr) async { - JsonMap? response; + dynamic response; try { - response = await _mm2.call(MaxMakerVolRequest(coin: abbr)); + response = await _call(MaxMakerVolRequest(coin: abbr)); } catch (e, s) { log( - 'Error getting max maker vol $abbr: $e', + 'Error getting max maker vol $abbr: ${e.toString()}', path: 'api => getMaxMakerVol => _call', trace: s, isError: true, - ).ignore(); - return null; + ); + return _fallbackToBalance(abbr); + } + + Map json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of max maker vol $abbr response: ${e.toString()}', + path: 'api => getMaxMakerVol => jsonDecode', + trace: s, + isError: true, + ); + return _fallbackToBalance(abbr); } - final error = response['error']; + final error = json['error']; if (error != null) { log( - 'Error parsing of max maker vol $abbr response: $error', + 'Error parsing of max maker vol $abbr response: ${error.toString()}', path: 'api => getMaxMakerVol => error', isError: true, - ).ignore(); + ); + return _fallbackToBalance(abbr); + } + + try { + return MaxMakerVolResponse.fromJson(json['result']); + } catch (e, s) { + log( + 'Error constructing MaxMakerVolResponse for $abbr: ${e.toString()}', + path: 'api => getMaxMakerVol => fromJson', + trace: s, + isError: true, + ); + return _fallbackToBalance(abbr); + } + } + + Future _fallbackToBalance(String abbr) async { + final balance = await getBalance(abbr); + if (balance == null) { + log( + 'Failed to retrieve balance for fallback construction of MaxMakerVolResponse for $abbr', + path: 'api => _fallbackToBalance', + isError: true, + ); return null; } - return MaxMakerVolResponse.fromJson( - Map.from(response['result'] as Map? ?? {}), + final balanceValue = MaxMakerVolResponseValue(decimal: balance); + return MaxMakerVolResponse( + volume: balanceValue, + balance: balanceValue, + ); + } + + Future _fallbackToBalanceTaker(String abbr) async { + final balance = await getBalance(abbr); + if (balance == null) { + log( + 'Failed to retrieve balance for fallback construction of MaxTakerVolResponse for $abbr', + path: 'api => _fallbackToBalanceTaker', + isError: true, + ); + return null; + } + final rational = Rational.parse(balance); + final result = MaxTakerVolumeResponseResult( + numer: rational.numerator.toString(), + denom: rational.denominator.toString(), + ); + + return MaxTakerVolResponse( + coin: abbr, + result: result, ); } @@ -297,14 +483,15 @@ class Mm2Api { ActiveSwapsRequest request, ) async { try { - return await _mm2.call(request) as Map?; + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error getting active swaps: $e', + 'Error getting active swaps: ${e.toString()}', path: 'api => getActiveSwaps', trace: s, isError: true, - ).ignore(); + ); return {'error': 'something went wrong'}; } } @@ -314,30 +501,36 @@ class Mm2Api { String address, ) async { try { - return await _mm2.call( + final dynamic response = await _call( ValidateAddressRequest(coin: coinAbbr, address: address), ); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error validating address $coinAbbr: $e', + 'Error validating address $coinAbbr: ${e.toString()}', path: 'api => validateAddress', trace: s, isError: true, - ).ignore(); + ); return null; } } Future?> withdraw(WithdrawRequest request) async { try { - return await _mm2.call(request) as Map?; + final dynamic response = await _call(request); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error withdrawing ${request.params.coin}: $e', + 'Error withdrawing ${request.params.coin}: ${e.toString()}', path: 'api => withdraw', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -346,14 +539,17 @@ class Mm2Api { SendRawTransactionRequest request, ) async { try { - return await _mm2.call(request) as Map?; + final dynamic response = await _call(request); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: $e', + 'Error sending raw transaction ${request.coin}: ${e.toString()}', path: 'api => sendRawTransaction', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -362,14 +558,17 @@ class Mm2Api { MyTxHistoryRequest request, ) async { try { - return await _mm2.call(request) as Map?; + final dynamic response = await _call(request); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: $e', + 'Error sending raw transaction ${request.coin}: ${e.toString()}', path: 'api => getTransactions', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -378,14 +577,17 @@ class Mm2Api { MyTxHistoryV2Request request, ) async { try { - return await _mm2.call(request) as Map?; + final dynamic response = await _call(request); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error sending raw transaction ${request.params.coin}: $e', + 'Error sending raw transaction ${request.params.coin}: ${e.toString()}', path: 'api => getTransactions', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -394,84 +596,92 @@ class Mm2Api { KmdRewardsInfoRequest request, ) async { try { - return await _mm2.call(request) as Map?; + final dynamic response = await _call(request); + final Map json = jsonDecode(response); + + return json; } catch (e, s) { log( - 'Error getting rewards info: $e', + 'Error getting rewards info: ${e.toString()}', path: 'api => getRewardsInfo', trace: s, isError: true, - ).ignore(); + ); return null; } } Future?> getBestOrders(BestOrdersRequest request) async { try { - return await _mm2.call(request) as Map?; + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error getting best orders ${request.coin}: $e', + 'Error getting best orders ${request.coin}: ${e.toString()}', path: 'api => getBestOrders', trace: s, isError: true, - ).ignore(); - return {'error': e.toString()}; + ); + return {'error': e}; } } Future> sell(SellRequest request) async { try { - return await _mm2.call(request); + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error sell ${request.base}/${request.rel}: $e', + 'Error sell ${request.base}/${request.rel}: ${e.toString()}', path: 'api => sell', trace: s, isError: true, - ).ignore(); + ); return {'error': e}; } } Future?> setprice(SetPriceRequest request) async { try { - return await _mm2.call(request) as Map?; + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error setprice ${request.base}/${request.rel}: $e', + 'Error setprice ${request.base}/${request.rel}: ${e.toString()}', path: 'api => setprice', trace: s, isError: true, - ).ignore(); + ); return {'error': e}; } } Future> cancelOrder(CancelOrderRequest request) async { try { - return await _mm2.call(request); + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error cancelOrder ${request.uuid}: $e', + 'Error cancelOrder ${request.uuid}: ${e.toString()}', path: 'api => cancelOrder', trace: s, isError: true, - ).ignore(); + ); return {'error': e}; } } Future> getSwapStatus(MySwapStatusReq request) async { try { - return await _mm2.call(request); + final String response = await _call(request); + return jsonDecode(response); } catch (e, s) { log( - 'Error sell getting swap status ${request.uuid}: $e', + 'Error sell getting swap status ${request.uuid}: ${e.toString()}', path: 'api => getSwapStatus', trace: s, isError: true, - ).ignore(); + ); return {'error': 'something went wrong'}; } } @@ -479,42 +689,44 @@ class Mm2Api { Future getMyOrders() async { try { final MyOrdersRequest request = MyOrdersRequest(); - final response = await _mm2.call(request); - if (response['error'] != null) { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { return null; } - return MyOrdersResponse.fromJson(response); + return MyOrdersResponse.fromJson(json); } catch (e, s) { log( - 'Error getting my orders: $e', + 'Error getting my orders: ${e.toString()}', path: 'api => getMyOrders', trace: s, isError: true, - ).ignore(); + ); return null; } } Future getRawSwapData(MyRecentSwapsRequest request) async { - return jsonEncode(await _mm2.call(request)); + return await _call(request); } Future getMyRecentSwaps( MyRecentSwapsRequest request, ) async { try { - final response = await _mm2.call(request); - if (response['error'] != null) { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { return null; } - return MyRecentSwapsResponse.fromJson(response); + return MyRecentSwapsResponse.fromJson(json); } catch (e, s) { log( - 'Error getting my recent swaps: $e', + 'Error getting my recent swaps: ${e.toString()}', path: 'api => getMyRecentSwaps', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -522,36 +734,38 @@ class Mm2Api { Future getOrderStatus(String uuid) async { try { final OrderStatusRequest request = OrderStatusRequest(uuid: uuid); - final response = await _mm2.call(request); - if (response['error'] != null) { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { return null; } - return OrderStatusResponse.fromJson(response); + return OrderStatusResponse.fromJson(json); } catch (e, s) { log( - 'Error getting order status $uuid: $e', + 'Error getting order status $uuid: ${e.toString()}', path: 'api => getOrderStatus', trace: s, isError: true, - ).ignore(); + ); return null; } } Future importSwaps(ImportSwapsRequest request) async { try { - final JsonMap response = await _mm2.call(request); - if (response['error'] != null) { + final String response = await _call(request); + final Map json = jsonDecode(response); + if (json['error'] != null) { return null; } - return ImportSwapsResponse.fromJson(response); + return ImportSwapsResponse.fromJson(json); } catch (e, s) { log( - 'Error import swaps : $e', + 'Error import swaps : ${e.toString()}', path: 'api => importSwaps', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -560,23 +774,24 @@ class Mm2Api { RecoverFundsOfSwapRequest request, ) async { try { - final JsonMap json = await _mm2.call(request); + final String response = await _call(request); + final Map json = jsonDecode(response); if (json['error'] != null) { log( 'Error recovering funds of swap ${request.uuid}: ${json['error']}', path: 'api => recoverFundsOfSwap', isError: true, - ).ignore(); + ); return null; } return RecoverFundsOfSwapResponse.fromJson(json); } catch (e, s) { log( - 'Error recovering funds of swap ${request.uuid}: $e', + 'Error recovering funds of swap ${request.uuid}: ${e.toString()}', path: 'api => recoverFundsOfSwap', trace: s, isError: true, - ).ignore(); + ); return null; } } @@ -585,19 +800,20 @@ class Mm2Api { MaxTakerVolRequest request, ) async { try { - final JsonMap json = await _mm2.call(request); + final String response = await _call(request); + final Map json = jsonDecode(response); if (json['error'] != null) { - return null; + return await _fallbackToBalanceTaker(request.coin); } return MaxTakerVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting max taker volume ${request.coin}: $e', + 'Error getting max taker volume ${request.coin}: ${e.toString()}', path: 'api => getMaxTakerVolume', trace: s, isError: true, - ).ignore(); - return null; + ); + return await _fallbackToBalanceTaker(request.coin); } } @@ -605,30 +821,32 @@ class Mm2Api { MinTradingVolRequest request, ) async { try { - final JsonMap json = await _mm2.call(request); + final String response = await _call(request); + final Map json = jsonDecode(response); if (json['error'] != null) { return null; } return MinTradingVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting min trading volume ${request.coin}: $e', + 'Error getting min trading volume ${request.coin}: ${e.toString()}', path: 'api => getMinTradingVol', trace: s, isError: true, - ).ignore(); + ); return null; } } Future getOrderbook(OrderbookRequest request) async { try { - final JsonMap json = await _mm2.call(request); + final String response = await _call(request); + final Map json = jsonDecode(response); if (json['error'] != null) { return OrderbookResponse( request: request, - error: json['error'] as String?, + error: json['error'], ); } @@ -638,11 +856,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting orderbook ${request.base}/${request.rel}: $e', + 'Error getting orderbook ${request.base}/${request.rel}: ${e.toString()}', path: 'api => getOrderbook', trace: s, isError: true, - ).ignore(); + ); return OrderbookResponse( request: request, @@ -656,17 +874,18 @@ class Mm2Api { ) async { final request = OrderBookDepthReq(pairs: pairs); try { - final JsonMap json = await _mm2.call(request); + final String response = await _call(request); + final Map json = jsonDecode(response); if (json['error'] != null) { return null; } return OrderBookDepthResponse.fromJson(json); } catch (e, s) { log( - 'Error getting orderbook depth $request: $e', + 'Error getting orderbook depth $request: ${e.toString()}', path: 'api => getOrderBookDepth', trace: s, - ).ignore(); + ); } return null; } @@ -677,7 +896,8 @@ class Mm2Api { TradePreimageRequest request, ) async { try { - final JsonMap responseJson = await _mm2.call(request); + final String response = await _call(request); + final Map responseJson = await jsonDecode(response); if (responseJson['error'] != null) { return ApiResponse(request: request, error: responseJson); } @@ -687,11 +907,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting trade preimage ${request.base}/${request.rel}: $e', + 'Error getting trade preimage ${request.base}/${request.rel}: ${e.toString()}', path: 'api => getTradePreimage', trace: s, isError: true, - ).ignore(); + ); return ApiResponse( request: request, ); @@ -711,22 +931,25 @@ class Mm2Api { MarketMakerBotRequest marketMakerBotRequest, ) async { try { - final JsonMap response = await _mm2.call(marketMakerBotRequest.toJson()); + final dynamic response = await _call(marketMakerBotRequest.toJson()); log( - response.toString(), + response, path: 'api => ${marketMakerBotRequest.method} => _call', - ).ignore(); + ); - if (response['error'] != null) { - throw RpcException(RpcError.fromJson(response)); + if (response is String) { + final Map responseJson = jsonDecode(response); + if (responseJson['error'] != null) { + throw RpcException(RpcError.fromJson(responseJson)); + } } } catch (e, s) { log( - 'Error starting or stopping simple market maker bot: $e', + 'Error starting or stopping simple market maker bot: ${e.toString()}', path: 'api => start_simple_market_maker_bot => _call', trace: s, isError: true, - ).ignore(); + ); rethrow; } } @@ -748,110 +971,32 @@ class Mm2Api { Future convertLegacyAddress(ConvertAddressRequest request) async { try { - final JsonMap responseJson = await _mm2.call(request); - return responseJson['result']?['address'] as String?; + final String response = await _call(request); + final Map responseJson = jsonDecode(response); + return responseJson['result']?['address']; } catch (e, s) { log( - 'Convert address error: $e', + 'Convert address error: ${e.toString()}', path: 'api => convertLegacyAddress', trace: s, isError: true, - ).ignore(); + ); return null; } } Future stop() async { - await _mm2.call(StopReq()); - } - - Future _callMany( - List requests, - Future Function(T request) processor, { - required String logPath, - bool concurrent = false, - bool enableLogging = true, - }) async { - try { - if (concurrent) { - await Future.wait( - requests.map((request) async { - final dynamic response = await processor(request); - if (enableLogging) { - log( - response.toString(), - path: logPath, - ).ignore(); - } - }), - ); - } else { - for (final request in requests) { - final dynamic response = await processor(request); - if (enableLogging) { - log( - response.toString(), - path: logPath, - ).ignore(); - } - } - } - } catch (e, s) { - if (enableLogging) { - log( - 'Error processing requests: $e', - path: logPath, - trace: s, - isError: true, - ).ignore(); - } - return; - } + await _call(StopReq()); } - Future showPrivKey( - ShowPrivKeyRequest request, - ) async { - try { - final JsonMap json = await _mm2.call(request); - if (json['error'] != null) { - return null; - } - return ShowPrivKeyResponse.fromJson(json); - } catch (e, s) { - log( - 'Error getting privkey ${request.coin}: ${e.toString()}', - path: 'api => showPrivKey', - trace: s, - isError: true, - ).ignore(); - return null; + Future _call(dynamic req) async { + final MM2Status mm2Status = await _mm2.status(); + if (mm2Status != MM2Status.rpcIsUp) { + return '{"error": "Error, mm2 status: $mm2Status"}'; } - } - Future getDirectlyConnectedPeers( - GetDirectlyConnectedPeers request, - ) async { - try { - final JsonMap json = await _mm2.call(request); - if (json['error'] != null) { - log( - 'Error getting directly connected peers: ${json['error']}', - isError: true, - path: 'api => getDirectlyConnectedPeers', - ).ignore(); - throw Exception('Failed to get directly connected peers'); - } + final dynamic response = await _mm2.call(req); - return GetDirectlyConnectedPeersResponse.fromJson(json); - } catch (e, s) { - log( - 'Error getting directly connected peers', - path: 'api => getDirectlyConnectedPeers', - trace: s, - isError: true, - ).ignore(); - rethrow; - } + return response; } } diff --git a/lib/mm2/mm2_api/rpc/my_balance/my_balance_req.dart b/lib/mm2/mm2_api/rpc/my_balance/my_balance_req.dart new file mode 100644 index 0000000000..fec0776921 --- /dev/null +++ b/lib/mm2/mm2_api/rpc/my_balance/my_balance_req.dart @@ -0,0 +1,17 @@ +class MyBalanceReq { + MyBalanceReq({ + required this.coin, + }); + + static const String method = 'my_balance'; + final String coin; + late String userpass; + + Map toJson() { + return { + 'method': method, + 'coin': coin, + 'userpass': userpass, + }; + } +} From 62b4b0553fefb632001a42c206eb868f38dfea6a Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 20 Dec 2024 14:04:17 +0200 Subject: [PATCH 20/34] fix(trezor): breaking hd wallet balance status rpc (#194) * add support for map in hd balance response * bump kdf release hash and wasm checksum --- app_build/build_config.json | 6 +++--- lib/model/hd_account/hd_account.dart | 27 ++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app_build/build_config.json b/app_build/build_config.json index 5afc8db6e4..de9e11b8bf 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -1,6 +1,6 @@ { "api": { - "api_commit_hash": "bb749a99344fb61e92410cb64b035dba1de35779", + "api_commit_hash": "8206c6ef61ee5c0f697fab5fcfd7256e3ebe13ac", "branch": "main", "fetch_at_build_enabled": true, "source_urls": [ @@ -11,7 +11,7 @@ "web": { "matching_keyword": "wasm", "valid_zip_sha256_checksums": [ - "91ac9f0e880ec49aedab21c5df130ffc9475cdbf0b6428cf6862842142176293" + "f82e330a8d9bc1c2011604648233922cf216a732858c5a6a935ad77a286b1993" ], "path": "web/src/mm2" } @@ -19,7 +19,7 @@ }, "coins": { "update_commit_on_build": true, - "bundled_coins_repo_commit": "5e12a42ea95f8f50667291c7adae881eef513bf0", + "bundled_coins_repo_commit": "642abea7172b81db24b16bffc13783b9a0e400f5", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", "coins_repo_branch": "master", diff --git a/lib/model/hd_account/hd_account.dart b/lib/model/hd_account/hd_account.dart index fa181d95c5..fdc4c2df94 100644 --- a/lib/model/hd_account/hd_account.dart +++ b/lib/model/hd_account/hd_account.dart @@ -52,10 +52,31 @@ class HdBalance { required this.unspendable, }); - factory HdBalance.fromJson(Map json) { + factory HdBalance.fromJson(Map coinBalanceMap) { + // Left in for backwards compatibility, but is no longer necessary from + // KDF v2.3.0-beta onwards + if (coinBalanceMap['spendable'] != null) { + return HdBalance( + spendable: double.tryParse(coinBalanceMap['spendable'].toString()) ?? 0, + unspendable: + double.tryParse(coinBalanceMap['unspendable'].toString()) ?? 0, + ); + } + + final balances = + coinBalanceMap.values.fold<({double spendable, double unspendable})>( + (spendable: 0.0, unspendable: 0.0), + (({double spendable, double unspendable}) sum, dynamic value) => ( + spendable: sum.spendable + + (double.tryParse(value['spendable']?.toString() ?? '0') ?? 0), + unspendable: sum.unspendable + + (double.tryParse(value['unspendable']?.toString() ?? '0') ?? 0), + ), + ); + return HdBalance( - spendable: double.parse(json['spendable'] ?? '0'), - unspendable: double.parse(json['unspendable'] ?? '0'), + spendable: balances.spendable, + unspendable: balances.unspendable, ); } From 9a18f5bb1d7cab23fc03e3eadebc0724af42d82d Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 24 Dec 2024 17:24:06 +0200 Subject: [PATCH 21/34] limit max_connected to 1 for electrum activation (#195) --- lib/mm2/mm2_api/rpc/electrum/electrum_req.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart b/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart index 5753968e23..92779c1613 100644 --- a/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart +++ b/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart @@ -1,3 +1,4 @@ +import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/electrum.dart'; class ElectrumReq { @@ -22,6 +23,12 @@ class ElectrumReq { 'method': method, 'coin': coin, 'servers': servers.map((server) => server.toJson()).toList(), + // limit the number of active connections to electrum servers to 1 to + // reduce device load & request spamming. Use 3 connections on desktop + // and 1 on mobile (web and native) using [isMobile] until a better + // alternative is found + // https://komodoplatform.com/en/docs/komodo-defi-framework/api/legacy/coin_activation/#electrum-method + 'max_connected': isMobile ? 1 : 3, 'userpass': userpass, 'mm2': mm2, 'tx_history': true, From 8d1d85e5952638f90d8f8bb4cf2ae034c9b1f3e5 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:42:29 +0100 Subject: [PATCH 22/34] Disable trading features --- app_build/build_config.json | 2 +- lib/app_config/app_config.dart | 2 + lib/bloc/app_bloc_root.dart | 2 +- lib/blocs/startup_bloc.dart | 2 +- lib/model/main_menu_value.dart | 27 ++++++++++++ .../page_content_router_delegate.dart | 16 ++++--- .../main_menu/main_menu_bar_mobile.dart | 4 +- .../common/main_menu/main_menu_desktop.dart | 9 +++- lib/views/dex/dex_page.dart | 4 ++ lib/views/main_layout/main_layout.dart | 44 +++++++++++++++++++ .../coin_details_info/coin_details_info.dart | 17 +++---- 11 files changed, 110 insertions(+), 19 deletions(-) diff --git a/app_build/build_config.json b/app_build/build_config.json index de9e11b8bf..2f102fa952 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -19,7 +19,7 @@ }, "coins": { "update_commit_on_build": true, - "bundled_coins_repo_commit": "642abea7172b81db24b16bffc13783b9a0e400f5", + "bundled_coins_repo_commit": "b27db8e6e1c6a9264219fef8292811122538088a", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", "coins_repo_branch": "master", diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 7c17621117..b48b10d638 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -18,6 +18,8 @@ const String coinsAssetsPath = 'packages/komodo_defi_framework/assets'; // TODO: Remove this flag after the feature is finalized const bool isBitrefillIntegrationEnabled = false; +const bool kIsWalletOnly = kDebugMode; + const Duration kPerformanceLogInterval = Duration(minutes: 1); // This information is here because it is not contextual and is branded. diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 217c5bff16..0fd1e5f22b 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -261,7 +261,7 @@ class _MyAppViewState extends State<_MyAppView> { @override void initState() { _airDexBackButtonDispatcher = AirDexBackButtonDispatcher(_routerDelegate); - routingState.selectedMenu = MainMenuValue.dex; + routingState.selectedMenu = MainMenuValue.defaultMenu(); if (kDebugMode) initDebugData(context.read()); diff --git a/lib/blocs/startup_bloc.dart b/lib/blocs/startup_bloc.dart index 58c26e0de2..72747fac73 100644 --- a/lib/blocs/startup_bloc.dart +++ b/lib/blocs/startup_bloc.dart @@ -42,7 +42,7 @@ class StartUpBloc implements BlocBase { coinsBloc.subscribeOnPrice(cexService); running = true; tradingEntitiesBloc.runUpdate(); - routingState.selectedMenu = MainMenuValue.dex; + routingState.selectedMenu = MainMenuValue.defaultMenu(); if (!wasAlreadyRunning) await authRepo.logIn(AuthorizeMode.noLogin); log('Application has started'); diff --git a/lib/model/main_menu_value.dart b/lib/model/main_menu_value.dart index 662ee2a572..0132b5c605 100644 --- a/lib/model/main_menu_value.dart +++ b/lib/model/main_menu_value.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; enum MainMenuValue { @@ -12,6 +13,32 @@ enum MainMenuValue { support, none; + static MainMenuValue defaultMenu() => + kIsWalletOnly ? MainMenuValue.wallet : MainMenuValue.dex; + + bool isEnabledInCurrentMode() { + return !(kIsWalletOnly && isDisabledWhenWalletOnly); + } + + // Getter to determine if the item is disabled if the wallet is in wallet-only mode + + bool get isDisabledWhenWalletOnly { + switch (this) { + case MainMenuValue.dex: + case MainMenuValue.bridge: + case MainMenuValue.marketMakerBot: + return true; + case MainMenuValue.wallet: + case MainMenuValue.fiat: + case MainMenuValue.nft: + case MainMenuValue.settings: + case MainMenuValue.support: + return false; + case MainMenuValue.none: + return false; + } + } + String get title { switch (this) { case MainMenuValue.wallet: diff --git a/lib/router/navigators/page_content/page_content_router_delegate.dart b/lib/router/navigators/page_content/page_content_router_delegate.dart index 6de6a7306b..5f1fa6a5c9 100644 --- a/lib/router/navigators/page_content/page_content_router_delegate.dart +++ b/lib/router/navigators/page_content/page_content_router_delegate.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/router/routes.dart'; import 'package:web_dex/router/state/routing_state.dart'; @@ -18,12 +19,16 @@ class PageContentRouterDelegate extends RouterDelegate @override Widget build(BuildContext context) { + // Redirect to the Wallet page if the selected menu is disabled in + // wallet-only mode and the wallet is in wallet-only mode. + if (routingState.selectedMenu.isDisabledWhenWalletOnly && kIsWalletOnly) { + return WalletPage( + coinAbbr: routingState.walletState.selectedCoin, + action: routingState.walletState.coinsManagerAction, + ); + } + switch (routingState.selectedMenu) { - case MainMenuValue.wallet: - return WalletPage( - coinAbbr: routingState.walletState.selectedCoin, - action: routingState.walletState.coinsManagerAction, - ); case MainMenuValue.fiat: return const FiatPage(); case MainMenuValue.dex: @@ -43,6 +48,7 @@ class PageContentRouterDelegate extends RouterDelegate selectedMenu: routingState.settingsState.selectedMenu); case MainMenuValue.support: return SupportPage(); + case MainMenuValue.wallet: default: return WalletPage( coinAbbr: routingState.walletState.selectedCoin, 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 008930eefc..a7924e7e28 100644 --- a/lib/views/common/main_menu/main_menu_bar_mobile.dart +++ b/lib/views/common/main_menu/main_menu_bar_mobile.dart @@ -70,7 +70,9 @@ class MainMenuBarMobile extends StatelessWidget { value: MainMenuValue.settings, isActive: selected == MainMenuValue.settings, ), - ], + ] + .where((element) => element.value.isEnabledInCurrentMode()) + .toList(), ), ), ), diff --git a/lib/views/common/main_menu/main_menu_desktop.dart b/lib/views/common/main_menu/main_menu_desktop.dart index 5bb8548ed2..bb285d7e1f 100644 --- a/lib/views/common/main_menu/main_menu_desktop.dart +++ b/lib/views/common/main_menu/main_menu_desktop.dart @@ -50,7 +50,7 @@ class _MainMenuDesktopState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, - children: [ + children: [ DesktopMenuDesktopItem( key: const Key('main-menu-wallet'), menu: MainMenuValue.wallet, @@ -131,7 +131,12 @@ class _MainMenuDesktopState extends State { }), ), const SizedBox(height: 48), - ], + ] + // Filter out disabled items + .where((item) => + item is! DesktopMenuDesktopItem || + item.menu.isEnabledInCurrentMode()) + .toList(), ), ), ); diff --git a/lib/views/dex/dex_page.dart b/lib/views/dex/dex_page.dart index 912e0f8fd1..38398469be 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -1,6 +1,7 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; @@ -41,6 +42,9 @@ class _DexPageState extends State { @override Widget build(BuildContext context) { + if (kIsWalletOnly) { + return const Placeholder(child: Text('You should not see this page')); + } return MultiBlocProvider( providers: [ BlocProvider( diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 434a32ca3d..b22deb72c6 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; @@ -30,6 +32,10 @@ class _MainLayoutState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { await AlphaVersionWarningService().run(); updateBloc.init(); + + if (kDebugMode && !await _hasAgreedNoTrading()) { + _showDebugModeDialog().ignore(); + } }); super.initState(); @@ -66,4 +72,42 @@ class _MainLayoutState extends State { return MainLayoutRouter(); }); } + + // Method to show an alert dialog with an option to agree if the app is in + // debug mode stating that trading features may not be used for actual trading + // and that only test assets/networks may be used. + Future _showDebugModeDialog() async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('Debug mode'), + content: const Text( + 'This app is in debug mode. Trading features may not be used for ' + 'actual trading. Only test assets/networks may be used.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _saveAgreedState().ignore(); + }, + child: const Text('I agree'), + ), + ], + ); + }, + ); + } + + Future _saveAgreedState() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool('wallet_only_agreed', true); + } + + Future _hasAgreedNoTrading() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('wallet_only_agreed') ?? false; + } } diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index 7bab57d075..f8f71e3c46 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -260,15 +260,16 @@ class _DesktopCoinDetails extends StatelessWidget { ), ], ), - Padding( - padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), - child: CoinDetailsCommonButtons( - isMobile: false, - selectWidget: setPageType, - clickSwapButton: () => _goToSwap(context, coin), - coin: coin, + if (MainMenuValue.dex.isEnabledInCurrentMode()) + Padding( + padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), + child: CoinDetailsCommonButtons( + isMobile: false, + selectWidget: setPageType, + clickSwapButton: () => _goToSwap(context, coin), + coin: coin, + ), ), - ), const Gap(16), if (areChartsSupported) Card( From f387a96c093c80ca9fb44618ccf6b4e58710d131 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:17:28 +0100 Subject: [PATCH 23/34] bug: Fix incorrect wallet mode logic --- lib/app_config/app_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index b48b10d638..4da6f33c4e 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -18,7 +18,7 @@ const String coinsAssetsPath = 'packages/komodo_defi_framework/assets'; // TODO: Remove this flag after the feature is finalized const bool isBitrefillIntegrationEnabled = false; -const bool kIsWalletOnly = kDebugMode; +const bool kIsWalletOnly = !kDebugMode; const Duration kPerformanceLogInterval = Duration(minutes: 1); From 9f46ca5c92bc7827cac933c3e3d8da577db4758c Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:21:20 +0100 Subject: [PATCH 24/34] feat: Hide incorrect time banner in wallet-only mode --- lib/shared/ui/clock_warning_banner.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/shared/ui/clock_warning_banner.dart b/lib/shared/ui/clock_warning_banner.dart index 027019f27e..dac7b0058e 100644 --- a/lib/shared/ui/clock_warning_banner.dart +++ b/lib/shared/ui/clock_warning_banner.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -13,7 +14,8 @@ class ClockWarningBanner extends StatelessWidget { return BlocBuilder( builder: (context, systemHealthState) { if (systemHealthState is SystemHealthLoadSuccess && - !systemHealthState.isValid) { + !systemHealthState.isValid && + !kIsWalletOnly) { return _buildWarningBanner(); } return const SizedBox.shrink(); From 7fe4ee494115f6e6042bbc69b6aacce3b1e37ea7 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:22:37 +0100 Subject: [PATCH 25/34] Bug: Fix missing coin detail buttons in wallet-only mode --- .../coin_details_common_buttons.dart | 4 ++-- .../coin_details_info/coin_details_info.dart | 19 ++++++++++--------- pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index f5947631be..2405e6b4b9 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -24,7 +24,7 @@ class CoinDetailsCommonButtons extends StatelessWidget { final bool isMobile; final Coin coin; final void Function(CoinPageType) selectWidget; - final VoidCallback clickSwapButton; + final VoidCallback? clickSwapButton; @override Widget build(BuildContext context) { @@ -163,7 +163,7 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { context: context, ), ), - if (!coin.walletOnly) + if (!coin.walletOnly && !kIsWalletOnly) Container( margin: const EdgeInsets.only(left: 21), constraints: const BoxConstraints(maxWidth: 120), diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index f8f71e3c46..f19f05b050 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -260,16 +260,17 @@ class _DesktopCoinDetails extends StatelessWidget { ), ], ), - if (MainMenuValue.dex.isEnabledInCurrentMode()) - Padding( - padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), - child: CoinDetailsCommonButtons( - isMobile: false, - selectWidget: setPageType, - clickSwapButton: () => _goToSwap(context, coin), - coin: coin, - ), + Padding( + padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), + child: CoinDetailsCommonButtons( + isMobile: false, + selectWidget: setPageType, + clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + ? null + : () => _goToSwap(context, coin), + coin: coin, ), + ), const Gap(16), if (areChartsSupported) Card( diff --git a/pubspec.yaml b/pubspec.yaml index 8a62cff237..a82de0cd2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.2+0 +version: 0.8.2+1 environment: sdk: ">=3.4.0 <4.0.0" # The recent 3.5.0 breaks the build. We will resolve this after this release. From 9512dd2b767fd367c8a1d5ef978ead2550213869 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:26:42 +0100 Subject: [PATCH 26/34] fix: missing conditional wallet mode check --- .../coin_details/coin_details_info/coin_details_info.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index f19f05b050..d7b0b681e7 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -375,7 +375,9 @@ class _MobileContent extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: true, selectWidget: setPageType, - clickSwapButton: () => _goToSwap(context, coin), + clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + ? null + : () => _goToSwap(context, coin), coin: coin, ), ), From 9eb8fedbb34b558258e206a8027c45b914431a0f Mon Sep 17 00:00:00 2001 From: Tolga Ay Date: Tue, 7 Jan 2025 20:38:56 +0300 Subject: [PATCH 27/34] SDK Integration & HD Coin Details page + Coin Addresses (#178) * add komodo_defi_sdk to dependencies update required dependencies, and fix build errors * WIP: replace mm2 classes with defi sdk * replace kdf rpc status checks with isSignedIn The new SDK abstracts away the KDF functionality behind an authentication class, so the previous status checks are not possible, and the isSignedIn is the closest alternative without removing the logic entirely (possibly breaking change) * bump kdf version * load coin assets from sdk package komodo_defi_framework package already downloads the configs and icons, so load them from the external package instead of redownloading and loading the same assets twice * ci: update paths in validate action * replace dynamic index.html with static version the drawbacks of bundling with webpack outweighed the negligible size reduction in the kdf wasm files, so it was dropped in favour of a static index.html in the sdk * fix failing unit tests caused by the `rational` and `decimal` package updates * update docs remove nodejs, api update script, and add fvm as flutter installation alternative * fix breaking changes to rpc method return types sdk returns different types which have to be accounted for, sometimes on a per-rpc basis * fix validation warnings * WIP: fix integration tests temporarily disable suspended assets test. The `coins_config.json` is no longer editable as it is obtained from an external package, so alternative means of invalidating the electrum URLs for an asset are required * re-enable suspended assets test on chrome * block electrum urls in chrome for suspended assets test * update logs location in ui-tests workflow * fix taker order integration test * coin addresses bloc init * coin addresses init list in coin details * coin addresses separate status for creation * create addresses cleanup * change from sdk to komodo_defi_framework * coin addresses realistic fake address * coin addresses hide middle part of the address * fix debug utils & withdraw test * remove driver start step from ui test workflow browser driver startup is now handled by integration test runner * coin addresses improve ui * coin addresses special exception type * coin addresses copy button functionality * coin addresses swap address tag * coin addresses bloc hide zero balance * coin addresses styling and hide 0 balance * improve driver management & fix `test_withdraw` test add and refactor integration test utility functions * add pub get flag flutter drive runs `flutter pub get` before each test by default, which slows down the current implementation, which runs each set of tests independently * add profile mode step to ui-tests-on-pr * add verbose logging to integration tests steps some errors, like pumpAndSettle timeouts, do not produce useful stacktraces, so adding print statements is necessary unless we run the integration tests in debug mode (not recommended) * coin addresses extract widget to file * coin addresses QR code * coin addresses extract widgets * coin addresses mobile view init * coin addresses extract widgets * coin addresses improve mobile view layout * coin addresses slightly larger swap address text * coin addresses improve QR dialog * coin addresses localize texts with existing keys * coin addresses localize texts * coin addresses localize texts * coin addresses polish * fix cex_prices integration test * fix theme switching on web * fix nft and trezor RPC call type errors type conversions missed during initial migration, but caught by integration tests * migrate web file_loader to js_interop and package:web * change file loader to conditional import structure js_interop causes builds to fail once again * fix seed file upload & add keep-running flag safaridriver logs do not include console logs, so we have to keep the browser window open to read console logs in the event of failure * fix intermittent test failures * bump sdk version & add debug statements * increase flutter drive timeout & remove tests.dart group structure is better suited to the new integration_test format rather than flutter_drive. `await app.main` fails after the second test when in the same group * fix merge error * fix validation warnings & flaky dex taker test * add port option & fix flaky dex ui test steps * tests: add longer wait after taker/maker confirmation button click * fix misc integration test errors when switching theme * fix trading bot order count in tab bar * fix analyze warnings and increase tab refresh rate * restore missing error check throw exception if error response is received from API. Market maker bot was failing to start/stop after this was removed as part of the sdk integration * fix trading bot & dex order list onClick events * fix merge issues & trade bot empty list check list null = empty * fix best orders and connected peers parsing v2 uses orderaddress object for address field instead of string * replace auth methods with KomodoDefiSdk * fix global variable initialization order and async issues Migrating away from the global variables in blocs.dart will involve a considerable number of code changes, so leaving as-is for now * use sdk in current wallet "bloc" default values used for hasBackup * move global bloc variables to repositoryprovider async sdk/kdf initialization does not work with the current global variable structure, so moved the global repositories to repositoryprovider * revert breaking wallet restore and import changes * skip orders, and swaps RPCs when not logged in also bump kdf sdk version * fix login to existing wallet and remove onlogout deactivate * catch and log uncaught (or async) errors * sdk txs disable tests and mock repository * sdk txs cex market data charts * sdk txs transactions import change * fix onlogout balance clearing throw exceptions with stacktraces * sdk txs use the new sdk Transaction model * sdk txs use the new sdk Transaction model * sdk txs use the new sdk Transaction model * sdk txs use the new sdk Transaction model * sdk txs change types import * tests: use faucet for doc/marty if balance is insufficient * fix maker form auth state listener * sdk txs fee coin * fix validation warnings and remove gha timeout at step * sdk txs fetch from sdk * coin details hide addresses if tx is selected * sdk txs always show transactions if filled * fix transactions missing one row during loading * sdk txs getSdkAsset temp helper function * sdk access added to CoinsRepo * sdk replace max_maker_vol with sdk * sdk getSdkPubkeys helper function * add wallet metadata and disable unsupported features - add `has_backup` and `WalletType` metadata fields to KdfUser - disable delete wallet feature - * sdk addresses port * replace current wallet stream with auth bloc * rename coins "bloc" and move more repos to DI * use new asset class for trezor coin activation trezor activation responds to 'UserActionRequired' events, which are not propagated through asset manager * migrate coins manager from legacy coins bloc to repository and new coins bloc * migrate legacy coins bloc to coins repostory * remove runtime coin updates `KomodoCoins` fetches coins from github cdn at runtime, so runtime updates are no longer necessary * remove coin activation rpc models and migrate legacy coins bloc refs * add activated coins metadata * refactor: migrate fiat bloc from legacy coins bloc * migrate remaining legacy coins bloc references to coins repo * migrate current wallet references to auth bloc * fix swap page validation issues * fix market metrics and transaction history loading * fix trading bot validation errors coins bloc emits not refreshing the widgets at the bottom of the tree * fix bridge page missing protocols * fix coin activation error handling * fix merge issues * fix timestamp in unit test transaction generation * sdk show pubkey addresses for all coins * sdk addresses active swap address tag * sdk addresses display balance * sdk addresses polish balance display * fix view seed and re-login flow * fix coin activation via coins manager activating coins were only displayed on the next balance refresh instead of immediately after activation * add parent coin to conversion * sdk addresses hide zero balances checkbox * sdk addresses reload after create * sdk remove coin addresses repository * fix portfolio growth concurrent modification * fix coin details tab controller error * sdk getSdkAsset use findAssetsByTicker * sdk addresses max 3 empty addresses * sdk addresses split variables * sdk txs add address * sdk txs add address mobile * sdk txs load 200 * fix sporadic activation in coins list * add legacy wallet migration * sdk asset getPubkeys * sdk use assetsFromTicker * pubspec upgrade * coin details history -> lastTransactions * coin details transactions better spacing * coin details polish transactions section * upgrade to flutter 3.27.0 * coin details redesign tx history section * coin details redesign tx history section mobile * fix new validation warnings from flutter upgrade * fix trezor login login does not work in debug mode because of an assert statement in the SDK * bump flutter version in workflows * fix macos and ios builds * decouple balance fetching & coin activation * sdk addresses use CantCreateNewAddressReason * coin details remove createAddressAllowed field * sdk localize CantCreateNewAddressReason texts * trim unused methods from coin model * fix coin balances persisting to another wallet login * fix initial trezor coin activation list * fix trezor coin add asset page * call faucet on all addresses should implement a more robust solution to HD wallet support for faucet * improve login activated coins metadata storing * upgrade sdk --------- Co-authored-by: Francois --- .github/actions/flutter-deps/action.yml | 7 +- .github/workflows/ui-tests-on-pr.yml | 32 +- app_theme/lib/src/dark/theme_global_dark.dart | 16 +- .../lib/src/light/theme_global_light.dart | 23 +- app_theme/pubspec.lock | 8 +- assets/translations/en.json | 14 + ios/Runner.xcodeproj/project.pbxproj | 8 - ios/Runner/AppDelegate.swift | 161 +--- ios/Runner/Runner-Bridging-Header.h | 1 - ios/Runner/mm2.h | 36 - ios/Runner/mm2.m | 235 ------ lib/app_config/app_config.dart | 6 - lib/app_config/coins_config_parser.dart | 146 ---- lib/bloc/app_bloc_root.dart | 135 +++- lib/bloc/auth_bloc/auth_bloc.dart | 243 ++++-- lib/bloc/auth_bloc/auth_bloc_event.dart | 39 +- lib/bloc/auth_bloc/auth_bloc_state.dart | 12 +- lib/bloc/auth_bloc/auth_repository.dart | 43 - lib/bloc/bridge_form/bridge_bloc.dart | 25 +- lib/bloc/bridge_form/bridge_repository.dart | 27 +- lib/bloc/bridge_form/bridge_validator.dart | 8 +- lib/bloc/cex_market_data/charts.dart | 15 +- .../mock_portfolio_growth_repository.dart | 10 +- .../mock_transaction_history_repository.dart | 4 +- .../portfolio_growth_bloc.dart | 104 ++- .../portfolio_growth_repository.dart | 25 +- .../models/price_chart_interval.dart | 8 +- .../price_chart/models/time_period.dart | 8 +- .../demo_profit_loss_repository.dart | 5 + .../profit_loss_transaction_extension.dart | 12 +- .../models/price_stamped_transaction.dart | 24 +- .../profit_loss/models/profit_loss.dart | 10 +- .../profit_loss/profit_loss_bloc.dart | 21 +- .../profit_loss/profit_loss_calculator.dart | 22 +- .../profit_loss/profit_loss_repository.dart | 26 +- .../bloc/coin_addresses_bloc.dart | 69 ++ .../bloc/coin_addresses_event.dart | 25 + .../bloc/coin_addresses_state.dart | 77 ++ lib/bloc/coins_bloc/asset_coin_extension.dart | 92 +++ lib/bloc/coins_bloc/coins_bloc.dart | 499 ++++++++++++ lib/bloc/coins_bloc/coins_event.dart | 79 ++ lib/bloc/coins_bloc/coins_repo.dart | 755 ++++++++++++------ lib/bloc/coins_bloc/coins_state.dart | 46 ++ .../coins_manager/coins_manager_bloc.dart | 91 +-- .../coins_manager/coins_manager_event.dart | 3 +- .../coins_manager/coins_manager_state.dart | 5 +- lib/bloc/dex_repository.dart | 18 +- lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart | 39 +- lib/bloc/dex_tab_bar/dex_tab_bar_event.dart | 8 +- .../fiat/fiat_onramp_form/fiat_form_bloc.dart | 23 +- lib/bloc/fiat/fiat_repository.dart | 11 +- ...arket_maker_bot_order_list_repository.dart | 4 +- .../market_maker_order_list_bloc.dart | 6 +- .../market_maker_trade_form_bloc.dart | 12 +- .../nft_receive/bloc/nft_receive_bloc.dart | 9 +- .../bloc/nft_transactions_bloc.dart | 24 +- .../nft_transactions/nft_txn_repository.dart | 2 +- lib/bloc/nft_withdraw/nft_withdraw_bloc.dart | 23 +- lib/bloc/nft_withdraw/nft_withdraw_repo.dart | 13 +- lib/bloc/nfts/nft_main_bloc.dart | 12 +- lib/bloc/nfts/nft_main_repo.dart | 8 +- .../coin_config_bloc.dart | 145 ---- .../coin_config_event.dart | 24 - .../coin_config_state.dart | 52 -- .../runtime_update_config_provider.dart | 21 - .../security_settings_bloc.dart | 35 +- .../security_settings_state.dart | 2 +- .../system_health/system_health_bloc.dart | 17 +- lib/bloc/taker_form/taker_bloc.dart | 22 +- lib/bloc/taker_form/taker_validator.dart | 24 +- .../transaction_history_bloc.dart | 77 +- .../transaction_history_event.dart | 14 +- .../transaction_history_repo.dart | 195 +---- .../transaction_history_state.dart | 53 +- lib/bloc/trezor_bloc/trezor_repo.dart | 67 +- .../trezor_connection_bloc.dart | 30 +- .../trezor_init_bloc/trezor_init_bloc.dart | 199 +++-- .../trezor_init_bloc/trezor_init_event.dart | 11 +- .../trezor_init_bloc/trezor_init_state.dart | 17 +- lib/bloc/wallets_bloc/wallets_repo.dart | 45 -- .../withdraw_form/withdraw_form_bloc.dart | 34 +- .../withdraw_form/withdraw_form_state.dart | 16 +- lib/blocs/blocs.dart | 58 -- lib/blocs/coins_bloc.dart | 515 ------------ lib/blocs/current_wallet_bloc.dart | 65 +- lib/blocs/dropdown_dismiss_bloc.dart | 42 - lib/blocs/kmd_rewards_bloc.dart | 16 +- lib/blocs/maker_form_bloc.dart | 49 +- lib/blocs/startup_bloc.dart | 50 -- lib/blocs/trading_entities_bloc.dart | 85 +- lib/blocs/trezor_coins_bloc.dart | 143 ++-- lib/blocs/wallets_bloc.dart | 206 ----- lib/blocs/wallets_repository.dart | 85 ++ lib/generated/codegen_loader.g.dart | 14 + lib/main.dart | 114 ++- lib/mm2/mm2.dart | 142 +--- lib/mm2/mm2_api/mm2_api.dart | 493 ++---------- lib/mm2/mm2_api/mm2_api_nft.dart | 42 +- .../mm2_api/rpc/electrum/electrum_req.dart | 41 - lib/mm2/mm2_api/rpc/enable/enable_req.dart | 123 --- ...eq.dart => get_enabled_coins_request.dart} | 0 .../rpc/max_maker_vol/max_maker_vol_req.dart | 20 - .../max_maker_vol/max_maker_vol_response.dart | 34 - .../orderbook_depth_response.dart | 18 +- .../trezor_enable_utxo_request.dart | 10 +- .../mm2_api/rpc/withdraw/withdraw_errors.dart | 17 +- lib/model/coin.dart | 289 ++----- lib/model/coin_utils.dart | 5 +- lib/model/electrum.dart | 29 - lib/model/forms/coin_select_input.dart | 7 +- lib/model/orderbook_model.dart | 8 +- lib/model/swap.dart | 4 +- lib/model/wallet.dart | 158 +++- .../navigators/app_router_delegate.dart | 25 +- lib/router/parsers/root_route_parser.dart | 21 +- lib/router/parsers/wallet_route_parser.dart | 12 +- lib/router/state/routing_state.dart | 4 +- lib/router/state/settings_section_state.dart | 6 +- lib/services/auth_checker/auth_checker.dart | 5 - .../auth_checker/get_auth_checker.dart | 10 - .../auth_checker/mock_auth_checker.dart | 14 - .../auth_checker/web_auth_checker.dart | 84 -- lib/services/cex_service/cex_service.dart | 154 ---- lib/services/coins_service/coins_service.dart | 17 - .../initializer/app_bootstrapper.dart | 47 +- lib/services/logger/get_logger.dart | 19 + .../logger/logger_metadata_mixin.dart | 2 +- lib/services/logger/universal_logger.dart | 11 +- .../orders_service/my_orders_service.dart | 12 +- lib/services/swaps_service/swaps_service.dart | 48 -- lib/shared/utils/debug_utils.dart | 60 +- .../utils/extensions/async_extensions.dart | 14 - lib/shared/utils/formatters.dart | 13 + lib/shared/utils/password.dart | 4 +- lib/shared/utils/utils.dart | 11 + lib/shared/widgets/auto_scroll_text.dart | 5 +- lib/shared/widgets/coin_item/coin_logo.dart | 2 +- .../connect_wallet/connect_wallet_button.dart | 2 +- lib/shared/widgets/hidden_with_wallet.dart | 16 +- lib/shared/widgets/hidden_without_wallet.dart | 27 +- lib/shared/widgets/logout_popup.dart | 98 +-- .../widgets/password_visibility_control.dart | 3 +- lib/views/bridge/bridge_confirmation.dart | 11 +- lib/views/bridge/bridge_exchange_form.dart | 78 +- lib/views/bridge/bridge_page.dart | 1 - lib/views/bridge/bridge_tab_bar.dart | 7 +- .../bridge_target_protocol_selector_tile.dart | 5 +- .../bridge/view/bridge_target_amount_row.dart | 5 +- .../table/bridge_target_protocols_table.dart | 5 +- .../view/table/bridge_tickers_list.dart | 2 +- .../header/actions/account_switcher.dart | 38 +- .../common/header/actions/header_actions.dart | 12 +- lib/views/common/header/app_header.dart | 9 - .../hw_wallet_dialog/hw_dialog_init.dart | 1 - .../hw_dialog_wallet_select.dart | 1 - .../show_trezor_passphrase_dialog.dart | 2 + .../show_trezor_pin_dialog.dart | 2 + .../trezor_steps/trezor_dialog_error.dart | 3 +- .../trezor_steps/trezor_dialog_pin_pad.dart | 4 +- .../main_menu/main_menu_bar_mobile.dart | 18 +- .../common/main_menu/main_menu_desktop.dart | 20 +- .../password_dialog_content.dart | 21 +- .../wallet_password_dialog.dart | 6 +- lib/views/dex/common/fiat_amount.dart | 2 +- lib/views/dex/common/front_plate.dart | 2 +- lib/views/dex/dex_helpers.dart | 77 +- .../desktop/dex_list_filter_coin_desktop.dart | 29 +- .../dex_list_filter_coins_list_mobile.dart | 7 +- .../mobile/dex_list_header_mobile.dart | 5 +- lib/views/dex/dex_page.dart | 20 +- .../common/buy_price_mobile.dart | 5 +- .../common/coin_amount_mobile.dart | 6 +- .../common/trade_amount_desktop.dart | 6 +- .../entities_list/history/history_item.dart | 11 +- .../entities_list/history/history_list.dart | 9 +- .../history/swap_history_sort_mixin.dart | 11 +- .../in_progress/in_progress_item.dart | 7 +- .../in_progress/in_progress_list.dart | 9 +- .../orders/order_cancel_button.dart | 5 +- .../dex/entities_list/orders/order_item.dart | 7 +- .../dex/entities_list/orders/orders_list.dart | 7 +- .../maker_order/maker_order_details_page.dart | 6 +- .../swap/swap_details_step.dart | 3 +- .../swap/swap_details_step_list.dart | 16 +- .../swap/swap_recover_button.dart | 9 +- .../dex/entity_details/trading_details.dart | 13 +- .../trading_details_coin_pair.dart | 8 +- lib/views/dex/orderbook/orderbook_table.dart | 14 +- .../dex/orderbook/orderbook_table_item.dart | 14 +- lib/views/dex/orderbook/orderbook_view.dart | 5 +- .../confirm/maker_order_confirmation.dart | 17 +- .../confirm/taker_order_confirmation.dart | 10 +- .../form/exchange_info/exchange_rate.dart | 15 +- .../simple/form/exchange_info/total_fees.dart | 23 +- .../form/maker/maker_form_buy_amount.dart | 7 +- .../form/maker/maker_form_buy_coin_table.dart | 4 +- .../form/maker/maker_form_buy_item.dart | 4 +- .../form/maker/maker_form_compare_to_cex.dart | 4 +- .../simple/form/maker/maker_form_content.dart | 6 +- .../form/maker/maker_form_error_list.dart | 4 +- .../form/maker/maker_form_exchange_rate.dart | 4 +- .../simple/form/maker/maker_form_layout.dart | 14 +- .../form/maker/maker_form_orderbook.dart | 32 +- .../form/maker/maker_form_price_item.dart | 14 +- .../form/maker/maker_form_sell_amount.dart | 7 +- .../maker/maker_form_sell_coin_table.dart | 4 +- .../form/maker/maker_form_sell_header.dart | 15 +- .../form/maker/maker_form_sell_item.dart | 4 +- .../form/maker/maker_form_total_fees.dart | 4 +- .../form/maker/maker_form_trade_button.dart | 10 +- .../coins_table/coins_table_content.dart | 11 +- .../orders_table/grouped_list_view.dart | 33 +- .../tables/orders_table/orders_table.dart | 5 +- .../orders_table/orders_table_content.dart | 1 + .../dex/simple/form/tables/table_utils.dart | 68 +- .../coin_item/taker_form_buy_amount.dart | 2 +- .../taker/coin_item/taker_form_buy_item.dart | 7 +- .../coin_item/taker_form_sell_amount.dart | 2 +- .../dex/simple/form/taker/taker_form.dart | 31 +- .../simple/form/taker/taker_form_content.dart | 9 +- .../form/taker/taker_form_exchange_info.dart | 3 +- .../simple/form/taker/taker_order_book.dart | 5 +- lib/views/fiat/fiat_form.dart | 162 ++-- lib/views/fiat/fiat_page.dart | 15 +- lib/views/fiat/fiat_payment_method_card.dart | 2 +- lib/views/fiat/fiat_payment_method_group.dart | 4 +- lib/views/fiat/fiat_select_button.dart | 8 +- lib/views/main_layout/main_layout.dart | 72 +- .../animated_bot_status_indicator.dart | 2 +- .../coin_search_dropdown.dart | 9 +- .../coin_selection_and_amount_input.dart | 8 +- .../coin_trade_amount_form_field.dart | 10 +- .../market_maker_bot_form.dart | 46 +- .../market_maker_bot_form_content.dart | 5 +- .../market_maker_bot_page.dart | 24 +- ...t_maker_form_error_message_extensions.dart | 8 +- .../trade_pair_list_item.dart | 6 +- .../nfts/details_page/nft_details_page.dart | 8 +- lib/views/nfts/nft_list/nft_list_item.dart | 4 +- lib/views/nfts/nft_page.dart | 4 +- .../nfts/nft_receive/nft_receive_page.dart | 8 +- .../common/widgets/nft_txn_hash.dart | 6 +- .../mobile/widgets/nft_txn_copied_text.dart | 10 +- .../nfts/nft_transactions/nft_txn_page.dart | 8 +- .../general_settings/app_version_number.dart | 11 +- .../general_settings/import_swaps.dart | 7 +- .../settings_reset_activated_coins.dart | 22 +- .../general_settings/show_swap_data.dart | 2 + .../password_update_page.dart | 22 +- .../security_settings/plate_seed_backup.dart | 17 +- .../security_settings_main_page.dart | 6 +- .../security_settings_page.dart | 58 +- .../backup_seed_notification.dart | 16 +- .../seed_settings/seed_show.dart | 9 +- .../seed_settings/seed_word_button.dart | 8 +- .../widgets/settings_menu/settings_menu.dart | 11 +- .../wallet/coin_details/coin_details.dart | 8 +- .../charts/portfolio_growth_chart.dart | 27 +- .../charts/portfolio_profit_loss_chart.dart | 5 +- .../coin_details_info/coin_addresses.dart | 489 ++++++++++++ .../coin_details_common_buttons.dart | 6 +- .../coin_details_info/coin_details_info.dart | 205 ++--- .../contract_address_button.dart | 7 +- .../faucet/cubit/faucet_cubit.dart | 25 +- .../coin_details/faucet/faucet_page.dart | 5 +- .../faucet/widgets/faucet_message.dart | 6 +- .../coin_details/receive/receive_address.dart | 6 +- .../coin_details/receive/receive_details.dart | 12 +- .../receive/request_address_button.dart | 14 +- .../rewards/kmd_reward_claim_success.dart | 6 +- .../rewards/kmd_rewards_info.dart | 21 +- .../transactions/transaction_details.dart | 39 +- .../transactions/transaction_list.dart | 129 ++- .../transactions/transaction_list_item.dart | 134 ++-- .../transactions/transaction_table.dart | 54 +- .../withdraw_form/pages/failed_page.dart | 7 +- .../widgets/fill_form/fill_form_title.dart | 2 +- .../send_confirm_form/send_confirm_item.dart | 4 +- .../withdraw_form/withdraw_form.dart | 10 +- .../coins_manager/coins_manager_controls.dart | 1 - .../coins_manager_filters_dropdown.dart | 20 +- .../coins_manager_list_wrapper.dart | 8 +- .../coins_manager/coins_manager_page.dart | 56 +- .../coins_manager_select_all_button.dart | 1 - .../coins_manager_selected_types_list.dart | 2 - .../coins_manager_switch_button.dart | 1 - .../wallet/common/address_copy_button.dart | 20 + lib/views/wallet/common/address_icon.dart | 29 + lib/views/wallet/common/address_text.dart | 18 + .../wallet_page/charts/coin_prices_chart.dart | 10 +- .../common/coin_list_item_desktop.dart | 12 +- .../wallet_main/active_coins_list.dart | 58 +- .../wallet_main/all_coins_list.dart | 37 +- .../wallet_page/wallet_main/wallet_main.dart | 191 +++-- .../wallet_main/wallet_manage_section.dart | 4 +- .../wallet_main/wallet_overview.dart | 16 +- lib/views/wallet/wallet_page/wallet_page.dart | 38 +- .../wallets_manager_wrapper.dart | 6 +- .../widgets/hardware_wallets_manager.dart | 40 +- .../widgets/iguana_wallets_manager.dart | 152 ++-- .../widgets/wallet_creation.dart | 6 +- .../widgets/wallet_deleting.dart | 48 +- .../widgets/wallet_import_by_file.dart | 11 +- .../widgets/wallet_list_item.dart | 10 +- .../wallets_manager/widgets/wallet_login.dart | 11 +- .../widgets/wallet_simple_import.dart | 6 +- .../wallets_manager/widgets/wallets_list.dart | 8 +- macos/Runner.xcodeproj/project.pbxproj | 36 +- macos/Runner/AppDelegate.swift | 76 +- macos/Runner/DebugProfile.entitlements | 2 + macos/Runner/MainFlutterWindow.swift | 6 +- macos/Runner/Release.entitlements | 2 + macos/Runner/Runner-Bridging-Header.h | 1 - macos/Runner/mm2.h | 36 - macos/Runner/mm2.m | 235 ------ packages/komodo_cex_market_data/pubspec.lock | 4 +- packages/komodo_cex_market_data/pubspec.yaml | 2 +- packages/komodo_coin_updates/.gitignore | 7 - packages/komodo_coin_updates/CHANGELOG.md | 5 - packages/komodo_coin_updates/README.md | 71 -- .../komodo_coin_updates/analysis_options.yaml | 250 ------ .../example/komodo_coin_updates_example.dart | 4 - .../lib/komodo_coin_updates.dart | 8 - .../lib/src/data/coin_config_provider.dart | 102 --- .../lib/src/data/coin_config_repository.dart | 164 ---- .../lib/src/data/coin_config_storage.dart | 67 -- .../lib/src/data/data.dart | 3 - .../lib/src/komodo_coin_updater.dart | 50 -- .../adapters/address_format_adapter.dart | 38 - .../adapters/checkpoint_block_adapter.dart | 44 - .../lib/src/models/adapters/coin_adapter.dart | 167 ---- .../models/adapters/coin_config_adapter.dart | 248 ------ .../models/adapters/coin_info_adapter.dart | 38 - .../adapters/consensus_params_adapter.dart | 65 -- .../src/models/adapters/contact_adapter.dart | 38 - .../src/models/adapters/electrum_adapter.dart | 41 - .../src/models/adapters/links_adapter.dart | 38 - .../lib/src/models/adapters/node_adapter.dart | 38 - .../src/models/adapters/protocol_adapter.dart | 41 - .../adapters/protocol_data_adapter.dart | 68 -- .../src/models/adapters/rpc_url_adapter.dart | 35 - .../lib/src/models/address_format.dart | 31 - .../lib/src/models/checkpoint_block.dart | 39 - .../lib/src/models/coin.dart | 219 ----- .../lib/src/models/coin_config.dart | 417 ---------- .../lib/src/models/coin_info.dart | 24 - .../lib/src/models/consensus_params.dart | 85 -- .../lib/src/models/contact.dart | 28 - .../lib/src/models/electrum.dart | 43 - .../lib/src/models/links.dart | 31 - .../lib/src/models/models.dart | 13 - .../lib/src/models/node.dart | 39 - .../lib/src/models/protocol.dart | 39 - .../lib/src/models/protocol_data.dart | 95 --- .../lib/src/models/rpc_url.dart | 25 - .../lib/src/models/runtime_update_config.dart | 45 -- packages/komodo_coin_updates/pubspec.yaml | 37 - .../test/komodo_coin_updates_test.dart | 14 - .../language_switcher/language_line.dart | 7 +- .../theme_switcher/theme_switcher.dart | 2 +- .../lib/src/buttons/ui_border_button.dart | 8 +- .../lib/src/buttons/upload_button.dart | 2 +- .../lib/src/display/statistic_card.dart | 2 +- .../lib/src/painter/focus_decorator.dart | 2 +- packages/komodo_ui_kit/pubspec.lock | 8 +- pubspec.lock | 71 +- pubspec.yaml | 7 +- run_integration_tests.dart | 3 +- test_integration/common/goto.dart | 2 +- test_integration/helpers/restore_wallet.dart | 2 +- .../runners/drivers/chrome_driver.dart | 8 +- .../tests/dex_tests/maker_orders_test.dart | 75 ++ .../suspended_assets_test.dart | 56 +- .../tests/wallets_tests/wallet_tools.dart | 3 +- .../tests/wallets_tests/wallets_tests.dart | 4 - .../profit_loss_repository_test.dart | 2 +- .../transaction_generation.dart | 61 +- test_units/tests/utils/test_util.dart | 11 +- 378 files changed, 5977 insertions(+), 9415 deletions(-) delete mode 100644 ios/Runner/mm2.h delete mode 100644 ios/Runner/mm2.m delete mode 100644 lib/app_config/coins_config_parser.dart delete mode 100644 lib/bloc/auth_bloc/auth_repository.dart create mode 100644 lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart create mode 100644 lib/bloc/coin_addresses/bloc/coin_addresses_event.dart create mode 100644 lib/bloc/coin_addresses/bloc/coin_addresses_state.dart create mode 100644 lib/bloc/coins_bloc/asset_coin_extension.dart create mode 100644 lib/bloc/coins_bloc/coins_bloc.dart create mode 100644 lib/bloc/coins_bloc/coins_event.dart create mode 100644 lib/bloc/coins_bloc/coins_state.dart delete mode 100644 lib/bloc/runtime_coin_updates/coin_config_bloc.dart delete mode 100644 lib/bloc/runtime_coin_updates/coin_config_event.dart delete mode 100644 lib/bloc/runtime_coin_updates/coin_config_state.dart delete mode 100644 lib/bloc/runtime_coin_updates/runtime_update_config_provider.dart delete mode 100644 lib/bloc/wallets_bloc/wallets_repo.dart delete mode 100644 lib/blocs/blocs.dart delete mode 100644 lib/blocs/coins_bloc.dart delete mode 100644 lib/blocs/dropdown_dismiss_bloc.dart delete mode 100644 lib/blocs/startup_bloc.dart delete mode 100644 lib/blocs/wallets_bloc.dart create mode 100644 lib/blocs/wallets_repository.dart delete mode 100644 lib/mm2/mm2_api/rpc/electrum/electrum_req.dart delete mode 100644 lib/mm2/mm2_api/rpc/enable/enable_req.dart rename lib/mm2/mm2_api/rpc/{get_enabled_coins/get_enabled_coins_req.dart => get_enabled_coins_request.dart} (100%) delete mode 100644 lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart delete mode 100644 lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart delete mode 100644 lib/model/electrum.dart delete mode 100644 lib/services/auth_checker/auth_checker.dart delete mode 100644 lib/services/auth_checker/get_auth_checker.dart delete mode 100644 lib/services/auth_checker/mock_auth_checker.dart delete mode 100644 lib/services/auth_checker/web_auth_checker.dart delete mode 100644 lib/services/cex_service/cex_service.dart delete mode 100644 lib/services/coins_service/coins_service.dart delete mode 100644 lib/services/swaps_service/swaps_service.dart create mode 100644 lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart create mode 100644 lib/views/wallet/common/address_copy_button.dart create mode 100644 lib/views/wallet/common/address_icon.dart create mode 100644 lib/views/wallet/common/address_text.dart delete mode 100644 macos/Runner/Runner-Bridging-Header.h delete mode 100644 macos/Runner/mm2.h delete mode 100644 macos/Runner/mm2.m delete mode 100644 packages/komodo_coin_updates/.gitignore delete mode 100644 packages/komodo_coin_updates/CHANGELOG.md delete mode 100644 packages/komodo_coin_updates/README.md delete mode 100644 packages/komodo_coin_updates/analysis_options.yaml delete mode 100644 packages/komodo_coin_updates/example/komodo_coin_updates_example.dart delete mode 100644 packages/komodo_coin_updates/lib/komodo_coin_updates.dart delete mode 100644 packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart delete mode 100644 packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart delete mode 100644 packages/komodo_coin_updates/lib/src/data/coin_config_storage.dart delete mode 100644 packages/komodo_coin_updates/lib/src/data/data.dart delete mode 100644 packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/address_format_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/checkpoint_block_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/coin_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/coin_config_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/coin_info_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/consensus_params_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/contact_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/electrum_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/links_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/node_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/protocol_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/protocol_data_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/adapters/rpc_url_adapter.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/address_format.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/checkpoint_block.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/coin.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/coin_config.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/coin_info.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/consensus_params.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/contact.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/electrum.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/links.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/models.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/node.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/protocol.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/protocol_data.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/rpc_url.dart delete mode 100644 packages/komodo_coin_updates/lib/src/models/runtime_update_config.dart delete mode 100644 packages/komodo_coin_updates/pubspec.yaml delete mode 100644 packages/komodo_coin_updates/test/komodo_coin_updates_test.dart diff --git a/.github/actions/flutter-deps/action.yml b/.github/actions/flutter-deps/action.yml index b11e27c2d1..331a114d3f 100644 --- a/.github/actions/flutter-deps/action.yml +++ b/.github/actions/flutter-deps/action.yml @@ -3,15 +3,10 @@ description: "Installs Flutter and any other dependencies required for the build runs: using: "composite" steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - name: Get stable flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.24.x" + flutter-version: "3.27.x" channel: "stable" - name: Prepare build directory diff --git a/.github/workflows/ui-tests-on-pr.yml b/.github/workflows/ui-tests-on-pr.yml index ca1148214c..0a44fc3da0 100644 --- a/.github/workflows/ui-tests-on-pr.yml +++ b/.github/workflows/ui-tests-on-pr.yml @@ -14,16 +14,13 @@ jobs: strategy: fail-fast: false matrix: - name: [ - web-app-linux-profile, - web-app-macos, - ] + name: [web-app-linux-profile, web-app-macos] include: - name: web-app-linux-profile os: [self-hosted, linux] browser: chrome - display: 'headless' - resolution: '1600,1024' + display: "headless" + resolution: "1600,1024" mode: profile # memory_profile.json should be generated in profile mode driver_logs: | @@ -33,8 +30,8 @@ jobs: - name: web-app-macos os: [self-hosted, macos] browser: safari - display: 'headless' # has no affect with safaridriver - resolution: '1600,1024' # has no affect with safaridriver + display: "headless" # has no affect with safaridriver + resolution: "1600,1024" # has no affect with safaridriver mode: release driver_logs: | ./*.log @@ -55,7 +52,6 @@ jobs: install-chromedriver: true install-dependencies: true - - name: Enable safaridriver (sudo) (MacOS) if: ${{ matrix.browser == 'safari' }} timeout-minutes: 1 @@ -79,7 +75,6 @@ jobs: - name: Test air_dex ${{ matrix.browser }} id: integration-tests continue-on-error: true - timeout-minutes: 35 env: GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -97,13 +92,16 @@ jobs: path: ${{ matrix.driver_logs }} if-no-files-found: warn - - name: Generate coverage report - if: ${{ matrix.browser == 'chrome' }} - continue-on-error: true - timeout-minutes: 35 - uses: ./.github/actions/code-coverage - with: - test_file: 'test_integration' + # TODO: re-enable once integration test coverage is fixed. + # there are errors related to Hive and other storage providers + # that will likely need to be mocked to support the new + # flutter integration test structure (flutter drive is deprecated) + # - name: Generate coverage report + # if: ${{ matrix.browser == 'chrome' }} + # continue-on-error: true + # uses: ./.github/actions/code-coverage + # with: + # test_file: 'test_integration' - name: Fail workflow if tests failed if: ${{ steps.integration-tests.outcome == 'failure' }} diff --git a/app_theme/lib/src/dark/theme_global_dark.dart b/app_theme/lib/src/dark/theme_global_dark.dart index 3526a8e8fc..21d7d24196 100644 --- a/app_theme/lib/src/dark/theme_global_dark.dart +++ b/app_theme/lib/src/dark/theme_global_dark.dart @@ -41,10 +41,10 @@ ThemeData get themeGlobalDark { bodyMedium: const TextStyle( fontSize: 16.0, color: textColor, fontWeight: FontWeight.w300), labelLarge: const TextStyle(fontSize: 16.0, color: textColor), - bodyLarge: TextStyle(fontSize: 14.0, color: textColor.withOpacity(0.5)), + bodyLarge: TextStyle(fontSize: 14.0, color: textColor.withValues(alpha: 0.5)), bodySmall: TextStyle( fontSize: 12.0, - color: textColor.withOpacity(0.8), + color: textColor.withValues(alpha: 0.8), fontWeight: FontWeight.w400, ), ); @@ -80,7 +80,7 @@ ThemeData get themeGlobalDark { snackBarTheme: snackBarThemeLight(), textSelectionTheme: TextSelectionThemeData( cursorColor: const Color.fromRGBO(57, 161, 238, 1), - selectionColor: const Color.fromRGBO(57, 161, 238, 1).withOpacity(0.3), + selectionColor: const Color.fromRGBO(57, 161, 238, 1).withValues(alpha: 0.3), selectionHandleColor: const Color.fromRGBO(57, 161, 238, 1), ), inputDecorationTheme: InputDecorationTheme( @@ -96,12 +96,12 @@ ThemeData get themeGlobalDark { filled: true, contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 22), hintStyle: TextStyle( - color: textColor.withOpacity(0.58), + color: textColor.withValues(alpha: 0.58), ), labelStyle: TextStyle( - color: textColor.withOpacity(0.58), + color: textColor.withValues(alpha: 0.58), ), - prefixIconColor: textColor.withOpacity(0.58), + prefixIconColor: textColor.withValues(alpha: 0.58), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( @@ -118,7 +118,7 @@ ThemeData get themeGlobalDark { backgroundColor: colorScheme.surfaceContainerLowest, surfaceTintColor: Colors.purple, selectedBackgroundColor: colorScheme.primary, - foregroundColor: textColor.withOpacity(0.7), + foregroundColor: textColor.withValues(alpha: 0.7), selectedForegroundColor: textColor, side: BorderSide(color: colorScheme.outlineVariant), shape: RoundedRectangleBorder( @@ -158,7 +158,7 @@ ThemeData get themeGlobalDark { textTheme: textTheme, scrollbarTheme: ScrollbarThemeData( thumbColor: - WidgetStateProperty.all(colorScheme.primary.withOpacity(0.8)), + WidgetStateProperty.all(colorScheme.primary.withValues(alpha: 0.8)), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( // remove icons shift diff --git a/app_theme/lib/src/light/theme_global_light.dart b/app_theme/lib/src/light/theme_global_light.dart index 8e985dacff..2cf8d19080 100644 --- a/app_theme/lib/src/light/theme_global_light.dart +++ b/app_theme/lib/src/light/theme_global_light.dart @@ -38,10 +38,11 @@ ThemeData get themeGlobalLight { bodyMedium: const TextStyle( fontSize: 16.0, color: textColor, fontWeight: FontWeight.w300), labelLarge: const TextStyle(fontSize: 16.0, color: textColor), - bodyLarge: TextStyle(fontSize: 14.0, color: textColor.withOpacity(0.5)), + bodyLarge: + TextStyle(fontSize: 14.0, color: textColor.withValues(alpha: 0.5)), bodySmall: TextStyle( fontSize: 12.0, - color: textColor.withOpacity(0.8), + color: textColor.withValues(alpha: 0.8), fontWeight: FontWeight.w400, ), ); @@ -77,7 +78,8 @@ ThemeData get themeGlobalLight { snackBarTheme: snackBarThemeLight(), textSelectionTheme: TextSelectionThemeData( cursorColor: const Color.fromRGBO(57, 161, 238, 1), - selectionColor: const Color.fromRGBO(57, 161, 238, 1).withOpacity(0.3), + selectionColor: + const Color.fromRGBO(57, 161, 238, 1).withValues(alpha: 0.3), selectionHandleColor: const Color.fromRGBO(57, 161, 238, 1), ), inputDecorationTheme: InputDecorationTheme( @@ -93,12 +95,12 @@ ThemeData get themeGlobalLight { filled: true, contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 22), hintStyle: TextStyle( - color: textColor.withOpacity(0.58), + color: textColor.withValues(alpha: 0.58), ), labelStyle: TextStyle( - color: textColor.withOpacity(0.58), + color: textColor.withValues(alpha: 0.58), ), - prefixIconColor: textColor.withOpacity(0.58), + prefixIconColor: textColor.withValues(alpha: 0.58), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( @@ -120,8 +122,8 @@ ThemeData get themeGlobalLight { ), textTheme: textTheme, scrollbarTheme: ScrollbarThemeData( - thumbColor: - WidgetStateProperty.all(colorScheme.primary.withOpacity(0.8)), + thumbColor: WidgetStateProperty.all( + colorScheme.primary.withValues(alpha: 0.8)), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( // remove icons shift @@ -139,7 +141,7 @@ ThemeData get themeGlobalLight { backgroundColor: const Color.fromRGBO(243, 245, 246, 1), surfaceTintColor: Colors.purple, selectedBackgroundColor: colorScheme.primary, - foregroundColor: textColor.withOpacity(0.7), + foregroundColor: textColor.withValues(alpha: 0.7), selectedForegroundColor: Colors.white, side: const BorderSide(color: Color.fromRGBO(208, 214, 237, 1)), shape: RoundedRectangleBorder( @@ -156,8 +158,7 @@ ThemeData get themeGlobalLight { color: colorScheme.primary, ), // Match the card's border radius - insets: const EdgeInsets.symmetric( - horizontal: 18), + insets: const EdgeInsets.symmetric(horizontal: 18), ), ), ); diff --git a/app_theme/pubspec.lock b/app_theme/pubspec.lock index 5b0b8ab2cf..5bb3e6a057 100644 --- a/app_theme/pubspec.lock +++ b/app_theme/pubspec.lock @@ -26,18 +26,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" plugin_platform_interface: dependency: "direct main" description: diff --git a/assets/translations/en.json b/assets/translations/en.json index 904b1bddda..c6f028d426 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -7,6 +7,7 @@ "price": "Price", "volume": "Volume", "history": "History", + "lastTransactions": "Last transactions", "active": "Active", "change24h": "Change 24h", "change24hRevert": "24h %", @@ -396,6 +397,12 @@ "withdrawNoSuchCoinError": "Invalid selection, {} does not exist", "txHistoryFetchError": "Error fetching tx history from the endpoint. Unsupported type: {}", "txHistoryNoTransactions": "Transactions are not available", + "maxGapLimitReached": "Maximum gap limit reached - please use existing unused addresses first", + "maxAddressesReached": "Maximum number of addresses reached for this asset", + "missingDerivationPath": "Missing derivation path configuration", + "protocolNotSupported": "Protocol does not support multiple addresses", + "derivationModeNotSupported": "Current wallet mode does not support multiple addresses", + "noActiveWallet": "No active wallet - please sign in first", "memo": "Memo", "gasPriceGwei": "Gas price [Gwei]", "gasLimit": "Gas limit", @@ -594,6 +601,13 @@ "mmBotFirstTradePreview": "Preview of the first order", "mmBotFirstTradeEstimate": "First trade estimate", "mmBotFirstOrderVolume": "This is an estimate of the first order only. Following orders will be placed automatically using the configured volume of the available {} balance.", + "onlySendToThisAddress": "Only send {} to this address", + "scanTheQrCode": "Scan the QR code on any mobile device wallet", + "swapAddress": "Swap Address", + "addresses": "Addresses", + "creating": "Creating", + "createAddress": "Create Address", + "hideZeroBalanceAddresses": "Hide 0 balance addresses", "important": "Important", "trend": "Trend", "growth": "Growth", diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f028d3b5b0..f560f4404f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -17,12 +17,10 @@ C2B3782B4B651E97F3AF9B7A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB340A008F6FECB3B82619D /* Pods_Runner.framework */; }; D63143E32701FFB500374C78 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E22701FFB500374C78 /* CoreFoundation.framework */; }; D63143E52702003500374C78 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E42701FFD100374C78 /* libc++.tbd */; }; - D63143E62702004B00374C78 /* libmm2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E12701FF6700374C78 /* libmm2.a */; }; D63143E92702008000374C78 /* libSystem.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E82702007200374C78 /* libSystem.tbd */; }; D63143EA2702008B00374C78 /* libSystem.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E82702007200374C78 /* libSystem.tbd */; }; D63143EB2702009900374C78 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143E72702005D00374C78 /* libresolv.tbd */; }; D63143ED270200B100374C78 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D63143EC270200B100374C78 /* SystemConfiguration.framework */; }; - D6C50BDA2702024E0095EE3C /* mm2.m in Sources */ = {isa = PBXBuildFile; fileRef = D6C50BD82702024E0095EE3C /* mm2.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -63,8 +61,6 @@ D63143E72702005D00374C78 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; D63143E82702007200374C78 /* libSystem.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libSystem.tbd; path = usr/lib/libSystem.tbd; sourceTree = SDKROOT; }; D63143EC270200B100374C78 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; - D6C50BD82702024E0095EE3C /* mm2.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = mm2.m; sourceTree = ""; }; - D6C50BD92702024E0095EE3C /* mm2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mm2.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,7 +72,6 @@ D63143EA2702008B00374C78 /* libSystem.tbd in Frameworks */, D63143E32701FFB500374C78 /* CoreFoundation.framework in Frameworks */, D63143EB2702009900374C78 /* libresolv.tbd in Frameworks */, - D63143E62702004B00374C78 /* libmm2.a in Frameworks */, D63143E92702008000374C78 /* libSystem.tbd in Frameworks */, D63143ED270200B100374C78 /* SystemConfiguration.framework in Frameworks */, C2B3782B4B651E97F3AF9B7A /* Pods_Runner.framework in Frameworks */, @@ -120,8 +115,6 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - D6C50BD92702024E0095EE3C /* mm2.h */, - D6C50BD82702024E0095EE3C /* mm2.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -327,7 +320,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D6C50BDA2702024E0095EE3C /* mm2.m in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0fddb44650..8be1cecd13 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,154 +1,13 @@ -import UIKit import Flutter -import Foundation -import CoreLocation -import os.log -import UserNotifications -import AVFoundation - -var mm2StartArgs: String? -var shouldRestartMM2: Bool = true; -var eventSink: FlutterEventSink? - -func flutterLog(_ log: String) { - eventSink?("{\"type\": \"log\", \"message\": \"\(log)\"}") -} - -func mm2Callback(line: UnsafePointer?) { - if let lineStr = line { - let logMessage = String(cString: lineStr) - flutterLog(logMessage) - } -} - -func performMM2Start() -> Int32 { - flutterLog("START MM2 --------------------------------") - let error = Int32(mm2_main(mm2StartArgs, mm2Callback)) - flutterLog("START MM2 RESULT: \(error) ---------------") - return error -} - -func performMM2Stop() -> Int32 { - flutterLog("STOP MM2 --------------------------------"); - let error = Int32(mm2_stop()); - flutterLog("STOP MM2 RESULT: \(error) ---------------"); - return error; -} - -func performMM2Restart() -> Int32 { - let _ = performMM2Stop() - var ticker: Int = 0 - // wait until mm2 stopped, but continue after 3s anyway - while(mm2_main_status() != 0 && ticker < 30) { - usleep(100000) // 0.1s - ticker += 1 - } - - let error = performMM2Start() - ticker = 0 - // wait until mm2 started, but continue after 10s anyway - while(mm2_main_status() != 3 && ticker < 100) { - usleep(100000) // 0.1s - ticker += 1 - } - - return error; -} - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { - var intentURI: String? - - - override func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { - self.intentURI = url.absoluteString - return true - } - - override func application (_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - guard let vc = window?.rootViewController as? FlutterViewController else { - fatalError ("rootViewController is not type FlutterViewController")} - let vcbm = vc as! FlutterBinaryMessenger - - let channelMain = FlutterMethodChannel (name: "komodo-web-dex", binaryMessenger: vcbm) - let eventChannel = FlutterEventChannel (name: "komodo-web-dex/event", binaryMessenger: vcbm) - eventChannel.setStreamHandler (self) - - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in - if success { - print("Notifications allowed!") - } else if let error = error { - print(error.localizedDescription) - } - } - - UIDevice.current.isBatteryMonitoringEnabled = true - - channelMain.setMethodCallHandler ({(call: FlutterMethodCall, result: FlutterResult) -> Void in - if call.method == "start" { - guard let arg = (call.arguments as! Dictionary)["params"] else { result(0); return } - mm2StartArgs = arg; - let error: Int32 = performMM2Start(); - - result(error) - } else if call.method == "status" { - let ret = Int32(mm2_main_status()); - result(ret) - } else if call.method == "stop" { - mm2StartArgs = nil; - let error: Int32 = performMM2Stop(); - - result(error) - } else if call.method == "restart" { - let error: Int32 = performMM2Restart(); - - result(error) - } else {result (FlutterMethodNotImplemented)}}) - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - @objc func onDidReceiveData(_ notification:Notification) { - if let data = notification.userInfo as? [String: String] - { - flutterLog(data["log"]!) - } - - } - - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - eventSink = events - NotificationCenter.default.addObserver(self, selector: #selector(onDidReceiveData(_:)), name: .didReceiveData, object: nil) - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - NotificationCenter.default.removeObserver(self) - eventSink = nil - return nil - } - - public override func applicationWillResignActive(_ application: UIApplication) { - let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark) - let blurEffectView = UIVisualEffectView(effect: blurEffect) - blurEffectView.frame = window!.frame - blurEffectView.tag = 61007 - - self.window?.addSubview(blurEffectView) - } - - public override func applicationDidBecomeActive(_ application: UIApplication) { - signal(SIGPIPE, SIG_IGN); - self.window?.viewWithTag(61007)?.removeFromSuperview() - - eventSink?("{\"type\": \"app_did_become_active\"}") - } - - override func applicationWillEnterForeground(_ application: UIApplication) { - signal(SIGPIPE, SIG_IGN); - } -} +import UIKit -extension Notification.Name { - static let didReceiveData = Notification.Name("didReceiveData") +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } } diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h index bee0ae5ae7..fae207f9e2 100644 --- a/ios/Runner/Runner-Bridging-Header.h +++ b/ios/Runner/Runner-Bridging-Header.h @@ -1,2 +1 @@ #import "GeneratedPluginRegistrant.h" -#import "mm2.h" diff --git a/ios/Runner/mm2.h b/ios/Runner/mm2.h deleted file mode 100644 index a132efafa8..0000000000 --- a/ios/Runner/mm2.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef mm2_h -#define mm2_h - -#include - -char* writeable_dir (void); - -void start_mm2 (const char* mm2_conf); - -/// Checks if the MM2 singleton thread is currently running or not. -/// 0 .. not running. -/// 1 .. running, but no context yet. -/// 2 .. context, but no RPC yet. -/// 3 .. RPC is up. -int8_t mm2_main_status (void); - -/// Defined in "common/for_c.rs". -uint8_t is_loopback_ip (const char* ip); -/// Defined in "mm2_lib.rs". -int8_t mm2_main (const char* conf, void (*log_cb) (const char* line)); - -/// Defined in "mm2_lib.rs". -/// 0 .. MM2 has been stopped successfully. -/// 1 .. not running. -/// 2 .. error stopping an MM2 instance. -int8_t mm2_stop (void); - -void lsof (void); - -/// Measurement of application metrics: network traffic, CPU usage, etc. -const char* metrics (void); - -/// Corresponds to the `applicationDocumentsDirectory` used in Dart. -const char* documentDirectory (void); - -#endif /* mm2_h */ diff --git a/ios/Runner/mm2.m b/ios/Runner/mm2.m deleted file mode 100644 index d2469813e0..0000000000 --- a/ios/Runner/mm2.m +++ /dev/null @@ -1,235 +0,0 @@ -#include "mm2.h" - -#import -#import -#import -#import -#import -#import // os_log -#import // NSException - -#include -#include - -#include // task_info, mach_task_self - -#include // strcpy -#include -#include -#include - -// Note that the network interface traffic is not the same as the application traffic. -// Might still be useful with picking some trends in how the application is using the network, -// and for troubleshooting. -void network (NSMutableDictionary* ret) { - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/getifaddrs.3.html - struct ifaddrs *addrs = NULL; - int rc = getifaddrs (&addrs); - if (rc != 0) return; - - for (struct ifaddrs *addr = addrs; addr != NULL; addr = addr->ifa_next) { - if (addr->ifa_addr->sa_family != AF_LINK) continue; - - // Known aliases: “en0” is wi-fi, “pdp_ip0” is mobile. - // AG: “lo0” on my iPhone 5s seems to be measuring the Wi-Fi traffic. - const char* name = addr->ifa_name; - - struct if_data *stats = (struct if_data*) addr->ifa_data; - if (name == NULL || stats == NULL) continue; - if (stats->ifi_ipackets == 0 || stats->ifi_opackets == 0) continue; - - int8_t log = 0; - if (log == 1) os_log (OS_LOG_DEFAULT, - "network] if %{public}s ipackets %lld ibytes %lld opackets %lld obytes %lld", - name, - (int64_t) stats->ifi_ipackets, - (int64_t) stats->ifi_ibytes, - (int64_t) stats->ifi_opackets, - (int64_t) stats->ifi_obytes); - - NSDictionary* readings = @{ - @"ipackets": @((int64_t) stats->ifi_ipackets), - @"ibytes": @((int64_t) stats->ifi_ibytes), - @"opackets": @((int64_t) stats->ifi_opackets), - @"obytes": @((int64_t) stats->ifi_obytes)}; - NSString* key = [[NSString alloc] initWithUTF8String:name]; - [ret setObject:readings forKey:key];} - - freeifaddrs (addrs);} - -// Results in a `EXC_CRASH (SIGABRT)` crash log. -void throw_example (void) { - @throw [NSException exceptionWithName:@"exceptionName" reason:@"throw_example" userInfo:nil];} - -const char* documentDirectory (void) { - NSFileManager* sharedFM = [NSFileManager defaultManager]; - NSArray* urls = [sharedFM URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; - //for (NSURL* url in urls) os_log (OS_LOG_DEFAULT, "documentDirectory] supp dir: %{public}s\n", url.fileSystemRepresentation); - if (urls.count < 1) {os_log (OS_LOG_DEFAULT, "documentDirectory] Can't get a NSApplicationSupportDirectory"); return NULL;} - const char* wr_dir = urls[0].fileSystemRepresentation; - return wr_dir; -} - -// “in_use” stops at 256. -void file_example (void) { - const char* documents = documentDirectory(); - NSString* dir = [[NSString alloc] initWithUTF8String:documents]; - NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:NULL]; - static int32_t total = 0; - [files enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) { - NSString* filename = (NSString*) obj; - os_log (OS_LOG_DEFAULT, "file_example] filename: %{public}s", filename.UTF8String); - - NSString* path = [NSString stringWithFormat:@"%@/%@", dir, filename]; - int fd = open (path.UTF8String, O_RDWR); - if (fd > 0) ++total;}]; - - int32_t in_use = 0; - for (int fd = 0; fd < (int) FD_SETSIZE; ++fd) if (fcntl (fd, F_GETFD, 0) != -1) ++in_use; - - os_log (OS_LOG_DEFAULT, "file_example] leaked %d; in_use %d / %d", total, in_use, (int32_t) FD_SETSIZE);} - -// On iPhone 5s the app stopped at “phys_footprint 646 MiB; rs 19 MiB”. -// It didn't get to a memory allocation failure but was killed by Jetsam instead -// (“JetsamEvent-2020-04-03-175018.ips” was generated in the iTunes crash logs directory). -void leak_example (void) { - static int8_t* leaks[9999]; // Preserve the pointers for GC - static int32_t next_leak = 0; - int32_t size = 9 * 1024 * 1024; - os_log (OS_LOG_DEFAULT, "leak_example] Leaking %d MiB…", size / 1024 / 1024); - int8_t* leak = malloc (size); - if (leak == NULL) {os_log (OS_LOG_DEFAULT, "leak_example] Allocation failed"); return;} - leaks[next_leak++] = leak; - // Fill with random junk to workaround memory compression - for (int ix = 0; ix < size; ++ix) leak[ix] = (int8_t) rand(); - os_log (OS_LOG_DEFAULT, "leak_example] Leak %d, allocated %d MiB", next_leak, size / 1024 / 1024);} - -int32_t fds_simple (void) { - int32_t fds = 0; - for (int fd = 0; fd < (int) FD_SETSIZE; ++fd) if (fcntl (fd, F_GETFD, 0) != -1) ++fds; - return fds;} - -int32_t fds (void) { - // fds_simple is likely to generate a number of interrupts - // (FD_SETSIZE of 1024 would likely mean 1024 interrupts). - // We should actually check it: maybe it will help us with reproducing the high number of `wakeups`. - // But for production use we want to reduce the number of `fcntl` invocations. - - // We'll skip the first portion of file descriptors because most of the time we have them opened anyway. - int fd = 66; - int32_t fds = 66; - int32_t gap = 0; - - while (fd < (int) FD_SETSIZE && fd < 333) { - if (fcntl (fd, F_GETFD, 0) != -1) { // If file descriptor exists - gap = 0; - if (fd < 220) { - // We will count the files by ten, hoping that iOS traditionally fills the gaps. - fd += 10; - fds += 10; - } else { - // Unless we're close to the limit, where we want more precision. - ++fd; ++fds;} - continue;} - // Sample with increasing step while inside the gap. - int step = 1 + gap / 3; - fd += step; - gap += step;} - - return fds;} - -const char* metrics (void) { - //file_example(); - //leak_example(); - - mach_port_t self = mach_task_self(); - if (self == MACH_PORT_NULL || self == MACH_PORT_DEAD) return "{}"; - - // cf. https://forums.developer.apple.com/thread/105088#357415 - int32_t footprint = 0, rs = 0; - task_vm_info_data_t vmInfo; - mach_msg_type_number_t count = TASK_VM_INFO_COUNT; - kern_return_t rc = task_info (self, TASK_VM_INFO, (task_info_t) &vmInfo, &count); - if (rc == KERN_SUCCESS) { - footprint = (int32_t) vmInfo.phys_footprint / (1024 * 1024); - rs = (int32_t) vmInfo.resident_size / (1024 * 1024);} - - // iOS applications are in danger of being killed if the number of iterrupts is too high, - // so it might be interesting to maintain some statistics on the number of interrupts. - int64_t wakeups = 0; - task_power_info_data_t powInfo; - count = TASK_POWER_INFO_COUNT; - rc = task_info (self, TASK_POWER_INFO, (task_info_t) &powInfo, &count); - if (rc == KERN_SUCCESS) wakeups = (int64_t) powInfo.task_interrupt_wakeups; - - int32_t files = fds(); - - NSMutableDictionary* ret = [NSMutableDictionary new]; - - //os_log (OS_LOG_DEFAULT, - // "metrics] phys_footprint %d MiB; rs %d MiB; wakeups %lld; files %d", footprint, rs, wakeups, files); - ret[@"footprint"] = @(footprint); - ret[@"rs"] = @(rs); - ret[@"wakeups"] = @(wakeups); - ret[@"files"] = @(files); - - network (ret); - - NSError *err; - NSData *js = [NSJSONSerialization dataWithJSONObject:ret options:0 error: &err]; - if (js == NULL) {os_log (OS_LOG_DEFAULT, "metrics] !json: %@", err); return "{}";} - NSString *jss = [[NSString alloc] initWithData:js encoding:NSUTF8StringEncoding]; - const char *cs = [jss UTF8String]; - return cs;} - -void lsof (void) -{ - // AG: For now `os_log` allows me to see the information in the logs, - // but in the future we might want to return the information to Flutter - // in order to gather statistics on the use of file descriptors in the app, etc. - - int flags; - int fd; - char buf[MAXPATHLEN+1] ; - int n = 1 ; - - for (fd = 0; fd < (int) FD_SETSIZE; fd++) { - errno = 0; - flags = fcntl(fd, F_GETFD, 0); - if (flags == -1 && errno) { - if (errno != EBADF) { - return ; - } - else - continue; - } - if (fcntl(fd , F_GETPATH, buf ) >= 0) - { - printf("File Descriptor %d number %d in use for: %s\n", fd, n, buf); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d number %d in use for: %{public}s", fd, n, buf); - } - else - { - //[...] - - struct sockaddr_in addr; - socklen_t addr_size = sizeof(struct sockaddr); - int res = getpeername(fd, (struct sockaddr*)&addr, &addr_size); - if (res >= 0) - { - char clientip[20]; - strcpy(clientip, inet_ntoa(addr.sin_addr)); - uint16_t port = \ - (uint16_t)((((uint16_t)(addr.sin_port) & 0xff00) >> 8) | \ - (((uint16_t)(addr.sin_port) & 0x00ff) << 8)); - printf("File Descriptor %d, %s:%d \n", fd, clientip, port); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d, %{public}s:%d", fd, clientip, port); - } - else { - printf("File Descriptor %d number %d couldn't get path or socket\n", fd, n); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d number %d couldn't get path or socket", fd, n); - } - } - ++n ; - } -} diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 4da6f33c4e..1203a73656 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -28,12 +28,6 @@ const Duration kPerformanceLogInterval = Duration(minutes: 1); String get appTitle => 'Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX'; String get appShortTitle => 'Komodo Wallet'; -// We're using a hardcoded seed for the hidden login instead -// of generating it on the fly. This will allow us to access -// previously connected Trezor wallet accounts data and speed up -// the reactivation of its coins. -String get seedForHiddenLogin => 'hidden-login'; - Map priorityCoinsAbbrMap = { 'KMD': 30, 'BTC-segwit': 20, diff --git a/lib/app_config/coins_config_parser.dart b/lib/app_config/coins_config_parser.dart deleted file mode 100644 index 9fd428b240..0000000000 --- a/lib/app_config/coins_config_parser.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/model/coin.dart'; - -final CoinConfigParser coinConfigParser = CoinConfigParser(); - -class CoinConfigParser { - List? _globalConfigCache; - - Future> getGlobalCoinsJson() async { - final List globalConfig = - _globalConfigCache ?? await _readGlobalConfig(); - final List filtered = _removeDelisted(globalConfig); - - return filtered; - } - - Future> _readGlobalConfig() async { - final String globalConfig = - await rootBundle.loadString('$coinsAssetsPath/config/coins.json'); - final List globalCoinsJson = jsonDecode(globalConfig); - - _globalConfigCache = globalCoinsJson; - return globalCoinsJson; - } - - /// Checks if the specified asset [path] exists. - /// Returns `true` if the asset exists, otherwise `false`. - Future doesAssetExist(String path) async { - try { - await rootBundle.load(path); - return true; - } catch (e) { - return false; - } - } - - /// Checks if the local coin configs exist. - /// Returns `true` if the local coin configs exist, otherwise `false`. - Future hasLocalConfigs({ - String coinsPath = '$coinsAssetsPath/config/coins.json', - String coinsConfigPath = '$coinsAssetsPath/config/coins_config.json', - }) async { - try { - final bool coinsFileExists = await doesAssetExist(coinsPath); - final bool coinsConfigFileExists = await doesAssetExist(coinsConfigPath); - return coinsFileExists && coinsConfigFileExists; - } catch (e) { - return false; - } - } - - Future> getUnifiedCoinsJson() async { - final Map json = await _readLocalConfig(); - final Map modifiedJson = _modifyLocalJson(json); - - return modifiedJson; - } - - Map _modifyLocalJson(Map source) { - final Map modifiedJson = {}; - - source.forEach((abbr, dynamic coinItem) { - if (_needSkipCoin(coinItem)) return; - - dynamic electrum = coinItem['electrum']; - // Web doesn't support SSL and TCP protocols, so we need to remove - // electrum servers with these protocols. - if (kIsWeb) { - removeElectrumsWithoutWss(electrum); - } - - coinItem['abbr'] = abbr; - coinItem['priority'] = priorityCoinsAbbrMap[abbr] ?? 0; - coinItem['active'] = enabledByDefaultCoins.contains(abbr); - modifiedJson[abbr] = coinItem; - }); - - return modifiedJson; - } - - /// Remove electrum servers without WSS protocol from [electrums]. - /// If [electrums] is a list, it will be modified in place. - /// Leaving as in-place modification for performance reasons. - void removeElectrumsWithoutWss(dynamic electrums) { - if (electrums is List) { - for (final e in electrums) { - if (e['protocol'] == 'WSS') { - e['ws_url'] = e['url']; - } - } - - electrums.removeWhere((dynamic e) => e['ws_url'] == null); - } - } - - Future> _readLocalConfig() async { - final String localConfig = - await rootBundle.loadString('$coinsAssetsPath/config/coins_config.json'); - final Map json = jsonDecode(localConfig); - - return json; - } - - bool _needSkipCoin(Map jsonCoin) { - final dynamic electrum = jsonCoin['electrum']; - if (excludedAssetList.contains(jsonCoin['coin'])) { - return true; - } - if (getCoinType(jsonCoin['type'], jsonCoin['coin']) == null) { - return true; - } - - return electrum is List && - electrum.every((dynamic e) => - e['ws_url'] == null && !_isProtocolSupported(e['protocol'])); - } - - /// Returns true if [protocol] is supported on the current platform. - /// On web, only WSS is supported. - /// On other (native) platforms, only SSL and TCP are supported. - bool _isProtocolSupported(String? protocol) { - if (protocol == null) { - return false; - } - - String uppercaseProtocol = protocol.toUpperCase(); - - if (kIsWeb) { - return uppercaseProtocol == 'WSS'; - } - - return uppercaseProtocol == 'SSL' || uppercaseProtocol == 'TCP'; - } - - List _removeDelisted(List all) { - final List filtered = List.from(all); - filtered.removeWhere( - (dynamic item) => excludedAssetList.contains(item['coin']), - ); - return filtered; - } -} diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 0fd1e5f22b..eb7b287938 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -5,9 +5,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:http/http.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; -import 'package:komodo_coin_updates/komodo_coin_updates.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; @@ -17,12 +16,9 @@ import 'package:web_dex/bloc/analytics/analytics_repo.dart'; import 'package:web_dex/bloc/assets_overview/bloc/asset_overview_bloc.dart'; import 'package:web_dex/bloc/assets_overview/investment_repository.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_repository.dart'; -import 'package:web_dex/bloc/cex_market_data/mockup/generator.dart'; -import 'package:web_dex/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart'; @@ -30,6 +26,7 @@ import 'package:web_dex/bloc/cex_market_data/price_chart/price_chart_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/price_chart/price_chart_event.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart'; @@ -37,7 +34,6 @@ import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_ import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; import 'package:web_dex/bloc/nfts/nft_main_bloc.dart'; import 'package:web_dex/bloc/nfts/nft_main_repo.dart'; -import 'package:web_dex/bloc/runtime_coin_updates/coin_config_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; import 'package:web_dex/bloc/system_health/system_clock_repository.dart'; @@ -47,7 +43,13 @@ import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; import 'package:web_dex/bloc/trezor_connection_bloc/trezor_connection_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_bloc.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; +import 'package:web_dex/blocs/orderbook_bloc.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; +import 'package:web_dex/blocs/trezor_coins_bloc.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/main.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/model/authorize_mode.dart'; @@ -66,11 +68,11 @@ class AppBlocRoot extends StatelessWidget { const AppBlocRoot({ Key? key, required this.storedPrefs, - required this.runtimeUpdateConfig, + required this.komodoDefiSdk, }); final StoredSettings storedPrefs; - final RuntimeUpdateConfig runtimeUpdateConfig; + final KomodoDefiSdk komodoDefiSdk; // TODO: Refactor to clean up the bloat in this main file void _clearCachesIfPerformanceModeChanged( @@ -100,14 +102,29 @@ class AppBlocRoot extends StatelessWidget { Widget build(BuildContext context) { final performanceMode = appDemoPerformanceMode; - final transactionsRepo = performanceMode != null + final mm2Api = RepositoryProvider.of(context); + final coinsRepository = RepositoryProvider.of(context); + final myOrdersService = MyOrdersService(mm2Api); + final tradingEntitiesBloc = TradingEntitiesBloc( + komodoDefiSdk, + mm2Api, + myOrdersService, + ); + final currentWalletBloc = RepositoryProvider.of(context); + final dexRepository = DexRepository(mm2Api); + final trezorRepo = RepositoryProvider.of(context); + final trezorBloc = RepositoryProvider.of(context); + + // TODO: SDK Port needed, not sure about this part + final transactionsRepo = /*performanceMode != null ? MockTransactionHistoryRepo( api: mm2Api, client: Client(), performanceMode: performanceMode, demoDataGenerator: DemoDataCache.withDefaults(), ) - : TransactionHistoryRepo(api: mm2Api, client: Client()); + : */ + TransactionHistoryRepo(sdk: komodoDefiSdk); final profitLossRepo = ProfitLossRepository.withDefaults( transactionHistoryRepo: transactionsRepo, @@ -115,12 +132,16 @@ class AppBlocRoot extends StatelessWidget { // Returns real data if performanceMode is null. Consider changing the // other repositories to use this pattern. demoMode: performanceMode, + coinsRepository: coinsRepository, + mm2Api: mm2Api, ); final portfolioGrowthRepo = PortfolioGrowthRepository.withDefaults( transactionHistoryRepo: transactionsRepo, cexRepository: binanceRepository, demoMode: performanceMode, + coinsRepository: coinsRepository, + mm2Api: mm2Api, ); _clearCachesIfPerformanceModeChanged( @@ -129,10 +150,42 @@ class AppBlocRoot extends StatelessWidget { portfolioGrowthRepo, ); + // startup bloc run steps + tradingEntitiesBloc.runUpdate(); + routingState.selectedMenu = MainMenuValue.dex; + return MultiRepositoryProvider( - providers: [RepositoryProvider(create: (_) => NftsRepo(api: mm2Api.nft))], + providers: [ + RepositoryProvider( + create: (_) => NftsRepo( + api: mm2Api.nft, + coinsRepo: coinsRepository, + )), + RepositoryProvider(create: (_) => tradingEntitiesBloc), + RepositoryProvider(create: (_) => dexRepository), + RepositoryProvider( + create: (_) => MakerFormBloc( + api: mm2Api, + kdfSdk: komodoDefiSdk, + coinsRepository: coinsRepository, + dexRepository: dexRepository, + ), + ), + RepositoryProvider(create: (_) => OrderbookBloc(api: mm2Api)), + RepositoryProvider(create: (_) => myOrdersService), + ], child: MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => CoinsBloc( + komodoDefiSdk, + currentWalletBloc, + coinsRepository, + trezorBloc, + mm2Api, + ) + ..add(CoinsStarted()) + ), BlocProvider( create: (context) => PriceChartBloc(binanceRepository) ..add( @@ -158,6 +211,7 @@ class AppBlocRoot extends StatelessWidget { BlocProvider( create: (BuildContext ctx) => PortfolioGrowthBloc( portfolioGrowthRepository: portfolioGrowthRepo, + coinsRepository: coinsRepository, ), ), BlocProvider( @@ -179,24 +233,28 @@ class AppBlocRoot extends StatelessWidget { ), BlocProvider( create: (context) => TakerBloc( - authRepo: authRepo, + kdfSdk: komodoDefiSdk, dexRepository: dexRepository, - coinsRepository: coinsBloc, + coinsRepository: coinsRepository, ), ), BlocProvider( create: (context) => BridgeBloc( - authRepository: authRepo, + kdfSdk: komodoDefiSdk, dexRepository: dexRepository, - bridgeRepository: BridgeRepository.instance, - coinsRepository: coinsBloc, + bridgeRepository: BridgeRepository( + mm2Api, + komodoDefiSdk, + coinsRepository, + ), + coinsRepository: coinsRepository, ), ), BlocProvider( create: (_) => TrezorConnectionBloc( trezorRepo: trezorRepo, - authRepo: authRepo, - walletRepo: currentWalletBloc, + kdfSdk: komodoDefiSdk, + walletRepo: RepositoryProvider.of(context), ), lazy: false, ), @@ -204,7 +262,7 @@ class AppBlocRoot extends StatelessWidget { lazy: false, create: (context) => NftMainBloc( repo: context.read(), - authRepo: authRepo, + kdfSdk: komodoDefiSdk, isLoggedIn: context.read().state.mode == AuthorizeMode.logIn, ), @@ -223,22 +281,19 @@ class AppBlocRoot extends StatelessWidget { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), - coinsBloc, + coinsRepository, ), ), ), BlocProvider( create: (_) => SystemHealthBloc(SystemClockRepository(), mm2Api), ), - BlocProvider( - lazy: false, - create: (_) => CoinConfigBloc( - coinsConfigRepo: CoinConfigRepository.withDefaults( - runtimeUpdateConfig, - ), - ) - ..add(CoinConfigLoadRequested()) - ..add(CoinConfigUpdateSubscribeRequested()), + BlocProvider( + create: (context) => TrezorInitBloc( + kdfSdk: komodoDefiSdk, + trezorRepo: trezorRepo, + coinsRepository: coinsRepository, + ), ), ], child: _MyAppView(), @@ -254,19 +309,24 @@ class _MyAppView extends StatefulWidget { class _MyAppViewState extends State<_MyAppView> { final AppRouterDelegate _routerDelegate = AppRouterDelegate(); - final RootRouteInformationParser _routeInformationParser = - RootRouteInformationParser(); + late final RootRouteInformationParser _routeInformationParser; late final AirDexBackButtonDispatcher _airDexBackButtonDispatcher; @override void initState() { + final coinsBloc = context.read(); + _routeInformationParser = RootRouteInformationParser(coinsBloc); _airDexBackButtonDispatcher = AirDexBackButtonDispatcher(_routerDelegate); routingState.selectedMenu = MainMenuValue.defaultMenu(); - if (kDebugMode) initDebugData(context.read()); - unawaited(_hideAppLoader()); + if (kDebugMode) { + final walletsRepo = RepositoryProvider.of(context); + final authBloc = context.read(); + initDebugData(authBloc, walletsRepo).ignore(); + } + super.initState(); } @@ -291,7 +351,8 @@ class _MyAppViewState extends State<_MyAppView> { void didChangeDependencies() { super.didChangeDependencies(); - _precacheCoinIcons().ignore(); + final coinsRepository = RepositoryProvider.of(context); + _precacheCoinIcons(coinsRepository).ignore(); } /// Hides the native app launch loader. Currently only implemented for web. @@ -318,7 +379,7 @@ class _MyAppViewState extends State<_MyAppView> { Completer? _currentPrecacheOperation; - Future _precacheCoinIcons() async { + Future _precacheCoinIcons(CoinsRepo coinsRepo) async { if (_currentPrecacheOperation != null && !_currentPrecacheOperation!.isCompleted) { // completeError throws an uncaught exception, which causes the UI @@ -330,7 +391,7 @@ class _MyAppViewState extends State<_MyAppView> { _currentPrecacheOperation = Completer(); try { - final coins = (await coinsRepo.getKnownCoins()).map((coin) => coin.abbr); + final coins = coinsRepo.getKnownCoinsMap().keys; await for (final abbr in Stream.fromIterable(coins)) { // TODO: Test if necessary to complete prematurely with error if build diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 53568d9e5b..d47b2ca947 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -1,105 +1,216 @@ import 'dart:async'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/services/auth_checker/auth_checker.dart'; -import 'package:web_dex/services/auth_checker/get_auth_checker.dart'; import 'package:web_dex/shared/utils/utils.dart'; -import 'auth_bloc_event.dart'; -import 'auth_bloc_state.dart'; -import 'auth_repository.dart'; +part 'auth_bloc_event.dart'; +part 'auth_bloc_state.dart'; +/// AuthBloc is responsible for managing the authentication state of the +/// application. It handles events such as login and logout changes. class AuthBloc extends Bloc { - AuthBloc({required AuthRepository authRepo}) - : _authRepo = authRepo, - super(AuthBlocState.initial()) { - on(_onAuthChanged); - on(_onLogout); - on(_onReLogIn); - _authorizationSubscription = _authRepo.authMode.listen((event) { - add(AuthChangedEvent(mode: event)); - }); + /// Handles [AuthBlocEvent]s and emits [AuthBlocState]s. + /// [_kdfSdk] is an instance of [KomodoDefiSdk] used for authentication. + AuthBloc(this._kdfSdk, this._walletsRepository) + : super(AuthBlocState.initial()) { + on(_onAuthChanged); + on(_onLogout); + on(_onLogIn); + on(_onRegister); + on(_onRestore); } - late StreamSubscription _authorizationSubscription; - final AuthRepository _authRepo; - final AuthChecker _authChecker = getAuthChecker(); - - Stream get outAuthorizeMode => _authRepo.authMode; + final KomodoDefiSdk _kdfSdk; + final WalletsRepository _walletsRepository; + StreamSubscription? _authorizationSubscription; @override Future close() async { - _authorizationSubscription.cancel(); - super.close(); - } - - Future isLoginAllowed(Wallet newWallet) async { - final String walletEncryptedSeed = newWallet.config.seedPhrase; - final bool isLoginAllowed = - await _authChecker.askConfirmLoginIfNeeded(walletEncryptedSeed); - return isLoginAllowed; + await _authorizationSubscription?.cancel(); + await super.close(); } Future _onLogout( - AuthLogOutEvent event, + AuthSignOutRequested event, Emitter emit, ) async { log( 'Logging out from a wallet', path: 'auth_bloc => _logOut', - ); - - await _logOut(); - await _authRepo.logIn(AuthorizeMode.noLogin); + ).ignore(); + await _kdfSdk.auth.signOut(); log( 'Logged out from a wallet', path: 'auth_bloc => _logOut', - ); + ).ignore(); + emit(const AuthBlocState(mode: AuthorizeMode.noLogin, currentUser: null)); } - Future _onReLogIn( - AuthReLogInEvent event, + Future _onLogIn( + AuthSignInRequested event, Emitter emit, ) async { - log( - 're-login from a wallet', - path: 'auth_bloc => _reLogin', - ); - - await _logOut(); - await _authRepo.logIn( - AuthorizeMode.logIn, - seed: event.seed, - walletName: event.wallet.name, - password: event.password, - ); - currentWalletBloc.wallet = event.wallet; - if (event.wallet.config.type == WalletType.iguana) { - _authChecker.addSession(event.wallet.config.seedPhrase); - } + try { + if (event.wallet.isLegacyWallet) { + return add( + AuthRestoreRequested( + wallet: event.wallet, + password: event.password, + seed: await event.wallet.getLegacySeed(event.password), + ), + ); + } - log( - 're-logged in from a wallet', - path: 'auth_bloc => _reLogin', - ); + log('login from a wallet', path: 'auth_bloc => _reLogin').ignore(); + await _kdfSdk.auth.signIn( + walletName: event.wallet.name, + password: event.password, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + log('logged in from a wallet', path: 'auth_bloc => _reLogin').ignore(); + emit( + AuthBlocState( + mode: AuthorizeMode.logIn, + currentUser: await _kdfSdk.auth.currentUser, + ), + ); + _listenToAuthStateChanges(); + } catch (e, s) { + log( + 'Failed to login wallet ${event.wallet.name}', + isError: true, + trace: s, + path: 'auth_bloc -> onLogin', + ).ignore(); + emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); + } } Future _onAuthChanged( - AuthChangedEvent event, Emitter emit) async { - emit(AuthBlocState(mode: event.mode)); + AuthModeChanged event, + Emitter emit, + ) async { + emit(AuthBlocState(mode: event.mode, currentUser: event.currentUser)); } - Future _logOut() async { - await _authRepo.logOut(); - final currentWallet = currentWalletBloc.wallet; - if (currentWallet != null && - currentWallet.config.type == WalletType.iguana) { - _authChecker.removeSession(currentWallet.config.seedPhrase); + Future _onRegister( + AuthRegisterRequested event, + Emitter emit, + ) async { + try { + final existingWallets = await _kdfSdk.auth.getUsers(); + final walletExists = existingWallets + .any((KdfUser user) => user.walletId.name == event.wallet.name); + if (walletExists) { + add( + AuthSignInRequested(wallet: event.wallet, password: event.password), + ); + log('Wallet ${event.wallet.name} already exist, attempting sign-in') + .ignore(); + return; + } + + log('register from a wallet', path: 'auth_bloc => _register').ignore(); + await _kdfSdk.auth.register( + password: event.password, + walletName: event.wallet.name, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + if (!await _kdfSdk.auth.isSignedIn()) { + throw Exception('Registration failed: user is not signed in'); + } + log('registered from a wallet', path: 'auth_bloc => _register').ignore(); + await _kdfSdk.setWalletType(event.wallet.config.type); + await _kdfSdk.confirmSeedBackup(hasBackup: false); + emit( + AuthBlocState( + mode: AuthorizeMode.logIn, + currentUser: await _kdfSdk.auth.currentUser, + ), + ); + _listenToAuthStateChanges(); + } catch (e, s) { + log( + 'Failed to register wallet ${event.wallet.name}', + isError: true, + trace: s, + path: 'auth_bloc -> onRegister', + ).ignore(); + emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); } - currentWalletBloc.wallet = null; + } + + Future _onRestore( + AuthRestoreRequested event, + Emitter emit, + ) async { + try { + final existingWallets = await _kdfSdk.auth.getUsers(); + final walletExists = existingWallets + .any((KdfUser user) => user.walletId.name == event.wallet.name); + if (walletExists) { + add( + AuthSignInRequested(wallet: event.wallet, password: event.password), + ); + log('Wallet ${event.wallet.name} already exist, attempting sign-in') + .ignore(); + return; + } + + log('restore from a wallet', path: 'auth_bloc => _restore').ignore(); + await _kdfSdk.auth.register( + password: event.password, + walletName: event.wallet.name, + mnemonic: Mnemonic.plaintext(event.seed), + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + if (!await _kdfSdk.auth.isSignedIn()) { + throw Exception('Registration failed: user is not signed in'); + } + log('restored from a wallet', path: 'auth_bloc => _restore').ignore(); + + await _kdfSdk.setWalletType(event.wallet.config.type); + await _kdfSdk.confirmSeedBackup(hasBackup: event.wallet.config.hasBackup); + + emit( + AuthBlocState( + mode: AuthorizeMode.logIn, + currentUser: await _kdfSdk.auth.currentUser, + ), + ); + + // Delete legacy wallet on successful restoration & login to avoid + // duplicates in the wallet list + if (event.wallet.isLegacyWallet) { + await _kdfSdk.addActivatedCoins(event.wallet.config.activatedCoins); + await _walletsRepository.deleteWallet(event.wallet); + } + + _listenToAuthStateChanges(); + } catch (e, s) { + log( + 'Failed to restore existing wallet ${event.wallet.name}', + isError: true, + trace: s, + path: 'auth_bloc -> onRestore', + ).ignore(); + emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); + } + } + + void _listenToAuthStateChanges() { + _authorizationSubscription?.cancel(); + _authorizationSubscription = _kdfSdk.auth.authStateChanges.listen((user) { + final AuthorizeMode event = + user != null ? AuthorizeMode.logIn : AuthorizeMode.noLogin; + add(AuthModeChanged(mode: event, currentUser: user)); + }); } } diff --git a/lib/bloc/auth_bloc/auth_bloc_event.dart b/lib/bloc/auth_bloc/auth_bloc_event.dart index f23d9eb64a..cc54b721f1 100644 --- a/lib/bloc/auth_bloc/auth_bloc_event.dart +++ b/lib/bloc/auth_bloc/auth_bloc_event.dart @@ -1,27 +1,42 @@ -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/model/wallet.dart'; +part of 'auth_bloc.dart'; abstract class AuthBlocEvent { const AuthBlocEvent(); } -class AuthChangedEvent extends AuthBlocEvent { - const AuthChangedEvent({required this.mode}); +class AuthModeChanged extends AuthBlocEvent { + const AuthModeChanged({required this.mode, required this.currentUser}); + final AuthorizeMode mode; + final KdfUser? currentUser; } -class AuthLogOutEvent extends AuthBlocEvent { - const AuthLogOutEvent(); +class AuthSignOutRequested extends AuthBlocEvent { + const AuthSignOutRequested(); } -class AuthReLogInEvent extends AuthBlocEvent { - const AuthReLogInEvent({ - required this.seed, - required this.password, +class AuthSignInRequested extends AuthBlocEvent { + const AuthSignInRequested({required this.wallet, required this.password}); + + final Wallet wallet; + final String password; +} + +class AuthRegisterRequested extends AuthBlocEvent { + const AuthRegisterRequested({required this.wallet, required this.password}); + + final Wallet wallet; + final String password; +} + +class AuthRestoreRequested extends AuthBlocEvent { + const AuthRestoreRequested({ required this.wallet, + required this.password, + required this.seed, }); - final String seed; - final String password; final Wallet wallet; + final String password; + final String seed; } diff --git a/lib/bloc/auth_bloc/auth_bloc_state.dart b/lib/bloc/auth_bloc/auth_bloc_state.dart index c935dd53ed..be5e392811 100644 --- a/lib/bloc/auth_bloc/auth_bloc_state.dart +++ b/lib/bloc/auth_bloc/auth_bloc_state.dart @@ -1,12 +1,16 @@ -import 'package:equatable/equatable.dart'; -import 'package:web_dex/model/authorize_mode.dart'; +part of 'auth_bloc.dart'; class AuthBlocState extends Equatable { - const AuthBlocState({required this.mode}); + const AuthBlocState({required this.mode, this.currentUser}); factory AuthBlocState.initial() => const AuthBlocState(mode: AuthorizeMode.noLogin); + + final KdfUser? currentUser; final AuthorizeMode mode; + + bool get isSignedIn => currentUser != null; + @override - List get props => [mode]; + List get props => [mode, currentUser]; } diff --git a/lib/bloc/auth_bloc/auth_repository.dart b/lib/bloc/auth_bloc/auth_repository.dart deleted file mode 100644 index abeeabcc58..0000000000 --- a/lib/bloc/auth_bloc/auth_repository.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class AuthRepository { - AuthRepository(); - final StreamController _authController = - StreamController.broadcast(); - Stream get authMode => _authController.stream; - - Future logIn( - AuthorizeMode mode, { - String? seed, - String? password, - String? walletName, - }) async { - try { - await mm2.start( - passphrase: seed, - walletName: walletName, - walletPassword: password, - ); - } catch (e) { - log('mm2 start error: ${e.toString()}'); - rethrow; - } - - setAuthMode(mode); - } - - void setAuthMode(AuthorizeMode mode) { - _authController.sink.add(mode); - } - - Future logOut() async { - await mm2.stop(); - setAuthMode(AuthorizeMode.noLogin); - } -} - -final AuthRepository authRepo = AuthRepository(); diff --git a/lib/bloc/bridge_form/bridge_bloc.dart b/lib/bloc/bridge_form/bridge_bloc.dart index 582a251e70..941cb1c43c 100644 --- a/lib/bloc/bridge_form/bridge_bloc.dart +++ b/lib/bloc/bridge_form/bridge_bloc.dart @@ -1,21 +1,21 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_repository.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; import 'package:web_dex/bloc/bridge_form/bridge_validator.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/transformers.dart'; -import 'package:web_dex/blocs/coins_bloc.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/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_response.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/available_balance_state.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/data_from_service.dart'; @@ -30,8 +30,8 @@ class BridgeBloc extends Bloc { BridgeBloc({ required BridgeRepository bridgeRepository, required DexRepository dexRepository, - required CoinsBloc coinsRepository, - required AuthRepository authRepository, + required CoinsRepo coinsRepository, + required KomodoDefiSdk kdfSdk, }) : _bridgeRepository = bridgeRepository, _dexRepository = dexRepository, _coinsRepository = coinsRepository, @@ -71,20 +71,20 @@ class BridgeBloc extends Bloc { dexRepository: dexRepository, ); - _authorizationSubscription = authRepository.authMode.listen((event) { - _isLoggedIn = event == AuthorizeMode.logIn; + _authorizationSubscription = kdfSdk.auth.authStateChanges.listen((event) { + _isLoggedIn = event != null; if (!_isLoggedIn) add(const BridgeLogout()); }); } final BridgeRepository _bridgeRepository; final DexRepository _dexRepository; - final CoinsBloc _coinsRepository; + final CoinsRepo _coinsRepository; bool _activatingAssets = false; bool _waitingForWallet = true; bool _isLoggedIn = false; - late StreamSubscription _authorizationSubscription; + late StreamSubscription _authorizationSubscription; late BridgeValidator _validator; Timer? _maxSellAmountTimer; Timer? _preimageTimer; @@ -94,8 +94,8 @@ class BridgeBloc extends Bloc { Emitter emit, ) { if (state.selectedTicker != null) return; - final Coin? defaultTickerCoin = _coinsRepository.getCoin(event.ticker); + final Coin? defaultTickerCoin = _coinsRepository.getCoin(event.ticker); emit(state.copyWith( selectedTicker: () => defaultTickerCoin?.abbr, )); @@ -160,8 +160,7 @@ class BridgeBloc extends Bloc { BridgeUpdateTickers event, Emitter emit, ) async { - final CoinsByTicker tickers = - await _bridgeRepository.getAvailableTickers(_coinsRepository); + final CoinsByTicker tickers = await _bridgeRepository.getAvailableTickers(); emit(state.copyWith( tickers: () => tickers, @@ -562,7 +561,7 @@ class BridgeBloc extends Bloc { _activatingAssets = true; final List activationErrors = - await activateCoinIfNeeded(abbr); + await activateCoinIfNeeded(abbr, _coinsRepository); _activatingAssets = false; if (activationErrors.isNotEmpty) { diff --git a/lib/bloc/bridge_form/bridge_repository.dart b/lib/bloc/bridge_form/bridge_repository.dart index 517fdbda1e..ba3733186f 100644 --- a/lib/bloc/bridge_form/bridge_repository.dart +++ b/lib/bloc/bridge_form/bridge_repository.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:web_dex/blocs/coins_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart'; import 'package:web_dex/model/coin.dart'; @@ -9,12 +10,13 @@ import 'package:web_dex/model/typedef.dart'; import 'package:web_dex/shared/utils/utils.dart'; class BridgeRepository { - BridgeRepository._(); + BridgeRepository(this._mm2Api, this._kdfSdk, this._coinsRepository); - static final BridgeRepository _instance = BridgeRepository._(); - static BridgeRepository get instance => _instance; + final Mm2Api _mm2Api; + final KomodoDefiSdk _kdfSdk; + final CoinsRepo _coinsRepository; - Future getSellCoins(CoinsByTicker? tickers) async { + Future getSellCoins( CoinsByTicker? tickers) async { if (tickers == null) return null; final List? depths = await _getDepths(tickers); @@ -48,11 +50,10 @@ class BridgeRepository { return sellCoins; } - Future getAvailableTickers(CoinsBloc coinsRepo) async { - List coins = List.from(coinsRepo.knownCoins); - + Future getAvailableTickers() async { + List coins = _coinsRepository.getKnownCoins(); coins = removeWalletOnly(coins); - coins = removeSuspended(coins); + coins = removeSuspended(coins, await _kdfSdk.auth.isSignedIn()); final CoinsByTicker coinsByTicker = convertToCoinsByTicker(coins); final CoinsByTicker multiProtocolCoins = @@ -82,7 +83,8 @@ class BridgeRepository { } Future?> _frequentRequestDepth( - List> depthsPairs) async { + List> depthsPairs, + ) async { int attempts = 5; List? orderBookDepthsLocal; @@ -102,9 +104,10 @@ class BridgeRepository { } Future?> _getNotEmptyDepths( - List> pairs) async { + List> pairs, + ) async { final OrderBookDepthResponse? depthResponse = - await mm2Api.getOrderBookDepth(pairs); + await _mm2Api.getOrderBookDepth(pairs, _coinsRepository); return depthResponse?.list .where((d) => d.bids != 0 || d.asks != 0) diff --git a/lib/bloc/bridge_form/bridge_validator.dart b/lib/bloc/bridge_form/bridge_validator.dart index 22ca4fba2a..0c79d416ee 100644 --- a/lib/bloc/bridge_form/bridge_validator.dart +++ b/lib/bloc/bridge_form/bridge_validator.dart @@ -3,8 +3,8 @@ import 'package:rational/rational.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; 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'; @@ -22,7 +22,7 @@ import 'package:web_dex/views/dex/simple/form/error_list/dex_form_error_with_act class BridgeValidator { BridgeValidator({ required BridgeBloc bloc, - required CoinsBloc coinsRepository, + required CoinsRepo coinsRepository, required DexRepository dexRepository, }) : _bloc = bloc, _coinsRepo = coinsRepository, @@ -30,7 +30,7 @@ class BridgeValidator { _add = bloc.add; final BridgeBloc _bloc; - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; final DexRepository _dexRepo; final Function(BridgeEvent) _add; @@ -226,7 +226,7 @@ class BridgeValidator { } bool _validateCoinAndParent(String abbr) { - final Coin? coin = _coinsRepo.getKnownCoin(abbr); + final Coin? coin = _coinsRepo.getCoin(abbr); if (coin == null) { _add(BridgeSetError(_unknownCoinError(abbr))); diff --git a/lib/bloc/cex_market_data/charts.dart b/lib/bloc/cex_market_data/charts.dart index 08b3105a5f..2cb886cd95 100644 --- a/lib/bloc/cex_market_data/charts.dart +++ b/lib/bloc/cex_market_data/charts.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; typedef ChartData = List>; @@ -249,9 +249,10 @@ class Charts { return List.empty(); } - final int firstTransactionDate = transactions.first.timestamp; + final int firstTransactionDate = + transactions.first.timestamp.millisecondsSinceEpoch; final ChartData ohlcFromFirstTransaction = spotValues - .where((Point spot) => (spot.x / 1000) >= firstTransactionDate) + .where((Point spot) => spot.x >= firstTransactionDate) .toList(); double runningTotal = 0; @@ -263,10 +264,9 @@ class Charts { for (final Point spot in ohlcFromFirstTransaction) { if (transactionIndex < transactions.length) { bool transactionPassed = - currentTransaction().timestamp <= (spot.x ~/ 1000); + currentTransaction().timestamp.millisecondsSinceEpoch <= spot.x; while (transactionPassed) { - final double changeAmount = - double.parse(currentTransaction().myBalanceChange); + final double changeAmount = currentTransaction().amount.toDouble(); runningTotal += changeAmount; transactionIndex++; @@ -295,7 +295,8 @@ class Charts { break; } - transactionPassed = currentTransaction().timestamp < (spot.x ~/ 1000); + transactionPassed = + currentTransaction().timestamp.millisecondsSinceEpoch < spot.x; } } diff --git a/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart b/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart index 80bfda5be6..0a506d2b78 100644 --- a/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart +++ b/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart @@ -7,6 +7,7 @@ import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/cex_market_data/models/graph_cache.dart'; import 'package:web_dex/bloc/cex_market_data/models/graph_type.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; class MockPortfolioGrowthRepository extends PortfolioGrowthRepository { @@ -17,10 +18,14 @@ class MockPortfolioGrowthRepository extends PortfolioGrowthRepository { required super.transactionHistoryRepo, required super.cacheProvider, required this.performanceMode, + required super.coinsRepository, }); - MockPortfolioGrowthRepository.withDefaults({required this.performanceMode}) - : super( + MockPortfolioGrowthRepository.withDefaults({ + required this.performanceMode, + required CoinsRepo coinsRepository, + required Mm2Api mm2Api, + }) : super( cexRepository: BinanceRepository( binanceProvider: const BinanceProvider(), ), @@ -33,5 +38,6 @@ class MockPortfolioGrowthRepository extends PortfolioGrowthRepository { cacheProvider: HiveLazyBoxProvider( name: GraphType.balanceGrowth.tableName, ), + coinsRepository: coinsRepository, ); } diff --git a/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart b/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart index 825ec6df1d..9003a73f20 100644 --- a/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart +++ b/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart @@ -15,9 +15,9 @@ class MockTransactionHistoryRepo extends TransactionHistoryRepo { required Client client, required this.performanceMode, required this.demoDataGenerator, - }) : super(api: api, client: client); + }) : super(); - @override + // TODO: SDK Port needed, not sure about this part Future> fetchTransactions(Coin coin) async { return demoDataGenerator.loadTransactionsDemoData( performanceMode, diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart index 8678e8a8df..515337177e 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart @@ -5,7 +5,7 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:web_dex/bloc/cex_market_data/charts.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/text_error.dart'; @@ -18,6 +18,7 @@ class PortfolioGrowthBloc extends Bloc { PortfolioGrowthBloc({ required this.portfolioGrowthRepository, + required this.coinsRepository, }) : super(const PortfolioGrowthInitial()) { // Use the restartable transformer for period change events to avoid // overlapping events if the user rapidly changes the period (i.e. faster @@ -34,6 +35,7 @@ class PortfolioGrowthBloc } final PortfolioGrowthRepository portfolioGrowthRepository; + final CoinsRepo coinsRepository; void _onClearPortfolioGrowth( PortfolioGrowthClearRequested event, @@ -70,40 +72,49 @@ class PortfolioGrowthBloc PortfolioGrowthLoadRequested event, Emitter emit, ) async { - List coins = await _removeUnsupportedCoins(event); - // Charts for individual coins (coin details) are parsed here as well, - // and should be hidden if not supported. - if (coins.isEmpty && event.coins.length <= 1) { - return emit( - PortfolioGrowthChartUnsupported(selectedPeriod: event.selectedPeriod), - ); - } - - await _loadChart(coins, event, useCache: true) - .then(emit.call) - .catchError((e, _) { - if (state is! PortfolioGrowthChartLoadSuccess) { - emit( - GrowthChartLoadFailure( - error: TextError(error: e.toString()), - selectedPeriod: event.selectedPeriod, - ), + try { + final List coins = await _removeUnsupportedCoins(event); + // Charts for individual coins (coin details) are parsed here as well, + // and should be hidden if not supported. + if (coins.isEmpty && event.coins.length <= 1) { + return emit( + PortfolioGrowthChartUnsupported(selectedPeriod: event.selectedPeriod), ); } - }); - // Only remove inactivate/activating coins after an attempt to load the - // cached chart, as the cached chart may contain inactive coins. - coins = _removeInactiveCoins(coins); - if (coins.isNotEmpty) { - await _loadChart(coins, event, useCache: false) + await _loadChart(coins, event, useCache: true) .then(emit.call) - .catchError((_, __) { - // Ignore un-cached errors, as a transaction loading exception should not - // make the graph disappear with a load failure emit, as the cached data - // is already displayed. The periodic updates will still try to fetch the - // data and update the graph. + .catchError((e, _) { + if (state is! PortfolioGrowthChartLoadSuccess) { + emit( + GrowthChartLoadFailure( + error: TextError(error: e.toString()), + selectedPeriod: event.selectedPeriod, + ), + ); + } }); + + // Only remove inactivate/activating coins after an attempt to load the + // cached chart, as the cached chart may contain inactive coins. + final activeCoins = await _removeInactiveCoins(coins); + if (activeCoins.isNotEmpty) { + await _loadChart(activeCoins, event, useCache: false) + .then(emit.call) + .catchError((_, __) { + // Ignore un-cached errors, as a transaction loading exception should not + // make the graph disappear with a load failure emit, as the cached data + // is already displayed. The periodic updates will still try to fetch the + // data and update the graph. + }); + } + } catch (e, s) { + log( + 'Failed to load portfolio growth: $e', + isError: true, + trace: s, + path: 'portfolio_growth_bloc => _onLoadPortfoliowGrowth', + ); } await emit.forEach( @@ -128,22 +139,26 @@ class PortfolioGrowthBloc PortfolioGrowthLoadRequested event, ) async { final List coins = List.from(event.coins); - await coins.removeWhereAsync( - (Coin coin) async { - final isCoinSupported = await portfolioGrowthRepository - .isCoinChartSupported(coin.abbr, event.fiatCoinId); - return !isCoinSupported; - }, - ); + for (final coin in event.coins) { + final isCoinSupported = await portfolioGrowthRepository + .isCoinChartSupported(coin.abbr, event.fiatCoinId); + if (!isCoinSupported) { + coins.remove(coin); + } + } return coins; } - List _removeInactiveCoins(List coins) { - final List coinsCopy = List.from(coins) - ..removeWhere((coin) { - final updatedCoin = coinsBlocRepository.getCoin(coin.abbr)!; - return updatedCoin.isActivating || !updatedCoin.isActive; - }); + Future> _removeInactiveCoins(List coins) async { + final List coinsCopy = List.from(coins); + for (final coin in coins) { + final updatedCoin = await coinsRepository.getEnabledCoin(coin.abbr); + if (updatedCoin == null || + updatedCoin.isActivating || + !updatedCoin.isActive) { + coinsCopy.remove(coin); + } + } return coinsCopy; } @@ -174,8 +189,9 @@ class PortfolioGrowthBloc PortfolioGrowthLoadRequested event, ) async { // Do not let transaction loading exceptions stop the periodic updates - final coins = _removeInactiveCoins(await _removeUnsupportedCoins(event)); try { + final supportedCoins = await _removeUnsupportedCoins(event); + final coins = await _removeInactiveCoins(supportedCoins); return await portfolioGrowthRepository.getPortfolioGrowthChart( coins, fiatCoinId: event.fiatCoinId, diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart index 1451946526..819955b931 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart @@ -9,9 +9,10 @@ import 'package:web_dex/bloc/cex_market_data/mockup/mock_portfolio_growth_reposi import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/cex_market_data/models/graph_type.dart'; import 'package:web_dex/bloc/cex_market_data/models/models.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/model/coin.dart'; /// A repository for fetching the growth chart for the portfolio and coins. @@ -21,9 +22,11 @@ class PortfolioGrowthRepository { required cex.CexRepository cexRepository, required TransactionHistoryRepo transactionHistoryRepo, required PersistenceProvider cacheProvider, + required CoinsRepo coinsRepository, }) : _transactionHistoryRepository = transactionHistoryRepo, _cexRepository = cexRepository, - _graphCache = cacheProvider; + _graphCache = cacheProvider, + _coinsRepository = coinsRepository; /// Create a new instance of the repository with default dependencies. /// The default dependencies are the [BinanceRepository] and the @@ -31,11 +34,15 @@ class PortfolioGrowthRepository { factory PortfolioGrowthRepository.withDefaults({ required TransactionHistoryRepo transactionHistoryRepo, required cex.CexRepository cexRepository, + required CoinsRepo coinsRepository, + required Mm2Api mm2Api, PerformanceMode? demoMode, }) { if (demoMode != null) { return MockPortfolioGrowthRepository.withDefaults( performanceMode: demoMode, + coinsRepository: coinsRepository, + mm2Api: mm2Api, ); } @@ -45,6 +52,7 @@ class PortfolioGrowthRepository { cacheProvider: HiveLazyBoxProvider( name: GraphType.balanceGrowth.tableName, ), + coinsRepository: coinsRepository, ); } @@ -57,6 +65,8 @@ class PortfolioGrowthRepository { /// The graph cache provider to store the portfolio growth graph data. final PersistenceProvider _graphCache; + final CoinsRepo _coinsRepository; + static Future ensureInitialized() async { Hive ..registerAdapter(GraphCacheAdapter()) @@ -111,7 +121,7 @@ class PortfolioGrowthRepository { // TODO: Refactor referenced coinsBloc method to a repository. // NB: Even though the class is called [CoinsBloc], it is not a Bloc. - final Coin coin = coinsBlocRepository.getCoin(coinId)!; + final Coin coin = _coinsRepository.getCoin(coinId)!; final List transactions = await _transactionHistoryRepository .fetchCompletedTransactions(coin) .then((value) => value.toList()) @@ -142,7 +152,7 @@ class PortfolioGrowthRepository { // Continue to cache an empty chart rather than trying to fetch transactions // again for each invocation. - startAt ??= transactions.first.timestampDate; + startAt ??= transactions.first.timestamp; endAt ??= DateTime.now(); final String baseCoinId = coin.abbr.split('-').first; @@ -235,6 +245,9 @@ class PortfolioGrowthRepository { rethrow; } } on Exception { + // Exception primarily thrown for cache misses + // TODO: create a custom exception for cache misses to avoid catching + // this broad exception type return Future.value(ChartData.empty()); } }); @@ -301,7 +314,7 @@ class PortfolioGrowthRepository { String fiatCoinId, { bool allowFiatAsBase = true, }) async { - final Coin coin = coinsBlocRepository.getCoin(coinId)!; + final Coin coin = _coinsRepository.getCoin(coinId)!; final supportedCoins = await _cexRepository.getCoinList(); final coinTicker = coin.abbr.split('-').firstOrNull?.toUpperCase() ?? ''; diff --git a/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart b/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart index 6e1f04929b..63ab49685b 100644 --- a/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart +++ b/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart @@ -20,9 +20,7 @@ enum PriceChartPeriod { return '1M'; case PriceChartPeriod.oneYear: return '1Y'; - default: - throw Exception('Unknown interval'); - } + } } String get intervalString { @@ -37,8 +35,6 @@ enum PriceChartPeriod { return '1M'; case PriceChartPeriod.oneYear: return '1y'; - default: - throw Exception('Unknown interval'); - } + } } } diff --git a/lib/bloc/cex_market_data/price_chart/models/time_period.dart b/lib/bloc/cex_market_data/price_chart/models/time_period.dart index e09a2c4fe6..ec29f28b2c 100644 --- a/lib/bloc/cex_market_data/price_chart/models/time_period.dart +++ b/lib/bloc/cex_market_data/price_chart/models/time_period.dart @@ -20,9 +20,7 @@ enum TimePeriod { return '1M'; case TimePeriod.oneYear: return '1Y'; - default: - throw Exception('Unknown interval'); - } + } } Duration get duration { @@ -37,8 +35,6 @@ enum TimePeriod { return const Duration(days: 30); case TimePeriod.oneYear: return const Duration(days: 365); - default: - throw Exception('Unknown interval'); - } + } } } diff --git a/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart b/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart index 21d70919c4..ba6af9ccb3 100644 --- a/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart +++ b/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart @@ -7,6 +7,7 @@ import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss_cache.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; class MockProfitLossRepository extends ProfitLossRepository { @@ -18,10 +19,13 @@ class MockProfitLossRepository extends ProfitLossRepository { required super.cexRepository, required super.profitLossCacheProvider, required super.profitLossCalculator, + required super.coinsRepository, }); factory MockProfitLossRepository.withDefaults({ required PerformanceMode performanceMode, + required CoinsRepo coinsRepository, + required Mm2Api mm2Api, String cacheTableName = 'mock_profit_loss', }) { return MockProfitLossRepository( @@ -42,6 +46,7 @@ class MockProfitLossRepository extends ProfitLossRepository { binanceProvider: const BinanceProvider(), ), ), + coinsRepository: coinsRepository, ); } } diff --git a/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart b/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart index 80aac5e031..d44be74c54 100644 --- a/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart +++ b/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart @@ -1,25 +1,25 @@ -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; extension ProfitLossTransactionExtension on Transaction { /// The total amount of the coin transferred in the transaction as a double. /// This is the absolute value of the [totalAmount]. - double get totalAmountAsDouble => double.parse(totalAmount).abs(); + double get totalAmountAsDouble => balanceChanges.totalAmount.toDouble().abs(); /// The amount of the coin received in the transaction as a double. /// This is the [receivedByMe] as a double. - double get amountReceived => double.parse(receivedByMe); + double get amountReceived => balanceChanges.receivedByMe.toDouble(); /// The amount of the coin spent in the transaction as a double. /// This is the [spentByMe] as a double. - double get amountSpent => double.parse(spentByMe); + double get amountSpent => balanceChanges.spentByMe.toDouble(); /// The net change in the coin balance as a double. /// This is the [myBalanceChange] as a double. - double get balanceChange => double.parse(myBalanceChange); + double get balanceChange => amount.toDouble(); /// The timestamp of the transaction as a [DateTime] at midnight. DateTime get timeStampMidnight => - DateTime(timestampDate.year, timestampDate.month, timestampDate.day); + DateTime(timestamp.year, timestamp.month, timestamp.day); /// Returns true if the transaction is a deposit. I.e. the user receives the /// coin and does not spend any of it. This is true if the transaction is diff --git a/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart b/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart index 4541990d8b..882ff690cc 100644 --- a/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart +++ b/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart @@ -1,5 +1,5 @@ import 'package:web_dex/bloc/cex_market_data/profit_loss/models/fiat_value.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; class PriceStampedTransaction extends Transaction { final FiatValue fiatValue; @@ -8,30 +8,26 @@ class PriceStampedTransaction extends Transaction { required Transaction transaction, required this.fiatValue, }) : super( - blockHeight: transaction.blockHeight, - coin: transaction.coin, - confirmations: transaction.confirmations, - feeDetails: transaction.feeDetails, - from: transaction.from, + id: transaction.id, internalId: transaction.internalId, - myBalanceChange: transaction.myBalanceChange, - receivedByMe: transaction.receivedByMe, - spentByMe: transaction.spentByMe, + assetId: transaction.assetId, timestamp: transaction.timestamp, + confirmations: transaction.confirmations, + blockHeight: transaction.blockHeight, + from: transaction.from, to: transaction.to, - totalAmount: transaction.totalAmount, + fee: transaction.fee, txHash: transaction.txHash, - txHex: transaction.txHex, memo: transaction.memo, + balanceChanges: transaction.balanceChanges, ); } class UsdPriceStampedTransaction extends PriceStampedTransaction { double get priceUsd => fiatValue.value; double get totalAmountUsd => - (double.parse(totalAmount) * fiatValue.value).abs(); - double get balanceChangeUsd => - double.parse(myBalanceChange) * fiatValue.value; + (balanceChanges.totalAmount.toDouble() * fiatValue.value).abs(); + double get balanceChangeUsd => amount.toDouble() * fiatValue.value; UsdPriceStampedTransaction(Transaction transaction, double priceUsd) : super( diff --git a/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart b/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart index f6c3dec714..9ef2c79c84 100644 --- a/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart +++ b/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/fiat_value.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; /// Represents a profit/loss for a specific coin. class ProfitLoss extends Equatable { @@ -80,15 +80,15 @@ class ProfitLoss extends Equatable { ) { return ProfitLoss( profitLoss: runningProfitLoss, - coin: transaction.coin, + coin: transaction.assetId.id, fiatPrice: fiatPrice, internalId: transaction.internalId, - myBalanceChange: transaction.balanceChange, + myBalanceChange: transaction.amount.toDouble(), receivedAmountFiatPrice: transaction.amountReceived * fiatPrice.value, spentAmountFiatPrice: transaction.amountSpent * fiatPrice.value, - timestamp: transaction.timestampDate, totalAmount: transaction.totalAmountAsDouble, - txHash: transaction.txHash, + timestamp: transaction.timestamp, + txHash: transaction.txHash ?? '', ); } diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index 836de88e95..6903b102ab 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -149,17 +149,16 @@ class ProfitLossBloc extends Bloc { bool allowInactiveCoins = true, }) async { final List coins = List.from(event.coins); - await coins.removeWhereAsync( - (Coin coin) async { - final isCoinSupported = - await _profitLossRepository.isCoinChartSupported( - coin.abbr, - event.fiatCoinId, - allowInactiveCoins: allowInactiveCoins, - ); - return coin.isTestCoin || !isCoinSupported; - }, - ); + for (final coin in event.coins) { + final isCoinSupported = await _profitLossRepository.isCoinChartSupported( + coin.abbr, + event.fiatCoinId, + allowInactiveCoins: allowInactiveCoins, + ); + if (coin.isTestCoin || !isCoinSupported) { + coins.remove(coin); + } + } return coins; } diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart index ee23097411..75045cd2e2 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart @@ -2,7 +2,7 @@ import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; class ProfitLossCalculator { ProfitLossCalculator(this._cexRepository); @@ -29,7 +29,7 @@ class ProfitLossCalculator { return []; } - transactions.sort((a, b) => a.timestampDate.compareTo(b.timestampDate)); + transactions.sort((a, b) => a.timestamp.compareTo(b.timestamp)); final todayAtMidnight = _getDateAtMidnight(DateTime.now()); final transactionDates = _getTransactionDates(transactions); @@ -47,15 +47,13 @@ class ProfitLossCalculator { Map usdPrices, ) { return transactions.map((transaction) { - final usdPrice = - usdPrices[_getDateAtMidnight(transaction.timestampDate)]!; + final usdPrice = usdPrices[_getDateAtMidnight(transaction.timestamp)]!; return UsdPriceStampedTransaction(transaction, usdPrice); }).toList(); } List _getTransactionDates(List transactions) { - return transactions.map((tx) => tx.timestampDate).toList() - ..add(DateTime.now()); + return transactions.map((tx) => tx.timestamp).toList()..add(DateTime.now()); } DateTime _getDateAtMidnight(DateTime date) { @@ -80,7 +78,7 @@ class ProfitLossCalculator { for (final transaction in transactions) { if (transaction.totalAmountAsDouble == 0) continue; - if (transaction.isReceived) { + if (transaction.amount.toDouble() > 0) { state = _processBuyTransaction(state, transaction); } else { state = _processSellTransaction(state, transaction); @@ -104,12 +102,12 @@ class ProfitLossCalculator { UsdPriceStampedTransaction transaction, ) { final newHolding = - (holdings: transaction.balanceChange, price: transaction.priceUsd); + (holdings: transaction.amount.toDouble(), price: transaction.priceUsd); return _ProfitLossState( holdings: [...state.holdings, newHolding], realizedProfitLoss: state.realizedProfitLoss, totalInvestment: state.totalInvestment + transaction.balanceChangeUsd, - currentHoldings: state.currentHoldings + transaction.balanceChange, + currentHoldings: state.currentHoldings + transaction.amount.toDouble(), ); } @@ -117,13 +115,13 @@ class ProfitLossCalculator { _ProfitLossState state, UsdPriceStampedTransaction transaction, ) { - if (state.currentHoldings < transaction.balanceChange) { + if (state.currentHoldings < transaction.amount.toDouble()) { throw Exception('Attempting to sell more than currently held'); } // Balance change is negative for sales, so we use the abs value to // calculate the cost basis (formula assumes positive "total" value). - var remainingToSell = transaction.balanceChange.abs(); + var remainingToSell = transaction.amount.toDouble().abs(); var costBasis = 0.0; final newHoldings = List<({double holdings, double price})>.from(state.holdings); @@ -151,7 +149,7 @@ class ProfitLossCalculator { // Balance change is negative for a sale, so subtract the abs value ( // or add the positive value) to get the new holdings. final double newCurrentHoldings = - state.currentHoldings - transaction.balanceChange.abs(); + state.currentHoldings - transaction.amount.toDouble().abs(); final double newTotalInvestment = state.totalInvestment - costBasis; return _ProfitLossState( diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart index ac11e20391..7cb387dbbb 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart @@ -12,8 +12,9 @@ import 'package:web_dex/bloc/cex_market_data/profit_loss/models/adapters/adapter import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss_cache.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/shared/utils/utils.dart'; class ProfitLossRepository { @@ -23,20 +24,24 @@ class ProfitLossRepository { required cex.CexRepository cexRepository, required TransactionHistoryRepo transactionHistoryRepo, required ProfitLossCalculator profitLossCalculator, + required CoinsRepo coinsRepository, }) : _transactionHistoryRepo = transactionHistoryRepo, _cexRepository = cexRepository, _profitLossCacheProvider = profitLossCacheProvider, - _profitLossCalculator = profitLossCalculator; + _profitLossCalculator = profitLossCalculator, + _coinsRepository = coinsRepository; final PersistenceProvider _profitLossCacheProvider; final cex.CexRepository _cexRepository; final TransactionHistoryRepo _transactionHistoryRepo; final ProfitLossCalculator _profitLossCalculator; + final CoinsRepo _coinsRepository; static Future ensureInitialized() async { - Hive..registerAdapter(FiatValueAdapter()) - ..registerAdapter(ProfitLossAdapter()) - ..registerAdapter(ProfitLossCacheAdapter()); + Hive + ..registerAdapter(FiatValueAdapter()) + ..registerAdapter(ProfitLossAdapter()) + ..registerAdapter(ProfitLossCacheAdapter()); } Future clearCache() async { @@ -50,12 +55,16 @@ class ProfitLossRepository { String cacheTableName = 'profit_loss', required TransactionHistoryRepo transactionHistoryRepo, required cex.CexRepository cexRepository, + required CoinsRepo coinsRepository, + required Mm2Api mm2Api, PerformanceMode? demoMode, }) { if (demoMode != null) { return MockProfitLossRepository.withDefaults( performanceMode: demoMode, + coinsRepository: coinsRepository, cacheTableName: 'mock_${cacheTableName}_${demoMode.name}', + mm2Api: mm2Api, ); } @@ -65,6 +74,7 @@ class ProfitLossRepository { HiveLazyBoxProvider(name: cacheTableName), cexRepository: cexRepository, profitLossCalculator: RealisedProfitLossCalculator(cexRepository), + coinsRepository: coinsRepository, ); } @@ -85,8 +95,8 @@ class ProfitLossRepository { bool allowInactiveCoins = false, }) async { if (!allowInactiveCoins) { - final coin = coinsBlocRepository.getCoin(coinId)!; - if (coin.isActivating || !coin.isActive) { + final coin = await _coinsRepository.getEnabledCoin(coinId); + if (coin == null || coin.isActivating || !coin.isActive) { return false; } } @@ -151,7 +161,7 @@ class ProfitLossRepository { await _transactionHistoryRepo.fetchCompletedTransactions( // TODO: Refactor referenced coinsBloc method to a repository. // NB: Even though the class is called [CoinsBloc], it is not a Bloc. - coinsBlocRepository.getCoin(coinId)!, + _coinsRepository.getCoin(coinId)!, ); if (transactions.isEmpty) { diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart new file mode 100644 index 0000000000..a4bc160d85 --- /dev/null +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -0,0 +1,69 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_state.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class CoinAddressesBloc extends Bloc { + final KomodoDefiSdk? sdk; + final String assetId; + + CoinAddressesBloc(this.sdk, this.assetId) + : super(const CoinAddressesState()) { + on(_onSubmitCreateAddress); + on(_onLoadAddresses); + on(_onUpdateHideZeroBalance); + } + + Future _onSubmitCreateAddress( + SubmitCreateAddressEvent event, Emitter emit) async { + emit(state.copyWith(createAddressStatus: () => FormStatus.submitting)); + + try { + if (sdk == null) { + throw Exception('Coin Addresses KDF SDK is null'); + } + + await sdk!.pubkeys.createNewPubkey(getSdkAsset(sdk, assetId)); + + add(const LoadAddressesEvent()); + + emit(state.copyWith( + createAddressStatus: () => FormStatus.success, + )); + } catch (e) { + emit(state.copyWith( + createAddressStatus: () => FormStatus.failure, + errorMessage: () => e.toString(), + )); + } + } + + Future _onLoadAddresses( + LoadAddressesEvent event, Emitter emit) async { + emit(state.copyWith(status: () => FormStatus.submitting)); + + try { + final asset = getSdkAsset(sdk, assetId); + final addresses = (await asset.getPubkeys()).keys; + + final reasons = await asset.getCantCreateNewAddressReasons(sdk); + + emit(state.copyWith( + status: () => FormStatus.success, + addresses: () => addresses, + cantCreateNewAddressReasons: () => reasons, + )); + } catch (e) { + emit(state.copyWith( + status: () => FormStatus.failure, + errorMessage: () => e.toString(), + )); + } + } + + void _onUpdateHideZeroBalance( + UpdateHideZeroBalanceEvent event, Emitter emit) { + emit(state.copyWith(hideZeroBalance: () => event.hideZeroBalance)); + } +} diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart new file mode 100644 index 0000000000..391396d142 --- /dev/null +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +abstract class CoinAddressesEvent extends Equatable { + const CoinAddressesEvent(); + + @override + List get props => []; +} + +class SubmitCreateAddressEvent extends CoinAddressesEvent { + const SubmitCreateAddressEvent(); +} + +class LoadAddressesEvent extends CoinAddressesEvent { + const LoadAddressesEvent(); +} + +class UpdateHideZeroBalanceEvent extends CoinAddressesEvent { + final bool hideZeroBalance; + + const UpdateHideZeroBalanceEvent(this.hideZeroBalance); + + @override + List get props => [hideZeroBalance]; +} diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_state.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_state.dart new file mode 100644 index 0000000000..fa430db51e --- /dev/null +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_state.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +enum FormStatus { initial, submitting, success, failure } + +class CoinAddressesState extends Equatable { + final FormStatus status; + final FormStatus createAddressStatus; + final String? errorMessage; + final List addresses; + final bool hideZeroBalance; + final Set? cantCreateNewAddressReasons; + + const CoinAddressesState({ + this.status = FormStatus.initial, + this.createAddressStatus = FormStatus.initial, + this.errorMessage, + this.addresses = const [], + this.hideZeroBalance = false, + this.cantCreateNewAddressReasons, + }); + + CoinAddressesState copyWith({ + FormStatus Function()? status, + FormStatus Function()? createAddressStatus, + String? Function()? errorMessage, + List Function()? addresses, + bool Function()? hideZeroBalance, + Set? Function()? cantCreateNewAddressReasons, + }) { + return CoinAddressesState( + status: status == null ? this.status : status(), + createAddressStatus: createAddressStatus == null + ? this.createAddressStatus + : createAddressStatus(), + errorMessage: errorMessage == null ? this.errorMessage : errorMessage(), + addresses: addresses == null ? this.addresses : addresses(), + hideZeroBalance: + hideZeroBalance == null ? this.hideZeroBalance : hideZeroBalance(), + cantCreateNewAddressReasons: cantCreateNewAddressReasons == null + ? this.cantCreateNewAddressReasons + : cantCreateNewAddressReasons(), + ); + } + + CoinAddressesState resetWith({ + FormStatus Function()? status, + FormStatus Function()? createAddressStatus, + String? Function()? errorMessage, + List Function()? addresses, + bool Function()? hideZeroBalance, + Set? Function()? cantCreateNewAddressReasons, + }) { + return CoinAddressesState( + status: status == null ? FormStatus.initial : status(), + createAddressStatus: createAddressStatus == null + ? FormStatus.initial + : createAddressStatus(), + errorMessage: errorMessage == null ? null : errorMessage(), + addresses: addresses == null ? [] : addresses(), + hideZeroBalance: hideZeroBalance == null ? false : hideZeroBalance(), + cantCreateNewAddressReasons: cantCreateNewAddressReasons == null + ? null + : cantCreateNewAddressReasons(), + ); + } + + @override + List get props => [ + status, + createAddressStatus, + errorMessage, + addresses, + hideZeroBalance, + cantCreateNewAddressReasons, + ]; +} diff --git a/lib/bloc/coins_bloc/asset_coin_extension.dart b/lib/bloc/coins_bloc/asset_coin_extension.dart new file mode 100644 index 0000000000..902c02a4fc --- /dev/null +++ b/lib/bloc/coins_bloc/asset_coin_extension.dart @@ -0,0 +1,92 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/coin_type.dart'; + +extension AssetCoinExtension on Asset { + Coin toCoin() { + // Create protocol data if needed + ProtocolData? protocolData; + protocolData = ProtocolData( + platform: id.parentId?.id ?? '', + contractAddress: '', + ); + + final CoinType? type = _getCoinTypeFromProtocol(protocol); + if (type == null) { + throw ArgumentError.value( + protocol.subClass, 'protocol type', 'Unsupported protocol type'); + } + + // temporary measure to get metadata, like `wallet_only`, that isn't exposed + // by the SDK (and might be phased out completely later on) + final config = protocol.config; + + return Coin( + type: type, + abbr: id.id, + name: id.name, + explorerUrl: config.valueOrNull('explorer_url') ?? '', + explorerTxUrl: config.valueOrNull('explorer_tx_url') ?? '', + explorerAddressUrl: + config.valueOrNull('explorer_address_url') ?? '', + protocolType: protocol.subClass.ticker, + protocolData: protocolData, + isTestCoin: protocol.isTestnet, + coingeckoId: id.symbol.coinGeckoId, + swapContractAddress: config.valueOrNull('swap_contract_address'), + fallbackSwapContract: + config.valueOrNull('fallback_swap_contract'), + priority: 0, // Default priority + state: CoinState.inactive, + walletOnly: config.valueOrNull('wallet_only') ?? false, + mode: CoinMode.standard, + derivationPath: id.derivationPath, + ); + } + + CoinType? _getCoinTypeFromProtocol(ProtocolClass protocol) { + switch (protocol.subClass) { + case CoinSubClass.ftm20: + return CoinType.ftm20; + case CoinSubClass.arbitrum: + return CoinType.arb20; + // ignore: deprecated_member_use + case CoinSubClass.slp: + return CoinType.slp; + case CoinSubClass.qrc20: + return CoinType.qrc20; + case CoinSubClass.avx20: + return CoinType.avx20; + case CoinSubClass.smartChain: + return CoinType.smartChain; + case CoinSubClass.moonriver: + return CoinType.mvr20; + case CoinSubClass.ethereumClassic: + return CoinType.etc; + case CoinSubClass.hecoChain: + return CoinType.hco20; + case CoinSubClass.hrc20: + return CoinType.hrc20; + case CoinSubClass.tendermintToken: + return CoinType.iris; + case CoinSubClass.tendermint: + return CoinType.cosmos; + case CoinSubClass.ubiq: + return CoinType.ubiq; + case CoinSubClass.bep20: + return CoinType.bep20; + case CoinSubClass.matic: + return CoinType.plg20; + case CoinSubClass.utxo: + return CoinType.utxo; + case CoinSubClass.smartBch: + return CoinType.sbch; + case CoinSubClass.erc20: + return CoinType.erc20; + case CoinSubClass.krc20: + return CoinType.krc20; + default: + return CoinType.utxo; + } + } +} diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart new file mode 100644 index 0000000000..4a5ab616d6 --- /dev/null +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -0,0 +1,499 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; +import 'package:web_dex/blocs/trezor_coins_bloc.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/model/cex_price.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/wallet.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +part 'coins_event.dart'; +part 'coins_state.dart'; + +/// Responsible for coin activation, deactivation, syncing, and fiat price +class CoinsBloc extends Bloc { + CoinsBloc( + this._kdfSdk, + this._currentWalletBloc, + this._coinsRepo, + this._trezorBloc, + this._mm2Api, + ) : super(CoinsState.initial()) { + on(_onCoinsStarted, transformer: droppable()); + // TODO: move auth listener to ui layer: bloclistener fires auth events + on(_onCoinsBalanceMonitoringStarted); + on(_onCoinsBalanceMonitoringStopped); + on(_onCoinsRefreshed, transformer: sequential()); + on(_onCoinsActivated, transformer: concurrent()); + on(_onCoinsDeactivated, transformer: concurrent()); + on(_onPricesUpdated, transformer: sequential()); + on(_onLogin, transformer: droppable()); + on(_onLogout, transformer: droppable()); + on( + _onReactivateSuspended, + transformer: droppable(), + ); + on(_onWalletCoinUpdated, transformer: sequential()); + } + + final KomodoDefiSdk _kdfSdk; + final CurrentWalletBloc _currentWalletBloc; + final CoinsRepo _coinsRepo; + final Mm2Api _mm2Api; + // TODO: refactor to use repository - pin/password input events need to be + // handled, which are currently done through the trezor "bloc" + final TrezorCoinsBloc _trezorBloc; + + StreamSubscription? _enabledCoinsSubscription; + Timer? _updateBalancesTimer; + Timer? _updatePricesTimer; + Timer? _reActivateSuspendedTimer; + + // prevents RPC spamming on startup & previous inconsistencies with sdk wallet + KdfUser? _currentUserCache; + + @override + Future close() async { + await _enabledCoinsSubscription?.cancel(); + _updateBalancesTimer?.cancel(); + _updatePricesTimer?.cancel(); + _reActivateSuspendedTimer?.cancel(); + + await super.close(); + } + + Future _onCoinsStarted( + CoinsStarted event, + Emitter emit, + ) async { + emit(state.copyWith(coins: _coinsRepo.getKnownCoinsMap())); + + add(CoinsPricesUpdated()); + _updatePricesTimer?.cancel(); + _updatePricesTimer = Timer.periodic( + const Duration(minutes: 1), + (_) => add(CoinsPricesUpdated()), + ); + } + + Future _onCoinsRefreshed( + CoinsBalancesRefreshed event, + Emitter emit, + ) async { + _currentUserCache ??= await _kdfSdk.auth.currentUser; + switch (_currentUserCache?.wallet.config.type) { + case WalletType.trezor: + final walletCoins = + await _coinsRepo.updateTrezorBalances(state.walletCoins); + emit( + state.copyWith( + walletCoins: walletCoins, + // update balances in all coins list as well + coins: {...state.coins, ...walletCoins}, + ), + ); + case WalletType.metamask: + case WalletType.keplr: + case WalletType.iguana: + case null: + final coinUpdateStream = + _coinsRepo.updateIguanaBalances(state.walletCoins); + await emit.forEach( + coinUpdateStream, + onData: (Coin coin) => state.copyWith( + walletCoins: {...state.walletCoins, coin.abbr: coin}, + coins: {...state.coins, coin.abbr: coin}, + ), + ); + } + } + + Future _onWalletCoinUpdated( + CoinsWalletCoinUpdated event, + Emitter emit, + ) async { + final coin = event.coin; + final walletCoins = Map.from(state.walletCoins); + + if (coin.isActivating || coin.isActive || coin.isSuspended) { + await _kdfSdk.addActivatedCoins([coin.abbr]); + emit( + state.copyWith( + walletCoins: {...walletCoins, coin.abbr: coin}, + coins: {...state.coins, coin.abbr: coin}, + ), + ); + } + + if (coin.isInactive) { + walletCoins.remove(coin.abbr); + await _currentWalletBloc.removeCoin(coin.abbr); + await _kdfSdk.removeActivatedCoins([coin.abbr]); + emit( + state.copyWith( + walletCoins: walletCoins, + coins: {...state.coins, coin.abbr: coin}, + ), + ); + } + } + + Future _onCoinsBalanceMonitoringStopped( + CoinsBalanceMonitoringStopped event, + Emitter emit, + ) async { + _updateBalancesTimer?.cancel(); + _reActivateSuspendedTimer?.cancel(); + await _enabledCoinsSubscription?.cancel(); + } + + Future _onCoinsBalanceMonitoringStarted( + CoinsBalanceMonitoringStarted event, + Emitter emit, + ) async { + _updateBalancesTimer?.cancel(); + _updateBalancesTimer = Timer.periodic( + const Duration(seconds: 30), + (timer) { + add(CoinsBalancesRefreshed()); + }, + ); + + _reActivateSuspendedTimer?.cancel(); + _reActivateSuspendedTimer = Timer.periodic( + const Duration(seconds: 30), + (_) => add(CoinsSuspendedReactivated()), + ); + + // This is used to connect [CoinsBloc] to [CoinsManagerBloc], since coins + // manager bloc activates and deactivates coins using the repository. + await _enabledCoinsSubscription?.cancel(); + _enabledCoinsSubscription = _coinsRepo.enabledAssetsChanges.stream.listen( + (Coin coin) => add(CoinsWalletCoinUpdated(coin)), + ); + } + + Future _onReactivateSuspended( + CoinsSuspendedReactivated event, + Emitter emit, + ) async { + await emit.forEach( + _reActivateSuspended(emit), + onData: (suspendedCoins) => state.copyWith( + walletCoins: { + ...state.walletCoins, + ...suspendedCoins.toMap(), + }, + ), + ); + } + + Future _onCoinsActivated( + CoinsActivated event, + Emitter emit, + ) async { + await _activateCoins(event.coinIds, emit); + + if (_currentUserCache?.wallet.config.type == WalletType.iguana) { + final coinUpdates = _syncIguanaCoinsStates(event.coinIds); + await emit.forEach( + coinUpdates, + onData: (coin) => state + .copyWith(walletCoins: {...state.walletCoins, coin.abbr: coin}), + ); + } + } + + Future _onCoinsDeactivated( + CoinsDeactivated event, + Emitter emit, + ) async { + for (final coinId in event.coinIds) { + final coin = state.walletCoins[coinId]!; + log( + 'Disabling a ${coin.name} ($coinId)', + path: 'coins_bloc => disable', + ).ignore(); + coin.reset(); + + await _kdfSdk.removeActivatedCoins([coin.abbr]); + await _currentWalletBloc.removeCoin(coin.abbr); + await _mm2Api.disableCoin(coin.abbr); + + final newWalletCoins = Map.from(state.walletCoins); + state.walletCoins.remove(coin.abbr); + final newCoins = Map.from(state.coins); + newCoins[coin.abbr]!.state = CoinState.inactive; + emit(state.copyWith(walletCoins: newWalletCoins, coins: newCoins)); + + log('${coin.name} has been disabled', path: 'coins_bloc => disable') + .ignore(); + } + } + + Future _onPricesUpdated( + CoinsPricesUpdated event, + Emitter emit, + ) async { + bool changed = false; + final prices = await _coinsRepo.fetchCurrentPrices(); + + if (prices == null) { + log( + 'Coin prices list empty/null', + isError: true, + path: 'coins_bloc => _onPricesUpdated', + ).ignore(); + return; + } + + final coins = Map.from(state.coins); + for (final entry in state.coins.entries) { + final coin = entry.value; + final CexPrice? usdPrice = prices[abbr2Ticker(coin.abbr)]; + + if (usdPrice != coin.usdPrice) { + changed = true; + // Create new coin instance with updated price + coins[entry.key] = coin.copyWith(usdPrice: usdPrice); + + // Update wallet coins if exists + if (state.walletCoins.containsKey(coin.abbr)) { + emit( + state.copyWith( + walletCoins: { + ...state.walletCoins, + coin.abbr: + state.walletCoins[entry.key]!.copyWith(usdPrice: usdPrice), + }, + ), + ); + } + } + } + + if (changed) { + emit(state.copyWith(coins: coins)); + } + + log('CEX prices updated', path: 'coins_bloc => updateCoinsCexPrices') + .ignore(); + } + + Future _onLogin( + CoinsSessionStarted event, + Emitter emit, + ) async { + _coinsRepo.flushCache(); + _currentUserCache = event.signedInUser; + await _activateLoginWalletCoins(emit); + emit(state.copyWith(loginActivationFinished: true)); + + add(CoinsBalancesRefreshed()); + add(CoinsBalanceMonitoringStarted()); + } + + Future _onLogout( + CoinsSessionEnded event, + Emitter emit, + ) async { + add(CoinsBalanceMonitoringStopped()); + _currentUserCache = null; + + final List coins = [...state.walletCoins.values]; + for (final Coin coin in coins) { + switch (coin.enabledType) { + case WalletType.iguana: + coin.reset(); + final newWalletCoins = Map.from(state.walletCoins); + newWalletCoins.remove(coin.abbr.toUpperCase()); + emit(state.copyWith(walletCoins: newWalletCoins)); + log('${coin.name} has been removed', path: 'coins_bloc => _onLogout') + .ignore(); + case WalletType.trezor: + case WalletType.metamask: + case WalletType.keplr: + case null: + break; + } + coin.reset(); + } + + emit( + state.copyWith( + walletCoins: {}, + loginActivationFinished: false, + coins: { + ...state.coins, + ...coins.map((coin) => coin.copyWith(balance: 0)).toList().toMap(), + }, + ), + ); + _coinsRepo.flushCache(); + } + + Future> _activateCoins( + Iterable coins, + Emitter emit, + ) async { + // Start off by emitting the newly activated coins so that they all appear + // in the list at once, rather than one at a time as they are activated + _prePopulateListWithActivatingCoins(coins, emit); + + await _kdfSdk.addActivatedCoins(coins); + for (final coin in coins) { + await _currentWalletBloc.addCoin(state.coins[coin]!); + } + final enableFutures = coins.map((coin) => _activateCoin(coin)).toList(); + final results = []; + await for (final coin + in Stream.fromFutures(enableFutures).asBroadcastStream()) { + results.add(coin); + final currentState = state; + emit( + currentState.copyWith( + walletCoins: {...currentState.walletCoins, coin.abbr: coin}, + coins: {...currentState.coins, coin.abbr: coin}, + ), + ); + } + + return results; + } + + void _prePopulateListWithActivatingCoins( + Iterable coins, + Emitter emit, + ) { + final activatingCoins = Map.fromIterable( + coins + .map( + (coin) => state.coins[coin]?.copyWith( + state: CoinState.activating, + enabledType: _currentUserCache?.wallet.config.type, + ), + ) + .where((coin) => coin != null) + .cast(), + key: (element) => (element as Coin).abbr, + ); + emit( + state.copyWith( + walletCoins: {...state.walletCoins, ...activatingCoins}, + coins: {...state.coins, ...activatingCoins}, + ), + ); + } + + Future _activateCoin(String coinId) async { + Coin coin = state.coins[coinId]!; + final isLoggedIn = _currentUserCache != null; + if (!isLoggedIn || coin.isActive) { + return coin; + } + + switch (_currentUserCache?.wallet.config.type) { + case WalletType.iguana: + coin = await _activateIguanaCoin(coin); + case WalletType.trezor: + final asset = _kdfSdk.assets.assetsFromTicker(coin.abbr).single; + final accounts = await _trezorBloc.activateCoin(asset); + final state = + accounts.isNotEmpty ? CoinState.active : CoinState.suspended; + coin = coin.copyWith(state: state, accounts: accounts); + case WalletType.metamask: + case WalletType.keplr: + case null: + break; + } + + return coin; + } + + Future _activateIguanaCoin(Coin coin) async { + try { + log('Enabling a ${coin.name}', path: 'coins_bloc => enable').ignore(); + await _coinsRepo.activateCoinsSync([coin]); + coin.state = CoinState.active; + log('${coin.name} has enabled', path: 'coins_bloc => enable').ignore(); + } catch (e, s) { + coin.state = CoinState.suspended; + log( + 'Failed to activate iguana coin: $e', + isError: true, + path: 'coins_bloc => _activateIguanaCoin', + trace: s, + ).ignore(); + } + return coin; + } + + Future> _activateLoginWalletCoins(Emitter emit) async { + final Wallet? currentWallet = _currentUserCache?.wallet; + if (currentWallet == null) { + return List.empty(); + } + + final List coins = currentWallet.config.activatedCoins + .map((abbr) => state.coins[abbr]) + .whereType() + .map((coin) => coin.abbr) + .toList(); + + return _activateCoins(coins, emit); + } + + Stream> _reActivateSuspended( + Emitter emit, { + int attempts = 1, + }) async* { + for (int i = 0; i < attempts; i++) { + final List suspended = state.walletCoins.values + .where((coin) => coin.isSuspended) + .map((coin) => coin.abbr) + .toList(); + if (suspended.isEmpty) return; + + yield await _activateCoins(suspended, emit); + } + } + + /// yields one coin at a time to provide visual feedback to the user as + /// coins are activated + Stream _syncIguanaCoinsStates(Iterable coins) async* { + final walletCoins = state.walletCoins; + + for (final coinId in coins) { + final Coin? apiCoin = await _coinsRepo.getEnabledCoin(coinId); + final coin = walletCoins[coinId]; + if (coin == null) { + log('Coin $coinId removed from wallet, skipping sync').ignore(); + continue; + } + + if (apiCoin != null) { + // enabled on gui side, but not on api side - suspend + if (coin.state != CoinState.active) { + yield coin.copyWith(state: CoinState.active); + } + } else { + // enabled on both sides - unsuspend + yield coin.copyWith(state: CoinState.suspended); + } + + for (final Coin apiCoin in await _coinsRepo.getEnabledCoins()) { + if (!walletCoins.containsKey(apiCoin.abbr)) { + // enabled on api side, but not on gui side - enable on gui side + yield apiCoin; + } + } + } + } +} diff --git a/lib/bloc/coins_bloc/coins_event.dart b/lib/bloc/coins_bloc/coins_event.dart new file mode 100644 index 0000000000..879b68f442 --- /dev/null +++ b/lib/bloc/coins_bloc/coins_event.dart @@ -0,0 +1,79 @@ +part of 'coins_bloc.dart'; + +sealed class CoinsEvent extends Equatable { + const CoinsEvent(); + + @override + List get props => []; +} + +/// Event emitted when the coins feature is started +final class CoinsStarted extends CoinsEvent {} + +/// Event emitted when user requests to refresh their coin balances manually +final class CoinsBalancesRefreshed extends CoinsEvent {} + +/// Event emitted when the bloc should start listening to authentication changes +final class CoinsAuthenticationStarted extends CoinsEvent {} + +/// Event emitted when the bloc should stop listening to authentication changes +final class CoinsAuthenticationStopped extends CoinsEvent {} + +/// Event emitted when the bloc should start monitoring balances +final class CoinsBalanceMonitoringStarted extends CoinsEvent {} + +/// Event emitted when the bloc should stop monitoring balances +final class CoinsBalanceMonitoringStopped extends CoinsEvent {} + +/// Event emitted when user activates a coin for tracking +final class CoinsActivated extends CoinsEvent { + const CoinsActivated(this.coinIds); + + final Iterable coinIds; + + @override + List get props => [coinIds]; +} + +/// Event emitted when user deactivates a coin from tracking +final class CoinsDeactivated extends CoinsEvent { + const CoinsDeactivated(this.coinIds); + + final Iterable coinIds; + + @override + List get props => [coinIds]; +} + +final class CoinsPricesUpdated extends CoinsEvent {} + +/// Successful user login (session) +/// NOTE: has to be called from the UI layer for now, to ensure that wallet +/// metadata is saved to the current user. Auth state changes from the SDK +/// do not include updates to user metadata currently required for the GUI to +/// function properly. +final class CoinsSessionStarted extends CoinsEvent { + const CoinsSessionStarted(this.signedInUser); + + final KdfUser signedInUser; + + @override + List get props => [signedInUser]; +} + +/// User session ended (logout) +final class CoinsSessionEnded extends CoinsEvent {} + +/// Suspended coins should be reactivated +final class CoinsSuspendedReactivated extends CoinsEvent {} + +/// Wallet coin is updated from the repository stream +/// Links [CoinsBloc] with [CoinsManagerBloc] +final class CoinsWalletCoinUpdated extends CoinsEvent { + const CoinsWalletCoinUpdated(this.coin); + + final Coin coin; + + @override + List get props => [coin]; +} diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 6733723417..0b0afa9185 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -1,332 +1,599 @@ import 'dart:async'; +import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:komodo_coin_updates/komodo_coin_updates.dart' as coin_updates; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/app_config/coins_config_parser.dart'; -import 'package:web_dex/bloc/runtime_coin_updates/runtime_update_config_provider.dart'; +import 'package:http/http.dart' as http; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart' + as kdf_rpc; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; +import 'package:web_dex/blocs/trezor_coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/electrum/electrum_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable/enable_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable_tendermint/enable_tendermint_token.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable_tendermint/enable_tendermint_with_assets.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_response.dart'; +import 'package:web_dex/mm2/mm2.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/bloc_response.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/get_enabled_coins_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_request.dart'; +import 'package:web_dex/model/cex_price.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/text_error.dart'; - -final CoinsRepo coinsRepo = CoinsRepo( - api: mm2Api, -); +import 'package:web_dex/model/wallet.dart'; +import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; +import 'package:web_dex/shared/constants.dart'; +import 'package:web_dex/shared/utils/utils.dart'; class CoinsRepo { CoinsRepo({ - required Mm2Api api, - }) : _api = api; - final Mm2Api _api; - coin_updates.CoinConfigRepository? _coinRepo; - - List? _cachedKnownCoins; - - // TODO: Consider refactoring to a Map - Future> getKnownCoins() async { - if (_cachedKnownCoins != null) return _cachedKnownCoins!; - - _coinRepo ??= coin_updates.CoinConfigRepository.withDefaults( - await RuntimeUpdateConfigProvider().getRuntimeUpdateConfig(), + required KomodoDefiSdk kdfSdk, + required MM2 mm2, + required TrezorCoinsBloc trezorBloc, + }) : _kdfSdk = kdfSdk, + _mm2 = mm2, + trezor = trezorBloc { + enabledAssetsChanges = StreamController.broadcast( + onListen: () => _enabledAssetListenerCount += 1, + onCancel: () => _enabledAssetListenerCount -= 1, ); - // If the bundled config files don't exist, then download the latest configs - // and load them from the storage provider. - final bool bundledConfigsExist = await coinConfigParser.hasLocalConfigs(); - if (!bundledConfigsExist) { - await _coinRepo!.updateCoinConfig(excludedAssets: excludedAssetList); + } + + final KomodoDefiSdk _kdfSdk; + final MM2 _mm2; + // TODO: refactor to use repository - pin/password input events need to be + // handled, which are currently done through the trezor "bloc" + final TrezorCoinsBloc trezor; + + /// { acc: { abbr: address }}, used in Fiat Page + final Map> _addressCache = {}; + Map _pricesCache = {}; + final Map _balancesCache = + {}; + + // why could they not implement this in streamcontroller or a wrapper :( + late final StreamController enabledAssetsChanges; + int _enabledAssetListenerCount = 0; + bool get _enabledAssetsHasListeners => _enabledAssetListenerCount > 0; + Future _broadcastAsset(Coin coin) async { + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser != null) { + coin.enabledType = currentUser.wallet.config.type; } - final bool hasUpdatedConfigs = await _coinRepo!.coinConfigExists(); - if (!bundledConfigsExist || hasUpdatedConfigs) { - final coins = await _getKnownCoinsFromStorage(); - if (coins.isNotEmpty) { - _cachedKnownCoins = coins; - return coins; - } + if (_enabledAssetsHasListeners) { + enabledAssetsChanges.add(coin); } + } - final coins = _cachedKnownCoins ?? await _getKnownCoinsFromConfig(); - return [...coins]; + void flushCache() { + _addressCache.clear(); + _balancesCache.clear(); } - /// Get the list of [coin_updates.Coin]s with the minimal fields from `coins.json`. - /// If the local coin configs exist, and there are no updates in storage, then - /// the coins from the bundled configs are loaded. - /// Otherwise, the coins from storage are loaded. - Future> getKnownGlobalCoins() async { - _coinRepo ??= coin_updates.CoinConfigRepository.withDefaults( - await RuntimeUpdateConfigProvider().getRuntimeUpdateConfig(), + List getKnownCoins() { + final assets = _kdfSdk.assets.available; + return assets.values.map(_assetToCoinWithoutAddress).toList(); + } + + Map getKnownCoinsMap() { + final assets = _kdfSdk.assets.available; + return Map.fromEntries( + assets.values.map( + (asset) => MapEntry(asset.id.id, _assetToCoinWithoutAddress(asset)), + ), ); + } - final bool bundledConfigsExist = await coinConfigParser.hasLocalConfigs(); - if (!bundledConfigsExist) { - await _coinRepo!.updateCoinConfig(excludedAssets: excludedAssetList); + Coin? getCoin(String coinId) { + try { + final asset = _kdfSdk.assets.assetsFromTicker(coinId).single; + return _assetToCoinWithoutAddress(asset); + } catch (_) { + return null; } + } - final bool hasUpdatedConfigs = await _coinRepo!.coinConfigExists(); - if (!bundledConfigsExist || hasUpdatedConfigs) { - final coins = - await _coinRepo!.getCoins(excludedAssets: excludedAssetList); - if (coins != null && coins.isNotEmpty) { - return coins - .where((coin) => !excludedAssetList.contains(coin.coin)) - .toList(); - } + Future> getWalletCoins() async { + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + return []; } - final globalCoins = await coinConfigParser.getGlobalCoinsJson(); - return globalCoins - .map((coin) => coin_updates.Coin.fromJson(coin as Map)) + final activatedCoins = currentUser.wallet.config.activatedCoins; + final knownCoins = getKnownCoinsMap(); + return activatedCoins + .map((String coinId) => knownCoins[coinId]) + .where((Coin? coin) => coin != null) + .cast() .toList(); } - /// Loads the known [coin_updates.Coin]s from the storage provider, maps it - /// to the existing [Coin] model with the parent coin assigned and - /// orphans removed. - Future> _getKnownCoinsFromStorage() async { - final List coins = - (await _coinRepo!.getCoinConfigs(excludedAssets: excludedAssetList))! - .values - .where((coin) => getCoinType(coin.type ?? '', coin.coin) != null) - .where((coin) => !_shouldSkipCoin(coin)) - .map(_mapCoinConfigToCoin) - .toList(); - - for (Coin coin in coins) { - coin.parentCoin = _getParentCoin(coin, coins); - } - - _removeOrphans(coins); + Future getEnabledCoin(String coinId) async { + final enabledAssets = await getEnabledCoinsMap(); + final coin = enabledAssets[coinId]; + if (coin == null) return null; + return coin; + } - final List unmodifiableCoins = List.unmodifiable(coins); - _cachedKnownCoins = unmodifiableCoins; - return unmodifiableCoins; + Future> getEnabledCoins() async { + final enabledCoinsMap = await getEnabledCoinsMap(); + return enabledCoinsMap.values.toList(); } - /// Maps the komodo_coin_updates package Coin class [coin] - /// to the app Coin class. - Coin _mapCoinConfigToCoin(coin_updates.CoinConfig coin) { - final coinJson = coin.toJson(); - coinJson['abbr'] = coin.coin; - coinJson['priority'] = priorityCoinsAbbrMap[coin.coin] ?? 0; - coinJson['active'] = enabledByDefaultCoins.contains(coin.coin); - if (kIsWeb) { - coinConfigParser.removeElectrumsWithoutWss(coinJson['electrum']); + Future> getEnabledCoinsMap() async { + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + return {}; } - final newCoin = Coin.fromJson(coinJson, coinJson); - return newCoin; + + final enabledCoins = await _kdfSdk.assets.getActivatedAssets(); + final entries = await Future.wait( + enabledCoins.map( + (asset) async => + MapEntry(asset.id.id, _assetToCoinWithoutAddress(asset)), + ), + ); + final coinsMap = Map.fromEntries(entries); + for (final coinId in coinsMap.keys) { + final coin = coinsMap[coinId]!; + final coinAddress = await _getCoinAddress(coin.abbr, coinsMap); + coinsMap[coinId] = coin.copyWith( + address: coinAddress, + state: CoinState.active, + enabledType: currentUser.wallet.config.type, + ); + } + return coinsMap; } - /// Checks if the coin should be skipped according to the following rules: - /// - If the coin is in the excluded asset list. - /// - If the coin type is not supported or empty. - /// - If the electrum servers are not supported on the current platform - /// (WSS on web, SSL and TCP on native platforms). - bool _shouldSkipCoin(coin_updates.CoinConfig coin) { - if (excludedAssetList.contains(coin.coin)) { - return true; + Coin _assetToCoinWithoutAddress(Asset asset) { + final coin = asset.toCoin(); + final balance = _balancesCache[coin.abbr]?.balance; + final sendableBalance = _balancesCache[coin.abbr]?.sendableBalance; + final price = _pricesCache[coin.abbr]; + + Coin? parentCoin; + if (asset.id.isChildAsset) { + final parentCoinId = asset.id.parentId!.id; + final parentAsset = _kdfSdk.assets.assetsFromTicker(parentCoinId).single; + parentCoin = _assetToCoinWithoutAddress(parentAsset); } - if (getCoinType(coin.type, coin.coin) == null) { - return true; + return coin.copyWith( + balance: balance, + sendableBalance: sendableBalance, + usdPrice: price, + parentCoin: parentCoin, + ); + } + + Future tryGetBalanceInfo(String abbr) async { + try { + return await _kdfSdk.client.rpc.wallet.myBalance(coin: abbr); + } catch (e, s) { + log( + 'Failed to get coin $abbr balance: $e', + isError: true, + path: 'coins_repo => tryGetBalanceInfo', + trace: s, + ).ignore(); + return kdf_rpc.MyBalanceResponse( + address: '', + balance: kdf_rpc.BalanceInfo.zero(), + coin: abbr, + mmrpc: '2', + ); } + } + + Future activateCoinsSync(List coins) async { + if (!await _kdfSdk.auth.isSignedIn()) return; + final enabledAssets = await getEnabledCoinsMap(); - if (coin.electrum != null && coin.electrum?.isNotEmpty == true) { - return coin.electrum! - .every((e) => !_isConnectionTypeSupported(e.protocol ?? '')); + for (final coin in coins) { + try { + if (enabledAssets.containsKey(coin.abbr)) { + continue; + } + + final asset = _kdfSdk.assets.findAssetsByTicker(coin.abbr).single; + await _broadcastAsset(coin.copyWith(state: CoinState.activating)); + + if (coin.parentCoin != null) { + await _activateParentAsset(coin); + } + // ignore: deprecated_member_use + await _kdfSdk.assets.activateAsset(asset).last; + + await _broadcastAsset(coin.copyWith(state: CoinState.active)); + } catch (e, s) { + log( + 'Error activating coin: ${coin.abbr} \n$e', + isError: true, + trace: s, + ).ignore(); + await _broadcastAsset(coin.copyWith(state: CoinState.suspended)); + } } + } - return false; + Future _activateParentAsset(Coin coin) async { + final parentAsset = + _kdfSdk.assets.findAssetsByTicker(coin.parentCoin!.abbr).single; + await _broadcastAsset( + coin.parentCoin!.copyWith(state: CoinState.activating), + ); + // ignore: deprecated_member_use + await _kdfSdk.assets.activateAsset(parentAsset).last; + await _broadcastAsset( + coin.parentCoin!.copyWith(state: CoinState.active), + ); } - /// Returns true if [networkProtocol] is supported on the current platform. - /// On web, only WSS is supported. - /// On other (native) platforms, only SSL and TCP are supported. - bool _isConnectionTypeSupported(String networkProtocol) { - String uppercaseProtocol = networkProtocol.toUpperCase(); + Future deactivateCoinsSync(List coins) async { + if (!await _kdfSdk.auth.isSignedIn()) return; - if (kIsWeb) { - return uppercaseProtocol == 'WSS'; + for (final coin in coins) { + await _disableCoin(coin.abbr); + await _broadcastAsset(coin.copyWith(state: CoinState.inactive)); } + } - return uppercaseProtocol == 'SSL' || uppercaseProtocol == 'TCP'; + Future _disableCoin(String coinId) async { + try { + await _mm2.call(DisableCoinReq(coin: coinId)); + } catch (e, s) { + log( + 'Error disabling $coinId: $e', + path: 'api=> disableCoin => _call', + trace: s, + isError: true, + ).ignore(); + return; + } } - Future> _getKnownCoinsFromConfig() async { - final List globalCoinsJson = - await coinConfigParser.getGlobalCoinsJson(); - final Map appCoinsJson = - await coinConfigParser.getUnifiedCoinsJson(); + Future getCoinAddress(String coinId) async { + final enabledCoins = await getEnabledCoinsMap(); + return _getCoinAddress(coinId, enabledCoins); + } - final List appItems = appCoinsJson.values.toList(); + Future _getCoinAddress( + String coinId, + Map walletCoins, + ) async { + final isLoggedIn = await _kdfSdk.auth.isSignedIn(); + final currentWallet = (await _kdfSdk.auth.currentUser)?.wallet; + final loggedIn = isLoggedIn && currentWallet != null; + if (!loggedIn) { + return null; + } - _removeUnknown(appItems, globalCoinsJson); + final String accountKey = currentWallet.id; + if (_addressCache.containsKey(accountKey) && + _addressCache[accountKey]!.containsKey(coinId)) { + return _addressCache[accountKey]![coinId]; + } else { + try { + if (walletCoins[coinId] == null) { + await activateCoinsSync([getCoin(coinId)!]); + } - final List coins = appItems.map((dynamic appItem) { - final dynamic globalItem = - _getGlobalItemByAbbr(appItem['coin'], globalCoinsJson); + // This function is also called within `getEnabledCoins`, so cannot use + // that function unless you enjoy recursive stackoverflow :) + final legacyEnabledCoins = await _getEnabledCoins(walletCoins.values); + final Coin? coin = legacyEnabledCoins + ?.firstWhereOrNull((enabledCoin) => enabledCoin.abbr == coinId); + + if (coin == null || coin.address == null) { + if (!_addressCache.containsKey(accountKey)) { + _addressCache[accountKey] = {}; + } + + // Cache this wallet's addresses + for (final walletCoin in walletCoins.values) { + if (walletCoin.address != null && + !_addressCache[accountKey]!.containsKey(walletCoin.abbr)) { + // Exit if the address already exists in a different account + // Address belongs to another account, this is a bug, + // gives outdated data + for (final entry in _addressCache.entries) { + if (entry.key != accountKey && + entry.value.containsValue(walletCoin.address)) { + return null; + } + } + + _addressCache[accountKey]![walletCoin.abbr] = walletCoin.address!; + } + } + + return _addressCache[accountKey]![coinId]; + } + } catch (e, s) { + log( + 'Failed to get coin address: $e', + isError: true, + path: 'coins_repo => _getCoinAddress', + trace: s, + ).ignore(); + } + } + return null; + } - return Coin.fromJson(appItem, globalItem); - }).toList(); + double? getUsdPriceByAmount(String amount, String coinAbbr) { + final Coin? coin = getCoin(coinAbbr); + final double? parsedAmount = double.tryParse(amount); + final double? usdPrice = coin?.usdPrice?.price; - for (Coin coin in coins) { - coin.parentCoin = _getParentCoin(coin, coins); + if (coin == null || usdPrice == null || parsedAmount == null) { + return null; } + return parsedAmount * usdPrice; + } + + Future?> fetchCurrentPrices() async { + final Map? prices = + await _updateFromMain() ?? await _updateFromFallback(); - _removeOrphans(coins); + if (prices != null) { + _pricesCache = prices; + } - final List unmodifiableCoins = List.unmodifiable(coins); - _cachedKnownCoins = unmodifiableCoins; - return unmodifiableCoins; + return _pricesCache; } - // 'Orphans' are coins that have 'parent' coin in config, - // but 'parent' coin wasn't found. - void _removeOrphans(List coins) { - final List original = List.from(coins); + Future fetchPrice(String ticker) async { + final Map? prices = await fetchCurrentPrices(); + if (prices == null || !prices.containsKey(ticker)) return null; - coins.removeWhere((coin) { - final String? platform = coin.protocolData?.platform; - if (platform == null) return false; + return prices[ticker]!; + } - final parentCoin = - original.firstWhereOrNull((coin) => coin.abbr == platform); + Future?> _updateFromMain() async { + http.Response res; + String body; + try { + res = await http.get(pricesUrlV3); + body = res.body; + } catch (e, s) { + log( + 'Error updating price from main: $e', + path: 'cex_services => _updateFromMain => http.get', + trace: s, + isError: true, + ).ignore(); + return null; + } - return parentCoin == null; - }); - } + Map? json; + try { + json = jsonDecode(body) as Map; + } catch (e, s) { + log( + 'Error parsing of update price from main response: $e', + path: 'cex_services => _updateFromMain => jsonDecode', + trace: s, + isError: true, + ).ignore(); + } - void _removeUnknown( - List appItems, - List globalItems, - ) { - appItems.removeWhere((dynamic appItem) { - return _getGlobalItemByAbbr(appItem['coin'], globalItems) == null; + if (json == null) return null; + final Map prices = {}; + json.forEach((String priceTicker, dynamic pricesData) { + final pricesJson = pricesData as Map? ?? {}; + prices[priceTicker] = CexPrice( + ticker: priceTicker, + price: double.tryParse(pricesJson['last_price'] as String? ?? '') ?? 0, + lastUpdated: DateTime.fromMillisecondsSinceEpoch( + (pricesJson['last_updated_timestamp'] as int? ?? 0) * 1000, + ), + priceProvider: + cexDataProvider(pricesJson['price_provider'] as String? ?? ''), + change24h: double.tryParse(pricesJson['change_24h'] as String? ?? ''), + changeProvider: + cexDataProvider(pricesJson['change_24h_provider'] as String? ?? ''), + volume24h: double.tryParse(pricesJson['volume24h'] as String? ?? ''), + volumeProvider: + cexDataProvider(pricesJson['volume_provider'] as String? ?? ''), + ); }); + return prices; } - dynamic _getGlobalItemByAbbr(String abbr, List globalItems) { - return globalItems.firstWhereOrNull((dynamic item) => abbr == item['coin']); - } + Future?> _updateFromFallback() async { + final List ids = (await getEnabledCoins()) + .map((c) => c.coingeckoId ?? '') + .toList() + ..removeWhere((id) => id.isEmpty); + final Uri fallbackUri = Uri.parse( + 'https://api.coingecko.com/api/v3/simple/price?ids=' + '${ids.join(',')}&vs_currencies=usd', + ); - Coin? _getParentCoin(Coin? coin, List coins) { - final String? parentCoinAbbr = coin?.protocolData?.platform; - if (parentCoinAbbr == null) return null; + http.Response res; + String body; + try { + res = await http.get(fallbackUri); + body = res.body; + } catch (e, s) { + log( + 'Error updating price from fallback: $e', + path: 'cex_services => _updateFromFallback => http.get', + trace: s, + isError: true, + ).ignore(); + return null; + } - return coins.firstWhereOrNull( - (item) => item.abbr.toUpperCase() == parentCoinAbbr.toUpperCase()); - } + Map? json; + try { + json = jsonDecode(body) as Map?; + } catch (e, s) { + log( + 'Error parsing of update price from fallback response: $e', + path: 'cex_services => _updateFromFallback => jsonDecode', + trace: s, + isError: true, + ).ignore(); + } - Future> getEnabledCoins(List knownCoins) async { - final enabledCoins = await _api.getEnabledCoins(knownCoins); - return enabledCoins ?? []; - } + if (json == null) return null; + final Map prices = {}; + + for (final MapEntry entry in json.entries) { + final coingeckoId = entry.key; + final pricesData = entry.value as Map? ?? {}; + if (coingeckoId == 'test-coin') continue; + + // Coins with the same coingeckoId supposedly have same usd price + // (e.g. KMD == KMD-BEP20) + final Iterable samePriceCoins = + getKnownCoins().where((coin) => coin.coingeckoId == coingeckoId); + + for (final Coin coin in samePriceCoins) { + prices[coin.abbr] = CexPrice( + ticker: coin.abbr, + price: double.parse(pricesData['usd'].toString()), + ); + } + } - Future getBalanceInfo(String abbr) async { - return await _api.getMaxMakerVol(abbr); + return prices; } - Future deactivateCoin(Coin coin) async { - await _api.disableCoin(coin.abbr); + Future getBalanceInfo(String abbr) async { + final pubkeys = await getSdkAsset(_kdfSdk, abbr).getPubkeys(); + return pubkeys.balance; } - Future?> validateCoinAddress( - Coin coin, String address) async { - return await _api.validateAddress(coin.abbr, address); + Future> updateTrezorBalances( + Map walletCoins, + ) async { + final walletCoinsCopy = Map.from(walletCoins); + final coins = + walletCoinsCopy.entries.where((entry) => entry.value.isActive).toList(); + for (final MapEntry entry in coins) { + walletCoinsCopy[entry.key]!.accounts = + await trezor.trezorRepo.getAccounts(entry.value); + } + + return walletCoinsCopy; } - Future?> withdraw(WithdrawRequest request) async { - return await _api.withdraw(request); + Stream updateIguanaBalances( + Map walletCoins, + ) async* { + final walletCoinsCopy = Map.from(walletCoins); + final coins = + walletCoinsCopy.values.where((coin) => coin.isActive).toList(); + + final newBalances = + await Future.wait(coins.map((coin) => tryGetBalanceInfo(coin.abbr))); + + for (int i = 0; i < coins.length; i++) { + final newBalance = newBalances[i].balance.total.toDouble(); + final newSendableBalance = newBalances[i].balance.spendable.toDouble(); + + final balanceChanged = newBalance != coins[i].balance; + final sendableBalanceChanged = + newSendableBalance != coins[i].sendableBalance; + if (balanceChanged || sendableBalanceChanged) { + yield coins[i].copyWith( + balance: newBalance, + sendableBalance: newSendableBalance, + ); + _balancesCache[coins[i].abbr] = + (balance: newBalance, sendableBalance: newSendableBalance); + } + } } - Future sendRawTransaction( - SendRawTransactionRequest request) async { - final response = await _api.sendRawTransaction(request); + Future> withdraw( + WithdrawRequest request, + ) async { + Map? response; + try { + response = await _mm2.call(request) as Map?; + } catch (e, s) { + log( + 'Error withdrawing ${request.params.coin}: $e', + path: 'api => withdraw', + trace: s, + isError: true, + ).ignore(); + } + if (response == null) { - return SendRawTransactionResponse( - txHash: null, + log('Withdraw error: response is null', isError: true).ignore(); + return BlocResponse( error: TextError(error: LocaleKeys.somethingWrong.tr()), ); } - return SendRawTransactionResponse.fromJson(response); + if (response['error'] != null) { + log('Withdraw error: ${response['error']}', isError: true).ignore(); + return BlocResponse( + error: withdrawErrorFactory.getError(response, request.params.coin), + ); + } + + final WithdrawDetails withdrawDetails = WithdrawDetails.fromJson( + response['result'] as Map? ?? {}, + ); + + return BlocResponse( + result: withdrawDetails, + ); } - Future activateCoins(List coins) async { - final List ethWithTokensRequests = []; - final List erc20Requests = []; - final List electrumCoinRequests = []; - final List tendermintRequests = []; - final List tendermintTokenRequests = []; - final List bchWithTokens = []; - final List slpTokens = []; - - for (Coin coin in coins) { - if (coin.type == CoinType.cosmos || coin.type == CoinType.iris) { - if (coin.isIrisToken) { - tendermintTokenRequests - .add(EnableTendermintTokenRequest(ticker: coin.abbr)); - } else { - tendermintRequests.add(EnableTendermintWithAssetsRequest( - ticker: coin.abbr, - rpcUrls: coin.rpcUrls, - )); - } - } else if (coin.type == CoinType.slp) { - slpTokens.add(EnableSlp(ticker: coin.abbr)); - } else if (coin.protocolType == 'BCH') { - bchWithTokens.add(EnableBchWithTokens( - ticker: coin.abbr, servers: coin.electrum, urls: coin.bchdUrls)); - } else if (coin.electrum.isNotEmpty) { - electrumCoinRequests.add(ElectrumReq( - coin: coin.abbr, - servers: coin.electrum, - swapContractAddress: coin.swapContractAddress, - fallbackSwapContract: coin.swapContractAddress, - )); - } else { - if (coin.protocolType == 'ETH') { - ethWithTokensRequests.add(EnableEthWithTokensRequest( - coin: coin.abbr, - swapContractAddress: coin.swapContractAddress, - fallbackSwapContract: coin.fallbackSwapContract, - nodes: coin.nodes, - )); - } else { - erc20Requests.add(EnableErc20Request(ticker: coin.abbr)); + /// This is needed to access the legacy `coin.address` field. The alternative + /// method is to use the [tryGetBalanceInfo] method, which also returns an + /// address field (although idk if it's equivalent at the time of writing) + Future?> _getEnabledCoins(Iterable knownCoins) async { + JsonMap response; + try { + response = await _mm2.call(GetEnabledCoinsReq()); + } catch (e) { + log( + 'Error getting enabled coins: $e', + path: 'api => getEnabledCoins => _call', + isError: true, + ).ignore(); + return null; + } + + dynamic resultJson; + try { + resultJson = response['result']; + } catch (e, s) { + log( + 'Error parsing of enabled coins response: $e', + path: 'api => getEnabledCoins => jsonDecode', + trace: s, + isError: true, + ).ignore(); + return null; + } + + final List list = []; + if (resultJson is List) { + for (final dynamic item in resultJson) { + final enabledCoinItem = item as Map? ?? {}; + final Coin? coin = knownCoins.firstWhereOrNull( + (Coin known) => known.abbr == enabledCoinItem['ticker'], + ); + + if (coin != null) { + coin.address = enabledCoinItem['address'] as String?; + list.add(coin); } } } - await _api.enableCoins( - ethWithTokensRequests: ethWithTokensRequests, - erc20Requests: erc20Requests, - electrumCoinRequests: electrumCoinRequests, - tendermintRequests: tendermintRequests, - tendermintTokenRequests: tendermintTokenRequests, - bchWithTokens: bchWithTokens, - slpTokens: slpTokens, - ); - } - Future convertLegacyAddress(Coin coin, String address) async { - final request = ConvertAddressRequest( - coin: coin.abbr, - from: address, - isErc: coin.isErcType, - ); - return await _api.convertLegacyAddress(request); + return list; } } diff --git a/lib/bloc/coins_bloc/coins_state.dart b/lib/bloc/coins_bloc/coins_state.dart new file mode 100644 index 0000000000..1a97046d3e --- /dev/null +++ b/lib/bloc/coins_bloc/coins_state.dart @@ -0,0 +1,46 @@ +part of 'coins_bloc.dart'; + +final class CoinsState extends Equatable { + const CoinsState({ + required this.coins, + required this.walletCoins, + required this.loginActivationFinished, + }); + + factory CoinsState.initial() => const CoinsState( + coins: {}, + walletCoins: {}, + loginActivationFinished: false, + ); + + final Map coins; + final Map walletCoins; + final bool loginActivationFinished; + + double? getUsdPriceByAmount(String amount, String coinAbbr) { + final Coin? coin = coins[coinAbbr]; + final double? parsedAmount = double.tryParse(amount); + final double? usdPrice = coin?.usdPrice?.price; + + if (coin == null || usdPrice == null || parsedAmount == null) { + return null; + } + return parsedAmount * usdPrice; + } + + CoinsState copyWith({ + Map? coins, + Map? walletCoins, + bool? loginActivationFinished, + }) { + return CoinsState( + coins: coins ?? this.coins, + walletCoins: walletCoins ?? this.walletCoins, + loginActivationFinished: + loginActivationFinished ?? this.loginActivationFinished, + ); + } + + @override + List get props => [coins, walletCoins, loginActivationFinished]; +} diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index 9ce11b82bb..e6f2a9f7c2 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -1,28 +1,26 @@ import 'dart:async'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart' show Bloc, Emitter; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/router/state/wallet_state.dart'; -import 'coins_manager_event.dart'; -import 'coins_manager_state.dart'; +part 'coins_manager_event.dart'; +part 'coins_manager_state.dart'; class CoinsManagerBloc extends Bloc { CoinsManagerBloc({ - required CoinsBloc coinsRepo, + required CoinsRepo coinsRepo, required CoinsManagerAction action, + required CurrentWalletBloc currentWalletBloc, }) : _coinsRepo = coinsRepo, - super( - CoinsManagerState.initial( - action: action, - coins: _getOriginalCoinList(coinsRepo, action), - ), - ) { + _currentWalletBloc = currentWalletBloc, + super(CoinsManagerState.initial(action: action, coins: [])) { on(_onCoinsUpdate); on(_onCoinTypeSelect); on(_onCoinsSwitch); @@ -30,18 +28,10 @@ class CoinsManagerBloc extends Bloc { on(_onSelectAll); on(_onSelectedTypesReset); on(_onSearchUpdate); - - _enabledCoinsListener = _coinsRepo.outWalletCoins - .listen((_) => add(const CoinsManagerCoinsUpdate())); } - final CoinsBloc _coinsRepo; - late StreamSubscription> _enabledCoinsListener; - @override - Future close() { - _enabledCoinsListener.cancel(); - return super.close(); - } + final CoinsRepo _coinsRepo; + final CurrentWalletBloc _currentWalletBloc; List mergeCoinLists(List originalList, List newList) { Map coinMap = {}; @@ -60,14 +50,16 @@ class CoinsManagerBloc extends Bloc { return list; } - void _onCoinsUpdate( + Future _onCoinsUpdate( CoinsManagerCoinsUpdate event, Emitter emit, - ) { + ) async { final List filters = []; List list = mergeCoinLists( - _getOriginalCoinList(_coinsRepo, state.action), state.coins); + await _getOriginalCoinList(_coinsRepo, state.action, _currentWalletBloc), + state.coins, + ); if (state.searchPhrase.isNotEmpty) { filters.add(_filterByPhrase); @@ -104,8 +96,8 @@ class CoinsManagerBloc extends Bloc { emit(state.copyWith(isSwitching: true)); final Future switchingFuture = state.action == CoinsManagerAction.add - ? _coinsRepo.activateCoins(selectedCoins) - : _coinsRepo.deactivateCoins(selectedCoins); + ? _coinsRepo.activateCoinsSync(selectedCoins) + : _coinsRepo.deactivateCoinsSync(selectedCoins); emit(state.copyWith(selectedCoins: [], isSwitching: false)); await switchingFuture; @@ -121,17 +113,17 @@ class CoinsManagerBloc extends Bloc { selectedCoins.remove(coin); if (state.action == CoinsManagerAction.add) { - _coinsRepo.deactivateCoins([event.coin]); + _coinsRepo.deactivateCoinsSync([event.coin]); } else { - _coinsRepo.activateCoins([event.coin]); + _coinsRepo.activateCoinsSync([event.coin]); } } else { selectedCoins.add(coin); if (state.action == CoinsManagerAction.add) { - _coinsRepo.activateCoins([event.coin]); + _coinsRepo.activateCoinsSync([event.coin]); } else { - _coinsRepo.deactivateCoins([event.coin]); + _coinsRepo.deactivateCoinsSync([event.coin]); } } emit(state.copyWith(selectedCoins: selectedCoins)); @@ -196,42 +188,45 @@ class CoinsManagerBloc extends Bloc { } } -List _getOriginalCoinList( - CoinsBloc coinsRepo, +Future> _getOriginalCoinList( + CoinsRepo coinsRepo, CoinsManagerAction action, -) { + CurrentWalletBloc currentWalletBloc, +) async { final WalletType? walletType = currentWalletBloc.wallet?.config.type; if (walletType == null) return []; switch (action) { case CoinsManagerAction.add: - return _getDeactivatedCoins(coinsRepo, walletType); + return await _getDeactivatedCoins(coinsRepo, walletType); case CoinsManagerAction.remove: - return _getActivatedCoins(coinsRepo); + return await _getActivatedCoins(coinsRepo); case CoinsManagerAction.none: return []; } } -List _getActivatedCoins(CoinsBloc coinsRepo) { - return coinsRepo.walletCoins.where((coin) => !coin.isActivating).toList(); +Future> _getActivatedCoins(CoinsRepo coinsRepo) async { + return (await coinsRepo.getEnabledCoins()) + .where((coin) => !coin.isActivating) + .toList(); } -List _getDeactivatedCoins(CoinsBloc coinsRepo, WalletType walletType) { - final Map disabledCoinsMap = Map.from(coinsRepo.knownCoinsMap) - ..removeWhere( - (key, coin) => - coinsRepo.walletCoinsMap.containsKey(key) || coin.isActivating, - ); +Future> _getDeactivatedCoins( + CoinsRepo coinsRepo, + WalletType walletType, +) async { + final Map enabledCoins = await coinsRepo.getEnabledCoinsMap(); + final Map disabledCoins = (coinsRepo.getKnownCoinsMap()) + ..removeWhere((key, coin) => enabledCoins.containsKey(key)); switch (walletType) { case WalletType.iguana: - return disabledCoinsMap.values.toList(); + return disabledCoins.values.toList(); case WalletType.trezor: - return (disabledCoinsMap - ..removeWhere((_, coin) => !coin.hasTrezorSupport)) - .values - .toList(); + final disabledCoinsWithTrezorSupport = + disabledCoins.values.where((coin) => coin.hasTrezorSupport); + return disabledCoinsWithTrezorSupport.toList(); case WalletType.metamask: case WalletType.keplr: return []; diff --git a/lib/bloc/coins_manager/coins_manager_event.dart b/lib/bloc/coins_manager/coins_manager_event.dart index fe9a77225e..dd6dd7275e 100644 --- a/lib/bloc/coins_manager/coins_manager_event.dart +++ b/lib/bloc/coins_manager/coins_manager_event.dart @@ -1,5 +1,4 @@ -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; +part of 'coins_manager_bloc.dart'; abstract class CoinsManagerEvent { const CoinsManagerEvent(); diff --git a/lib/bloc/coins_manager/coins_manager_state.dart b/lib/bloc/coins_manager/coins_manager_state.dart index 104fcbe3ea..3296f85ffc 100644 --- a/lib/bloc/coins_manager/coins_manager_state.dart +++ b/lib/bloc/coins_manager/coins_manager_state.dart @@ -1,7 +1,4 @@ -import 'package:equatable/equatable.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; -import 'package:web_dex/router/state/wallet_state.dart'; +part of 'coins_manager_bloc.dart'; class CoinsManagerState extends Equatable { const CoinsManagerState({ diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index c1833f6aa5..2c073ddc19 100644 --- a/lib/bloc/dex_repository.dart +++ b/lib/bloc/dex_repository.dart @@ -22,12 +22,14 @@ import 'package:web_dex/model/trade_preimage.dart'; import 'package:web_dex/services/mappers/trade_preimage_mappers.dart'; import 'package:web_dex/shared/utils/utils.dart'; -final dexRepository = DexRepository(); - class DexRepository { + DexRepository(this._mm2Api); + + final Mm2Api _mm2Api; + Future sell(SellRequest request) async { try { - final Map response = await mm2Api.sell(request); + final Map response = await _mm2Api.sell(request); return SellResponse.fromJson(response); } catch (e) { return SellResponse(error: TextError.fromString(e.toString())); @@ -46,7 +48,7 @@ class DexRepository { max: max, ); final ApiResponse> response = await mm2Api.getTradePreimage(request); + Map> response = await _mm2Api.getTradePreimage(request); final Map? error = response.error; final TradePreimageResponseResult? result = response.result; @@ -76,7 +78,7 @@ class DexRepository { Future getMaxTakerVolume(String coinAbbr) async { final MaxTakerVolResponse? response = - await mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr)); + await _mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr)); if (response == null) { return null; } @@ -86,7 +88,7 @@ class DexRepository { Future getMinTradingVolume(String coinAbbr) async { final MinTradingVolResponse? response = - await mm2Api.getMinTradingVol(MinTradingVolRequest(coin: coinAbbr)); + await _mm2Api.getMinTradingVol(MinTradingVolRequest(coin: coinAbbr)); if (response == null) { return null; } @@ -101,7 +103,7 @@ class DexRepository { Future getBestOrders(BestOrdersRequest request) async { Map? response; try { - response = await mm2Api.getBestOrders(request); + response = await _mm2Api.getBestOrders(request); } catch (e) { return BestOrders(error: TextError.fromString(e.toString())); } @@ -132,7 +134,7 @@ class DexRepository { Future getSwapStatus(String swapUuid) async { final response = - await mm2Api.getSwapStatus(MySwapStatusReq(uuid: swapUuid)); + await _mm2Api.getSwapStatus(MySwapStatusReq(uuid: swapUuid)); if (response['error'] != null) { throw TextError(error: response['error']); diff --git a/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart b/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart index 0e98c14b3f..6476d3262e 100644 --- a/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart +++ b/lib/bloc/dex_tab_bar/dex_tab_bar_bloc.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; import 'package:web_dex/blocs/trading_entities_bloc.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/model/trading_entities_filter.dart'; @@ -17,29 +17,24 @@ part 'dex_tab_bar_state.dart'; class DexTabBarBloc extends Bloc { DexTabBarBloc( - AuthRepository authRepo, + this._kdfSdk, this._tradingEntitiesBloc, this._tradingBotRepository, ) : super(const DexTabBarState.initial()) { on(_onTabChanged); on(_onFilterChanged); - on(_onStartListening); - on(_onStopListening); + on(_onStartListening); + on(_onStopListening); on(_onMyOrdersUpdated); on(_onSwapsUpdated); on(_onTradeBotOrdersUpdated); - - _authorizationSubscription = authRepo.authMode.listen((event) { - if (event == AuthorizeMode.noLogin) { - add(const TabChanged(0)); - } - }); } final TradingEntitiesBloc _tradingEntitiesBloc; final MarketMakerBotOrderListRepository _tradingBotRepository; + final KomodoDefiSdk _kdfSdk; - StreamSubscription? _authorizationSubscription; + StreamSubscription? _authorizationSubscription; StreamSubscription>? _myOrdersSubscription; StreamSubscription>? _swapsSubscription; StreamSubscription>? _tradeBotOrdersSubscription; @@ -53,12 +48,20 @@ class DexTabBarBloc extends Bloc { return super.close(); } - Future _onStartListening( - StartListening event, + int get tabIndex => state.tabIndex; + + void _onStartListening( + ListenToOrdersRequested event, Emitter emit, - ) async { - _myOrdersSubscription = _tradingEntitiesBloc.outMyOrders.listen((myOrders) { - add(MyOrdersUpdated(myOrders)); + ) { + _authorizationSubscription = _kdfSdk.auth.authStateChanges.listen((event) { + if (event != null) { + add(const TabChanged(0)); + } + }); + + _myOrdersSubscription = _tradingEntitiesBloc.outMyOrders.listen((orders) { + add(MyOrdersUpdated(orders)); }); _swapsSubscription = _tradingEntitiesBloc.outSwaps.listen((swaps) { @@ -73,7 +76,7 @@ class DexTabBarBloc extends Bloc { } Future _onStopListening( - StopListening event, + StopListeningToOrdersRequested event, Emitter emit, ) async { await _myOrdersSubscription?.cancel(); diff --git a/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart b/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart index 85bf68b5c4..7e3680922d 100644 --- a/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart +++ b/lib/bloc/dex_tab_bar/dex_tab_bar_event.dart @@ -24,12 +24,12 @@ class FilterChanged extends DexTabBarEvent { List get props => [tabType, filter]; } -class StartListening extends DexTabBarEvent { - const StartListening(); +class ListenToOrdersRequested extends DexTabBarEvent { + const ListenToOrdersRequested(); } -class StopListening extends DexTabBarEvent { - const StopListening(); +class StopListeningToOrdersRequested extends DexTabBarEvent { + const StopListeningToOrdersRequested(); } class MyOrdersUpdated extends DexTabBarEvent { diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart index ca2b53f509..4bf44852a5 100644 --- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart @@ -5,13 +5,12 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; import 'package:web_dex/bloc/fiat/fiat_repository.dart'; import 'package:web_dex/bloc/fiat/models/models.dart'; import 'package:web_dex/bloc/transformers.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/forms/fiat/currency_input.dart'; import 'package:web_dex/model/forms/fiat/fiat_amount_input.dart'; @@ -23,11 +22,10 @@ part 'fiat_form_state.dart'; class FiatFormBloc extends Bloc { FiatFormBloc({ - FiatRepository? repository, - // TODO: update to respository reference once refactored - CoinsBloc? coinsRepository, - }) : _fiatRepository = repository ?? fiatRepository, - _coinsRepository = coinsRepository ?? coinsBloc, + required FiatRepository repository, + required CoinsRepo coinsRepository, + }) : _fiatRepository = repository, + _coinsRepository = coinsRepository, super(const FiatFormState.initial()) { // all user input fields are debounced using the debounce stream transformer on( @@ -56,7 +54,7 @@ class FiatFormBloc extends Bloc { } final FiatRepository _fiatRepository; - final CoinsBloc _coinsRepository; + final CoinsRepo _coinsRepository; Future _onChangeSelectedFiatCoin( SelectedFiatCurrencyChanged event, @@ -375,8 +373,8 @@ class FiatFormBloc extends Bloc { Emitter emit, ) async { try { - final fiatList = await fiatRepository.getFiatList(); - final coinList = await fiatRepository.getCoinList(); + final fiatList = await _fiatRepository.getFiatList(); + final coinList = await _fiatRepository.getCoinList(); emit(state.copyWith(fiatList: fiatList, coinList: coinList)); } catch (e, s) { log( @@ -403,7 +401,7 @@ class FiatFormBloc extends Bloc { state.orderId, ); - return emit.forEach( + return await emit.forEach( orderStatusStream, onData: (data) { return state.copyWith(fiatOrderStatus: data); @@ -443,9 +441,6 @@ class FiatFormBloc extends Bloc { String? getFormIssue() { // TODO: ? show on the UI and localise? These are currently used as more of // a boolean "is there an error?" rather than "what is the error?" - if (!_coinsRepository.isLoggedIn) { - return 'Please connect your wallet to purchase coins'; - } if (state.paymentMethods.isEmpty) { return 'No payment method for this pair'; } diff --git a/lib/bloc/fiat/fiat_repository.dart b/lib/bloc/fiat/fiat_repository.dart index 63546d3c19..522ae1e7ac 100644 --- a/lib/bloc/fiat/fiat_repository.dart +++ b/lib/bloc/fiat/fiat_repository.dart @@ -1,17 +1,14 @@ import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/bloc/fiat/banxa_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; import 'package:web_dex/bloc/fiat/models/models.dart'; -import 'package:web_dex/bloc/fiat/ramp/ramp_fiat_provider.dart'; import 'package:web_dex/shared/utils/utils.dart'; -final fiatRepository = - FiatRepository([BanxaFiatProvider(), RampFiatProvider()]); - class FiatRepository { - FiatRepository(this.fiatProviders); + FiatRepository(this.fiatProviders, this._coinsRepo); + final List fiatProviders; + final CoinsRepo _coinsRepo; String? _paymentMethodFiat; ICurrency? _paymentMethodsCoin; @@ -56,7 +53,7 @@ class FiatRepository { Set? knownCoinAbbreviations; if (isCoin) { - final knownCoins = await coinsRepo.getKnownCoins(); + final knownCoins = _coinsRepo.getKnownCoins(); knownCoinAbbreviations = knownCoins.map((coin) => coin.abbr).toSet(); } diff --git a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart index a1d93d47bd..e74ad33c44 100644 --- a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart +++ b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart @@ -1,19 +1,19 @@ import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/services/orders_service/my_orders_service.dart'; class MarketMakerBotOrderListRepository { - final CoinsBloc _coinsRepository; const MarketMakerBotOrderListRepository( this._ordersService, this._settingsRepository, this._coinsRepository, ); + final CoinsRepo _coinsRepository; final MyOrdersService _ordersService; final SettingsRepository _settingsRepository; diff --git a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_order_list_bloc.dart b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_order_list_bloc.dart index 81a8e7014b..899e7a14b7 100644 --- a/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_order_list_bloc.dart +++ b/lib/bloc/market_maker_bot/market_maker_order_list/market_maker_order_list_bloc.dart @@ -46,7 +46,7 @@ class MarketMakerOrderListBloc ), ); - return emit.forEach( + return await emit.forEach( Stream.periodic(event.updateInterval) .asyncMap((_) => _orderListRepository.getTradePairs()), onData: (orders) { @@ -169,7 +169,9 @@ List _applyFilters( return false; } if ((shownSides != null && shownSides.isNotEmpty) && - !shownSides.contains(order.order!.orderType)) return false; + !shownSides.contains(order.order!.orderType)) { + return false; + } } return true; 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 1512609ffa..b087d6eb94 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 @@ -4,9 +4,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:formz/formz.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/trade_volume.dart'; @@ -33,11 +33,11 @@ class MarketMakerTradeFormBloc /// The [DexRepository] is used to get the trade preimage, which is used /// to pre-emptively check if a trade will be successful. /// - /// The [CoinsBloc] is used to activate coins that are not active when + /// The [CoinsRepo] is used to activate coins that are not active when /// they are selected in the trade form. MarketMakerTradeFormBloc({ required DexRepository dexRepo, - required CoinsBloc coinsRepo, + required CoinsRepo coinsRepo, }) : _dexRepository = dexRepo, _coinsRepo = coinsRepo, super(MarketMakerTradeFormState.initial()) { @@ -62,7 +62,7 @@ class MarketMakerTradeFormBloc /// The coins repository is used to activate coins that are not active /// when they are selected in the trade form - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; Future _onSellCoinChanged( MarketMakerTradeFormSellCoinChanged event, @@ -508,11 +508,11 @@ class MarketMakerTradeFormBloc } if (!coin.isActive) { - await _coinsRepo.activateCoins([coin]); + await _coinsRepo.activateCoinsSync([coin]); } else { final Coin? parentCoin = coin.parentCoin; if (parentCoin != null && !parentCoin.isActive) { - await _coinsRepo.activateCoins([parentCoin]); + await _coinsRepo.activateCoinsSync([parentCoin]); } } } diff --git a/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart b/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart index eba799889f..b825933425 100644 --- a/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart +++ b/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/nft.dart'; @@ -11,7 +11,7 @@ part 'nft_receive_state.dart'; class NftReceiveBloc extends Bloc { NftReceiveBloc({ - required CoinsBloc coinsRepo, + required CoinsRepo coinsRepo, required CurrentWalletBloc currentWalletBloc, }) : _coinsRepo = coinsRepo, _currentWalletBloc = currentWalletBloc, @@ -21,7 +21,7 @@ class NftReceiveBloc extends Bloc { on(_onChangeAddress); } - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; final CurrentWalletBloc _currentWalletBloc; NftBlockchains? chain; @@ -40,7 +40,8 @@ class NftReceiveBloc extends Bloc { } if (coin.address?.isEmpty ?? true) { - final activationErrors = await activateCoinIfNeeded(coin.abbr); + final activationErrors = + await activateCoinIfNeeded(coin.abbr, _coinsRepo); if (activationErrors.isNotEmpty) { return emit( NftReceiveFailure( diff --git a/lib/bloc/nft_transactions/bloc/nft_transactions_bloc.dart b/lib/bloc/nft_transactions/bloc/nft_transactions_bloc.dart index 8a5ff73ac1..1d3af77a9a 100644 --- a/lib/bloc/nft_transactions/bloc/nft_transactions_bloc.dart +++ b/lib/bloc/nft_transactions/bloc/nft_transactions_bloc.dart @@ -3,14 +3,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/nft_transactions/bloc/nft_transactions_filters.dart'; import 'package:web_dex/bloc/nft_transactions/nft_txn_repository.dart'; import 'package:web_dex/bloc/nfts/nft_main_repo.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/rpc/nft_transaction/nft_transactions_response.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/nft.dart'; import 'package:web_dex/shared/utils/utils.dart' as utils; import 'package:web_dex/views/dex/dex_helpers.dart'; @@ -21,13 +21,12 @@ part 'nft_transactions_state.dart'; class NftTransactionsBloc extends Bloc { NftTransactionsBloc({ required NftTxnRepository nftTxnRepository, - required AuthRepository authRepo, - required CoinsBloc coinsBloc, + required KomodoDefiSdk kdfSdk, + required CoinsRepo coinsRepository, required bool isLoggedIn, required NftsRepo nftsRepository, }) : _nftTxnRepository = nftTxnRepository, - _authRepo = authRepo, - _coinsBloc = coinsBloc, + _coinsBloc = coinsRepository, _nftsRepository = nftsRepository, _isLoggedIn = isLoggedIn, super(NftTransactionsInitial()) { @@ -42,9 +41,9 @@ class NftTransactionsBloc extends Bloc { on(_changeFullFilter); on(_noLogin); - _authorizationSubscription = _authRepo.authMode.listen((event) { + _authorizationSubscription = kdfSdk.auth.authStateChanges.listen((event) { final bool prevLoginState = _isLoggedIn; - _isLoggedIn = event == AuthorizeMode.logIn; + _isLoggedIn = event != null; if (_isLoggedIn && prevLoginState) { if (_isLoggedIn) { @@ -58,12 +57,11 @@ class NftTransactionsBloc extends Bloc { final NftTxnRepository _nftTxnRepository; final NftsRepo _nftsRepository; - final AuthRepository _authRepo; - final CoinsBloc _coinsBloc; + final CoinsRepo _coinsBloc; final List _transactions = []; bool _isLoggedIn = false; - late final StreamSubscription _authorizationSubscription; + late final StreamSubscription _authorizationSubscription; PersistentBottomSheetController? _bottomSheetController; set bottomSheetController(PersistentBottomSheetController controller) => _bottomSheetController = controller; @@ -303,7 +301,7 @@ class NftTransactionsBloc extends Bloc { Future viewNftOnExplorer(NftTransaction transaction) async { final abbr = transaction.chain.coinAbbr(); - final activationErrors = await activateCoinIfNeeded(abbr); + final activationErrors = await activateCoinIfNeeded(abbr, _coinsBloc); var coin = _coinsBloc.getCoin(abbr); if (coin != null) { if (activationErrors.isEmpty) { diff --git a/lib/bloc/nft_transactions/nft_txn_repository.dart b/lib/bloc/nft_transactions/nft_txn_repository.dart index 69895f1f0b..212aff1c7d 100644 --- a/lib/bloc/nft_transactions/nft_txn_repository.dart +++ b/lib/bloc/nft_transactions/nft_txn_repository.dart @@ -93,7 +93,7 @@ class NftTxnRepository { } Future getUsdPricesOfCoins(Iterable coinAbbr) async { - final coins = await _coinsRepo.getKnownCoins(); + final coins = _coinsRepo.getKnownCoins(); for (var abbr in coinAbbr) { final coin = coins.firstWhere((c) => c.abbr == abbr); _abbrToUsdPrices[abbr] = coin.usdPrice?.price; diff --git a/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart b/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart index 9786eacd24..161cef61a0 100644 --- a/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart +++ b/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart @@ -6,9 +6,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/nft_withdraw/nft_withdraw_repo.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/withdraw/withdraw_nft_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_response.dart'; @@ -23,9 +24,11 @@ class NftWithdrawBloc extends Bloc { NftWithdrawBloc({ required NftWithdrawRepo repo, required NftToken nft, - required CoinsBloc coinsBloc, + required Mm2Api mm2Api, + required CoinsRepo coinsRepository, }) : _repo = repo, - _coinsBloc = coinsBloc, + _coinsRepository = coinsRepository, + _mm2Api = mm2Api, super(NftWithdrawFillState.initial(nft)) { on(_onAddressChanged); on(_onAmountChanged); @@ -37,7 +40,8 @@ class NftWithdrawBloc extends Bloc { } final NftWithdrawRepo _repo; - final CoinsBloc _coinsBloc; + final Mm2Api _mm2Api; + final CoinsRepo _coinsRepository; Future _onSend( NftWithdrawSendEvent event, @@ -223,9 +227,12 @@ class NftWithdrawBloc extends Bloc { final state = this.state; if (state is! NftWithdrawFillState) return; - final result = await coinsRepo.convertLegacyAddress( - state.nft.parentCoin, - state.address, + final result = await _mm2Api.convertLegacyAddress( + ConvertAddressRequest( + coin: state.nft.parentCoin.abbr, + from: state.address, + isErc: state.nft.parentCoin.isErcType, + ), ); if (result == null) return; @@ -236,7 +243,7 @@ class NftWithdrawBloc extends Bloc { final parentCoin = state.nft.parentCoin; if (!parentCoin.isActive) { - await _coinsBloc.activateCoins([parentCoin]); + await _coinsRepository.activateCoinsSync([parentCoin]); } } } diff --git a/lib/bloc/nft_withdraw/nft_withdraw_repo.dart b/lib/bloc/nft_withdraw/nft_withdraw_repo.dart index ca8c09089c..38a1142dd5 100644 --- a/lib/bloc/nft_withdraw/nft_withdraw_repo.dart +++ b/lib/bloc/nft_withdraw/nft_withdraw_repo.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/withdraw/withdraw_nft_request.dart'; @@ -16,9 +15,9 @@ import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/utils/utils.dart'; class NftWithdrawRepo { - const NftWithdrawRepo({required Mm2ApiNft api}) : _api = api; + const NftWithdrawRepo({required Mm2Api api}) : _api = api; - final Mm2ApiNft _api; + final Mm2Api _api; Future withdraw({ required NftToken nft, required String address, @@ -32,7 +31,7 @@ class NftWithdrawRepo { tokenId: nft.tokenId, amount: amount, ); - final Map json = await _api.withdraw(request); + final Map json = await _api.nft.withdraw(request); if (json['error'] != null) { log(json['error'] ?? 'unknown error', path: 'nft_main_repo => getNfts', isError: true); @@ -58,7 +57,7 @@ class NftWithdrawRepo { String coin, String txHex) async { try { final request = SendRawTransactionRequest(coin: coin, txHex: txHex); - final response = await coinsRepo.sendRawTransaction(request); + final response = await _api.sendRawTransaction(request); return response; } catch (e) { return SendRawTransactionResponse( @@ -73,7 +72,7 @@ class NftWithdrawRepo { ) async { try { final Map? responseRaw = - await coinsRepo.validateCoinAddress(coin, address); + await _api.validateAddress(coin.abbr, address); if (responseRaw == null) { throw ApiError(message: LocaleKeys.somethingWrong.tr()); } diff --git a/lib/bloc/nfts/nft_main_bloc.dart b/lib/bloc/nfts/nft_main_bloc.dart index d19bc2730a..5f8aa32a22 100644 --- a/lib/bloc/nfts/nft_main_bloc.dart +++ b/lib/bloc/nfts/nft_main_bloc.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/nfts/nft_main_repo.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/nft.dart'; import 'package:web_dex/model/text_error.dart'; @@ -15,7 +15,7 @@ part 'nft_main_state.dart'; class NftMainBloc extends Bloc { NftMainBloc({ required NftsRepo repo, - required AuthRepository authRepo, + required KomodoDefiSdk kdfSdk, required bool isLoggedIn, }) : _repo = repo, _isLoggedIn = isLoggedIn, @@ -27,8 +27,8 @@ class NftMainBloc extends Bloc { on(_onStartUpdate); on(_onStopUpdate); - _authorizationSubscription = authRepo.authMode.listen((event) { - _isLoggedIn = event == AuthorizeMode.logIn; + _authorizationSubscription = kdfSdk.auth.authStateChanges.listen((event) { + _isLoggedIn = event != null; if (_isLoggedIn) { add(const UpdateChainNftsEvent()); @@ -39,7 +39,7 @@ class NftMainBloc extends Bloc { } final NftsRepo _repo; - late StreamSubscription _authorizationSubscription; + late StreamSubscription _authorizationSubscription; Timer? _updateTimer; bool _isLoggedIn = false; bool get isLoggedIn => _isLoggedIn; diff --git a/lib/bloc/nfts/nft_main_repo.dart b/lib/bloc/nfts/nft_main_repo.dart index f2699543fa..bd8fb5f81e 100644 --- a/lib/bloc/nfts/nft_main_repo.dart +++ b/lib/bloc/nfts/nft_main_repo.dart @@ -11,7 +11,11 @@ import 'package:web_dex/shared/utils/utils.dart'; class NftsRepo { NftsRepo({ required Mm2ApiNft api, - }) : _api = api; + required CoinsRepo coinsRepo, + }) : _coinsRepo = coinsRepo, + _api = api; + + final CoinsRepo _coinsRepo; final Mm2ApiNft _api; Future updateNft(List chains) async { @@ -47,7 +51,7 @@ class NftsRepo { try { final response = GetNftListResponse.fromJson(json); final nfts = response.result.nfts; - final coins = await coinsRepo.getKnownCoins(); + final coins = _coinsRepo.getKnownCoins(); for (NftToken nft in nfts) { final coin = coins.firstWhere((c) => c.type == nft.coinType); final parentCoin = coin.parentCoin ?? coin; diff --git a/lib/bloc/runtime_coin_updates/coin_config_bloc.dart b/lib/bloc/runtime_coin_updates/coin_config_bloc.dart deleted file mode 100644 index 09d6a894d0..0000000000 --- a/lib/bloc/runtime_coin_updates/coin_config_bloc.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:komodo_coin_updates/komodo_coin_updates.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/runtime_coin_updates/runtime_update_config_provider.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -part 'coin_config_event.dart'; -part 'coin_config_state.dart'; - -/// A BLoC that manages the coin config state. -/// The BLoC fetches the coin configs from the repository and stores them -/// in the storage provider. -/// The BLoC emits the coin configs to the UI. -class CoinConfigBloc extends Bloc { - CoinConfigBloc({ - required this.coinsConfigRepo, - }) : super(const CoinConfigState()) { - on(_onLoadRequested); - on(_onUpdateRequested); - on(_onPeriodicUpdateRequested); - on(_onUnsubscribeRequested); - } - - /// The repository that fetches the coins and coin configs. - final CoinConfigRepository coinsConfigRepo; - - /// Full, platform-dependent, path to the app folder. - String? _appFolderPath; - Timer? _updateCoinConfigTimer; - final _updateTime = const Duration(hours: 1); - - Future _onLoadRequested( - CoinConfigLoadRequested event, - Emitter emit, - ) async { - String? activeFetchedCommitHash; - - emit(const CoinConfigLoadInProgress()); - - try { - activeFetchedCommitHash = (state is CoinConfigLoadSuccess) - ? (state as CoinConfigLoadSuccess).updatedCommitHash - : await coinsConfigRepo.getCurrentCommit(); - - _appFolderPath ??= await applicationDocumentsDirectory; - await compute(updateCoinConfigs, _appFolderPath!); - } catch (e) { - emit(CoinConfigLoadFailure(error: e.toString())); - log('Failed to update coin config: $e', isError: true); - return; - } - - final List coins = (await coinsConfigRepo.getCoins())!; - emit( - CoinConfigLoadSuccess( - coins: coins, - updatedCommitHash: activeFetchedCommitHash, - ), - ); - } - - String? get stateActiveFetchedCommitHash { - if (state is CoinConfigLoadSuccess) { - return (state as CoinConfigLoadSuccess).updatedCommitHash; - } - return null; - } - - Future _onUpdateRequested( - CoinConfigUpdateRequested event, - Emitter emit, - ) async { - String? currentCommit = stateActiveFetchedCommitHash; - - emit(const CoinConfigLoadInProgress()); - - try { - _appFolderPath ??= await applicationDocumentsDirectory; - await compute(updateCoinConfigs, _appFolderPath!); - } catch (e) { - emit(CoinConfigLoadFailure(error: e.toString())); - log('Failed to update coin config: $e', isError: true); - return; - } - - final List coins = (await coinsConfigRepo.getCoins())!; - emit( - CoinConfigLoadSuccess( - coins: coins, - updatedCommitHash: currentCommit, - ), - ); - } - - Future _onPeriodicUpdateRequested( - CoinConfigUpdateSubscribeRequested event, - Emitter emit, - ) async { - _updateCoinConfigTimer = Timer.periodic(_updateTime, (timer) async { - add(CoinConfigUpdateRequested()); - }); - } - - void _onUnsubscribeRequested( - CoinConfigUpdateUnsubscribeRequested event, - Emitter emit, - ) { - _updateCoinConfigTimer?.cancel(); - _updateCoinConfigTimer = null; - } -} - -Future updateCoinConfigs(String appFolderPath) async { - final RuntimeUpdateConfigProvider runtimeUpdateConfigProvider = - RuntimeUpdateConfigProvider(); - final CoinConfigRepository repo = CoinConfigRepository.withDefaults( - await runtimeUpdateConfigProvider.getRuntimeUpdateConfig(), - ); - // On native platforms, Isolates run in a separate process, so we need to - // ensure that the Hive Box is initialized in the isolate. - if (!kIsWeb) { - final isMainThread = Isolate.current.debugName == 'main'; - if (!isMainThread) { - KomodoCoinUpdater.ensureInitializedIsolate(appFolderPath); - } - } - - final bool isUpdated = await repo.isLatestCommit(); - - Stopwatch stopwatch = Stopwatch()..start(); - - if (!isUpdated) { - await repo.updateCoinConfig( - excludedAssets: excludedAssetList, - ); - } - - log('Coin config updated in ${stopwatch.elapsedMilliseconds}ms'); - stopwatch.stop(); -} diff --git a/lib/bloc/runtime_coin_updates/coin_config_event.dart b/lib/bloc/runtime_coin_updates/coin_config_event.dart deleted file mode 100644 index 1f6e108638..0000000000 --- a/lib/bloc/runtime_coin_updates/coin_config_event.dart +++ /dev/null @@ -1,24 +0,0 @@ -part of 'coin_config_bloc.dart'; - -sealed class CoinConfigEvent extends Equatable { - const CoinConfigEvent(); - - @override - List get props => []; -} - -/// Request for the coin configs to be loaded from disk. -/// Emits [CoinConfigLoadInProgress] followed by [CoinConfigLoadSuccess] or -/// [CoinConfigLoadFailure]. -final class CoinConfigLoadRequested extends CoinConfigEvent {} - -/// Request for the coin configs to be updated from the repository. -/// Emits [CoinConfigLoadInProgress] followed by [CoinConfigLoadSuccess] or -/// [CoinConfigLoadFailure]. -final class CoinConfigUpdateRequested extends CoinConfigEvent {} - -/// Request for periodic updates of the coin configs. -final class CoinConfigUpdateSubscribeRequested extends CoinConfigEvent {} - -/// Request to stop periodic updates of the coin configs. -final class CoinConfigUpdateUnsubscribeRequested extends CoinConfigEvent {} diff --git a/lib/bloc/runtime_coin_updates/coin_config_state.dart b/lib/bloc/runtime_coin_updates/coin_config_state.dart deleted file mode 100644 index b3a9997f3c..0000000000 --- a/lib/bloc/runtime_coin_updates/coin_config_state.dart +++ /dev/null @@ -1,52 +0,0 @@ -part of 'coin_config_bloc.dart'; - -class CoinConfigState extends Equatable { - const CoinConfigState(); - - @override - List get props => []; -} - -class CoinConfigInitial extends CoinConfigState { - const CoinConfigInitial(); - - @override - List get props => []; -} - -/// The coin config is currently being loaded from disk or network. -class CoinConfigLoadInProgress extends CoinConfigState { - const CoinConfigLoadInProgress(); - - @override - List get props => []; -} - -/// The coin config has been successfully loaded. -/// [coins] is a list of [Coin] objects. -class CoinConfigLoadSuccess extends CoinConfigState { - const CoinConfigLoadSuccess({ - required this.coins, - this.updatedCommitHash, - }); - - final List coins; - - final String? updatedCommitHash; - - @override - List get props => [coins, updatedCommitHash]; -} - -/// The coin config failed to load. -/// [error] is the error message. -class CoinConfigLoadFailure extends CoinConfigState { - const CoinConfigLoadFailure({ - required this.error, - }); - - final String error; - - @override - List get props => [error]; -} diff --git a/lib/bloc/runtime_coin_updates/runtime_update_config_provider.dart b/lib/bloc/runtime_coin_updates/runtime_update_config_provider.dart deleted file mode 100644 index fa7b64eca5..0000000000 --- a/lib/bloc/runtime_coin_updates/runtime_update_config_provider.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/services.dart'; -import 'package:komodo_coin_updates/komodo_coin_updates.dart'; - -class RuntimeUpdateConfigProvider { - RuntimeUpdateConfigProvider({ - this.configFilePath = 'app_build/build_config.json', - }); - - final String configFilePath; - - /// Fetches the runtime update config from the repository. - /// Returns a [RuntimeUpdateConfig] object. - /// Throws an [Exception] if the request fails. - Future getRuntimeUpdateConfig() async { - final config = jsonDecode(await rootBundle.loadString(configFilePath)) - as Map; - return RuntimeUpdateConfig.fromJson(config['coins']); - } -} diff --git a/lib/bloc/security_settings/security_settings_bloc.dart b/lib/bloc/security_settings/security_settings_bloc.dart index 07f78a8d5d..b78e5a53a1 100644 --- a/lib/bloc/security_settings/security_settings_bloc.dart +++ b/lib/bloc/security_settings/security_settings_bloc.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; import 'package:web_dex/bloc/security_settings/security_settings_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; class SecuritySettingsBloc extends Bloc { - SecuritySettingsBloc(SecuritySettingsState state) : super(state) { + SecuritySettingsBloc(super.state, this.currentWalletBloc) { on(_onReset); on(_onShowSeed); on(_onSeedConfirm); @@ -17,6 +17,8 @@ class SecuritySettingsBloc on(_onSeedCopied); } + final CurrentWalletBloc currentWalletBloc; + void _onReset( ResetEvent event, Emitter emit, @@ -35,14 +37,18 @@ class SecuritySettingsBloc emit(newState); } - void _onShowSeedWords( + Future _onShowSeedWords( ShowSeedWordsEvent event, Emitter emit, - ) { + ) async { + final isSeedSaved = state.isSeedSaved || event.isShow; + if (isSeedSaved) { + await currentWalletBloc.confirmBackup(); + } final newState = state.copyWith( step: SecuritySettingsStep.seedShow, showSeedWords: event.isShow, - isSeedSaved: state.isSeedSaved || event.isShow, + isSeedSaved: isSeedSaved, ); emit(newState); } @@ -51,11 +57,13 @@ class SecuritySettingsBloc PasswordUpdateEvent event, Emitter emit, ) { - final newState = state.copyWith( - step: SecuritySettingsStep.passwordUpdate, - showSeedWords: false, - ); - emit(newState); + // TODO!: re-enable once password change is implemented + // final newState = state.copyWith( + // step: SecuritySettingsStep.passwordUpdate, + // showSeedWords: false, + // ); + // emit(newState); + emit(state); } void _onSeedConfirm( @@ -81,8 +89,11 @@ class SecuritySettingsBloc emit(newState); } - FutureOr _onSeedCopied( - ShowSeedCopiedEvent event, Emitter emit) { + Future _onSeedCopied( + ShowSeedCopiedEvent event, + Emitter emit, + ) async { + await currentWalletBloc.confirmBackup(); emit(state.copyWith(isSeedSaved: true)); } } diff --git a/lib/bloc/security_settings/security_settings_state.dart b/lib/bloc/security_settings/security_settings_state.dart index 949d4e4751..d9faec57a8 100644 --- a/lib/bloc/security_settings/security_settings_state.dart +++ b/lib/bloc/security_settings/security_settings_state.dart @@ -5,7 +5,7 @@ enum SecuritySettingsStep { seedShow, seedConfirm, seedSuccess, - passwordUpdate, + // passwordUpdate, } class SecuritySettingsState extends Equatable { diff --git a/lib/bloc/system_health/system_health_bloc.dart b/lib/bloc/system_health/system_health_bloc.dart index 9736932f3c..7635960f57 100644 --- a/lib/bloc/system_health/system_health_bloc.dart +++ b/lib/bloc/system_health/system_health_bloc.dart @@ -19,9 +19,7 @@ class SystemHealthBloc extends Bloc { final Mm2Api _api; void _startPeriodicCheck() { - add(CheckSystemClock()); - - _timer = Timer.periodic(const Duration(seconds: 30), (timer) { + _timer = Timer.periodic(const Duration(seconds: 60), (timer) { add(CheckSystemClock()); }); } @@ -50,17 +48,8 @@ class SystemHealthBloc extends Bloc { final connectedPeersHealthy = directlyConnectedPeers.peers.length >= 2; return connectedPeersHealthy; } on Exception catch (_) { - // TODO: remove once the breaking rpc name change is in main - try { - final directlyConnectedPeers = await _api.getDirectlyConnectedPeers( - GetDirectlyConnectedPeers(method: 'get_peers_info'), - ); - final connectedPeersHealthy = directlyConnectedPeers.peers.length >= 2; - return connectedPeersHealthy; - } catch (_) { - // fall through and return false - } - + // do not prevent usage if no peers are connected + // mm2 api is responsible for logging, so only return result here return false; } } diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index 97f8159d89..0fcf7a4bef 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -1,21 +1,21 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; import 'package:web_dex/bloc/taker_form/taker_validator.dart'; import 'package:web_dex/bloc/transformers.dart'; -import 'package:web_dex/blocs/coins_bloc.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/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_response.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/available_balance_state.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/data_from_service.dart'; @@ -28,8 +28,8 @@ import 'package:web_dex/views/dex/dex_helpers.dart'; class TakerBloc extends Bloc { TakerBloc({ required DexRepository dexRepository, - required CoinsBloc coinsRepository, - required AuthRepository authRepo, + required CoinsRepo coinsRepository, + required KomodoDefiSdk kdfSdk, }) : _dexRepo = dexRepository, _coinsRepo = coinsRepository, super(TakerState.initial()) { @@ -65,12 +65,12 @@ class TakerBloc extends Bloc { on(_onVerifyOrderVolume); on(_onSetWalletReady); - _authorizationSubscription = authRepo.authMode.listen((event) { - if (event == AuthorizeMode.noLogin && state.step == TakerStep.confirm) { + _authorizationSubscription = kdfSdk.auth.authStateChanges.listen((event) { + if (event != null && state.step == TakerStep.confirm) { add(TakerBackButtonClick()); } final bool prevLoginState = _isLoggedIn; - _isLoggedIn = event == AuthorizeMode.logIn; + _isLoggedIn = event != null; if (prevLoginState != _isLoggedIn) { add(const TakerUpdateMaxSellAmount(true)); @@ -80,13 +80,13 @@ class TakerBloc extends Bloc { } final DexRepository _dexRepo; - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; Timer? _maxSellAmountTimer; bool _activatingAssets = false; bool _waitingForWallet = true; bool _isLoggedIn = false; late TakerValidator _validator; - late StreamSubscription _authorizationSubscription; + late StreamSubscription _authorizationSubscription; Future _onStartSwap( TakerStartSwap event, Emitter emit) async { @@ -493,7 +493,7 @@ class TakerBloc extends Bloc { _activatingAssets = true; final List activationErrors = - await activateCoinIfNeeded(abbr); + await activateCoinIfNeeded(abbr, _coinsRepo); _activatingAssets = false; if (activationErrors.isNotEmpty) { diff --git a/lib/bloc/taker_form/taker_validator.dart b/lib/bloc/taker_form/taker_validator.dart index 62673392b5..c5b1eeb445 100644 --- a/lib/bloc/taker_form/taker_validator.dart +++ b/lib/bloc/taker_form/taker_validator.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; 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'; @@ -22,7 +22,7 @@ import 'package:web_dex/views/dex/simple/form/error_list/dex_form_error_with_act class TakerValidator { TakerValidator({ required TakerBloc bloc, - required CoinsBloc coinsRepo, + required CoinsRepo coinsRepo, required DexRepository dexRepo, }) : _bloc = bloc, _coinsRepo = coinsRepo, @@ -30,17 +30,17 @@ class TakerValidator { add = bloc.add; final TakerBloc _bloc; - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; final DexRepository _dexRepo; final Function(TakerEvent) add; TakerState get state => _bloc.state; Future validate() async { - final bool isFormValid = validateForm(); + final bool isFormValid = await validateForm(); if (!isFormValid) return false; - final bool tradingWithSelf = _checkTradeWithSelf(); + final bool tradingWithSelf = await _checkTradeWithSelf(); if (tradingWithSelf) return false; final bool isPreimageValid = await _validatePreimage(); @@ -96,7 +96,7 @@ class TakerValidator { return null; } - bool validateForm() { + Future validateForm() async { add(TakerClearErrors()); if (!_isSellCoinSelected) { @@ -109,8 +109,8 @@ class TakerValidator { return false; } - if (!_validateCoinAndParent(state.sellCoin!.abbr)) return false; - if (!_validateCoinAndParent(state.selectedOrder!.coin)) return false; + if (!await _validateCoinAndParent(state.sellCoin!.abbr)) return false; + if (!await _validateCoinAndParent(state.selectedOrder!.coin)) return false; if (!_validateAmount()) return false; @@ -124,14 +124,14 @@ class TakerValidator { return true; } - bool _checkTradeWithSelf() { + Future _checkTradeWithSelf() async { add(TakerClearErrors()); if (state.selectedOrder == null) return false; final BestOrder selectedOrder = state.selectedOrder!; final selectedOrderAddress = selectedOrder.address; - final coin = _coinsRepo.getCoin(selectedOrder.coin); + final coin = await _coinsRepo.getEnabledCoin(selectedOrder.coin); final ownAddress = coin?.address; if (selectedOrderAddress.addressData == ownAddress) { @@ -209,8 +209,8 @@ class TakerValidator { return true; } - bool _validateCoinAndParent(String abbr) { - final Coin? coin = _coinsRepo.getKnownCoin(abbr); + Future _validateCoinAndParent(String abbr) async { + final Coin? coin = await _coinsRepo.getEnabledCoin(abbr); if (coin == null) { add(TakerAddError(_unknownCoinError(abbr))); diff --git a/lib/bloc/transaction_history/transaction_history_bloc.dart b/lib/bloc/transaction_history/transaction_history_bloc.dart index 33899e8e95..eff4d271f0 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -6,11 +6,8 @@ import 'package:web_dex/bloc/transaction_history/transaction_history_event.dart' import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_state.dart'; 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/my_tx_history/my_tx_history_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/data_from_service.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -19,9 +16,10 @@ class TransactionHistoryBloc TransactionHistoryBloc({ required TransactionHistoryRepo repo, }) : _repo = repo, - super(TransactionHistoryInitialState()) { + super(const TransactionHistoryState.initial()) { on(_onSubscribe); on(_onUnsubscribe); + on(_onStartedLoading); on(_onUpdated); on(_onFailure); } @@ -37,7 +35,7 @@ class TransactionHistoryBloc if (!hasTxHistorySupport(event.coin)) { return; } - emit(TransactionHistoryInitialState()); + emit(const TransactionHistoryState.initial()); await _update(event.coin); _stopTimers(); _updateTransactionsTimer = Timer.periodic(_updateTime, (_) async { @@ -56,33 +54,55 @@ class TransactionHistoryBloc TransactionHistoryUpdated event, Emitter emit, ) { - if (event.isInProgress) { - emit(TransactionHistoryInProgressState(transactions: event.transactions)); - return; - } - emit(TransactionHistoryLoadedState(transactions: event.transactions)); + emit(state.copyWith( + transactions: event.transactions, + loading: false, + )); + } + + void _onStartedLoading( + TransactionHistoryStartedLoading event, + Emitter emit, + ) { + emit(state.copyWith(loading: true)); } void _onFailure( TransactionHistoryFailure event, Emitter emit, ) { - emit(TransactionHistoryFailureState(error: event.error)); + emit(state.copyWith( + loading: false, + error: event.error, + )); } Future _update(Coin coin) async { - final DataFromService - transactionsResponse = await _repo.fetch(coin); if (isClosed) { return; } - final TransactionHistoryResponseResult? result = transactionsResponse.data; - final BaseError? responseError = transactionsResponse.error; - if (responseError != null) { - add(TransactionHistoryFailure(error: responseError)); - return; - } else if (result == null) { + try { + add(const TransactionHistoryStartedLoading()); + final transactions = await _repo.fetch(coin); + if (isClosed) { + return; + } + + if (transactions == null) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + return; + } + + transactions.sort(_sortTransactions); + _flagTransactions(transactions, coin); + + add(TransactionHistoryUpdated(transactions: transactions)); + } catch (e) { add( TransactionHistoryFailure( error: TextError(error: LocaleKeys.somethingWrong.tr()), @@ -90,17 +110,6 @@ class TransactionHistoryBloc ); return; } - - final List transactions = List.from(result.transactions); - transactions.sort(_sortTransactions); - _flagTransactions(transactions, coin); - - add( - TransactionHistoryUpdated( - transactions: transactions, - isInProgress: result.syncStatus.state == SyncStatusState.inProgress, - ), - ); } @override @@ -117,9 +126,9 @@ class TransactionHistoryBloc } int _sortTransactions(Transaction tx1, Transaction tx2) { - if (tx2.timestamp == 0) { + if (tx2.timestamp == DateTime.fromMillisecondsSinceEpoch(0)) { return 1; - } else if (tx1.timestamp == 0) { + } else if (tx1.timestamp == DateTime.fromMillisecondsSinceEpoch(0)) { return -1; } return tx2.timestamp.compareTo(tx1.timestamp); @@ -133,7 +142,7 @@ void _flagTransactions(List transactions, Coin coin) { if (!coin.isErcType) return; for (final Transaction tx in List.from(transactions)) { - if (double.tryParse(tx.totalAmount) == 0.0) { + if (tx.balanceChanges.totalAmount.toDouble() == 0.0) { transactions.remove(tx); } } diff --git a/lib/bloc/transaction_history/transaction_history_event.dart b/lib/bloc/transaction_history/transaction_history_event.dart index 921c1d2a02..fd6ccb05bf 100644 --- a/lib/bloc/transaction_history/transaction_history_event.dart +++ b/lib/bloc/transaction_history/transaction_history_event.dart @@ -1,6 +1,6 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:komodo_defi_types/types.dart'; abstract class TransactionHistoryEvent { const TransactionHistoryEvent(); @@ -17,12 +17,12 @@ class TransactionHistoryUnsubscribe extends TransactionHistoryEvent { } class TransactionHistoryUpdated extends TransactionHistoryEvent { - const TransactionHistoryUpdated({ - required this.transactions, - required this.isInProgress, - }); - final List transactions; - final bool isInProgress; + const TransactionHistoryUpdated({required this.transactions}); + final List? transactions; +} + +class TransactionHistoryStartedLoading extends TransactionHistoryEvent { + const TransactionHistoryStartedLoading(); } class TransactionHistoryFailure extends TransactionHistoryEvent { diff --git a/lib/bloc/transaction_history/transaction_history_repo.dart b/lib/bloc/transaction_history/transaction_history_repo.dart index 9c951a124d..3bc8ec7ba7 100644 --- a/lib/bloc/transaction_history/transaction_history_repo.dart +++ b/lib/bloc/transaction_history/transaction_history_repo.dart @@ -1,193 +1,46 @@ import 'dart:async'; -import 'dart:convert'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:http/http.dart' show Client, Response; -import 'package:http/http.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/my_tx_history_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/my_tx_history_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/my_tx_history_v2_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; -import 'package:web_dex/model/data_from_service.dart'; -import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/utils.dart'; class TransactionHistoryRepo { - TransactionHistoryRepo({required Mm2Api api, required Client client}) - : _api = api, - _client = client; - final Mm2Api _api; - final Client _client; + TransactionHistoryRepo({ + KomodoDefiSdk? sdk, + }) : _sdk = sdk; + final KomodoDefiSdk? _sdk; - Future> fetch( - Coin coin) async { - if (_checkV2RequestSupport(coin)) { - return await fetchTransactionHistoryV2(MyTxHistoryV2Request( - coin: coin.abbr, - type: coin.enabledType ?? WalletType.iguana, - )); - } - return coin.isErcType - ? await fetchErcTransactionHistory(coin) - : await fetchTransactionHistory( - MyTxHistoryRequest( - coin: coin.abbr, - max: true, - ), - ); - } - - Future> fetchTransactions(Coin coin) async { - final historyResponse = await fetch(coin); - final TransactionHistoryResponseResult? result = historyResponse.data; + Future?> fetch(Coin coin) async { + final asset = getSdkAsset(_sdk, coin.abbr); - final BaseError? responseError = historyResponse.error; - // TODO: add custom exceptions here? - if (responseError != null) { - throw TransactionFetchException('Transaction fetch error: ${responseError.message}'); - } else if (result == null) { - throw TransactionFetchException('Transaction fetch result is null'); + try { + final transactionHistory = await _sdk?.transactions.getTransactionHistory( + asset, + pagination: const PagePagination( + pageNumber: 1, + itemsPerPage: 200, + ), + ); + return transactionHistory?.transactions; + } catch (e) { + return null; } - - return result.transactions; } /// Fetches transactions for the provided [coin] where the transaction /// timestamp is not 0 (transaction is completed and/or confirmed). Future> fetchCompletedTransactions(Coin coin) async { - final List transactions = await fetchTransactions(coin) + final List transactions = (await fetch(coin) ?? []) ..sort( (a, b) => a.timestamp.compareTo(b.timestamp), + ) + ..removeWhere( + (transaction) => + transaction.timestamp == DateTime.fromMillisecondsSinceEpoch(0), ); - transactions.removeWhere((transaction) => transaction.timestamp <= 0); return transactions; } - - Future> - fetchTransactionHistoryV2(MyTxHistoryV2Request request) async { - final Map? response = - await _api.getTransactionsHistoryV2(request); - if (response == null) { - return DataFromService( - data: null, - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ); - } - - if (response['error'] != null) { - log(response['error'], - path: 'transaction_history_service => fetchTransactionHistoryV2', - isError: true); - return DataFromService( - data: null, - error: TextError(error: response['error']), - ); - } - - final MyTxHistoryResponse transactionHistory = - MyTxHistoryResponse.fromJson(response); - - return DataFromService( - data: transactionHistory.result, - ); - } - - Future> - fetchTransactionHistory(MyTxHistoryRequest request) async { - final Map? response = - await _api.getTransactionsHistory(request); - if (response == null) { - return DataFromService( - data: null, - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ); - } - - if (response['error'] != null) { - log(response['error'], - path: 'transaction_history_service => fetchTransactionHistory', - isError: true); - return DataFromService( - data: null, - error: TextError(error: response['error']), - ); - } - - final MyTxHistoryResponse transactionHistory = - MyTxHistoryResponse.fromJson(response); - - return DataFromService( - data: transactionHistory.result, - ); - } - - Future> - fetchErcTransactionHistory(Coin coin) async { - final String? url = getErcTransactionHistoryUrl(coin); - if (url == null) { - return DataFromService( - data: null, - error: TextError( - error: LocaleKeys.txHistoryFetchError.tr(args: [coin.typeName]), - ), - ); - } - - try { - final Response response = await _client.get(Uri.parse(url)); - final String body = response.body; - final String result = - body.isNotEmpty ? body : '{"result": {"transactions": []}}'; - final MyTxHistoryResponse transactionHistory = - MyTxHistoryResponse.fromJson(json.decode(result)); - - return DataFromService( - data: _fixTestCoinsNaming(transactionHistory.result, coin), - error: null); - } catch (e, s) { - final String errorString = e.toString(); - log(errorString, - path: 'transaction_history_service => fetchErcTransactionHistory', - trace: s, - isError: true); - return DataFromService( - data: null, - error: TextError( - error: errorString, - ), - ); - } - } - - TransactionHistoryResponseResult _fixTestCoinsNaming( - TransactionHistoryResponseResult result, - Coin originalCoin, - ) { - if (!originalCoin.isTestCoin) return result; - - final String? parentCoin = originalCoin.protocolData?.platform; - final String feeCoin = parentCoin ?? originalCoin.abbr; - - for (Transaction tx in result.transactions) { - tx.coin = originalCoin.abbr; - tx.feeDetails.coin = feeCoin; - } - - return result; - } - - bool _checkV2RequestSupport(Coin coin) => - coin.enabledType == WalletType.trezor || - coin.protocolType == 'BCH' || - coin.type == CoinType.slp || - coin.type == CoinType.iris || - coin.type == CoinType.cosmos; } class TransactionFetchException implements Exception { diff --git a/lib/bloc/transaction_history/transaction_history_state.dart b/lib/bloc/transaction_history/transaction_history_state.dart index 11e3a68777..0db2ff8488 100644 --- a/lib/bloc/transaction_history/transaction_history_state.dart +++ b/lib/bloc/transaction_history/transaction_history_state.dart @@ -1,34 +1,35 @@ import 'package:equatable/equatable.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; -abstract class TransactionHistoryState extends Equatable {} +final class TransactionHistoryState extends Equatable { + const TransactionHistoryState({ + required this.transactions, + required this.loading, + required this.error, + }); -class TransactionHistoryInitialState extends TransactionHistoryState { - @override - List get props => []; -} - -class TransactionHistoryInProgressState extends TransactionHistoryState { - TransactionHistoryInProgressState({required this.transactions}); - final List transactions; - - @override - List get props => [transactions]; -} - -class TransactionHistoryLoadedState extends TransactionHistoryState { - TransactionHistoryLoadedState({required this.transactions}); final List transactions; + final bool loading; + final BaseError? error; @override - List get props => [transactions]; -} - -class TransactionHistoryFailureState extends TransactionHistoryState { - TransactionHistoryFailureState({required this.error}); - final BaseError error; - - @override - List get props => [error]; + List get props => [transactions, loading]; + + const TransactionHistoryState.initial() + : transactions = const [], + loading = false, + error = null; + + TransactionHistoryState copyWith({ + List? transactions, + bool? loading, + BaseError? error, + }) { + return TransactionHistoryState( + transactions: transactions ?? this.transactions, + loading: loading ?? this.loading, + error: error ?? this.error, + ); + } } diff --git a/lib/bloc/trezor_bloc/trezor_repo.dart b/lib/bloc/trezor_bloc/trezor_repo.dart index 3fb742c3f3..d875eadaac 100644 --- a/lib/bloc/trezor_bloc/trezor_repo.dart +++ b/lib/bloc/trezor_bloc/trezor_repo.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_response.dart'; @@ -24,15 +25,22 @@ import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw_cancel/t import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw_status/trezor_withdraw_status_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw_status/trezor_withdraw_status_response.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/hd_account/hd_account.dart'; import 'package:web_dex/model/hw_wallet/trezor_connection_status.dart'; +import 'package:web_dex/model/hw_wallet/trezor_status.dart'; import 'package:web_dex/model/hw_wallet/trezor_task.dart'; +import 'package:web_dex/model/wallet.dart'; +import 'package:web_dex/shared/utils/utils.dart'; class TrezorRepo { TrezorRepo({ required Mm2ApiTrezor api, - }) : _api = api; + required KomodoDefiSdk kdfSdk, + }) : _api = api, + _kdfSdk = kdfSdk; final Mm2ApiTrezor _api; + final KomodoDefiSdk _kdfSdk; Future init() async { return await _api.init(InitTrezorReq()); @@ -74,8 +82,8 @@ class TrezorRepo { return await _api.balanceStatus(TrezorBalanceStatusRequest(taskId: taskId)); } - Future enableUtxo(Coin coin) async { - return await _api.enableUtxo(TrezorEnableUtxoReq(coin: coin)); + Future enableUtxo(Asset asset) async { + return await _api.enableUtxo(TrezorEnableUtxoReq(coin: asset)); } Future getEnableUtxoStatus(int taskId) async { @@ -87,10 +95,6 @@ class TrezorRepo { return await _api.initNewAddress(coin); } - Future getNewAddressStatus(int taskId) async { - return await _api.getNewAddressStatus(taskId); - } - Future cancelGetNewAddress(int taskId) async { await _api.cancelGetNewAddress(taskId); } @@ -126,6 +130,49 @@ class TrezorRepo { _connectionStatusTimer?.cancel(); _connectionStatusTimer = null; } -} -final TrezorRepo trezorRepo = TrezorRepo(api: mm2Api.trezor); + Future isTrezorWallet() async { + final currentWallet = await _kdfSdk.currentWallet(); + return currentWallet?.config.type == WalletType.trezor; + } + + Future?> getAccounts(Coin coin) async { + final TrezorBalanceInitResponse initResponse = + await _api.balanceInit(TrezorBalanceInitRequest(coin: coin)); + final int? taskId = initResponse.result?.taskId; + if (taskId == null) return null; + + final int started = nowMs; + // todo(yurii): change timeout to some reasonable value (10000?) + while (nowMs - started < 100000) { + final statusResponse = + await _api.balanceStatus(TrezorBalanceStatusRequest(taskId: taskId)); + final InitTrezorStatus? status = statusResponse.result?.status; + + if (status == InitTrezorStatus.error) return null; + + if (status == InitTrezorStatus.ok) { + return statusResponse.result?.balanceDetails?.accounts; + } + + await Future.delayed(const Duration(milliseconds: 500)); + } + + return null; + } + + Future getNewAddressStatus( + int taskId, + Coin coin, + ) async { + final GetNewAddressResponse response = + await _api.getNewAddressStatus(taskId); + final GetNewAddressStatus? status = response.result?.status; + final GetNewAddressResultDetails? details = response.result?.details; + if (status == GetNewAddressStatus.ok && + details is GetNewAddressResultOkDetails) { + coin.accounts = await getAccounts(coin); + } + return response; + } +} diff --git a/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart b/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart index 7706ed4fab..cfb0366947 100644 --- a/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart +++ b/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; import 'package:web_dex/bloc/trezor_connection_bloc/trezor_connection_event.dart'; import 'package:web_dex/bloc/trezor_connection_bloc/trezor_connection_state.dart'; import 'package:web_dex/blocs/current_wallet_bloc.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/hw_wallet/trezor_connection_status.dart'; import 'package:web_dex/model/wallet.dart'; @@ -15,14 +14,15 @@ class TrezorConnectionBloc extends Bloc { TrezorConnectionBloc({ required TrezorRepo trezorRepo, - required AuthRepository authRepo, + required KomodoDefiSdk kdfSdk, required CurrentWalletBloc walletRepo, - }) : _authRepo = authRepo, + }) : _kdfSdk = kdfSdk, _walletRepo = walletRepo, + _trezorRepo = trezorRepo, super(TrezorConnectionState.initial()) { _trezorConnectionStatusListener = trezorRepo.connectionStatusStream .listen(_onTrezorConnectionStatusChanged); - _authModeListener = _authRepo.authMode.listen(_onAuthModeChanged); + _authModeListener = kdfSdk.auth.authStateChanges.listen(_onAuthModeChanged); on(_onConnectionStatusChange); } @@ -31,26 +31,27 @@ class TrezorConnectionBloc add(TrezorConnectionStatusChange(status: status)); } - void _onAuthModeChanged(AuthorizeMode mode) { - if (mode == AuthorizeMode.logIn) { + void _onAuthModeChanged(KdfUser? user) { + if (user != null) { final Wallet? currentWallet = _walletRepo.wallet; if (currentWallet == null) return; if (currentWallet.config.type != WalletType.trezor) return; - final String? pubKey = currentWallet.config.pubKey; + final String? pubKey = user.walletId.pubkeyHash; if (pubKey == null) return; - trezorRepo.subscribeOnConnectionStatus(pubKey); + _trezorRepo.subscribeOnConnectionStatus(pubKey); } else { - trezorRepo.unsubscribeFromConnectionStatus(); + _trezorRepo.unsubscribeFromConnectionStatus(); } } - final AuthRepository _authRepo; + final KomodoDefiSdk _kdfSdk; + final TrezorRepo _trezorRepo; final CurrentWalletBloc _walletRepo; late StreamSubscription _trezorConnectionStatusListener; - late StreamSubscription _authModeListener; + late StreamSubscription _authModeListener; Future _onConnectionStatusChange(TrezorConnectionStatusChange event, Emitter emit) async { @@ -59,8 +60,7 @@ class TrezorConnectionBloc switch (status) { case TrezorConnectionStatus.unreachable: - if (await mm2.isSignedIn()) await authRepo.logOut(); - await _authRepo.logIn(AuthorizeMode.noLogin); + await _kdfSdk.auth.signOut(); return; case TrezorConnectionStatus.unknown: case TrezorConnectionStatus.connected: diff --git a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart index 0883c8907d..1600f93f2e 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart @@ -1,34 +1,34 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_event.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/init/init_trezor/init_trezor_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/init/init_trezor_status/init_trezor_status_response.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/hw_wallet/init_trezor.dart'; import 'package:web_dex/model/hw_wallet/trezor_status.dart'; import 'package:web_dex/model/hw_wallet/trezor_status_error.dart'; import 'package:web_dex/model/hw_wallet/trezor_task.dart'; -import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/shared/utils/utils.dart'; +part 'trezor_init_event.dart'; +part 'trezor_init_state.dart'; + class TrezorInitBloc extends Bloc { TrezorInitBloc({ - required AuthRepository authRepo, + required KomodoDefiSdk kdfSdk, required TrezorRepo trezorRepo, + required CoinsRepo coinsRepository, }) : _trezorRepo = trezorRepo, - _authRepo = authRepo, + _kdfSdk = kdfSdk, + _coinsRepository = coinsRepository, super(TrezorInitState.initial()) { on(_onSubscribeStatus); on(_onInit); @@ -39,14 +39,15 @@ class TrezorInitBloc extends Bloc { on(_onSendPassphrase); on(_onAuthModeChange); - _authorizationSubscription = _authRepo.authMode.listen((event) { - add(TrezorInitUpdateAuthMode(event)); + _authorizationSubscription = _kdfSdk.auth.authStateChanges.listen((user) { + add(TrezorInitUpdateAuthMode(user)); }); } - late StreamSubscription _authorizationSubscription; + late StreamSubscription _authorizationSubscription; final TrezorRepo _trezorRepo; - final AuthRepository _authRepo; + final KomodoDefiSdk _kdfSdk; + final CoinsRepo _coinsRepository; Timer? _statusTimer; void _unsubscribeStatus() { @@ -54,19 +55,41 @@ class TrezorInitBloc extends Bloc { _statusTimer = null; } - void _checkAndHandleSuccess(InitTrezorStatusData status) { + bool _checkAndHandleSuccess(InitTrezorStatusData status) { final InitTrezorStatus trezorStatus = status.trezorStatus; final TrezorDeviceDetails? deviceDetails = status.details.deviceDetails; if (trezorStatus == InitTrezorStatus.ok && deviceDetails != null) { - add(TrezorInitSuccess(deviceDetails)); + add(TrezorInitSuccess(status)); + return true; } + + return false; } Future _onInit(TrezorInit event, Emitter emit) async { if (state.inProgress) return; emit(state.copyWith(inProgress: () => true)); - await _loginToHiddenMode(); + try { + // device status isn't available until after trezor init completes, but + // requires kdf to be running with a seed value. + // Alternative is to use a static 'hidden-login' to init trezor, then logout + // and log back in to another account using the obtained trezor device + // details + await _loginToTrezorWallet(); + } catch (e, s) { + log( + 'Failed to login to hidden mode: $e', + path: 'trezor_init_bloc => _loginToHiddenMode', + isError: true, + trace: s, + ).ignore(); + emit(state.copyWith( + error: () => TextError(error: LocaleKeys.somethingWrong.tr()), + inProgress: () => false, + )); + return; + } final InitTrezorRes response = await _trezorRepo.init(); final String? responseError = response.error; @@ -77,7 +100,7 @@ class TrezorInitBloc extends Bloc { error: () => TextError(error: responseError), inProgress: () => false, )); - await _logoutFromHiddenMode(); + await _logout(); return; } if (responseResult == null) { @@ -85,7 +108,7 @@ class TrezorInitBloc extends Bloc { error: () => TextError(error: LocaleKeys.somethingWrong.tr()), inProgress: () => false, )); - await _logoutFromHiddenMode(); + await _logout(); return; } @@ -97,15 +120,18 @@ class TrezorInitBloc extends Bloc { } Future _onSubscribeStatus( - TrezorInitSubscribeStatus event, Emitter emit) async { - add(const TrezorInitUpdateStatus()); + TrezorInitSubscribeStatus event, + Emitter emit, + ) async { _statusTimer = Timer.periodic(const Duration(milliseconds: 1000), (timer) { add(const TrezorInitUpdateStatus()); }); } FutureOr _onUpdateStatus( - TrezorInitUpdateStatus event, Emitter emit) async { + TrezorInitUpdateStatus event, + Emitter emit, + ) async { final int? taskId = state.taskId; if (taskId == null) return; @@ -114,7 +140,7 @@ class TrezorInitBloc extends Bloc { if (response.errorType == 'NoSuchTask') { _unsubscribeStatus(); emit(state.copyWith(taskId: () => null)); - await _logoutFromHiddenMode(); + await _logout(); return; } @@ -122,7 +148,7 @@ class TrezorInitBloc extends Bloc { if (responseError != null) { emit(state.copyWith(error: () => TextError(error: responseError))); - await _logoutFromHiddenMode(); + await _logout(); return; } @@ -132,49 +158,49 @@ class TrezorInitBloc extends Bloc { error: () => TextError(error: 'Something went wrong. Empty init status.'))); - await _logoutFromHiddenMode(); + await _logout(); return; } - _checkAndHandleSuccess(initTrezorStatus); + if (!_checkAndHandleSuccess(initTrezorStatus)) { + emit(state.copyWith(status: () => initTrezorStatus)); + } + if (_checkAndHandleInvalidPin(initTrezorStatus)) { emit(state.copyWith(taskId: () => null)); _unsubscribeStatus(); } - - emit(state.copyWith(status: () => initTrezorStatus)); } Future _onInitSuccess( - TrezorInitSuccess event, Emitter emit) async { + TrezorInitSuccess event, + Emitter emit, + ) async { _unsubscribeStatus(); - final deviceDetails = event.details; + final deviceDetails = event.status.details.deviceDetails!; - final String name = deviceDetails.name ?? 'My Trezor'; - final Wallet? wallet = await walletsBloc.importTrezorWallet( - name: name, - pubKey: deviceDetails.pubKey, - ); + // final String name = deviceDetails.name ?? 'My Trezor'; - if (wallet == null) { - emit(state.copyWith( - error: () => TextError( - error: LocaleKeys.trezorImportFailed.tr(args: [name])))); - - await _logoutFromHiddenMode(); - return; + try { + await _coinsRepository + .deactivateCoinsSync(await _coinsRepository.getEnabledCoins()); + } catch (e) { + // ignore } - - await coinsBloc.deactivateWalletCoins(); - currentWalletBloc.wallet = wallet; - routingState.selectedMenu = MainMenuValue.wallet; - _authRepo.setAuthMode(AuthorizeMode.logIn); _trezorRepo.subscribeOnConnectionStatus(deviceDetails.pubKey); - rebuildAll(null); + emit( + state.copyWith( + inProgress: () => false, + kdfUser: await _kdfSdk.auth.currentUser, + status: () => event.status, + ), + ); } Future _onSendPin( - TrezorInitSendPin event, Emitter emit) async { + TrezorInitSendPin event, + Emitter emit, + ) async { final int? taskId = state.taskId; if (taskId == null) return; @@ -188,7 +214,9 @@ class TrezorInitBloc extends Bloc { } Future _onSendPassphrase( - TrezorInitSendPassphrase event, Emitter emit) async { + TrezorInitSendPassphrase event, + Emitter emit, + ) async { final int? taskId = state.taskId; if (taskId == null) return; @@ -203,14 +231,16 @@ class TrezorInitBloc extends Bloc { } FutureOr _onReset( - TrezorInitReset event, Emitter emit) async { + TrezorInitReset event, + Emitter emit, + ) async { _unsubscribeStatus(); final taskId = state.taskId; if (taskId != null) { await _trezorRepo.initCancel(taskId); } - _logoutFromHiddenMode(); + _logout(); emit(state.copyWith( taskId: () => null, status: () => null, @@ -219,41 +249,70 @@ class TrezorInitBloc extends Bloc { } FutureOr _onAuthModeChange( - TrezorInitUpdateAuthMode event, Emitter emit) { - emit(state.copyWith(authMode: () => event.authMode)); + TrezorInitUpdateAuthMode event, + Emitter emit, + ) { + emit(state.copyWith(kdfUser: event.kdfUser)); } - Future _loginToHiddenMode() async { - final bool mm2SignedIn = await mm2.isSignedIn(); - if (state.authMode == AuthorizeMode.hiddenLogin && mm2SignedIn) return; + /// KDF has to be running with a seed/wallet to init a trezor, so this signs + /// into a static 'hidden' wallet to init trezor + Future _loginToTrezorWallet( + {String walletName = 'My Trezor', + String password = 'hidden-login'}) async { + final bool mm2SignedIn = await _kdfSdk.auth.isSignedIn(); + if (state.kdfUser != null && mm2SignedIn) { + return; + } - if (mm2SignedIn) await _authRepo.logOut(); - await _authRepo.logIn(AuthorizeMode.hiddenLogin, seed: seedForHiddenLogin); - } + // final walletName = state.status?.trezorStatus.name ?? 'My Trezor'; + // final password = + // state.status?.details.deviceDetails?.deviceId ?? 'hidden-login'; + final existingWallets = await _kdfSdk.auth.getUsers(); + if (existingWallets.any((wallet) => wallet.walletId.name == walletName)) { + await _kdfSdk.auth.signIn( + walletName: walletName, + password: password, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + await _kdfSdk.setWalletType(WalletType.trezor); + await _kdfSdk.confirmSeedBackup(); + return; + } - Future _logoutFromHiddenMode() async { - final bool mm2SignedIn = await mm2.isSignedIn(); + await _kdfSdk.auth.register( + walletName: walletName, + password: password, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + await _kdfSdk.setWalletType(WalletType.trezor); + await _kdfSdk.confirmSeedBackup(); + } - if (state.authMode != AuthorizeMode.hiddenLogin && mm2SignedIn) return; + Future _logout() async { + final bool isSignedIn = await _kdfSdk.auth.isSignedIn(); + if (!isSignedIn && state.kdfUser == null) { + return; + } - if (mm2SignedIn) await _authRepo.logOut(); - await _authRepo.logIn(AuthorizeMode.noLogin); + await _kdfSdk.auth.signOut(); } bool _checkAndHandleInvalidPin(InitTrezorStatusData status) { if (status.trezorStatus != InitTrezorStatus.error) return false; if (status.details.errorDetails == null) return false; if (status.details.errorDetails!.errorData != - TrezorStatusErrorData.invalidPin) return false; + TrezorStatusErrorData.invalidPin) { + return false; + } return true; } @override - Future close() { + Future close() async { _unsubscribeStatus(); - _authorizationSubscription.cancel(); - _logoutFromHiddenMode(); + await _authorizationSubscription.cancel(); return super.close(); } } diff --git a/lib/bloc/trezor_init_bloc/trezor_init_event.dart b/lib/bloc/trezor_init_bloc/trezor_init_event.dart index b508173bc5..b018354e33 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_event.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_event.dart @@ -1,13 +1,12 @@ -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/model/hw_wallet/init_trezor.dart'; +part of 'trezor_init_bloc.dart'; abstract class TrezorInitEvent { const TrezorInitEvent(); } class TrezorInitUpdateAuthMode extends TrezorInitEvent { - const TrezorInitUpdateAuthMode(this.authMode); - final AuthorizeMode authMode; + const TrezorInitUpdateAuthMode(this.kdfUser); + final KdfUser? kdfUser; } class TrezorInit extends TrezorInitEvent { @@ -27,9 +26,9 @@ class TrezorInitUpdateStatus extends TrezorInitEvent { } class TrezorInitSuccess extends TrezorInitEvent { - const TrezorInitSuccess(this.details); + const TrezorInitSuccess(this.status); - final TrezorDeviceDetails details; + final InitTrezorStatusData status; } class TrezorInitSendPin extends TrezorInitEvent { diff --git a/lib/bloc/trezor_init_bloc/trezor_init_state.dart b/lib/bloc/trezor_init_bloc/trezor_init_state.dart index 5a27a72f1d..d909ba6616 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_state.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_state.dart @@ -1,19 +1,16 @@ -import 'package:equatable/equatable.dart'; -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/model/hw_wallet/init_trezor.dart'; -import 'package:web_dex/model/text_error.dart'; +part of 'trezor_init_bloc.dart'; class TrezorInitState extends Equatable { const TrezorInitState({ required this.taskId, - required this.authMode, required this.status, + this.kdfUser, this.error, required this.inProgress, }); static TrezorInitState initial() => const TrezorInitState( taskId: null, - authMode: null, + kdfUser: null, error: null, status: null, inProgress: false, @@ -21,25 +18,25 @@ class TrezorInitState extends Equatable { TrezorInitState copyWith({ int? Function()? taskId, - AuthorizeMode? Function()? authMode, + KdfUser? kdfUser, TextError? Function()? error, InitTrezorStatusData? Function()? status, bool Function()? inProgress, }) => TrezorInitState( taskId: taskId != null ? taskId() : this.taskId, - authMode: authMode != null ? authMode() : this.authMode, + kdfUser: kdfUser ?? this.kdfUser, status: status != null ? status() : this.status, error: error != null ? error() : this.error, inProgress: inProgress != null ? inProgress() : this.inProgress, ); final int? taskId; - final AuthorizeMode? authMode; final InitTrezorStatusData? status; final TextError? error; final bool inProgress; + final KdfUser? kdfUser; @override - List get props => [taskId, authMode, status, error, inProgress]; + List get props => [taskId, kdfUser, status, error, inProgress]; } diff --git a/lib/bloc/wallets_bloc/wallets_repo.dart b/lib/bloc/wallets_bloc/wallets_repo.dart deleted file mode 100644 index b7c6674e1e..0000000000 --- a/lib/bloc/wallets_bloc/wallets_repo.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/services/storage/base_storage.dart'; -import 'package:web_dex/services/storage/get_storage.dart'; - -class WalletsRepo { - WalletsRepo({required BaseStorage storage}) : _storage = storage; - final BaseStorage _storage; - - Future> getAll() async { - final List> json = - (await _storage.read(allWalletsStorageKey) as List?) - ?.cast>() ?? - >[]; - final List wallets = - json.map((Map w) => Wallet.fromJson(w)).toList(); - - return wallets; - } - - Future save(Wallet wallet) async { - final wallets = await getAll(); - final int walletIndex = wallets.indexWhere((w) => w.id == wallet.id); - - if (walletIndex == -1) { - wallets.add(wallet); - } else { - wallets[walletIndex] = wallet; - } - - return _write(wallets); - } - - Future delete(Wallet wallet) async { - final wallets = await getAll(); - wallets.removeWhere((w) => w.id == wallet.id); - return _write(wallets); - } - - Future _write(List wallets) { - return _storage.write(allWalletsStorageKey, wallets); - } -} - -final WalletsRepo walletsRepo = WalletsRepo(storage: getStorage()); diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index a0dc502416..6b0bbc1a35 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -5,9 +5,10 @@ import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_event.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_state.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw/trezor_withdraw_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/validateaddress/validateaddress_response.dart'; @@ -27,10 +28,14 @@ export 'package:web_dex/bloc/withdraw_form/withdraw_form_state.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; class WithdrawFormBloc extends Bloc { - WithdrawFormBloc( - {required Coin coin, required CoinsBloc coinsBloc, required this.goBack}) - : _coinsRepo = coinsBloc, - super(WithdrawFormState.initial(coin, coinsBloc)) { + WithdrawFormBloc({ + required Coin coin, + required CoinsRepo coinsRepository, + required Mm2Api api, + required this.goBack, + }) : _coinsRepo = coinsRepository, + _mm2Api = api, + super(WithdrawFormState.initial(coin, coinsRepository)) { on(_onAddressChanged); on(_onAmountChanged); on(_onCustomFeeChanged); @@ -51,7 +56,8 @@ class WithdrawFormBloc extends Bloc { } // will use actual CoinsRepo when implemented - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; + final Mm2Api _mm2Api; final VoidCallback goBack; // Event handlers @@ -268,9 +274,12 @@ class WithdrawFormBloc extends Bloc { WithdrawFormConvertAddress event, Emitter emitter, ) async { - final result = await coinsRepo.convertLegacyAddress( - state.coin, - state.address, + final result = await _mm2Api.convertLegacyAddress( + ConvertAddressRequest( + coin: state.coin.abbr, + from: state.address, + isErc: state.coin.isErcType, + ), ); add(WithdrawFormAddressChanged(address: result ?? '')); @@ -294,7 +303,7 @@ class WithdrawFormBloc extends Bloc { return; } - final response = await _coinsRepo.sendRawTransaction( + final response = await _mm2Api.sendRawTransaction( SendRawTransactionRequest( coin: state.withdrawDetails.coin, txHex: state.withdrawDetails.txHex, @@ -325,7 +334,6 @@ class WithdrawFormBloc extends Bloc { )); return; } - await _coinsRepo.updateBalances(); emitter(state.copyWith(step: WithdrawFormStep.success)); } @@ -400,8 +408,8 @@ class WithdrawFormBloc extends Bloc { } final Map? validateRawResponse = - await coinsRepo.validateCoinAddress( - state.coin, + await _mm2Api.validateAddress( + state.coin.abbr, state.address, ); if (validateRawResponse == null) { diff --git a/lib/bloc/withdraw_form/withdraw_form_state.dart b/lib/bloc/withdraw_form/withdraw_form_state.dart index 4c8ee32be4..92801d8d14 100644 --- a/lib/bloc/withdraw_form/withdraw_form_state.dart +++ b/lib/bloc/withdraw_form/withdraw_form_state.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/withdraw/fee/fee_request.dart'; import 'package:web_dex/model/coin.dart'; @@ -31,11 +31,11 @@ class WithdrawFormState extends Equatable { required this.gasPriceError, required this.isCustomFeeEnabled, required this.memo, - required CoinsBloc coinsBloc, + required CoinsRepo coinsRepository, }) : _isMaxAmount = isMaxAmount, - _coinsRepo = coinsBloc; + _coinsRepo = coinsRepository; - static WithdrawFormState initial(Coin coin, CoinsBloc coinsBloc) { + static WithdrawFormState initial(Coin coin, CoinsRepo coinsRepository) { final List initSenderAddresses = coin.nonEmptyHdAddresses(); final String selectedSenderAddress = initSenderAddresses.isNotEmpty ? initSenderAddresses.first.address : ''; @@ -60,7 +60,7 @@ class WithdrawFormState extends Equatable { gasLimitError: TextError.empty(), gasPriceError: TextError.empty(), memo: null, - coinsBloc: coinsBloc, + coinsRepository: coinsRepository, ); } @@ -84,7 +84,7 @@ class WithdrawFormState extends Equatable { bool? isCustomFeeEnabled, String? trezorProgressStatus, String? memo, - CoinsBloc? coinsBloc, + CoinsRepo? coinsRepository, }) { return WithdrawFormState( coin: coin ?? this.coin, @@ -107,7 +107,7 @@ class WithdrawFormState extends Equatable { isCustomFeeEnabled: isCustomFeeEnabled ?? this.isCustomFeeEnabled, trezorProgressStatus: trezorProgressStatus, memo: memo ?? this.memo, - coinsBloc: coinsBloc ?? _coinsRepo, + coinsRepository: coinsRepository ?? _coinsRepo, ); } @@ -130,7 +130,7 @@ class WithdrawFormState extends Equatable { final BaseError gasPriceError; final bool _isMaxAmount; final String? memo; - final CoinsBloc _coinsRepo; + final CoinsRepo _coinsRepo; @override List get props => [ diff --git a/lib/blocs/blocs.dart b/lib/blocs/blocs.dart deleted file mode 100644 index 14455eb207..0000000000 --- a/lib/blocs/blocs.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/bloc/wallets_bloc/wallets_repo.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; -import 'package:web_dex/blocs/current_wallet_bloc.dart'; -import 'package:web_dex/blocs/dropdown_dismiss_bloc.dart'; -import 'package:web_dex/blocs/maker_form_bloc.dart'; -import 'package:web_dex/blocs/orderbook_bloc.dart'; -import 'package:web_dex/blocs/trading_entities_bloc.dart'; -import 'package:web_dex/blocs/wallets_bloc.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/services/cex_service/cex_service.dart'; -import 'package:web_dex/services/file_loader/file_loader.dart'; -import 'package:web_dex/shared/utils/encryption_tool.dart'; - -// todo(yurii): recommended bloc arch refactoring order: - -/// [AlphaVersionWarningService] can be converted to Bloc -/// and [AlphaVersionWarningService.isShown] might be stored in [StoredSettings] - -// 1) -CexService cexService = CexService(); -// 2) -TradingEntitiesBloc tradingEntitiesBloc = TradingEntitiesBloc(); -// 3) -WalletsBloc walletsBloc = WalletsBloc( - walletsRepo: walletsRepo, - encryptionTool: EncryptionTool(), -); -// 4) -CurrentWalletBloc currentWalletBloc = CurrentWalletBloc( - fileLoader: FileLoader.fromPlatform(), - authRepo: authRepo, - walletsRepo: walletsRepo, - encryptionTool: EncryptionTool(), -); - -/// Returns a global singleton instance of [CurrentWalletBloc]. -/// -/// NB! Even though the class is called [CoinsBloc], it is not a Bloc. -CoinsBloc coinsBloc = CoinsBloc( - api: mm2Api, - currentWalletBloc: currentWalletBloc, - authRepo: authRepo, - coinsRepo: coinsRepo, -); - -/// Returns the same instance of [CoinsBloc] as [coinsBloc]. The purpose of this -/// is to identify which methods of [CoinsBloc] need to be refacored into a -/// the existing [CoinsRepository] or a new repository. -/// -/// NB! Even though the class is called [CoinsBloc], it is not a Bloc. -CoinsBloc get coinsBlocRepository => coinsBloc; - -MakerFormBloc makerFormBloc = MakerFormBloc(api: mm2Api); -OrderbookBloc orderbookBloc = OrderbookBloc(api: mm2Api); - -DropdownDismissBloc globalCancelBloc = DropdownDismissBloc(); diff --git a/lib/blocs/coins_bloc.dart b/lib/blocs/coins_bloc.dart deleted file mode 100644 index 625a6c1174..0000000000 --- a/lib/blocs/coins_bloc.dart +++ /dev/null @@ -1,515 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; -import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/blocs/current_wallet_bloc.dart'; -import 'package:web_dex/blocs/trezor_coins_bloc.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/bloc_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_request.dart'; -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/model/cex_price.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; -import 'package:web_dex/services/cex_service/cex_service.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class CoinsBloc implements BlocBase { - CoinsBloc({ - required Mm2Api api, - required CurrentWalletBloc currentWalletBloc, - required AuthRepository authRepo, - required CoinsRepo coinsRepo, - }) : _coinsRepo = coinsRepo, - _currentWalletBloc = currentWalletBloc { - trezor = TrezorCoinsBloc( - trezorRepo: trezorRepo, - walletRepo: currentWalletBloc, - ); - } - - Future init() async { - _authorizationSubscription = authRepo.authMode.listen((event) async { - switch (event) { - case AuthorizeMode.noLogin: - _isLoggedIn = false; - await _onLogout(); - break; - case AuthorizeMode.logIn: - _isLoggedIn = true; - await _onLogIn(); - break; - case AuthorizeMode.hiddenLogin: - break; - } - }); - _updateBalancesTimer = Timer.periodic(const Duration(seconds: 10), (timer) { - if (loginActivationFinished) { - updateBalances(); - } - }); - - _loadKnownCoins(); - } - - Map> addressCache = - {}; // { acc: { abbr: address }}, used in Fiat Page - - late StreamSubscription _authorizationSubscription; - late TrezorCoinsBloc trezor; - final CoinsRepo _coinsRepo; - - final CurrentWalletBloc _currentWalletBloc; - late StreamSubscription> _pricesSubscription; - - bool _isLoggedIn = false; - bool get isLoggedIn => _isLoggedIn; - - late Timer _updateBalancesTimer; - - final StreamController> _knownCoinsController = - StreamController>.broadcast(); - Sink> get _inKnownCoins => _knownCoinsController.sink; - Stream> get outKnownCoins => _knownCoinsController.stream; - - List _knownCoins = []; - - List get knownCoins => _knownCoins; - - Map _knownCoinsMap = {}; - Map get knownCoinsMap => _knownCoinsMap; - - final StreamController> _walletCoinsController = - StreamController>.broadcast(); - Sink> get _inWalletCoins => _walletCoinsController.sink; - Stream> get outWalletCoins => _walletCoinsController.stream; - - List _walletCoins = []; - List get walletCoins => _walletCoins; - set walletCoins(List coins) { - _walletCoins = coins; - _walletCoinsMap = Map.fromEntries( - coins.map((coin) => MapEntry(coin.abbr.toUpperCase(), coin)), - ); - _inWalletCoins.add(_walletCoins); - } - - Map _walletCoinsMap = {}; - Map get walletCoinsMap => _walletCoinsMap; - - final StreamController _loginActivationFinishedController = - StreamController.broadcast(); - Sink get _inLoginActivationFinished => - _loginActivationFinishedController.sink; - Stream get outLoginActivationFinished => - _loginActivationFinishedController.stream; - - bool _loginActivationFinished = false; - bool get loginActivationFinished => _loginActivationFinished; - set loginActivationFinished(bool value) { - _loginActivationFinished = value; - _inLoginActivationFinished.add(_loginActivationFinished); - } - - Future _activateLoginWalletCoins() async { - final Wallet? currentWallet = _currentWalletBloc.wallet; - if (currentWallet == null || !_isLoggedIn) { - return; - } - - final List coins = currentWallet.config.activatedCoins - .map((abbr) => getCoin(abbr)) - .whereType() - .where((coin) => !coin.isActive) - .toList(); - - await activateCoins(coins, skipUpdateBalance: true); - await updateBalances(); - await reActivateSuspended(attempts: 2); - - loginActivationFinished = true; - } - - Future _onLogIn() async { - await _activateLoginWalletCoins(); - await updateBalances(); - } - - Coin? getCoin(String abbr) { - return getWalletCoin(abbr) ?? getKnownCoin(abbr); - } - - Future _loadKnownCoins() async { - _knownCoins = await _coinsRepo.getKnownCoins(); - _knownCoinsMap = Map.fromEntries( - _knownCoins.map((coin) => MapEntry(coin.abbr.toUpperCase(), coin)), - ); - _inKnownCoins.add(_knownCoins); - } - - Coin? getWalletCoin(String abbr) { - return _walletCoinsMap[abbr.toUpperCase()]; - } - - Coin? getKnownCoin(String abbr) { - return _knownCoinsMap[abbr.toUpperCase()]; - } - - Future updateBalances() async { - switch (_currentWalletBloc.wallet?.config.type) { - case WalletType.trezor: - await _updateTrezorBalances(); - break; - case WalletType.iguana: - await _updateIguanaBalances(); - break; - case WalletType.metamask: - case WalletType.keplr: - case null: - await _updateIguanaBalances(); - break; - } - } - - Future _updateTrezorBalances() async { - final coins = _walletCoins.where((coin) => coin.isActive).toList(); - for (Coin coin in coins) { - coin.accounts = await trezor.getAccounts(coin); - } - _updateCoins(); - } - - Future _updateIguanaBalances() async { - bool changed = false; - final coins = _walletCoins.where((coin) => coin.isActive).toList(); - - final newBalances = await Future.wait( - coins.map((coin) => _coinsRepo.getBalanceInfo(coin.abbr))); - - for (int i = 0; i < coins.length; i++) { - if (newBalances[i] != null) { - final newBalance = double.parse(newBalances[i]!.balance.decimal); - final newSendableBalance = double.parse(newBalances[i]!.volume.decimal); - - if (newBalance != coins[i].balance || - newSendableBalance != coins[i].sendableBalance) { - changed = true; - coins[i].balance = newBalance; - coins[i].sendableBalance = newSendableBalance; - } - } - } - - if (changed) { - _updateCoins(); - } - } - - void _updateCoinsCexPrices(Map prices) { - bool changed = false; - for (Coin coin in _knownCoins) { - final CexPrice? usdPrice = prices[abbr2Ticker(coin.abbr)]; - - changed = changed || usdPrice != coin.usdPrice; - coin.usdPrice = usdPrice; - - final Coin? enabledCoin = getWalletCoin(coin.abbr); - enabledCoin?.usdPrice = usdPrice; - - _inKnownCoins.add(_knownCoins); - } - if (changed) { - _updateCoins(); - } - - log('CEX prices updated', path: 'coins_bloc => updateCoinsCexPrices'); - } - - Future _activateCoin(Coin coin, - {bool skipUpdateBalance = false}) async { - if (!_isLoggedIn || coin.isActivating || coin.isActive) return; - - coin.state = CoinState.activating; - await _addCoinToWallet(coin); - _updateCoins(); - - switch (currentWalletBloc.wallet?.config.type) { - case WalletType.iguana: - await _activateIguanaCoin(coin, skipUpdateBalance: skipUpdateBalance); - break; - case WalletType.trezor: - await _activateTrezorCoin(coin); - break; - case WalletType.metamask: - case WalletType.keplr: - case null: - break; - } - _updateCoins(); - } - - Future _activateIguanaCoin(Coin coin, - {bool skipUpdateBalance = false}) async { - log('Enabling a ${coin.name}', path: 'coins_bloc => enable'); - await _activateParentOf(coin, skipUpdateBalance: skipUpdateBalance); - await _coinsRepo.activateCoins([coin]); - await _syncIguanaCoinState(coin); - - if (!skipUpdateBalance) await updateBalances(); - log('${coin.name} has enabled', path: 'coins_bloc => enable'); - } - - Future _activateTrezorCoin(Coin coin) async { - await trezor.activateCoin(coin); - } - - Future _activateParentOf(Coin coin, - {bool skipUpdateBalance = false}) async { - final Coin? parentCoin = coin.parentCoin; - if (parentCoin == null) return; - - if (parentCoin.isInactive) { - await activateCoins([parentCoin], skipUpdateBalance: skipUpdateBalance); - } - - await pauseWhile( - () => parentCoin.isActivating, - timeout: const Duration(seconds: 100), - ); - } - - Future _onLogout() async { - final List coins = [...walletCoins]; - for (Coin coin in coins) { - switch (coin.enabledType) { - case WalletType.iguana: - await _deactivateApiCoin(coin); - break; - case WalletType.trezor: - case WalletType.metamask: - case WalletType.keplr: - case null: - break; - } - coin.reset(); - } - walletCoins = []; - loginActivationFinished = false; - } - - Future deactivateWalletCoins() async { - await deactivateCoins(walletCoins); - } - - Future deactivateCoins(List coins) async { - await Future.wait(coins.map(deactivateCoin)); - } - - Future deactivateCoin(Coin coin) async { - log('Disabling a ${coin.name}', path: 'coins_bloc => disable'); - await _removeCoinFromWallet(coin); - _updateCoins(); - await _deactivateApiCoin(coin); - _updateCoins(); - - log( - '${coin.name} has been disabled', - path: 'coins_bloc => disable', - ); - } - - Future _deactivateApiCoin(Coin coin) async { - if (coin.isSuspended || coin.isActivating) return; - await _coinsRepo.deactivateCoin(coin); - } - - Future _removeCoinFromWallet(Coin coin) async { - coin.reset(); - _walletCoins.removeWhere((enabledCoin) => enabledCoin.abbr == coin.abbr); - _walletCoinsMap.remove(coin.abbr.toUpperCase()); - await _currentWalletBloc.removeCoin(coin.abbr); - } - - double? getUsdPriceByAmount(String amount, String coinAbbr) { - final Coin? coin = getCoin(coinAbbr); - final double? parsedAmount = double.tryParse(amount); - final double? usdPrice = coin?.usdPrice?.price; - - if (coin == null || usdPrice == null || parsedAmount == null) { - return null; - } - return parsedAmount * usdPrice; - } - - Future> withdraw( - WithdrawRequest request) async { - final Map? response = await _coinsRepo.withdraw(request); - - if (response == null) { - log('Withdraw error: response is null', isError: true); - return BlocResponse( - result: null, - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ); - } - - if (response['error'] != null) { - log('Withdraw error: ${response['error']}', isError: true); - return BlocResponse( - result: null, - error: withdrawErrorFactory.getError(response, request.params.coin), - ); - } - - final WithdrawDetails withdrawDetails = WithdrawDetails.fromJson( - response['result'] as Map? ?? {}, - ); - - return BlocResponse( - result: withdrawDetails, - error: null, - ); - } - - Future sendRawTransaction( - SendRawTransactionRequest request) async { - final SendRawTransactionResponse response = - await _coinsRepo.sendRawTransaction(request); - - return response; - } - - Future activateCoins(List coins, - {bool skipUpdateBalance = false}) async { - final List> enableFutures = coins - .map( - (coin) => _activateCoin(coin, skipUpdateBalance: skipUpdateBalance)) - .toList(); - await Future.wait(enableFutures); - } - - Future _addCoinToWallet(Coin coin) async { - if (getWalletCoin(coin.abbr) != null) return; - - coin.enabledType = _currentWalletBloc.wallet?.config.type; - _walletCoins.add(coin); - _walletCoinsMap[coin.abbr.toUpperCase()] = coin; - await _currentWalletBloc.addCoin(coin); - } - - Future _syncIguanaCoinState(Coin coin) async { - final List apiCoins = await _coinsRepo.getEnabledCoins([coin]); - final Coin? apiCoin = - apiCoins.firstWhereOrNull((coin) => coin.abbr == coin.abbr); - - if (apiCoin != null) { - // enabled on gui side, but not on api side - suspend - coin.state = CoinState.active; - } else { - // enabled on both sides - unsuspend - coin.state = CoinState.suspended; - } - - for (Coin apiCoin in apiCoins) { - if (getWalletCoin(apiCoin.abbr) == null) { - // enabled on api side, but not on gui side - enable on gui side - _walletCoins.add(apiCoin); - _walletCoinsMap[apiCoin.abbr.toUpperCase()] = apiCoin; - } - } - _updateCoins(); - } - - Future reactivateAll() async { - for (Coin coin in _walletCoins) { - coin.state = CoinState.inactive; - } - - await activateCoins(_walletCoins); - } - - Future reActivateSuspended({int attempts = 1}) async { - for (int i = 0; i < attempts; i++) { - final List suspended = - _walletCoins.where((coin) => coin.isSuspended).toList(); - if (suspended.isEmpty) return; - - await activateCoins(suspended); - } - } - - void subscribeOnPrice(CexService cexService) { - _pricesSubscription = cexService.pricesStream - .listen((prices) => _updateCoinsCexPrices(prices)); - } - - void _updateCoins() { - walletCoins = _walletCoins; - } - - Future getCoinAddress(String abbr) async { - final loggedIn = isLoggedIn && currentWalletBloc.wallet != null; - if (!loggedIn) { - return null; - } - - final accountKey = currentWalletBloc.wallet!.id; - final abbrKey = abbr; - - if (addressCache.containsKey(accountKey) && - addressCache[accountKey]!.containsKey(abbrKey)) { - return addressCache[accountKey]![abbrKey]; - } else { - await activateCoins([getCoin(abbr)!]); - final coin = walletCoins.firstWhereOrNull((c) => c.abbr == abbr); - - if (coin != null && coin.address != null) { - if (!addressCache.containsKey(accountKey)) { - addressCache[accountKey] = {}; - } - - // Cache this wallet's addresses - for (final walletCoin in walletCoins) { - if (walletCoin.address != null && - !addressCache[accountKey]!.containsKey(walletCoin.abbr)) { - // Exit if the address already exists in a different account - // Address belongs to another account, this is a bug, - // gives outdated data - for (final entry in addressCache.entries) { - if (entry.key != accountKey && - entry.value.containsValue(walletCoin.address)) { - return null; - } - } - - addressCache[accountKey]![walletCoin.abbr] = walletCoin.address!; - } - } - - return addressCache[accountKey]![abbrKey]; - } - } - return null; - } - - @override - void dispose() { - _walletCoinsController.close(); - _knownCoinsController.close(); - _updateBalancesTimer.cancel(); - _authorizationSubscription.cancel(); - _pricesSubscription.cancel(); - } -} diff --git a/lib/blocs/current_wallet_bloc.dart b/lib/blocs/current_wallet_bloc.dart index 6e1de765ac..615d184ee3 100644 --- a/lib/blocs/current_wallet_bloc.dart +++ b/lib/blocs/current_wallet_bloc.dart @@ -1,65 +1,46 @@ import 'dart:async'; import 'dart:convert'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/bloc/wallets_bloc/wallets_repo.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/shared/utils/encryption_tool.dart'; +@Deprecated('Please use AuthBloc or KomodoDefiSdk instead.') class CurrentWalletBloc implements BlocBase { CurrentWalletBloc({ + required KomodoDefiSdk kdfSdk, required EncryptionTool encryptionTool, required FileLoader fileLoader, - required WalletsRepo walletsRepo, - required AuthRepository authRepo, }) : _encryptionTool = encryptionTool, _fileLoader = fileLoader, - _walletsRepo = walletsRepo; + _kdfSdk = kdfSdk; + final KomodoDefiSdk _kdfSdk; final EncryptionTool _encryptionTool; final FileLoader _fileLoader; - final WalletsRepo _walletsRepo; - late StreamSubscription _authModeListener; - - final StreamController _walletController = - StreamController.broadcast(); - Sink get _inWallet => _walletController.sink; - Stream get outWallet => _walletController.stream; Wallet? _wallet; + // ignore: unnecessary_getters_setters Wallet? get wallet => _wallet; set wallet(Wallet? wallet) { _wallet = wallet; - _inWallet.add(_wallet); } @override - void dispose() { - _walletController.close(); - _authModeListener.cancel(); - } + void dispose() {} Future updatePassword( - String oldPassword, String password, Wallet wallet) async { - final walletCopy = wallet.copy(); - - final String? decryptedSeed = await _encryptionTool.decryptData( - oldPassword, walletCopy.config.seedPhrase); - final String encryptedSeed = - await _encryptionTool.encryptData(password, decryptedSeed!); - walletCopy.config.seedPhrase = encryptedSeed; - final bool isSaved = await _walletsRepo.save(walletCopy); - - if (isSaved) { - this.wallet = walletCopy; - return true; - } else { - return false; - } + String oldPassword, + String password, + Wallet wallet, + ) async { + // TODO!: re-implement via sdk + throw UnimplementedError( + 'Update password operation is not currently supported', + ); } Future addCoin(Coin coin) async { @@ -72,9 +53,8 @@ class CurrentWalletBloc implements BlocBase { return false; } wallet.config.activatedCoins.add(coinAbbr); - - final bool isSuccess = await _walletsRepo.save(wallet); - return isSuccess; + this.wallet = wallet; + return true; } Future removeCoin(String coinAbbr) async { @@ -84,20 +64,19 @@ class CurrentWalletBloc implements BlocBase { } wallet.config.activatedCoins.remove(coinAbbr); - final bool isSuccess = await _walletsRepo.save(wallet); this.wallet = wallet; - return isSuccess; + return true; } Future downloadCurrentWallet(String password) async { - final Wallet? wallet = this.wallet; + final Wallet? wallet = (await _kdfSdk.auth.currentUser)?.wallet; if (wallet == null) return; final String data = jsonEncode(wallet.config); final String encryptedData = await _encryptionTool.encryptData(password, data); - _fileLoader.save( + await _fileLoader.save( fileName: wallet.name, data: encryptedData, type: LoadFileType.text, @@ -108,11 +87,11 @@ class CurrentWalletBloc implements BlocBase { } Future confirmBackup() async { - final Wallet? wallet = this.wallet; + final Wallet? wallet = (await _kdfSdk.auth.currentUser)?.wallet; if (wallet == null || wallet.config.hasBackup) return; wallet.config.hasBackup = true; - await _walletsRepo.save(wallet); + await _kdfSdk.confirmSeedBackup(); this.wallet = wallet; } } diff --git a/lib/blocs/dropdown_dismiss_bloc.dart b/lib/blocs/dropdown_dismiss_bloc.dart deleted file mode 100644 index 77a0f5f086..0000000000 --- a/lib/blocs/dropdown_dismiss_bloc.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; -import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; -import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; -import 'package:web_dex/bloc/taker_form/taker_event.dart'; - -import 'blocs.dart'; - -class DropdownDismissBloc { - final dropdownDismissController = StreamController.broadcast(); - StreamSink get _inDropdownDismiss => dropdownDismissController.sink; - Stream get outDropdownDismiss => dropdownDismissController.stream; - - void runDropdownDismiss({BuildContext? context}) { - if (context != null) { - // Taker form - context.read().add(TakerCoinSelectorOpen(false)); - context.read().add(TakerOrderSelectorOpen(false)); - - // Maker form - makerFormBloc.showSellCoinSelect = false; - makerFormBloc.showBuyCoinSelect = false; - - // Bridge form - context.read().add(const BridgeShowTickerDropdown(false)); - context.read().add(const BridgeShowSourceDropdown(false)); - context.read().add(const BridgeShowTargetDropdown(false)); - } - - // In case there's need to make it available in a stream for future use - _inDropdownDismiss.add(true); - Future.delayed(const Duration(seconds: 1)) - .then((_) => _inDropdownDismiss.add(false)); - } - - void dispose() { - dropdownDismissController.close(); - } -} diff --git a/lib/blocs/kmd_rewards_bloc.dart b/lib/blocs/kmd_rewards_bloc.dart index a717663b35..ec31b5d469 100644 --- a/lib/blocs/kmd_rewards_bloc.dart +++ b/lib/blocs/kmd_rewards_bloc.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; @@ -14,9 +14,11 @@ import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; -KmdRewardsBloc kmdRewardsBloc = KmdRewardsBloc(); - class KmdRewardsBloc implements BlocBase { + KmdRewardsBloc(this._coinsBlocRepository, this._mm2Api); + + final CoinsRepo _coinsBlocRepository; + final Mm2Api _mm2Api; bool _claimInProgress = false; Future> claim(BuildContext context) async { @@ -39,7 +41,7 @@ class KmdRewardsBloc implements BlocBase { ); } - final tx = await coinsBloc.sendRawTransaction(SendRawTransactionRequest( + final tx = await _mm2Api.sendRawTransaction(SendRawTransactionRequest( coin: 'KMD', txHex: withdrawDetails.txHex, )); @@ -60,7 +62,7 @@ class KmdRewardsBloc implements BlocBase { Future> getInfo() async { final Map? response = - await mm2Api.getRewardsInfo(KmdRewardsInfoRequest()); + await _mm2Api.getRewardsInfo(KmdRewardsInfoRequest()); if (response != null && response['result'] != null) { return response['result'] .map( @@ -81,7 +83,7 @@ class KmdRewardsBloc implements BlocBase { } Future> _withdraw() async { - final Coin? kmdCoin = coinsBloc.getWalletCoin('KMD'); + final Coin? kmdCoin = _coinsBlocRepository.getCoin('KMD'); if (kmdCoin == null) { return BlocResponse( error: TextError(error: LocaleKeys.plsActivateKmd.tr())); @@ -91,7 +93,7 @@ class KmdRewardsBloc implements BlocBase { error: TextError(error: LocaleKeys.noKmdAddress.tr())); } - return await coinsBloc.withdraw(WithdrawRequest( + return await _coinsBlocRepository.withdraw(WithdrawRequest( coin: 'KMD', max: true, to: kmdCoin.address!, diff --git a/lib/blocs/maker_form_bloc.dart b/lib/blocs/maker_form_bloc.dart index c07c2d1aa2..a3bcb10ae0 100644 --- a/lib/blocs/maker_form_bloc.dart +++ b/lib/blocs/maker_form_bloc.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_errors.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/available_balance_state.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/data_from_service.dart'; @@ -24,21 +24,19 @@ import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/views/dex/simple/form/error_list/dex_form_error_with_action.dart'; class MakerFormBloc implements BlocBase { - MakerFormBloc({required this.api}); - - void onChangeAuthStatus(AuthorizeMode event) { - final bool prevLoginState = _isLoggedIn; - _isLoggedIn = event == AuthorizeMode.logIn; - - if (prevLoginState != _isLoggedIn) { - sellCoin = sellCoin; - } - } + MakerFormBloc({ + required this.api, + required this.kdfSdk, + required this.coinsRepository, + required this.dexRepository, + }); final Mm2Api api; + final KomodoDefiSdk kdfSdk; + final CoinsRepo coinsRepository; + final DexRepository dexRepository; String currentEntityUuid = ''; - bool _isLoggedIn = false; bool _showConfirmation = false; final StreamController _showConfirmationCtrl = @@ -114,7 +112,7 @@ class MakerFormBloc implements BlocBase { if (coin == buyCoin) buyCoin = null; _autoActivate(sellCoin) - .then((_) => _updateMaxSellAmountListener()) + .then((_) async => await _updateMaxSellAmountListener()) .then((_) => _updatePreimage()) .then((_) => _reValidate()); } @@ -244,25 +242,26 @@ class MakerFormBloc implements BlocBase { } Timer? _maxSellAmountTimer; - void _updateMaxSellAmountListener() { + Future _updateMaxSellAmountListener() async { _maxSellAmountTimer?.cancel(); maxSellAmount = null; availableBalanceState = AvailableBalanceState.loading; isMaxActive = false; - _updateMaxSellAmount(); - _maxSellAmountTimer = Timer.periodic(const Duration(seconds: 10), (_) { - _updateMaxSellAmount(); + await _updateMaxSellAmount(); + _maxSellAmountTimer = + Timer.periodic(const Duration(seconds: 10), (_) async { + await _updateMaxSellAmount(); }); } - void _updateMaxSellAmount() { + Future _updateMaxSellAmount() async { final Coin? coin = sellCoin; if (availableBalanceState == AvailableBalanceState.initial) { availableBalanceState = AvailableBalanceState.loading; } - if (!_isLoggedIn) { + if (!await kdfSdk.auth.isSignedIn()) { availableBalanceState = AvailableBalanceState.unavailable; } else { if (coin == null) { @@ -475,7 +474,7 @@ class MakerFormBloc implements BlocBase { if (coin == null) return; inProgress = true; final List activationErrors = - await activateCoinIfNeeded(coin.abbr); + await activateCoinIfNeeded(coin.abbr, coinsRepository); inProgress = false; if (activationErrors.isNotEmpty) { _setFormErrors(activationErrors); @@ -664,14 +663,16 @@ class MakerFormBloc implements BlocBase { } Future reInitForm() async { - if (sellCoin != null) sellCoin = coinsBloc.getKnownCoin(sellCoin!.abbr); - if (buyCoin != null) buyCoin = coinsBloc.getKnownCoin(buyCoin!.abbr); + if (sellCoin != null) { + sellCoin = coinsRepository.getCoin(sellCoin!.abbr); + } + if (buyCoin != null) buyCoin = coinsRepository.getCoin(buyCoin!.abbr); } void setDefaultSellCoin() { if (sellCoin != null) return; - final Coin? defaultSellCoin = coinsBloc.getCoin(defaultDexCoin); + final Coin? defaultSellCoin = coinsRepository.getCoin(defaultDexCoin); if (defaultSellCoin == null) return; sellCoin = defaultSellCoin; diff --git a/lib/blocs/startup_bloc.dart b/lib/blocs/startup_bloc.dart deleted file mode 100644 index 72747fac73..0000000000 --- a/lib/blocs/startup_bloc.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:async'; - -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/mm2/mm2.dart'; -import 'package:web_dex/model/authorize_mode.dart'; -import 'package:web_dex/model/main_menu_value.dart'; -import 'package:web_dex/router/state/routing_state.dart'; -import 'package:web_dex/services/coins_service/coins_service.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -StartUpBloc startUpBloc = StartUpBloc(); - -class StartUpBloc implements BlocBase { - bool _running = false; - - @override - void dispose() { - _runningController.close(); - } - - final StreamController _runningController = - StreamController.broadcast(); - Sink get _inRunning => _runningController.sink; - Stream get outRunning => _runningController.stream; - - bool get running => _running; - set running(bool value) { - _running = value; - _inRunning.add(_running); - } - - Future run() async { - await mm2.init(); - final wasAlreadyRunning = running; - - authRepo.authMode.listen((event) { - makerFormBloc.onChangeAuthStatus(event); - }); - coinsService.init(); - coinsBloc.subscribeOnPrice(cexService); - running = true; - tradingEntitiesBloc.runUpdate(); - routingState.selectedMenu = MainMenuValue.defaultMenu(); - if (!wasAlreadyRunning) await authRepo.logIn(AuthorizeMode.noLogin); - - log('Application has started'); - } -} diff --git a/lib/blocs/trading_entities_bloc.dart b/lib/blocs/trading_entities_bloc.dart index 2f09d96773..7dd9b4f891 100644 --- a/lib/blocs/trading_entities_bloc.dart +++ b/lib/blocs/trading_entities_bloc.dart @@ -2,28 +2,37 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/cancel_order/cancel_order_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_swap_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_swap_response.dart'; -import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/services/orders_service/my_orders_service.dart'; -import 'package:web_dex/services/swaps_service/swaps_service.dart'; +import 'package:web_dex/shared/utils/utils.dart'; class TradingEntitiesBloc implements BlocBase { - TradingEntitiesBloc() { - _authModeListener = authRepo.authMode.listen((mode) => _authMode = mode); - } - - AuthorizeMode? _authMode; - StreamSubscription? _authModeListener; + TradingEntitiesBloc( + KomodoDefiSdk kdfSdk, + Mm2Api mm2Api, + MyOrdersService myOrdersService, + ) : _mm2Api = mm2Api, + _myOrdersService = myOrdersService, + _kdfSdk = kdfSdk; + + final KomodoDefiSdk _kdfSdk; + final MyOrdersService _myOrdersService; + final Mm2Api _mm2Api; + StreamSubscription? _authModeListener; List _myOrders = []; List _swaps = []; Timer? timer; @@ -52,8 +61,10 @@ class TradingEntitiesBloc implements BlocBase { } Future fetch() async { - myOrders = await myOrdersService.getOrders() ?? []; - swaps = await swapsService.getRecentSwaps(MyRecentSwapsRequest()) ?? []; + if (!await _kdfSdk.auth.isSignedIn()) return; + + myOrders = await _myOrdersService.getOrders() ?? []; + swaps = await getRecentSwaps(MyRecentSwapsRequest()) ?? []; } @override @@ -61,20 +72,12 @@ class TradingEntitiesBloc implements BlocBase { _authModeListener?.cancel(); } - bool get _shouldFetchDexUpdates { - if (_authMode == AuthorizeMode.noLogin) return false; - if (_authMode == AuthorizeMode.hiddenLogin) return false; - if (currentWalletBloc.wallet?.isHW == true) return false; - - return true; - } - void runUpdate() { bool updateInProgress = false; timer = Timer.periodic(const Duration(seconds: 1), (_) async { - if (!_shouldFetchDexUpdates) return; if (updateInProgress) return; + // TODO!: do not run for hidden login or HW updateInProgress = true; await fetch(); @@ -82,13 +85,9 @@ class TradingEntitiesBloc implements BlocBase { }); } - Future recoverFundsOfSwap(String uuid) async { - return swapsService.recoverFundsOfSwap(uuid); - } - Future cancelOrder(String uuid) async { final Map response = - await mm2Api.cancelOrder(CancelOrderRequest(uuid: uuid)); + await _mm2Api.cancelOrder(CancelOrderRequest(uuid: uuid)); return response['error']; } @@ -135,4 +134,38 @@ class TradingEntitiesBloc implements BlocBase { final futures = myOrders.map((o) => cancelOrder(o.uuid)); Future.wait(futures); } + + Future?> getRecentSwaps(MyRecentSwapsRequest request) async { + final MyRecentSwapsResponse? response = + await _mm2Api.getMyRecentSwaps(request); + if (response == null) { + return null; + } + + return response.result.swaps; + } + + Future recoverFundsOfSwap(String uuid) async { + final RecoverFundsOfSwapRequest request = + RecoverFundsOfSwapRequest(uuid: uuid); + final RecoverFundsOfSwapResponse? response = + await _mm2Api.recoverFundsOfSwap(request); + if (response != null) { + log( + response.toJson().toString(), + path: 'swaps_service => recoverFundsOfSwap', + ); + } + return response; + } + + Future getMaxTakerVolume(String coinAbbr) async { + final MaxTakerVolResponse? response = + await _mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr)); + if (response == null) { + return null; + } + + return fract2rat(response.result.toJson()); + } } diff --git a/lib/blocs/trezor_coins_bloc.dart b/lib/blocs/trezor_coins_bloc.dart index 07a292d0c4..4f3d84c526 100644 --- a/lib/blocs/trezor_coins_bloc.dart +++ b/lib/blocs/trezor_coins_bloc.dart @@ -1,23 +1,20 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; -import 'package:web_dex/blocs/current_wallet_bloc.dart'; 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/bloc_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/get_new_address/get_new_address_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw/trezor_withdraw_request.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/hd_account/hd_account.dart'; import 'package:web_dex/model/hw_wallet/init_trezor.dart'; import 'package:web_dex/model/hw_wallet/trezor_progress_status.dart'; import 'package:web_dex/model/hw_wallet/trezor_status.dart'; import 'package:web_dex/model/hw_wallet/trezor_task.dart'; import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/show_trezor_passphrase_dialog.dart'; @@ -25,74 +22,71 @@ import 'package:web_dex/views/common/hw_wallet_dialog/show_trezor_pin_dialog.dar class TrezorCoinsBloc { TrezorCoinsBloc({ - required TrezorRepo trezorRepo, - required CurrentWalletBloc walletRepo, - }) : _trezorRepo = trezorRepo, - _walletRepo = walletRepo; - - final TrezorRepo _trezorRepo; - final CurrentWalletBloc _walletRepo; - bool get _loggedInTrezor => - _walletRepo.wallet?.config.type == WalletType.trezor; - Timer? _initNewAddressStatusTimer; - - Future?> getAccounts(Coin coin) async { - final TrezorBalanceInitResponse initResponse = - await _trezorRepo.initBalance(coin); - final int? taskId = initResponse.result?.taskId; - if (taskId == null) return null; + required this.trezorRepo, + }); - final int started = nowMs; - // todo(yurii): change timeout to some reasonable value (10000?) - while (nowMs - started < 100000) { - final statusResponse = await _trezorRepo.getBalanceStatus(taskId); - final InitTrezorStatus? status = statusResponse.result?.status; + final TrezorRepo trezorRepo; + Timer? _initNewAddressStatusTimer; - if (status == InitTrezorStatus.error) return null; + Future initNewAddress(Coin coin) async { + final TrezorGetNewAddressInitResponse response = + await trezorRepo.initNewAddress(coin.abbr); + final result = response.result; - if (status == InitTrezorStatus.ok) { - return statusResponse.result?.balanceDetails?.accounts; - } + return result?.taskId; + } - await Future.delayed(const Duration(milliseconds: 500)); - } + void subscribeOnNewAddressStatus( + int taskId, + Coin coin, + Function(GetNewAddressResponse) callback, + ) { + _initNewAddressStatusTimer = + Timer.periodic(const Duration(seconds: 1), (timer) async { + final GetNewAddressResponse initNewAddressStatus = + await trezorRepo.getNewAddressStatus(taskId, coin); + callback(initNewAddressStatus); + }); + } - return null; + void unsubscribeFromNewAddressStatus() { + _initNewAddressStatusTimer?.cancel(); + _initNewAddressStatusTimer = null; } - Future activateCoin(Coin coin) async { - switch (coin.type) { - case CoinType.utxo: - case CoinType.smartChain: - await _enableUtxo(coin); - break; + Future> activateCoin(Asset asset) async { + switch (asset.id.subClass) { + case CoinSubClass.utxo: + case CoinSubClass.smartChain: + return await _enableUtxo(asset); default: - {} + return List.empty(); } } - Future _enableUtxo(Coin coin) async { - final enableResponse = await _trezorRepo.enableUtxo(coin); + Future> _enableUtxo(Asset asset) async { + final enableResponse = await trezorRepo.enableUtxo(asset); final taskId = enableResponse.result?.taskId; - if (taskId == null) return; + if (taskId == null) return List.empty(); - while (_loggedInTrezor) { - final statusResponse = await _trezorRepo.getEnableUtxoStatus(taskId); + while (await trezorRepo.isTrezorWallet()) { + final statusResponse = await trezorRepo.getEnableUtxoStatus(taskId); final InitTrezorStatus? status = statusResponse.result?.status; switch (status) { case InitTrezorStatus.error: - coin.state = CoinState.suspended; - return; + return List.empty(); case InitTrezorStatus.userActionRequired: final TrezorUserAction? action = statusResponse.result?.actionDetails; if (action == TrezorUserAction.enterTrezorPin) { + // TODO! :( await showTrezorPinDialog(TrezorTask( taskId: taskId, type: TrezorTaskType.enableUtxo, )); } else if (action == TrezorUserAction.enterTrezorPassphrase) { + // TODO! :( await showTrezorPassphraseDialog(TrezorTask( taskId: taskId, type: TrezorTaskType.enableUtxo, @@ -103,66 +97,23 @@ class TrezorCoinsBloc { case InitTrezorStatus.ok: final details = statusResponse.result?.details; if (details != null) { - coin.accounts = details.accounts; - coin.state = CoinState.active; + return details.accounts; } - return; default: } await Future.delayed(const Duration(milliseconds: 500)); } - } - Future initNewAddress(Coin coin) async { - final TrezorGetNewAddressInitResponse response = - await _trezorRepo.initNewAddress(coin.abbr); - final result = response.result; - - return result?.taskId; - } - - Future getNewAddressStatus( - int taskId, Coin coin) async { - final GetNewAddressResponse response = - await _trezorRepo.getNewAddressStatus(taskId); - final GetNewAddressStatus? status = response.result?.status; - final GetNewAddressResultDetails? details = response.result?.details; - if (status == GetNewAddressStatus.ok && - details is GetNewAddressResultOkDetails) { - coin.accounts = await getAccounts(coin); - } - return response; - } - - void subscribeOnNewAddressStatus( - int taskId, - Coin coin, - Function(GetNewAddressResponse) callback, - ) { - _initNewAddressStatusTimer = - Timer.periodic(const Duration(seconds: 1), (timer) async { - final GetNewAddressResponse initNewAddressStatus = - await getNewAddressStatus(taskId, coin); - callback(initNewAddressStatus); - }); - } - - void unsubscribeFromNewAddressStatus() { - _initNewAddressStatusTimer?.cancel(); - _initNewAddressStatusTimer = null; - } - - Future cancelGetNewAddress(int taskId) async { - await _trezorRepo.cancelGetNewAddress(taskId); + return List.empty(); } Future> withdraw( TrezorWithdrawRequest request, { required void Function(TrezorProgressStatus?) onProgressUpdated, }) async { - final withdrawResponse = await _trezorRepo.withdraw(request); + final withdrawResponse = await trezorRepo.withdraw(request); if (withdrawResponse.error != null) { return BlocResponse( @@ -181,7 +132,7 @@ class TrezorCoinsBloc { final int started = nowMs; while (nowMs - started < 1000 * 60 * 3) { - final statusResponse = await _trezorRepo.getWithdrawStatus(taskId); + final statusResponse = await trezorRepo.getWithdrawStatus(taskId); if (statusResponse.error != null) { return BlocResponse( @@ -235,14 +186,10 @@ class TrezorCoinsBloc { await Future.delayed(const Duration(milliseconds: 500)); } - await _withdrawCancel(taskId); + await trezorRepo.cancelWithdraw(taskId); return BlocResponse( result: null, error: TextError(error: LocaleKeys.timeout.tr()), ); } - - Future _withdrawCancel(int taskId) async { - await _trezorRepo.cancelWithdraw(taskId); - } } diff --git a/lib/blocs/wallets_bloc.dart b/lib/blocs/wallets_bloc.dart deleted file mode 100644 index 10b06aae26..0000000000 --- a/lib/blocs/wallets_bloc.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:uuid/uuid.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/wallets_bloc/wallets_repo.dart'; -import 'package:web_dex/blocs/bloc_base.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/shared/utils/encryption_tool.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class WalletsBloc implements BlocBase { - WalletsBloc({ - required WalletsRepo walletsRepo, - required EncryptionTool encryptionTool, - }) : _walletsRepo = walletsRepo, - _encryptionTool = encryptionTool; - - final WalletsRepo _walletsRepo; - final EncryptionTool _encryptionTool; - - List _wallets = []; - List get wallets => _wallets; - set wallets(List newWallets) { - _wallets = newWallets; - _inWallets.add(_wallets); - } - - final StreamController> _walletsController = - StreamController>.broadcast(); - Sink> get _inWallets => _walletsController.sink; - Stream> get outWallets => _walletsController.stream; - - @override - void dispose() { - _walletsController.close(); - } - - Future createNewWallet({ - required String name, - required String password, - required String seed, - }) async { - try { - bool isWalletCreationSuccessfully = false; - - final String encryptedSeed = - await _encryptionTool.encryptData(password, seed); - - final Wallet newWallet = Wallet( - id: const Uuid().v1(), - name: name, - config: WalletConfig( - type: WalletType.iguana, - seedPhrase: encryptedSeed, - activatedCoins: enabledByDefaultCoins, - hasBackup: false, - ), - ); - log('Creating a new wallet ${newWallet.id}', - path: 'wallet_bloc => createNewWallet'); - - isWalletCreationSuccessfully = await _addWallet(newWallet); - - if (isWalletCreationSuccessfully) { - log('The wallet ${newWallet.id} has created', - path: 'wallet_bloc => createNewWallet'); - return newWallet; - } else { - return null; - } - } catch (_) { - return null; - } - } - - Future importWallet({ - required String name, - required String password, - required WalletConfig walletConfig, - WalletType type = WalletType.iguana, - }) async { - log('Importing a wallet $name', path: 'wallet_bloc => importWallet'); - try { - bool isWalletCreationSuccessfully = false; - - final String encryptedSeed = - await _encryptionTool.encryptData(password, walletConfig.seedPhrase); - final Wallet newWallet = Wallet( - id: const Uuid().v1(), - name: name, - config: WalletConfig( - type: type, - seedPhrase: encryptedSeed, - activatedCoins: walletConfig.activatedCoins, - hasBackup: true, - ), - ); - - isWalletCreationSuccessfully = await _addWallet(newWallet); - - if (isWalletCreationSuccessfully) { - log('The Wallet $name has imported', - path: 'wallet_bloc => importWallet'); - return newWallet; - } else { - return null; - } - } catch (_) { - return null; - } - } - - Future importTrezorWallet({ - required String name, - required String pubKey, - }) async { - try { - final Wallet? existedWallet = - wallets.firstWhereOrNull((w) => w.config.pubKey == pubKey); - if (existedWallet != null) return existedWallet; - - final Wallet newWallet = Wallet( - id: const Uuid().v1(), - name: name, - config: WalletConfig( - type: WalletType.trezor, - seedPhrase: '', - activatedCoins: enabledByDefaultTrezorCoins, - hasBackup: true, - pubKey: pubKey, - ), - ); - - final bool isWalletImportSuccessfully = await _addWallet(newWallet); - - if (isWalletImportSuccessfully) { - log('The Wallet $name has imported', - path: 'wallet_bloc => importWallet'); - return newWallet; - } else { - return null; - } - } catch (_) { - return null; - } - } - - Future fetchSavedWallets() async { - wallets = await _walletsRepo.getAll(); - } - - Future deleteWallet(Wallet wallet) async { - log( - 'Deleting a wallet ${wallet.id}', - path: 'wallet_bloc => deleteWallet', - ); - - final bool isDeletingSuccess = await _walletsRepo.delete(wallet); - if (isDeletingSuccess) { - final newWallets = _wallets.where((w) => w.id != wallet.id).toList(); - wallets = newWallets; - log( - 'The wallet ${wallet.id} has deleted', - path: 'wallet_bloc => deleteWallet', - ); - } - - return isDeletingSuccess; - } - - Future _addWallet(Wallet wallet) async { - final bool isSavingSuccess = await _walletsRepo.save(wallet); - if (isSavingSuccess) { - final List newWallets = [..._wallets]; - newWallets.add(wallet); - wallets = newWallets; - } - - return isSavingSuccess; - } - - String? validateWalletName(String name) { - if (wallets.firstWhereOrNull((w) => w.name == name) != null) { - return LocaleKeys.walletCreationExistNameError.tr(); - } else if (name.isEmpty || name.length > 40) { - return LocaleKeys.walletCreationNameLengthError.tr(); - } - return null; - } - - Future resetSpecificWallet(Wallet wallet) async { - WalletConfig updatedConfig = wallet.config.copy() - ..activatedCoins = enabledByDefaultCoins; - - Wallet updatedWallet = Wallet( - id: wallet.id, - name: wallet.name, - config: updatedConfig, - ); - - await _walletsRepo.save(updatedWallet); - } -} diff --git a/lib/blocs/wallets_repository.dart b/lib/blocs/wallets_repository.dart new file mode 100644 index 0000000000..9737a96e12 --- /dev/null +++ b/lib/blocs/wallets_repository.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/model/wallet.dart'; +import 'package:web_dex/services/storage/base_storage.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class WalletsRepository { + WalletsRepository(this._kdfSdk, this._mm2Api, this._legacyWalletStorage); + + final KomodoDefiSdk _kdfSdk; + final Mm2Api _mm2Api; + final BaseStorage _legacyWalletStorage; + + List? _cachedWallets; + List? get wallets => _cachedWallets; + + Future> getWallets() async { + final legacyWallets = await _getLegacyWallets(); + _cachedWallets = (await _kdfSdk.wallets) + .where( + (wallet) => wallet.config.type != WalletType.trezor, + ) + .toList(); + return [..._cachedWallets!, ...legacyWallets]; + } + + Future> _getLegacyWallets() async { + var newVariable = + await _legacyWalletStorage.read(allWalletsStorageKey) as List?; + final List> json = + newVariable?.cast>() ?? >[]; + + return json + .map((Map w) => + Wallet.fromJson(w)..config.isLegacyWallet = true) + .toList(); + } + + Future deleteWallet(Wallet wallet) async { + log( + 'Deleting a wallet ${wallet.id}', + path: 'wallet_bloc => deleteWallet', + ).ignore(); + + if (wallet.isLegacyWallet) { + final wallets = await _getLegacyWallets(); + wallets.removeWhere((w) => w.id == wallet.id); + await _legacyWalletStorage.write(allWalletsStorageKey, wallets); + return true; + } + + // TODO!: implement + throw UnimplementedError('Not yet supported'); + } + + String? validateWalletName(String name) { + // This shouldn't happen, but just in case. + if (_cachedWallets == null) { + getWallets().ignore(); + return null; + } + + if (_cachedWallets!.firstWhereOrNull((w) => w.name == name) != null) { + return LocaleKeys.walletCreationExistNameError.tr(); + } else if (name.isEmpty || name.length > 40) { + return LocaleKeys.walletCreationNameLengthError.tr(); + } + + return null; + } + + Future resetSpecificWallet(Wallet wallet) async { + final coinsToDeactivate = wallet.config.activatedCoins + .where((coin) => !enabledByDefaultCoins.contains(coin)); + for (final coin in coinsToDeactivate) { + await _mm2Api.disableCoin(coin); + } + } +} diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 3fc2a35baf..1b0948b52a 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -9,6 +9,7 @@ abstract class LocaleKeys { static const price = 'price'; static const volume = 'volume'; static const history = 'history'; + static const lastTransactions = 'lastTransactions'; static const active = 'active'; static const change24h = 'change24h'; static const change24hRevert = 'change24hRevert'; @@ -391,6 +392,12 @@ abstract class LocaleKeys { static const withdrawNoSuchCoinError = 'withdrawNoSuchCoinError'; static const txHistoryFetchError = 'txHistoryFetchError'; static const txHistoryNoTransactions = 'txHistoryNoTransactions'; + static const maxGapLimitReached = 'maxGapLimitReached'; + static const maxAddressesReached = 'maxAddressesReached'; + static const missingDerivationPath = 'missingDerivationPath'; + static const protocolNotSupported = 'protocolNotSupported'; + static const derivationModeNotSupported = 'derivationModeNotSupported'; + static const noActiveWallet = 'noActiveWallet'; static const memo = 'memo'; static const gasPriceGwei = 'gasPriceGwei'; static const gasLimit = 'gasLimit'; @@ -589,6 +596,13 @@ abstract class LocaleKeys { static const mmBotFirstTradePreview = 'mmBotFirstTradePreview'; static const mmBotFirstTradeEstimate = 'mmBotFirstTradeEstimate'; static const mmBotFirstOrderVolume = 'mmBotFirstOrderVolume'; + static const onlySendToThisAddress = 'onlySendToThisAddress'; + static const scanTheQrCode = 'scanTheQrCode'; + static const swapAddress = 'swapAddress'; + static const addresses = 'addresses'; + static const creating = 'creating'; + static const createAddress = 'createAddress'; + static const hideZeroBalanceAddresses = 'hideZeroBalanceAddresses'; static const important = 'important'; static const trend = 'trend'; static const growth = 'growth'; diff --git a/lib/main.dart b/lib/main.dart index b2b1c2bd0a..0f93e70075 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,24 +5,35 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; -import 'package:komodo_coin_updates/komodo_coin_updates.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/app_config/package_information.dart'; import 'package:web_dex/bloc/app_bloc_observer.dart'; import 'package:web_dex/bloc/app_bloc_root.dart' deferred as app_bloc_root; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; import 'package:web_dex/bloc/cex_market_data/cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; -import 'package:web_dex/bloc/runtime_coin_updates/runtime_update_config_provider.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/blocs/startup_bloc.dart'; +import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; +import 'package:web_dex/blocs/trezor_coins_bloc.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; +import 'package:web_dex/mm2/mm2.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; import 'package:web_dex/model/stored_settings.dart'; import 'package:web_dex/performance_analytics/performance_analytics.dart'; +import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/services/logger/get_logger.dart'; +import 'package:web_dex/services/storage/get_storage.dart'; +import 'package:web_dex/shared/constants.dart'; +import 'package:web_dex/shared/utils/encryption_tool.dart'; import 'package:web_dex/shared/utils/platform_tuner.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -34,28 +45,84 @@ PerformanceMode? get appDemoPerformanceMode => _appDemoPerformanceMode ?? _getPerformanceModeFromUrl(); Future main() async { - usePathUrlStrategy(); + await runZonedGuarded( + () async { + usePathUrlStrategy(); + WidgetsFlutterBinding.ensureInitialized(); + Bloc.observer = AppBlocObserver(); + PerformanceAnalytics.init(); - WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (FlutterErrorDetails details) { + catchUnhandledExceptions(details.exception, details.stack); + }; - await AppBootstrapper.instance.ensureInitialized(); + final KomodoDefiSdk komodoDefiSdk = await mm2.initialize(); - Bloc.observer = AppBlocObserver(); + // This is necessary for now, as the runtime coin updates is reliant on the + // [CoinsBloc] to be initialized before it can be used. This is a temporary + // solution that should be removed once the CoinsBloc is refactored + final currentWalletRepo = CurrentWalletBloc( + kdfSdk: komodoDefiSdk, + encryptionTool: EncryptionTool(), + fileLoader: FileLoader.fromPlatform(), + ); + final trezorRepo = TrezorRepo( + api: Mm2ApiTrezor(mm2.call), + kdfSdk: komodoDefiSdk, + ); + final trezor = TrezorCoinsBloc(trezorRepo: trezorRepo); + final coinsRepo = CoinsRepo( + kdfSdk: komodoDefiSdk, + mm2: mm2, + trezorBloc: trezor, + ); + final mm2Api = Mm2Api(mm2: mm2, coinsRepo: coinsRepo); + final walletsRepository = WalletsRepository( + komodoDefiSdk, + mm2Api, + getStorage(), + ); - PerformanceAnalytics.init(); + await AppBootstrapper.instance.ensureInitialized(komodoDefiSdk); + await initializeLogger(mm2Api); - runApp( - EasyLocalization( - supportedLocales: localeList, - fallbackLocale: localeList.first, - useFallbackTranslations: true, - useOnlyLangCode: true, - path: '$assetsPath/translations', - child: MyApp(), - ), + runApp( + EasyLocalization( + supportedLocales: localeList, + fallbackLocale: localeList.first, + useFallbackTranslations: true, + useOnlyLangCode: true, + path: '$assetsPath/translations', + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider(create: (_) => komodoDefiSdk), + RepositoryProvider(create: (_) => currentWalletRepo), + RepositoryProvider(create: (_) => mm2Api), + RepositoryProvider(create: (_) => coinsRepo), + RepositoryProvider(create: (_) => trezorRepo), + RepositoryProvider(create: (_) => trezor), + RepositoryProvider(create: (_) => walletsRepository), + ], + child: const MyApp(), + ), + ), + ); + }, + catchUnhandledExceptions, ); } +void catchUnhandledExceptions(Object error, StackTrace? stack) { + log('Uncaught exception: $error.\n$stack'); + debugPrintStack(stackTrace: stack, label: error.toString(), maxFrames: 50); + + // Rethrow the error if it has a stacktrace (valid, traceable error) + // async errors from the sdk are not traceable so do not rethrow them. + if (!isTestMode && stack != null && stack.toString().isNotEmpty) { + Error.throwWithStackTrace(error, stack); + } +} + PerformanceMode? _getPerformanceModeFromUrl() { String? maybeEnvPerformanceMode; @@ -83,17 +150,22 @@ PerformanceMode? _getPerformanceModeFromUrl() { } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { + final komodoDefiSdk = RepositoryProvider.of(context); + final walletsRepository = RepositoryProvider.of(context); + return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => AuthBloc(authRepo: authRepo), + create: (_) => AuthBloc(komodoDefiSdk, walletsRepository), ), ], child: app_bloc_root.AppBlocRoot( storedPrefs: _storedSettings!, - runtimeUpdateConfig: _runtimeUpdateConfig!, + komodoDefiSdk: komodoDefiSdk, ), ); } diff --git a/lib/mm2/mm2.dart b/lib/mm2/mm2.dart index 2339603b6c..3d8615084e 100644 --- a/lib/mm2/mm2.dart +++ b/lib/mm2/mm2.dart @@ -1,69 +1,43 @@ -import 'package:komodo_defi_framework/komodo_defi_framework.dart'; +import 'dart:async'; + +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/get_my_peer_id/get_my_peer_id_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_response.dart'; -import 'package:web_dex/shared/utils/password.dart'; import 'package:web_dex/shared/utils/utils.dart'; final MM2 mm2 = MM2(); final class MM2 { MM2() { - final String newRpcPassword = generatePassword(); - - if (!validateRPCPassword(newRpcPassword)) { - log( - "If you're seeing this, there's a bug in the rpcPassword generation code.", - path: 'auth_bloc => _startMM2', - ); - throw Exception('invalid rpc password'); - } - _rpcPassword = newRpcPassword; + _kdfSdk = KomodoDefiSdk(config: const KomodoDefiSdkConfig()); } - late final String _rpcPassword; - late final KomodoDefiFramework _kdf; - Future isSignedIn() => _kdf.isRunning(); + late final KomodoDefiSdk _kdfSdk; + bool _isInitializing = false; + final Completer _initCompleter = Completer(); - Future init() async { - final hostConfig = LocalConfig(rpcPassword: _rpcPassword, https: false); - final startupConfig = await KdfStartupConfig.noAuthStartup( - rpcPassword: _rpcPassword, - ); + Future isSignedIn() => _kdfSdk.auth.isSignedIn(); - _kdf = KomodoDefiFramework.create(hostConfig: hostConfig); - _kdf.startKdf(startupConfig); - } - - Future start({ - String? passphrase, - String? walletName, - String? walletPassword, - }) async { - if (passphrase == null) { - log('Passpharse is null, and SDK is already initialised, ' - 'so skipping KDF start call') - .ignore(); - return; - } + Future initialize() async { + if (_initCompleter.isCompleted) return _kdfSdk; + if (_isInitializing) return _initCompleter.future; - if (await _kdf.isRunning()) { - await _kdf.kdfStop(); + try { + _isInitializing = true; + + await _kdfSdk.initialize(); + // Hack to ensure that kdf is running in noauth mode + await _kdfSdk.auth.getUsers(); + + _initCompleter.complete(_kdfSdk); + return _kdfSdk; + } catch (e) { + _initCompleter.completeError(e); + rethrow; + } finally { + _isInitializing = false; } - - final startupConfig = await KdfStartupConfig.generateWithDefaults( - walletName: walletName ?? '', - walletPassword: walletPassword ?? '', - enableHd: false, - rpcPassword: _rpcPassword, - seed: passphrase, - ); - await _kdf.startKdf(startupConfig); - } - - Future stop() async { - await _kdf.kdfStop(); } Future version() async { @@ -73,56 +47,24 @@ final class MM2 { return response.result; } - Future isLive() async { + Future call(dynamic request) async { try { - final JsonMap response = await call(GetMyPeerIdRequest()); - return (response['result'] as String?)?.isNotEmpty ?? false; - } catch (e, s) { - log( - 'Get my peer id error: $e', - path: 'mm2 => isLive', - trace: s, - isError: true, - ).ignore(); - return false; + final dynamic requestWithUserpass = _assertPass(request); + final JsonMap jsonRequest = requestWithUserpass is Map + ? JsonMap.from(requestWithUserpass) + // ignore: avoid_dynamic_calls + : (requestWithUserpass?.toJson != null + // ignore: avoid_dynamic_calls + ? requestWithUserpass.toJson() as JsonMap + : requestWithUserpass as JsonMap); + + return await _kdfSdk.client.executeRpc(jsonRequest); + } catch (e) { + log('RPC call error: $e', path: 'mm2 => call', isError: true).ignore(); + rethrow; } } - Future call(dynamic request) async { - final dynamic requestWithUserpass = _assertPass(request); - final JsonMap jsonRequest = requestWithUserpass is Map - ? JsonMap.from(requestWithUserpass) - // ignore: avoid_dynamic_calls - : (requestWithUserpass?.toJson != null - // ignore: avoid_dynamic_calls - ? requestWithUserpass.toJson() as JsonMap - : requestWithUserpass as JsonMap); - - final response = await _kdf.client.executeRpc(jsonRequest); - return _deepConvertMap(response as Map); - } - - /// Recursively converts the provided map to JsonMap. This is required, as - /// many of the responses received from the sdk are - /// LinkedHashMap - Map _deepConvertMap(Map map) { - return map.map((key, value) { - if (value is Map) return MapEntry(key.toString(), _deepConvertMap(value)); - if (value is List) { - return MapEntry(key.toString(), _deepConvertList(value)); - } - return MapEntry(key.toString(), value); - }); - } - - List _deepConvertList(List list) { - return list.map((value) { - if (value is Map) return _deepConvertMap(value); - if (value is List) return _deepConvertList(value); - return value; - }).toList(); - } - // this is a necessary evil for now becuase of the RPC models that override // or use the `late String? userpass` field, which would require refactoring // most of the RPC models and directly affected code. @@ -130,14 +72,14 @@ final class MM2 { if (req is List) { for (final dynamic element in req) { // ignore: avoid_dynamic_calls - element.userpass = _rpcPassword; + element.userpass = ''; } } else { if (req is Map) { - req['userpass'] = _rpcPassword; + req['userpass'] = ''; } else { // ignore: avoid_dynamic_calls - req.userpass = _rpcPassword; + req.userpass = ''; } } diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 2c93834309..43ef4552b1 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:collection/collection.dart'; -import 'package:rational/rational.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; @@ -12,16 +12,9 @@ import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/cancel_order/cancel_order_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/electrum/electrum_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable/enable_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable_tendermint/enable_tendermint_token.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/enable_tendermint/enable_tendermint_with_assets.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/kmd_rewards_info/kmd_rewards_info_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/market_maker_bot/market_maker_bot_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; @@ -46,6 +39,7 @@ import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_s import 'package:web_dex/mm2/mm2_api/rpc/rpc_error.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/stop/stop_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.dart'; @@ -53,18 +47,17 @@ import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.d import 'package:web_dex/mm2/mm2_api/rpc/validateaddress/validateaddress_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_request.dart'; -import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/orderbook/orderbook.dart'; +import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/utils/utils.dart'; -final Mm2Api mm2Api = Mm2Api(mm2: mm2); - class Mm2Api { Mm2Api({ required MM2 mm2, + required CoinsRepo coinsRepo, }) : _mm2 = mm2 { - trezor = Mm2ApiTrezor(_call); - nft = Mm2ApiNft(_call); + trezor = Mm2ApiTrezor(_mm2.call); + nft = Mm2ApiNft(_mm2.call, coinsRepo); } final MM2 _mm2; @@ -72,285 +65,12 @@ class Mm2Api { late Mm2ApiNft nft; VersionResponse? _versionResponse; - Future?> getEnabledCoins(List knownCoins) async { - dynamic response; - try { - response = await _call(GetEnabledCoinsReq()); - } catch (e) { - log( - 'Error getting enabled coins: ${e.toString()}', - path: 'api => getEnabledCoins => _call', - isError: true, - ); - return null; - } - - dynamic resultJson; - try { - resultJson = jsonDecode(response)['result']; - } catch (e, s) { - log( - 'Error parsing of enabled coins response: ${e.toString()}', - path: 'api => getEnabledCoins => jsonDecode', - trace: s, - isError: true, - ); - return null; - } - - final List list = []; - if (resultJson is List) { - for (dynamic item in resultJson) { - final Coin? coin = knownCoins.firstWhereOrNull( - (Coin known) => known.abbr == item['ticker'], - ); - - if (coin != null) { - coin.address = item['address']; - list.add(coin); - } - } - } - - return list; - } - - Future enableCoins({ - required List? ethWithTokensRequests, - required List? electrumCoinRequests, - required List? erc20Requests, - required List? tendermintRequests, - required List? tendermintTokenRequests, - required List? bchWithTokens, - required List? slpTokens, - }) async { - if (ethWithTokensRequests != null && ethWithTokensRequests.isNotEmpty) { - await _enableEthWithTokensCoins(ethWithTokensRequests); - } - if (erc20Requests != null && erc20Requests.isNotEmpty) { - await _enableErc20Coins(erc20Requests); - } - if (electrumCoinRequests != null && electrumCoinRequests.isNotEmpty) { - await _enableElectrumCoins(electrumCoinRequests); - } - if (tendermintRequests != null && tendermintRequests.isNotEmpty) { - await _enableTendermintWithAssets(tendermintRequests); - } - if (tendermintTokenRequests != null && tendermintTokenRequests.isNotEmpty) { - await _enableTendermintTokens(tendermintTokenRequests, null); - } - if (bchWithTokens != null && bchWithTokens.isNotEmpty) { - await _enableBchWithTokens(bchWithTokens); - } - if (slpTokens != null && slpTokens.isNotEmpty) { - await _enableSlpTokens(slpTokens); - } - } - - Future _enableEthWithTokensCoins( - List coinRequests, - ) async { - dynamic response; - try { - response = await _call(coinRequests); - log( - response, - path: 'api => _enableEthWithTokensCoins', - ); - } catch (e, s) { - log( - 'Error enabling coins: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => _call', - trace: s, - isError: true, - ); - return; - } - - dynamic json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of enable coins response: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => jsonDecode', - trace: s, - isError: true, - ); - return; - } - - if (json is List) { - for (var item in json) { - if (item['error'] != null) { - log( - item['error'], - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ); - } - } - - return; - } else if (json is Map && json['error'] != null) { - log( - json['error'], - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ); - return; - } - } - - Future _enableErc20Coins(List coinRequests) async { - dynamic response; - try { - response = await _call(coinRequests); - log( - response, - path: 'api => _enableErc20Coins', - ); - } catch (e, s) { - log( - 'Error enabling coins: ${e.toString()}', - path: 'api => _enableErc20Coins => _call', - trace: s, - isError: true, - ); - return; - } - - List json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of enable coins response: ${e.toString()}', - path: 'api => _enableEthWithTokensCoins => jsonDecode', - trace: s, - isError: true, - ); - return; - } - for (dynamic item in json) { - if (item['error'] != null) { - log( - item['error'], - path: 'api => _enableEthWithTokensCoins:', - isError: true, - ); - } - } - } - - Future _enableElectrumCoins(List electrumRequests) async { - try { - final dynamic response = await _call(electrumRequests); - log( - response, - path: 'api => _enableElectrumCoins => _call', - ); - } catch (e, s) { - log( - 'Error enabling electrum coins: ${e.toString()}', - path: 'api => _enableElectrumCoins => _call', - trace: s, - isError: true, - ); - return; - } - } - - Future _enableTendermintWithAssets( - List request, - ) async { - try { - final dynamic response = await _call(request); - log( - response, - path: 'api => _enableTendermintWithAssets => _call', - ); - } catch (e, s) { - log( - 'Error enabling tendermint coins: ${e.toString()}', - path: 'api => _enableTendermintWithAssets => _call', - trace: s, - isError: true, - ); - return; - } - } - - Future _enableTendermintTokens( - List request, - EnableTendermintWithAssetsRequest? tendermintWithAssetsRequest, - ) async { - try { - if (tendermintWithAssetsRequest != null) { - await _call(tendermintWithAssetsRequest); - } - final dynamic response = await _call(request); - log( - response, - path: 'api => _enableTendermintToken => _call', - ); - } catch (e, s) { - log( - 'Error enabling tendermint tokens: ${e.toString()}', - path: 'api => _enableTendermintToken => _call', - trace: s, - isError: true, - ); - return; - } - } - - Future _enableSlpTokens( - List requests, - ) async { - try { - final dynamic response = await _call(requests); - log( - response, - path: 'api => _enableSlpTokens => _call', - ); - } catch (e, s) { - log( - 'Error enabling bch coins: ${e.toString()}', - path: 'api => _enableSlpTokens => _call', - trace: s, - isError: true, - ); - return; - } - } - - Future _enableBchWithTokens( - List requests, - ) async { - try { - final dynamic response = await _call(requests); - log( - response, - path: 'api => _enableBchWithTokens => _call', - ); - } catch (e, s) { - log( - 'Error enabling bch coins: ${e.toString()}', - path: 'api => _enableBchWithTokens => _call', - trace: s, - isError: true, - ); - return; - } - } - - Future disableCoin(String coin) async { + Future disableCoin(String coinId) async { try { - await _call(DisableCoinReq(coin: coin)); + await _mm2.call(DisableCoinReq(coin: coinId)); } catch (e, s) { log( - 'Error disabling $coin: ${e.toString()}', + 'Error disabling $coinId: $e', path: 'api=> disableCoin => _call', trace: s, isError: true, @@ -359,126 +79,6 @@ class Mm2Api { } } - Future getBalance(String abbr) async { - dynamic response; - try { - response = await _call(MyBalanceReq(coin: abbr)); - } catch (e, s) { - log( - 'Error getting balance $abbr: ${e.toString()}', - path: 'api => getBalance => _call', - trace: s, - isError: true, - ); - return null; - } - - Map json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of get balance $abbr response: ${e.toString()}', - path: 'api => getBalance => jsonDecode', - trace: s, - isError: true, - ); - return null; - } - - return json['balance']; - } - - Future getMaxMakerVol(String abbr) async { - dynamic response; - try { - response = await _call(MaxMakerVolRequest(coin: abbr)); - } catch (e, s) { - log( - 'Error getting max maker vol $abbr: ${e.toString()}', - path: 'api => getMaxMakerVol => _call', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - - Map json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of max maker vol $abbr response: ${e.toString()}', - path: 'api => getMaxMakerVol => jsonDecode', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - - final error = json['error']; - if (error != null) { - log( - 'Error parsing of max maker vol $abbr response: ${error.toString()}', - path: 'api => getMaxMakerVol => error', - isError: true, - ); - return _fallbackToBalance(abbr); - } - - try { - return MaxMakerVolResponse.fromJson(json['result']); - } catch (e, s) { - log( - 'Error constructing MaxMakerVolResponse for $abbr: ${e.toString()}', - path: 'api => getMaxMakerVol => fromJson', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - } - - Future _fallbackToBalance(String abbr) async { - final balance = await getBalance(abbr); - if (balance == null) { - log( - 'Failed to retrieve balance for fallback construction of MaxMakerVolResponse for $abbr', - path: 'api => _fallbackToBalance', - isError: true, - ); - return null; - } - - final balanceValue = MaxMakerVolResponseValue(decimal: balance); - return MaxMakerVolResponse( - volume: balanceValue, - balance: balanceValue, - ); - } - - Future _fallbackToBalanceTaker(String abbr) async { - final balance = await getBalance(abbr); - if (balance == null) { - log( - 'Failed to retrieve balance for fallback construction of MaxTakerVolResponse for $abbr', - path: 'api => _fallbackToBalanceTaker', - isError: true, - ); - return null; - } - final rational = Rational.parse(balance); - final result = MaxTakerVolumeResponseResult( - numer: rational.numerator.toString(), - denom: rational.denominator.toString(), - ); - - return MaxTakerVolResponse( - coin: abbr, - result: result, - ); - } - Future?> getActiveSwaps( ActiveSwapsRequest request, ) async { @@ -535,22 +135,29 @@ class Mm2Api { } } - Future?> sendRawTransaction( + Future sendRawTransaction( SendRawTransactionRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + final response = await _mm2.call(request) as Map?; + if (response == null) { + return SendRawTransactionResponse( + txHash: null, + error: TextError(error: 'null response'), + ); + } + return SendRawTransactionResponse.fromJson(response); } catch (e, s) { log( 'Error sending raw transaction ${request.coin}: ${e.toString()}', path: 'api => sendRawTransaction', trace: s, isError: true, + ).ignore(); + return SendRawTransactionResponse( + txHash: null, + error: TextError(error: 'null response'), ); - return null; } } @@ -688,6 +295,10 @@ class Mm2Api { Future getMyOrders() async { try { + if (!await _mm2.isSignedIn()) { + return null; + } + final MyOrdersRequest request = MyOrdersRequest(); final String response = await _call(request); final Map json = jsonDecode(response); @@ -871,6 +482,7 @@ class Mm2Api { Future getOrderBookDepth( List> pairs, + CoinsRepo coinsRepository, ) async { final request = OrderBookDepthReq(pairs: pairs); try { @@ -879,7 +491,7 @@ class Mm2Api { if (json['error'] != null) { return null; } - return OrderBookDepthResponse.fromJson(json); + return OrderBookDepthResponse.fromJson(json, coinsRepository); } catch (e, s) { log( 'Error getting orderbook depth $request: ${e.toString()}', @@ -989,14 +601,49 @@ class Mm2Api { await _call(StopReq()); } - Future _call(dynamic req) async { - final MM2Status mm2Status = await _mm2.status(); - if (mm2Status != MM2Status.rpcIsUp) { - return '{"error": "Error, mm2 status: $mm2Status"}'; + Future showPrivKey( + ShowPrivKeyRequest request, + ) async { + try { + final JsonMap json = await _mm2.call(request); + if (json['error'] != null) { + return null; + } + return ShowPrivKeyResponse.fromJson(json); + } catch (e, s) { + log( + 'Error getting privkey ${request.coin}: ${e.toString()}', + path: 'api => showPrivKey', + trace: s, + isError: true, + ).ignore(); + return null; } + } - final dynamic response = await _mm2.call(req); + Future getDirectlyConnectedPeers( + GetDirectlyConnectedPeers request, + ) async { + try { + final JsonMap json = await _mm2.call(request); + if (json['error'] != null) { + log( + 'Error getting directly connected peers: ${json['error']}', + isError: true, + path: 'api => getDirectlyConnectedPeers', + ).ignore(); + throw Exception('Failed to get directly connected peers'); + } - return response; + return GetDirectlyConnectedPeersResponse.fromJson(json); + } catch (e, s) { + log( + 'Error getting directly connected peers', + path: 'api => getDirectlyConnectedPeers', + trace: s, + isError: true, + ).ignore(); + rethrow; + } } } diff --git a/lib/mm2/mm2_api/mm2_api_nft.dart b/lib/mm2/mm2_api/mm2_api_nft.dart index 71a5096683..43531a314d 100644 --- a/lib/mm2/mm2_api/mm2_api_nft.dart +++ b/lib/mm2/mm2_api/mm2_api_nft.dart @@ -2,21 +2,22 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/get_nft_list/get_nft_list_req.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/nft/update_nft/update_nft_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/refresh_nft_metadata/refresh_nft_metadata_req.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/nft/update_nft/update_nft_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/withdraw/withdraw_nft_request.dart'; import 'package:web_dex/mm2/rpc/nft_transaction/nft_transactions_request.dart'; -import 'package:web_dex/model/nft.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/model/nft.dart'; import 'package:web_dex/shared/constants.dart'; import 'package:web_dex/shared/utils/utils.dart'; class Mm2ApiNft { - Mm2ApiNft(this.call); + Mm2ApiNft(this.call, this._coinsRepo); + final CoinsRepo _coinsRepo; final Future Function(dynamic) call; Future> updateNftList( @@ -27,7 +28,7 @@ class Mm2ApiNft { return { 'error': 'Please ensure an NFT chain is activated and patiently await ' - 'while your NFTs are loaded.' + 'while your NFTs are loaded.' }; } final UpdateNftRequest request = UpdateNftRequest(chains: nftChains); @@ -63,7 +64,8 @@ class Mm2ApiNft { return await call(request); } catch (e) { log(e.toString(), - path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true).ignore(); + path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true) + .ignore(); throw TransportError(message: e.toString()); } } @@ -93,7 +95,8 @@ class Mm2ApiNft { return json; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftList', isError: true).ignore(); + log(e.toString(), path: 'Mm2ApiNft => getNftList', isError: true) + .ignore(); throw TransportError(message: e.toString()); } } @@ -117,7 +120,8 @@ class Mm2ApiNft { } return json; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftTransactions', isError: true).ignore(); + log(e.toString(), path: 'Mm2ApiNft => getNftTransactions', isError: true) + .ignore(); throw TransportError(message: e.toString()); } } @@ -129,31 +133,34 @@ class Mm2ApiNft { .getTxDetailsByHash(request.chain, request.txHash); return additionalTxInfo; } catch (e) { - log(e.toString(), path: 'Mm2ApiNft => getNftTxDetails', isError: true).ignore(); + log(e.toString(), path: 'Mm2ApiNft => getNftTxDetails', isError: true) + .ignore(); throw TransportError(message: e.toString()); } } Future> getActiveNftChains(List chains) async { - final List knownCoins = await coinsRepo.getKnownCoins(); - // log(knownCoins.toString(), path: 'Mm2ApiNft => knownCoins', isError: true); - final List apiCoins = await coinsRepo.getEnabledCoins(knownCoins); + final List apiCoins = await _coinsRepo.getEnabledCoins(); // log(apiCoins.toString(), path: 'Mm2ApiNft => apiCoins', isError: true); final List enabledCoins = apiCoins.map((c) => c.abbr).toList(); log(enabledCoins.toString(), - path: 'Mm2ApiNft => enabledCoins', isError: true).ignore(); + path: 'Mm2ApiNft => enabledCoins', isError: true) + .ignore(); final List nftCoins = chains.map((c) => c.coinAbbr()).toList(); - log(nftCoins.toString(), path: 'Mm2ApiNft => nftCoins', isError: true).ignore(); + log(nftCoins.toString(), path: 'Mm2ApiNft => nftCoins', isError: true) + .ignore(); final List activeChains = chains .map((c) => c) .toList() .where((c) => enabledCoins.contains(c.coinAbbr())) .toList(); log(activeChains.toString(), - path: 'Mm2ApiNft => activeChains', isError: true).ignore(); + path: 'Mm2ApiNft => activeChains', isError: true) + .ignore(); final List nftChains = activeChains.map((c) => c.toApiRequest()).toList(); - log(nftChains.toString(), path: 'Mm2ApiNft => nftChains', isError: true).ignore(); + log(nftChains.toString(), path: 'Mm2ApiNft => nftChains', isError: true) + .ignore(); return nftChains; } } @@ -162,7 +169,8 @@ class ProxyApiNft { static const _errorBaseMessage = 'ProxyApiNft API: '; const ProxyApiNft(); Future> addDetailsToTx(Map json) async { - final transactions = List.from(json['result']['transfer_history'] as List? ?? []); + final transactions = + List.from(json['result']['transfer_history'] as List? ?? []); final listOfAdditionalData = transactions .map((tx) => { 'blockchain': convertChainForProxy(tx['chain'] as String), diff --git a/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart b/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart deleted file mode 100644 index 92779c1613..0000000000 --- a/lib/mm2/mm2_api/rpc/electrum/electrum_req.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:web_dex/common/screen.dart'; -import 'package:web_dex/model/electrum.dart'; - -class ElectrumReq { - ElectrumReq({ - this.mm2 = 1, - required this.coin, - required this.servers, - this.swapContractAddress, - this.fallbackSwapContract, - }); - - static const String method = 'electrum'; - final int mm2; - final String coin; - final List servers; - final String? swapContractAddress; - final String? fallbackSwapContract; - late String userpass; - - Map toJson() { - return { - 'method': method, - 'coin': coin, - 'servers': servers.map((server) => server.toJson()).toList(), - // limit the number of active connections to electrum servers to 1 to - // reduce device load & request spamming. Use 3 connections on desktop - // and 1 on mobile (web and native) using [isMobile] until a better - // alternative is found - // https://komodoplatform.com/en/docs/komodo-defi-framework/api/legacy/coin_activation/#electrum-method - 'max_connected': isMobile ? 1 : 3, - 'userpass': userpass, - 'mm2': mm2, - 'tx_history': true, - if (swapContractAddress != null) - 'swap_contract_address': swapContractAddress, - if (fallbackSwapContract != null) - 'swap_contract_address': swapContractAddress, - }; - } -} diff --git a/lib/mm2/mm2_api/rpc/enable/enable_req.dart b/lib/mm2/mm2_api/rpc/enable/enable_req.dart deleted file mode 100644 index 3be375b9b5..0000000000 --- a/lib/mm2/mm2_api/rpc/enable/enable_req.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/electrum.dart'; - -class EnableEthWithTokensRequest implements BaseRequest { - EnableEthWithTokensRequest({ - required this.coin, - required this.nodes, - required this.swapContractAddress, - required this.fallbackSwapContract, - this.tokens = const [], - }); - - final String coin; - final List nodes; - final String? swapContractAddress; - final String? fallbackSwapContract; - final List tokens; - - @override - final String method = 'enable_eth_with_tokens'; - @override - late String userpass; - - @override - Map toJson() { - return { - 'userpass': userpass, - 'mmrpc': '2.0', - 'method': method, - 'params': { - 'ticker': coin, - 'nodes': nodes.map>((n) => n.toJson()).toList(), - 'swap_contract_address': swapContractAddress, - if (fallbackSwapContract != null) - 'fallback_swap_contract': fallbackSwapContract, - 'erc20_tokens_requests': - tokens.map((t) => {'ticker': t}).toList(), - }, - 'id': 0, - }; - } -} - -class EnableErc20Request implements BaseRequest { - EnableErc20Request({required this.ticker}); - final String ticker; - @override - late String userpass; - @override - final String method = 'enable_erc20'; - - @override - Map toJson() { - return { - 'mmrpc': '2.0', - 'userpass': userpass, - 'method': method, - 'params': { - 'ticker': ticker, - 'activation_params': {}, - }, - 'id': 0 - }; - } -} - -class EnableBchWithTokens implements BaseRequest { - EnableBchWithTokens({ - required this.ticker, - required this.urls, - required this.servers, - }); - final String ticker; - final List urls; - final List servers; - @override - late String userpass; - @override - String get method => 'enable_bch_with_tokens'; - - @override - Map toJson() { - return { - 'mmrpc': '2.0', - 'userpass': userpass, - 'method': method, - 'params': { - 'ticker': ticker, - 'allow_slp_unsafe_conf': false, - 'bchd_urls': urls, - 'mode': { - 'rpc': 'Electrum', - 'rpc_data': { - 'servers': servers.map((server) => server.toJson()).toList(), - } - }, - 'tx_history': true, - 'slp_tokens_requests': [], - } - }; - } -} - -class EnableSlp implements BaseRequest { - EnableSlp({required this.ticker}); - - final String ticker; - @override - late String userpass; - @override - String get method => 'enable_slp'; - - @override - Map toJson() { - return { - 'mmrpc': '2.0', - 'userpass': userpass, - 'method': method, - 'params': {'ticker': ticker, 'activation_params': {}} - }; - } -} diff --git a/lib/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req.dart b/lib/mm2/mm2_api/rpc/get_enabled_coins_request.dart similarity index 100% rename from lib/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req.dart rename to lib/mm2/mm2_api/rpc/get_enabled_coins_request.dart diff --git a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart deleted file mode 100644 index 5b1423db50..0000000000 --- a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart +++ /dev/null @@ -1,20 +0,0 @@ -class MaxMakerVolRequest { - MaxMakerVolRequest({ - required this.coin, - }); - - static const String method = 'max_maker_vol'; - final String coin; - late String userpass; - - Map toJson() { - return { - 'method': method, - 'mmrpc': '2.0', - 'userpass': userpass, - 'params': { - 'coin': coin, - }, - }; - } -} diff --git a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart deleted file mode 100644 index 1d6e685c70..0000000000 --- a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart +++ /dev/null @@ -1,34 +0,0 @@ -class MaxMakerVolResponse { - MaxMakerVolResponse({ - required this.volume, - required this.balance, - }); - - factory MaxMakerVolResponse.fromJson(Map json) => - MaxMakerVolResponse( - volume: MaxMakerVolResponseValue.fromJson( - Map.from(json['volume'] as Map? ?? {}), - ), - balance: MaxMakerVolResponseValue.fromJson( - Map.from(json['balance'] as Map? ?? {}), - ), - ); - - final MaxMakerVolResponseValue volume; - final MaxMakerVolResponseValue balance; -} - -class MaxMakerVolResponseValue { - MaxMakerVolResponseValue({ - required this.decimal, - }); - - factory MaxMakerVolResponseValue.fromJson(Map json) => - MaxMakerVolResponseValue(decimal: json['decimal'] as String); - - final String decimal; - - Map toJson() => { - 'decimal': decimal, - }; -} diff --git a/lib/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart b/lib/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart index 3b990d4290..ab17bb8e22 100644 --- a/lib/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart +++ b/lib/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart @@ -1,16 +1,19 @@ -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; class OrderBookDepthResponse { OrderBookDepthResponse(this.list); - factory OrderBookDepthResponse.fromJson(Map json) { + factory OrderBookDepthResponse.fromJson( + Map json, + CoinsRepo coinsRepository, + ) { final List list = []; final List result = json['result']; for (int i = 0; i < result.length; i++) { final Map item = result[i]; - final pair = OrderBookDepth.fromJson(item); + final pair = OrderBookDepth.fromJson(item, coinsRepository); if (pair != null) list.add(pair); } list.sort((a, b) => a.source.abbr.compareTo(b.source.abbr)); @@ -28,15 +31,18 @@ class OrderBookDepth { int asks; int bids; - static OrderBookDepth? fromJson(Map map) { + static OrderBookDepth? fromJson( + Map map, + CoinsRepo coinsRepository, + ) { final List pair = map['pair']; final Map depth = map['depth']; final String sourceName = (pair[0] ?? '').replaceAll('"', ''); final String targetName = (pair[1] ?? '').replaceAll('"', ''); - final Coin? source = coinsBloc.getCoin(sourceName); - final Coin? target = coinsBloc.getCoin(targetName); + final Coin? source = coinsRepository.getCoin(sourceName); + final Coin? target = coinsRepository.getCoin(targetName); if (source == null || target == null) return null; diff --git a/lib/mm2/mm2_api/rpc/trezor/enable_utxo/trezor_enable_utxo/trezor_enable_utxo_request.dart b/lib/mm2/mm2_api/rpc/trezor/enable_utxo/trezor_enable_utxo/trezor_enable_utxo_request.dart index cde67a4749..0c5f56c268 100644 --- a/lib/mm2/mm2_api/rpc/trezor/enable_utxo/trezor_enable_utxo/trezor_enable_utxo_request.dart +++ b/lib/mm2/mm2_api/rpc/trezor/enable_utxo/trezor_enable_utxo/trezor_enable_utxo_request.dart @@ -1,11 +1,11 @@ -import 'package:web_dex/model/coin.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; class TrezorEnableUtxoReq { TrezorEnableUtxoReq({required this.coin}); static const String method = 'task::enable_utxo::init'; late String userpass; - final Coin coin; + final Asset coin; Map toJson() { return { @@ -13,13 +13,15 @@ class TrezorEnableUtxoReq { 'userpass': userpass, 'mmrpc': '2.0', 'params': { - 'ticker': coin.abbr, + 'ticker': coin.id.id, 'activation_params': { 'tx_history': true, 'mode': { 'rpc': 'Electrum', 'rpc_data': { - 'servers': coin.electrum, + 'servers': coin.protocol.requiredServers.electrum! + .map((server) => server.toJsonRequest()) + .toList(), }, }, 'scan_policy': 'scan_if_new_wallet', diff --git a/lib/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart b/lib/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart index 1f02ed4dc9..2dc9f4d762 100644 --- a/lib/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart +++ b/lib/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart @@ -1,8 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/text_error.dart'; abstract class ErrorNeedsSetCoinAbbr { @@ -198,13 +196,14 @@ class WithdrawTransportError @override void setCoinAbbr(String coinAbbr) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); - if (coin == null) { - return; - } - final String? platform = coin.protocolData?.platform; - - _feeCoin = platform ?? coinAbbr; + // TODO!: reimplemen? + // final Coin? coin = coinsBloc.getCoin(coinAbbr); + // if (coin == null) { + // return; + // } + // final String? platform = coin.protocolData?.platform; + + // _feeCoin = platform ?? coinAbbr; } } diff --git a/lib/model/coin.dart b/lib/model/coin.dart index 3e0a8e4011..af783be988 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -3,11 +3,9 @@ import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/model/cex_price.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/coin_utils.dart'; -import 'package:web_dex/model/electrum.dart'; import 'package:web_dex/model/hd_account/hd_account.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/formatters.dart'; -import 'package:web_dex/shared/utils/utils.dart'; class Coin { Coin({ @@ -22,15 +20,10 @@ class Coin { required this.isTestCoin, required this.coingeckoId, required this.fallbackSwapContract, - required this.electrum, - required this.nodes, - required this.rpcUrls, - required this.bchdUrls, required this.priority, required this.state, this.decimals = 8, this.parentCoin, - this.trezorCoin, this.derivationPath, this.accounts, this.usdPrice, @@ -39,79 +32,15 @@ class Coin { required String? swapContractAddress, required bool walletOnly, required this.mode, + double? balance, }) : _swapContractAddress = swapContractAddress, - _walletOnly = walletOnly; - - factory Coin.fromJson( - Map json, - Map globalCoinJson, - ) { - final List electrumList = _getElectrumFromJson(json); - final List nodesList = _getNodesFromJson(json); - final List bchdUrls = _getBchdUrlsFromJson(json); - final List rpcUrls = _getRpcUrlsFromJson(json); - final String explorerUrl = _getExplorerFromJson(json); - final String explorerTxUrl = _getExplorerTxUrlFromJson(json); - final String explorerAddressUrl = _getExplorerAddressUrlFromJson(json); - - final String? jsonType = json['type']; - final String coinAbbr = json['abbr']; - final CoinType? type = getCoinType(jsonType, coinAbbr); - if (type == null) { - throw ArgumentError.value(jsonType, 'json[\'type\']'); - } - // The code below is commented out because of the latest changes - // to coins config to include "offline" coins so that the user can - // see the coins fail to activate instead of disappearing from the - // We should still figure out if there is a new criteria instead of - // blindly parsing the JSON as-is. - // if (type != CoinType.slp) { - // assert( - // electrumList.isNotEmpty || - // nodesList.isNotEmpty || - // rpcUrls.isNotEmpty || - // bchdUrls.isNotEmpty, - // 'The ${json['abbr']} doesn\'t have electrum, nodes and rpc_urls', - // ); - // } - - return Coin( - type: type, - abbr: coinAbbr, - coingeckoId: json['coingecko_id'], - coinpaprikaId: json['coinpaprika_id'], - name: json['name'], - electrum: electrumList, - nodes: nodesList, - rpcUrls: rpcUrls, - bchdUrls: bchdUrls, - swapContractAddress: json['swap_contract_address'], - fallbackSwapContract: json['fallback_swap_contract'], - activeByDefault: json['active'] ?? false, - explorerUrl: explorerUrl, - explorerTxUrl: explorerTxUrl, - explorerAddressUrl: explorerAddressUrl, - protocolType: _getProtocolType(globalCoinJson), - protocolData: _parseProtocolData(globalCoinJson), - isTestCoin: json['is_testnet'] ?? false, - walletOnly: json['wallet_only'] ?? false, - trezorCoin: globalCoinJson['trezor_coin'], - derivationPath: globalCoinJson['derivation_path'], - decimals: json['decimals'] ?? 8, - priority: json['priority'], - mode: _getCoinMode(json), - state: CoinState.inactive, - ); - } + _walletOnly = walletOnly, + _balance = balance ?? 0; final String abbr; final String name; final String? coingeckoId; final String? coinpaprikaId; - final List electrum; - final List nodes; - final List bchdUrls; - final List rpcUrls; final CoinType type; final bool activeByDefault; final String protocolType; @@ -119,18 +48,17 @@ class Coin { final String explorerUrl; final String explorerTxUrl; final String explorerAddressUrl; - final String? trezorCoin; final String? derivationPath; final int decimals; CexPrice? usdPrice; final bool isTestCoin; String? address; List? accounts; - double _balance = 0; - String? _swapContractAddress; + final double _balance; + final String? _swapContractAddress; String? fallbackSwapContract; WalletType? enabledType; - bool _walletOnly; + final bool _walletOnly; final int priority; Coin? parentCoin; final CoinMode mode; @@ -138,35 +66,6 @@ class Coin { bool get walletOnly => _walletOnly || appWalletOnlyAssetList.contains(abbr); - Map toJson() { - return { - 'coin': abbr, - 'name': name, - 'coingecko_id': coingeckoId, - 'coinpaprika_id': coinpaprikaId, - 'electrum': electrum.map((Electrum e) => e.toJson()).toList(), - 'nodes': nodes.map((CoinNode n) => n.toJson()).toList(), - 'rpc_urls': rpcUrls.map((CoinNode n) => n.toJson()).toList(), - 'bchd_urls': bchdUrls, - 'type': getCoinTypeName(type), - 'active': activeByDefault, - 'protocol': { - 'type': protocolType, - 'protocol_data': protocolData?.toJson(), - }, - 'is_testnet': isTestCoin, - 'wallet_only': walletOnly, - 'trezor_coin': trezorCoin, - 'derivation_path': derivationPath, - 'decimals': decimals, - 'priority': priority, - 'mode': mode.toString(), - 'state': state.toString(), - 'swap_contract_address': _swapContractAddress, - 'fallback_swap_contract': fallbackSwapContract, - }; - } - String? get swapContractAddress => _swapContractAddress ?? parentCoin?.swapContractAddress; bool get isSuspended => state == CoinState.suspended; @@ -185,17 +84,6 @@ class Coin { } } - set balance(double value) { - switch (enabledType) { - case WalletType.trezor: - log('Warning: Trying to set $abbr balance,' - ' while it was activated in ${enabledType!.name} mode. Ignoring.'); - break; - default: - _balance = value; - } - } - double? get _totalHdBalance { if (accounts == null) return null; @@ -211,15 +99,24 @@ class Coin { return totalBalance; } + double calculateUsdAmount(double amount) { + if (usdPrice == null) return 0; + return amount * usdPrice!.price; + } + double? get usdBalance { if (usdPrice == null) return null; if (balance == 0) return 0; - return balance.toDouble() * (usdPrice?.price.toDouble() ?? 0.00); + return calculateUsdAmount(balance.toDouble()); } - String get getFormattedUsdBalance => - usdBalance == null ? '\$0.00' : '\$${formatAmt(usdBalance!)}'; + String amountToFormattedUsd(double amount) { + if (usdPrice == null) return '\$0.00'; + return '\$${formatAmt(calculateUsdAmount(amount))}'; + } + + String get getFormattedUsdBalance => amountToFormattedUsd(balance); String get typeName => getCoinTypeName(type); String get typeNameWithTestnet => typeName + (isTestCoin ? ' (TESTNET)' : ''); @@ -249,7 +146,6 @@ class Coin { bool get hasFaucet => coinsWithFaucet.contains(abbr); bool get hasTrezorSupport { - if (trezorCoin == null) return false; if (excludedAssetListTrezor.contains(abbr)) return false; if (checkSegwitByAbbr(abbr)) return false; if (type == CoinType.utxo) return true; @@ -302,7 +198,6 @@ class Coin { } void reset() { - balance = 0; enabledType = null; accounts = null; state = CoinState.inactive; @@ -320,10 +215,6 @@ class Coin { isTestCoin: isTestCoin, coingeckoId: coingeckoId, fallbackSwapContract: fallbackSwapContract, - electrum: electrum, - nodes: nodes, - rpcUrls: rpcUrls, - bchdUrls: bchdUrls, priority: priority, state: state, swapContractAddress: swapContractAddress, @@ -331,7 +222,6 @@ class Coin { mode: mode, usdPrice: usdPrice, parentCoin: parentCoin, - trezorCoin: trezorCoin, derivationPath: derivationPath, accounts: accounts, coinpaprikaId: coinpaprikaId, @@ -339,80 +229,66 @@ class Coin { protocolData: null, ); } -} - -String _getExplorerFromJson(Map json) { - return json['explorer_url'] ?? ''; -} - -String _getExplorerAddressUrlFromJson(Map json) { - final url = json['explorer_address_url']; - if (url == null || url.isEmpty) { - return 'address/'; - } - return url; -} - -String _getExplorerTxUrlFromJson(Map json) { - final String? url = json['explorer_tx_url']; - if (url == null || url.isEmpty) { - return 'tx/'; - } - return url; -} - -List _getNodesFromJson(Map json) { - final dynamic nodes = json['nodes']; - if (nodes is List) { - return nodes.map((dynamic n) => CoinNode.fromJson(n)).toList(); - } - - return []; -} -List _getRpcUrlsFromJson(Map json) { - final dynamic rpcUrls = json['rpc_urls']; - if (rpcUrls is List) { - return rpcUrls.map((dynamic n) => CoinNode.fromJson(n)).toList(); - } - - return []; -} - -List _getBchdUrlsFromJson(Map json) { - final dynamic urls = json['bchd_urls']; - if (urls is List) { - return List.from(urls); - } - - return []; -} - -List _getElectrumFromJson(Map json) { - final dynamic electrum = json['electrum']; - if (electrum is List) { - return electrum - .map((dynamic item) => Electrum.fromJson(item)) - .toList(); + Coin copyWith({ + CoinType? type, + String? abbr, + String? name, + String? explorerUrl, + String? explorerTxUrl, + String? explorerAddressUrl, + String? protocolType, + ProtocolData? protocolData, + bool? isTestCoin, + String? coingeckoId, + String? fallbackSwapContract, + int? priority, + CoinState? state, + int? decimals, + Coin? parentCoin, + String? derivationPath, + List? accounts, + CexPrice? usdPrice, + String? coinpaprikaId, + bool? activeByDefault, + String? swapContractAddress, + bool? walletOnly, + CoinMode? mode, + String? address, + WalletType? enabledType, + double? balance, + double? sendableBalance, + }) { + return Coin( + type: type ?? this.type, + abbr: abbr ?? this.abbr, + name: name ?? this.name, + explorerUrl: explorerUrl ?? this.explorerUrl, + explorerTxUrl: explorerTxUrl ?? this.explorerTxUrl, + explorerAddressUrl: explorerAddressUrl ?? this.explorerAddressUrl, + protocolType: protocolType ?? this.protocolType, + protocolData: protocolData ?? this.protocolData, + isTestCoin: isTestCoin ?? this.isTestCoin, + coingeckoId: coingeckoId ?? this.coingeckoId, + fallbackSwapContract: fallbackSwapContract ?? this.fallbackSwapContract, + priority: priority ?? this.priority, + state: state ?? this.state, + decimals: decimals ?? this.decimals, + parentCoin: parentCoin ?? this.parentCoin, + derivationPath: derivationPath ?? this.derivationPath, + accounts: accounts ?? this.accounts, + usdPrice: usdPrice ?? this.usdPrice, + coinpaprikaId: coinpaprikaId ?? this.coinpaprikaId, + activeByDefault: activeByDefault ?? this.activeByDefault, + swapContractAddress: swapContractAddress ?? _swapContractAddress, + walletOnly: walletOnly ?? _walletOnly, + mode: mode ?? this.mode, + balance: balance ?? _balance, + ) + ..address = address ?? this.address + ..enabledType = enabledType ?? this.enabledType + ..sendableBalance = sendableBalance ?? this.sendableBalance; } - - return []; -} - -String _getProtocolType(Map coin) { - return coin['protocol']['type']; -} - -ProtocolData? _parseProtocolData(Map json) { - final Map? protocolData = json['protocol']['protocol_data']; - - if (protocolData == null || - protocolData['platform'] == null || - (protocolData['contract_address'] == null && - protocolData['platform'] != 'BCH' && - protocolData['platform'] != 'tBCH' && - protocolData['platform'] != 'IRIS')) return null; - return ProtocolData.fromJson(protocolData); } CoinType? getCoinType(String? jsonType, String coinAbbr) { @@ -538,13 +414,6 @@ CoinType? getCoinType(String? jsonType, String coinAbbr) { return null; } -CoinMode _getCoinMode(Map json) { - if ((json['abbr'] as String).contains('-segwit')) { - return CoinMode.segwit; - } - return CoinMode.standard; -} - class ProtocolData { ProtocolData({ required this.platform, @@ -591,3 +460,9 @@ enum CoinState { suspended, hidden, } + +extension CoinListExtension on List { + Map toMap() { + return Map.fromEntries(map((coin) => MapEntry(coin.abbr, coin))); + } +} diff --git a/lib/model/coin_utils.dart b/lib/model/coin_utils.dart index 935ed12120..ee399051fe 100644 --- a/lib/model/coin_utils.dart +++ b/lib/model/coin_utils.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/mm2/mm2_api/rpc/orderbook_depth/orderbook_depth_response.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; @@ -43,8 +42,8 @@ List removeWalletOnly(List coins) { return list; } -List removeSuspended(List coins) { - if (!coinsBloc.isLoggedIn) return coins; +List removeSuspended(List coins, bool isLoggedIn) { + if (!isLoggedIn) return coins; final List list = List.from(coins); list.removeWhere((Coin coin) => coin.isSuspended); diff --git a/lib/model/electrum.dart b/lib/model/electrum.dart deleted file mode 100644 index 79ef283c9b..0000000000 --- a/lib/model/electrum.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class Electrum { - Electrum({ - required this.url, - required this.protocol, - required this.disableCertVerification, - }); - - factory Electrum.fromJson(Map json) { - return Electrum( - url: kIsWeb ? json['ws_url'] : json['url'], - protocol: kIsWeb ? 'WSS' : (json['protocol'] ?? 'TCP'), - disableCertVerification: json['disable_cert_verification'] ?? false, - ); - } - - final String url; - final String protocol; - final bool disableCertVerification; - - Map toJson() { - return { - 'url': url, - 'protocol': protocol, - 'disable_cert_verification': disableCertVerification, - }; - } -} diff --git a/lib/model/forms/coin_select_input.dart b/lib/model/forms/coin_select_input.dart index f70ebbaa5e..1e9003a0ad 100644 --- a/lib/model/forms/coin_select_input.dart +++ b/lib/model/forms/coin_select_input.dart @@ -39,9 +39,10 @@ class CoinSelectInput extends FormzInput { return CoinSelectValidationError.empty; } - if (!value.isActive) { - return CoinSelectValidationError.inactive; - } + // not applicable, since only enabled coins should be shown /selectable + // if (!value.isActive) { + // return CoinSelectValidationError.inactive; + // } if (value.balance <= minBalance) { return CoinSelectValidationError.insufficientBalance; diff --git a/lib/model/orderbook_model.dart b/lib/model/orderbook_model.dart index b9dcbfd5c2..2d0e0e0470 100644 --- a/lib/model/orderbook_model.dart +++ b/lib/model/orderbook_model.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/orderbook_bloc.dart'; import 'package:web_dex/mm2/mm2_api/rpc/orderbook/orderbook_response.dart'; import 'package:web_dex/model/coin.dart'; @@ -8,12 +8,15 @@ class OrderbookModel { OrderbookModel({ required Coin? base, required Coin? rel, + required this.orderBookRepository, }) { _base = base; _rel = rel; _updateListener(); } + final OrderbookBloc orderBookRepository; + Coin? _base; Coin? get base => _base; set base(Coin? value) { @@ -56,7 +59,8 @@ class OrderbookModel { response = null; if (base == null || rel == null) return; - final stream = orderbookBloc.getOrderbookStream(base!.abbr, rel!.abbr); + final stream = + orderBookRepository.getOrderbookStream(base!.abbr, rel!.abbr); _orderbookListener = stream.listen((resp) => response = resp); } diff --git a/lib/model/swap.dart b/lib/model/swap.dart index e37ff1f2c9..d3576b0f31 100644 --- a/lib/model/swap.dart +++ b/lib/model/swap.dart @@ -141,9 +141,7 @@ class Swap extends Equatable { return 0; case SwapStatus.negotiated: return 0; - default: - } - return 0; + } } static String getSwapStatusString(SwapStatus status) { diff --git a/lib/model/wallet.dart b/lib/model/wallet.dart index 8e817ac382..025c979c18 100644 --- a/lib/model/wallet.dart +++ b/lib/model/wallet.dart @@ -1,3 +1,7 @@ +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/shared/utils/encryption_tool.dart'; class Wallet { @@ -8,18 +12,56 @@ class Wallet { }); factory Wallet.fromJson(Map json) => Wallet( - id: json['id'] ?? '', - name: json['name'] ?? '', - config: WalletConfig.fromJson(json['config']), + id: json['id'] as String? ?? '', + name: json['name'] as String? ?? '', + config: WalletConfig.fromJson( + json['config'] as Map? ?? {}, + ), ); + /// Creates a wallet from a name and the optional parameters. + /// [name] - The name of the wallet. + /// [walletType] - The [WalletType] of the wallet. Defaults to [WalletType.iguana]. + /// [activatedCoins] - The list of activated coins. If not provided, the + /// default list of enabled coins ([enabledByDefaultCoins]) will be used. + /// [hasBackup] - Whether the wallet has been backed up. Defaults to false. + factory Wallet.fromName({ + required String name, + WalletType walletType = WalletType.iguana, + List? activatedCoins, + bool hasBackup = false, + }) { + return Wallet( + id: const Uuid().v1(), + name: name, + config: WalletConfig( + activatedCoins: activatedCoins ?? enabledByDefaultCoins, + hasBackup: hasBackup, + type: walletType, + seedPhrase: '', + ), + ); + } + + /// Creates a wallet from a name and the optional parameters. + factory Wallet.fromConfig({ + required String name, + required WalletConfig config, + }) { + return Wallet( + id: const Uuid().v1(), + name: name, + config: config, + ); + } + String id; String name; WalletConfig config; bool get isHW => config.type != WalletType.iguana; - - Future getSeed(String password) async => + bool get isLegacyWallet => config.isLegacyWallet; + Future getLegacySeed(String password) async => await EncryptionTool().decryptData(password, config.seedPhrase) ?? ''; Map toJson() => { @@ -40,23 +82,33 @@ class Wallet { class WalletConfig { WalletConfig({ required this.seedPhrase, - this.pubKey, required this.activatedCoins, required this.hasBackup, + this.pubKey, this.type = WalletType.iguana, + this.isLegacyWallet = false, }); factory WalletConfig.fromJson(Map json) { return WalletConfig( - type: WalletType.fromJson(json['type'] ?? WalletType.iguana.name), - seedPhrase: json['seed_phrase'], - pubKey: json['pub_key'], + type: WalletType.fromJson( + json['type'] as String? ?? WalletType.iguana.name), + seedPhrase: json['seed_phrase'] as String? ?? '', + pubKey: json['pub_key'] as String?, activatedCoins: - List.from(json['activated_coins'] ?? []).toList(), - hasBackup: json['has_backup'] ?? false, + List.from(json['activated_coins'] as List? ?? []) + .toList(), + hasBackup: json['has_backup'] as bool? ?? false, ); } + String seedPhrase; + String? pubKey; + List activatedCoins; + bool hasBackup; + WalletType type; + bool isLegacyWallet; + Map toJson() { return { 'type': type.name, @@ -67,12 +119,6 @@ class WalletConfig { }; } - String seedPhrase; - String? pubKey; - List activatedCoins; - bool hasBackup; - WalletType type; - WalletConfig copy() { return WalletConfig( activatedCoins: [...activatedCoins], @@ -86,6 +132,7 @@ class WalletConfig { enum WalletType { iguana, + // TODO! add HD wallet type trezor, metamask, keplr; @@ -103,3 +150,80 @@ enum WalletType { } } } + +extension KdfUserWalletExtension on KdfUser { + Wallet get wallet { + final walletType = + WalletType.fromJson(metadata['type'] as String? ?? 'iguana'); + return Wallet( + id: walletId.name, + name: walletId.name, + config: WalletConfig( + seedPhrase: '', + pubKey: walletId.pubkeyHash, + activatedCoins: _parseActivatedCoins(walletType), + hasBackup: metadata['has_backup'] as bool? ?? false, + type: walletType, + ), + ); + } + + List _parseActivatedCoins(WalletType walletType) { + final activatedCoins = + metadata.valueOrNull>('activated_coins'); + if (activatedCoins == null || activatedCoins.isEmpty) { + if (walletType == WalletType.trezor) { + return enabledByDefaultTrezorCoins; + } + + return enabledByDefaultCoins; + } + + return activatedCoins; + } +} + +extension KdfSdkWalletExtension on KomodoDefiSdk { + Future> get wallets async => + (await auth.getUsers()).map((user) => user.wallet); +} + +extension KdfAuthExtension on KomodoDefiSdk { + Future walletExists(String walletId) async { + final users = await auth.getUsers(); + return users.any((user) => user.walletId.name == walletId); + } + + Future currentWallet() async { + final user = await auth.currentUser; + return user?.wallet; + } + + Future addActivatedCoins(Iterable coins) async { + final existingCoins = (await auth.currentUser) + ?.metadata + .valueOrNull>('activated_coins') ?? + []; + + final mergedCoins = {...existingCoins, ...coins}.toList(); + await auth.setOrRemoveActiveUserKeyValue('activated_coins', mergedCoins); + } + + Future removeActivatedCoins(List coins) async { + final existingCoins = (await auth.currentUser) + ?.metadata + .valueOrNull>('activated_coins') ?? + []; + + existingCoins.removeWhere((coin) => coins.contains(coin)); + await auth.setOrRemoveActiveUserKeyValue('activated_coins', existingCoins); + } + + Future confirmSeedBackup({bool hasBackup = true}) async { + await auth.setOrRemoveActiveUserKeyValue('has_backup', true); + } + + Future setWalletType(WalletType type) async { + await auth.setOrRemoveActiveUserKeyValue('type', type.name); + } +} diff --git a/lib/router/navigators/app_router_delegate.dart b/lib/router/navigators/app_router_delegate.dart index 7f18cbb2ff..73b2c2e483 100644 --- a/lib/router/navigators/app_router_delegate.dart +++ b/lib/router/navigators/app_router_delegate.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; +import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; +import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; +import 'package:web_dex/bloc/taker_form/taker_event.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/settings_menu_value.dart'; @@ -31,7 +36,7 @@ class AppRouterDelegate extends RouterDelegate builder: (context) { materialPageContext = context; return GestureDetector( - onTap: () => globalCancelBloc.runDropdownDismiss(context: context), + onTap: () => runDropdownDismiss(context), child: MainLayout(), ); }, @@ -47,6 +52,22 @@ class AppRouterDelegate extends RouterDelegate ); } + void runDropdownDismiss(BuildContext context) { + // Taker form + context.read().add(TakerCoinSelectorOpen(false)); + context.read().add(TakerOrderSelectorOpen(false)); + + // Maker form + final makerFormBloc = RepositoryProvider.of(context); + makerFormBloc.showSellCoinSelect = false; + makerFormBloc.showBuyCoinSelect = false; + + // Bridge form + context.read().add(const BridgeShowTickerDropdown(false)); + context.read().add(const BridgeShowSourceDropdown(false)); + context.read().add(const BridgeShowTargetDropdown(false)); + } + @override Future setNewRoutePath(AppRoutePath configuration) async { final configurationToSet = routingState.isBrowserNavigationBlocked diff --git a/lib/router/parsers/root_route_parser.dart b/lib/router/parsers/root_route_parser.dart index dc19a34730..78eb65c0d4 100644 --- a/lib/router/parsers/root_route_parser.dart +++ b/lib/router/parsers/root_route_parser.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/model/first_uri_segment.dart'; import 'package:web_dex/router/parsers/base_route_parser.dart'; import 'package:web_dex/router/parsers/bridge_route_parser.dart'; @@ -10,14 +11,18 @@ import 'package:web_dex/router/parsers/wallet_route_parser.dart'; import 'package:web_dex/router/routes.dart'; class RootRouteInformationParser extends RouteInformationParser { - final Map _parsers = { - firstUriSegment.wallet: walletRouteParser, - firstUriSegment.fiat: fiatRouteParser, - firstUriSegment.dex: dexRouteParser, - firstUriSegment.bridge: bridgeRouteParser, - firstUriSegment.nfts: nftRouteParser, - firstUriSegment.settings: settingsRouteParser, - }; + RootRouteInformationParser(this.coinsBloc); + + final CoinsBloc coinsBloc; + + Map get _parsers => { + firstUriSegment.wallet: WalletRouteParser(coinsBloc), + firstUriSegment.fiat: fiatRouteParser, + firstUriSegment.dex: dexRouteParser, + firstUriSegment.bridge: bridgeRouteParser, + firstUriSegment.nfts: nftRouteParser, + firstUriSegment.settings: settingsRouteParser, + }; @override Future parseRouteInformation( diff --git a/lib/router/parsers/wallet_route_parser.dart b/lib/router/parsers/wallet_route_parser.dart index 167707fdd3..bd20cf7a25 100644 --- a/lib/router/parsers/wallet_route_parser.dart +++ b/lib/router/parsers/wallet_route_parser.dart @@ -1,10 +1,12 @@ -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/router/parsers/base_route_parser.dart'; import 'package:web_dex/router/routes.dart'; -class _WalletRouteParser implements BaseRouteParser { - const _WalletRouteParser(); +class WalletRouteParser implements BaseRouteParser { + const WalletRouteParser(this._coinsBloc); + + final CoinsBloc _coinsBloc; @override AppRoutePath getRoutePath(Uri uri) { @@ -16,12 +18,10 @@ class _WalletRouteParser implements BaseRouteParser { return WalletRoutePath.action(uri.pathSegments[1]); } - final Coin? coin = coinsBloc.getWalletCoin(uri.pathSegments[1]); + final Coin? coin = _coinsBloc.state.walletCoins[uri.pathSegments[1]]; return coin == null ? WalletRoutePath.wallet() : WalletRoutePath.coinDetails(coin.abbr); } } - -const walletRouteParser = _WalletRouteParser(); diff --git a/lib/router/state/routing_state.dart b/lib/router/state/routing_state.dart index c35b2e784f..d388386e78 100644 --- a/lib/router/state/routing_state.dart +++ b/lib/router/state/routing_state.dart @@ -66,7 +66,9 @@ class RoutingState { if (_mainMenu.selectedMenu != menu) return true; if (_mainMenu.selectedMenu == menu && menu == MainMenuValue.settings && - !isMobile) return false; + !isMobile) { + return false; + } return true; } diff --git a/lib/router/state/settings_section_state.dart b/lib/router/state/settings_section_state.dart index a0683a75c1..f5f67203a7 100644 --- a/lib/router/state/settings_section_state.dart +++ b/lib/router/state/settings_section_state.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/settings_menu_value.dart'; import 'package:web_dex/router/state/menu_state_interface.dart'; @@ -15,7 +14,10 @@ class SettingsSectionState extends ChangeNotifier } final isSecurity = menu == SettingsMenuValue.security; - final showSecurity = currentWalletBloc.wallet?.isHW == false; + // final showSecurity = currentWalletBloc.wallet?.isHW == false; + // TODO! reimplement + const showSecurity = true; + // ignore: dead_code if (isSecurity && !showSecurity) return; _selectedMenu = menu; diff --git a/lib/services/auth_checker/auth_checker.dart b/lib/services/auth_checker/auth_checker.dart deleted file mode 100644 index a89b839bab..0000000000 --- a/lib/services/auth_checker/auth_checker.dart +++ /dev/null @@ -1,5 +0,0 @@ -abstract class AuthChecker { - Future askConfirmLoginIfNeeded(String walletEncryptedSeed); - void addSession(String walletEncryptedSeed); - void removeSession(String walletEncryptedSeed); -} diff --git a/lib/services/auth_checker/get_auth_checker.dart b/lib/services/auth_checker/get_auth_checker.dart deleted file mode 100644 index 3da80344e8..0000000000 --- a/lib/services/auth_checker/get_auth_checker.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/services/auth_checker/auth_checker.dart'; -import 'package:web_dex/services/auth_checker/mock_auth_checker.dart'; -import 'package:web_dex/services/auth_checker/web_auth_checker.dart'; - -final AuthChecker _authChecker = - kIsWeb ? WebAuthChecker(authRepo: authRepo) : MockAuthChecker(); - -AuthChecker getAuthChecker() => _authChecker; diff --git a/lib/services/auth_checker/mock_auth_checker.dart b/lib/services/auth_checker/mock_auth_checker.dart deleted file mode 100644 index a4a062d3a9..0000000000 --- a/lib/services/auth_checker/mock_auth_checker.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:web_dex/services/auth_checker/auth_checker.dart'; - -class MockAuthChecker implements AuthChecker { - @override - Future askConfirmLoginIfNeeded(String? walletEncryptedSeed) async { - return true; - } - - @override - void removeSession(String? walletEncryptedSeed) {} - - @override - void addSession(String walletEncryptedSeed) {} -} diff --git a/lib/services/auth_checker/web_auth_checker.dart b/lib/services/auth_checker/web_auth_checker.dart deleted file mode 100644 index c25cb294b2..0000000000 --- a/lib/services/auth_checker/web_auth_checker.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:universal_html/html.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/services/auth_checker/auth_checker.dart'; - -const _appCloseCommandKey = 'web_dex_command'; - -class WebAuthChecker implements AuthChecker { - WebAuthChecker({required AuthRepository authRepo}) : _authRepo = authRepo { - _initListeners(); - } - - String? _currentSeed; - final AuthRepository _authRepo; - - @override - Future askConfirmLoginIfNeeded(String encryptedSeed) async { - final String localStorageValue = window.localStorage[encryptedSeed] ?? '0'; - final isLoggedIn = int.tryParse(localStorageValue) ?? 0; - if (isLoggedIn == 0) { - return true; - } - - final confirmAnswer = - window.confirm(LocaleKeys.confirmLogoutOnAnotherTab.tr()); - if (confirmAnswer) { - window.localStorage[_appCloseCommandKey] = encryptedSeed; - window.localStorage.remove(_appCloseCommandKey); - - _currentSeed = encryptedSeed; - return true; - } - - return false; - } - - @override - void removeSession(String encryptedSeed) { - if (_currentSeed == encryptedSeed) { - window.localStorage.remove(encryptedSeed); - _currentSeed = null; - } - } - - @override - void addSession(String encryptedSeed) { - window.localStorage.addAll({encryptedSeed: '1'}); - _currentSeed = encryptedSeed; - } - - void _initListeners() { - window.addEventListener( - 'storage', - _onStorageListener, - ); - - window.addEventListener( - 'beforeunload', - _onBeforeUnloadListener, - ); - } - - Future _onStorageListener(Event event) async { - if (event is! StorageEvent) return; - - if (event.key != _appCloseCommandKey) { - return; - } - - if (event.newValue != null && event.newValue == _currentSeed) { - _currentSeed = null; - await _authRepo.logOut(); - } - } - - void _onBeforeUnloadListener(Event event) { - if (event is! BeforeUnloadEvent) return; - final currentSeed = _currentSeed; - if (currentSeed != null) { - removeSession(currentSeed); - } - } -} diff --git a/lib/services/cex_service/cex_service.dart b/lib/services/cex_service/cex_service.dart deleted file mode 100644 index eea5c027ad..0000000000 --- a/lib/services/cex_service/cex_service.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/model/cex_price.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/constants.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -class CexService { - CexService() { - updatePrices(); - _pricesTimer = Timer.periodic(const Duration(minutes: 1), (_) { - updatePrices(); - }); - } - - late Timer _pricesTimer; - final StreamController> _pricesController = - StreamController>.broadcast(); - Stream> get pricesStream => _pricesController.stream; - - Future updatePrices() async { - final prices = await fetchCurrentPrices(); - if (prices == null) return; - - _pricesController.sink.add(prices); - } - - Future?> fetchCurrentPrices() async { - final Map? prices = - await _updateFromMain() ?? await _updateFromFallback(); - - return prices; - } - - Future fetchPrice(String ticker) async { - final Map? prices = await fetchCurrentPrices(); - if (prices == null || !prices.containsKey(ticker)) return null; - - return prices[ticker]!; - } - - void dispose() { - _pricesTimer.cancel(); - _pricesController.close(); - } - - Future?> _updateFromMain() async { - http.Response res; - String body; - try { - res = await http.get(pricesUrlV3); - body = res.body; - } catch (e, s) { - log( - 'Error updating price from main: ${e.toString()}', - path: 'cex_services => _updateFromMain => http.get', - trace: s, - isError: true, - ); - return null; - } - - Map? json; - try { - json = jsonDecode(body); - } catch (e, s) { - log( - 'Error parsing of update price from main response: ${e.toString()}', - path: 'cex_services => _updateFromMain => jsonDecode', - trace: s, - isError: true, - ); - } - - if (json == null) return null; - final Map prices = {}; - json.forEach((String priceTicker, dynamic pricesData) { - prices[priceTicker] = CexPrice( - ticker: priceTicker, - price: double.tryParse(pricesData['last_price'] ?? '') ?? 0, - lastUpdated: DateTime.fromMillisecondsSinceEpoch( - pricesData['last_updated_timestamp'] * 1000, - ), - priceProvider: cexDataProvider(pricesData['price_provider']), - change24h: double.tryParse(pricesData['change_24h'] ?? ''), - changeProvider: cexDataProvider(pricesData['change_24h_provider']), - volume24h: double.tryParse(pricesData['volume24h'] ?? ''), - volumeProvider: cexDataProvider(pricesData['volume_provider']), - ); - }); - return prices; - } - - Future?> _updateFromFallback() async { - final List ids = coinsBloc.walletCoinsMap.values - .map((c) => c.coingeckoId ?? '') - .toList(); - ids.removeWhere((id) => id.isEmpty); - final Uri fallbackUri = Uri.parse( - 'https://api.coingecko.com/api/v3/simple/price?ids=' - '${ids.join(',')}&vs_currencies=usd', - ); - - http.Response res; - String body; - try { - res = await http.get(fallbackUri); - body = res.body; - } catch (e, s) { - log( - 'Error updating price from fallback: ${e.toString()}', - path: 'cex_services => _updateFromFallback => http.get', - trace: s, - isError: true, - ); - return null; - } - - Map? json; - try { - json = jsonDecode(body); - } catch (e, s) { - log( - 'Error parsing of update price from fallback response: ${e.toString()}', - path: 'cex_services => _updateFromFallback => jsonDecode', - trace: s, - isError: true, - ); - } - - if (json == null) return null; - Map prices = {}; - json.forEach((String coingeckoId, dynamic pricesData) { - if (coingeckoId == 'test-coin') return; - - // Coins with the same coingeckoId supposedly have same usd price - // (e.g. KMD == KMD-BEP20) - final Iterable samePriceCoins = - coinsBloc.knownCoins.where((coin) => coin.coingeckoId == coingeckoId); - - for (Coin coin in samePriceCoins) { - prices[coin.abbr] = CexPrice( - ticker: coin.abbr, - price: double.parse(pricesData['usd'].toString()), - ); - } - }); - - return prices; - } -} diff --git a/lib/services/coins_service/coins_service.dart b/lib/services/coins_service/coins_service.dart deleted file mode 100644 index e97a963aba..0000000000 --- a/lib/services/coins_service/coins_service.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:async'; - -import 'package:web_dex/blocs/blocs.dart'; - -final coinsService = CoinsService(); - -class CoinsService { - void init() { - Timer.periodic(const Duration(seconds: 30), (timer) async { - await _reEnableSuspended(); - }); - } - - Future _reEnableSuspended() async { - await coinsBloc.reActivateSuspended(); - } -} diff --git a/lib/services/initializer/app_bootstrapper.dart b/lib/services/initializer/app_bootstrapper.dart index b272b0003a..62ccaf313e 100644 --- a/lib/services/initializer/app_bootstrapper.dart +++ b/lib/services/initializer/app_bootstrapper.dart @@ -9,7 +9,7 @@ final class AppBootstrapper { bool _isInitialized = false; - Future ensureInitialized() async { + Future ensureInitialized(KomodoDefiSdk kdfSdk) async { if (_isInitialized) return; final timer = Stopwatch()..start(); @@ -18,7 +18,7 @@ final class AppBootstrapper { log('AppBootstrapper: Log initialized in ${timer.elapsedMilliseconds}ms'); timer.reset(); - await _warmUpInitializers.awaitAll(); + await _warmUpInitializers().awaitAll(); log('AppBootstrapper: Warm-up initializers completed in ${timer.elapsedMilliseconds}ms'); timer.stop(); @@ -27,25 +27,30 @@ final class AppBootstrapper { /// A list of futures that should be completed before the app starts /// ([runApp]) which do not depend on each other. - final List> _warmUpInitializers = [ - app_bloc_root.loadLibrary(), - packageInformation.init(), - EasyLocalization.ensureInitialized(), - CexMarketData.ensureInitialized(), - PlatformTuner.setWindowTitleAndSize(), - startUpBloc.run(), - SettingsRepository.loadStoredSettings() - .then((stored) => _storedSettings = stored), - RuntimeUpdateConfigProvider() - .getRuntimeUpdateConfig() - .then((config) => _runtimeUpdateConfig = config), - // Hive has to be initialised before runtime coin operations can be used - // in the coins repository - KomodoCoinUpdater.ensureInitialized(appFolder, isWeb: kIsWeb) - .then((_) => coinsBloc.init()) - .then((_) => sparklineRepository.init()), - ]; + List> _warmUpInitializers() { + return [ + app_bloc_root.loadLibrary(), + packageInformation.init(), + EasyLocalization.ensureInitialized(), + CexMarketData.ensureInitialized(), + PlatformTuner.setWindowTitleAndSize(), + SettingsRepository.loadStoredSettings() + .then((stored) => _storedSettings = stored), + _initHive(isWeb: kIsWeb || kIsWasm, appFolder: appFolder).then( + (_) => sparklineRepository.init(), + ), + ]; + } +} + +Future _initHive({required bool isWeb, required String appFolder}) async { + if (isWeb) { + return Hive.initFlutter(appFolder); + } + + final appDirectory = await getApplicationDocumentsDirectory(); + final path = p.join(appDirectory.path, appFolder); + return Hive.init(path); } StoredSettings? _storedSettings; -RuntimeUpdateConfig? _runtimeUpdateConfig; diff --git a/lib/services/logger/get_logger.dart b/lib/services/logger/get_logger.dart index 130dd27585..f799256cba 100644 --- a/lib/services/logger/get_logger.dart +++ b/lib/services/logger/get_logger.dart @@ -1,10 +1,14 @@ import 'dart:io'; +import 'package:dragon_logs/dragon_logs.dart'; import 'package:flutter/foundation.dart'; +import 'package:web_dex/app_config/package_information.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/services/logger/logger.dart'; import 'package:web_dex/services/logger/mock_logger.dart'; import 'package:web_dex/services/logger/universal_logger.dart'; import 'package:web_dex/services/platform_info/plaftorm_info.dart'; +import 'package:web_dex/services/storage/get_storage.dart'; final LoggerInterface logger = _getLogger(); LoggerInterface _getLogger() { @@ -21,3 +25,18 @@ LoggerInterface _getLogger() { return const MockLogger(); } + +Future initializeLogger(Mm2Api mm2Api) async { + final platformInfo = PlatformInfo.getInstance(); + final localeName = + await getStorage().read('locale').catchError((_) => null) as String? ?? + ''; + DragonLogs.setSessionMetadata({ + 'appVersion': packageInformation.packageVersion, + 'mm2Version': await mm2Api.version(), + 'appLanguage': localeName, + 'platform': platformInfo.platform, + 'osLanguage': platformInfo.osLanguage, + 'screenSize': platformInfo.screenSize, + }); +} diff --git a/lib/services/logger/logger_metadata_mixin.dart b/lib/services/logger/logger_metadata_mixin.dart index 2fb9e4c0b4..1cd08409eb 100644 --- a/lib/services/logger/logger_metadata_mixin.dart +++ b/lib/services/logger/logger_metadata_mixin.dart @@ -22,7 +22,7 @@ mixin LoggerMetadataMixin { ); } - FutureOr apiVersion() { + FutureOr apiVersion(Mm2Api mm2Api) { if (_apiVersion != null) return _apiVersion; return Future( diff --git a/lib/services/logger/universal_logger.dart b/lib/services/logger/universal_logger.dart index e75464c2cd..c2afae8979 100644 --- a/lib/services/logger/universal_logger.dart +++ b/lib/services/logger/universal_logger.dart @@ -27,15 +27,6 @@ class UniversalLogger with LoggerMetadataMixin implements LoggerInterface { try { await DragonLogs.init(); - DragonLogs.setSessionMetadata({ - 'appVersion': packageInformation.packageVersion, - 'mm2Version': await apiVersion(), - 'appLanguage': await localeName(), - 'platform': platformInfo.platform, - 'osLanguage': platformInfo.osLanguage, - 'screenSize': platformInfo.screenSize, - }); - initialised_logger .log('Logger initialized in ${timer.elapsedMilliseconds}ms'); @@ -59,7 +50,7 @@ class UniversalLogger with LoggerMetadataMixin implements LoggerInterface { final LogMessage logMessage = LogMessage( path: path, appVersion: packageInformation.packageVersion ?? '', - mm2Version: await apiVersion(), + mm2Version: DragonLogs.sessionMetadata?['mm2Version'], appLocale: await localeName(), platform: platformInfo.platform, osLanguage: platformInfo.osLanguage, diff --git a/lib/services/orders_service/my_orders_service.dart b/lib/services/orders_service/my_orders_service.dart index 6966c632cf..15da6e1ef7 100644 --- a/lib/services/orders_service/my_orders_service.dart +++ b/lib/services/orders_service/my_orders_service.dart @@ -7,11 +7,13 @@ import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/model/my_orders/taker_order.dart'; import 'package:web_dex/services/mappers/my_orders_mappers.dart'; -MyOrdersService myOrdersService = MyOrdersService(); - class MyOrdersService { + MyOrdersService(this._mm2Api); + + final Mm2Api _mm2Api; + Future?> getOrders() async { - final MyOrdersResponse? response = await mm2Api.getMyOrders(); + final MyOrdersResponse? response = await _mm2Api.getMyOrders(); if (response == null) { return null; @@ -22,7 +24,7 @@ class MyOrdersService { Future getStatus(String uuid) async { try { - final OrderStatusResponse? response = await mm2Api.getOrderStatus(uuid); + final OrderStatusResponse? response = await _mm2Api.getOrderStatus(uuid); if (response == null) { return null; } @@ -51,7 +53,7 @@ class MyOrdersService { Future cancelOrder(String uuid) async { final Map response = - await mm2Api.cancelOrder(CancelOrderRequest(uuid: uuid)); + await _mm2Api.cancelOrder(CancelOrderRequest(uuid: uuid)); return response['error']; } diff --git a/lib/services/swaps_service/swaps_service.dart b/lib/services/swaps_service/swaps_service.dart deleted file mode 100644 index 2ba6e2fecb..0000000000 --- a/lib/services/swaps_service/swaps_service.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:rational/rational.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_swap_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_swap_response.dart'; -import 'package:web_dex/model/swap.dart'; -import 'package:web_dex/shared/utils/utils.dart'; - -SwapsService swapsService = SwapsService(); - -class SwapsService { - Future?> getRecentSwaps(MyRecentSwapsRequest request) async { - final MyRecentSwapsResponse? response = - await mm2Api.getMyRecentSwaps(request); - if (response == null) { - return null; - } - - return response.result.swaps; - } - - Future recoverFundsOfSwap(String uuid) async { - final RecoverFundsOfSwapRequest request = - RecoverFundsOfSwapRequest(uuid: uuid); - final RecoverFundsOfSwapResponse? response = - await mm2Api.recoverFundsOfSwap(request); - if (response != null) { - log( - response.toJson().toString(), - path: 'swaps_service => recoverFundsOfSwap', - ); - } - return response; - } - - Future getMaxTakerVolume(String coinAbbr) async { - final MaxTakerVolResponse? response = - await mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr)); - if (response == null) { - return null; - } - - return fract2rat(response.result.toJson()); - } -} diff --git a/lib/shared/utils/debug_utils.dart b/lib/shared/utils/debug_utils.dart index 3d38c45a4b..38fbf905bc 100644 --- a/lib/shared/utils/debug_utils.dart +++ b/lib/shared/utils/debug_utils.dart @@ -4,14 +4,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:uuid/uuid.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_event.dart'; -import 'package:web_dex/bloc/wallets_bloc/wallets_repo.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/shared/utils/encryption_tool.dart'; -Future initDebugData(AuthBloc authBloc) async { +Future initDebugData( + AuthBloc authBloc, + WalletsRepository walletsRepository, +) async { try { final String testWalletStr = await rootBundle.loadString('assets/debug_data.json'); @@ -21,19 +20,23 @@ Future initDebugData(AuthBloc authBloc) async { return; } - final Wallet? debugWallet = await _createDebugWallet( - newWalletJson, - hasBackup: true, - ); - if (debugWallet == null) { - return; - } if (newWalletJson['automateLogin'] == true) { - authBloc.add(AuthReLogInEvent( - seed: newWalletJson['seed'], - wallet: debugWallet, - password: newWalletJson['password'], - )); + final Wallet? debugWallet = await _createDebugWallet( + walletsRepository, + newWalletJson, + hasBackup: true, + ); + if (debugWallet == null) { + return; + } + + authBloc.add( + AuthRestoreRequested( + seed: newWalletJson['seed'], + wallet: debugWallet, + password: newWalletJson["password"], + ), + ); } } catch (e) { return; @@ -41,39 +44,28 @@ Future initDebugData(AuthBloc authBloc) async { } Future _createDebugWallet( + WalletsRepository walletsBloc, Map walletJson, { bool hasBackup = false, }) async { - final wallets = await walletsRepo.getAll(); + final wallets = walletsBloc.wallets; final Wallet? existedDebugWallet = - wallets.firstWhereOrNull((w) => w.name == walletJson['name']); + wallets?.firstWhereOrNull((w) => w.name == walletJson['name']); if (existedDebugWallet != null) return existedDebugWallet; - final EncryptionTool encryptionTool = EncryptionTool(); final String name = walletJson['name']; - final String seed = walletJson['seed']; - final String password = walletJson['password']; final List activatedCoins = List.from(walletJson['activated_coins'] ?? []); - final String encryptedSeed = await encryptionTool.encryptData(password, seed); - - final Wallet wallet = Wallet( + return Wallet( id: const Uuid().v1(), name: name, config: WalletConfig( - seedPhrase: encryptedSeed, activatedCoins: activatedCoins, hasBackup: hasBackup, + seedPhrase: walletJson['seed'], ), ); - final bool isSuccess = await walletsRepo.save(wallet); - return isSuccess ? wallet : null; -} - -Future importSwapsData(List swapsJson) async { - final ImportSwapsRequest request = ImportSwapsRequest(swaps: swapsJson); - await mm2Api.importSwaps(request); } Future?> loadDebugSwaps() async { diff --git a/lib/shared/utils/extensions/async_extensions.dart b/lib/shared/utils/extensions/async_extensions.dart index b6d8a0fa53..2c97b7ff08 100644 --- a/lib/shared/utils/extensions/async_extensions.dart +++ b/lib/shared/utils/extensions/async_extensions.dart @@ -7,17 +7,3 @@ extension WaitAllFutures on List> { Future> awaitAll() => Future.wait(this); } -extension AsyncRemoveWhere on List { - Future removeWhereAsync(Future Function(T element) test) async { - final List> futures = map(test).toList(); - final List results = await Future.wait(futures); - - final List newList = [ - for (int i = 0; i < length; i++) - if (!results[i]) this[i], - ]; - - clear(); - addAll(newList); - } -} diff --git a/lib/shared/utils/formatters.dart b/lib/shared/utils/formatters.dart index aaa2e618b4..d1ce7628c8 100644 --- a/lib/shared/utils/formatters.dart +++ b/lib/shared/utils/formatters.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/shared/constants.dart'; +import 'package:komodo_defi_types/types.dart'; final List currencyInputFormatters = [ DecimalTextInputFormatter(decimalRange: decimalRange), @@ -340,3 +341,15 @@ String truncateMiddleSymbols(String text, final String secondPart = text.substring(text.length - endCount, text.length); return '$firstPart...$secondPart'; } + +String formatTransactionDateTime(Transaction tx) { + if (tx.timestamp == DateTime.fromMillisecondsSinceEpoch(0) && + tx.confirmations == 0) { + return 'unconfirmed'; + } else if (tx.timestamp == DateTime.fromMillisecondsSinceEpoch(0) && + tx.confirmations > 0) { + return 'confirmed'; + } else { + return DateFormat('dd MMM yyyy HH:mm').format(tx.timestamp); + } +} diff --git a/lib/shared/utils/password.dart b/lib/shared/utils/password.dart index 9d15540f4d..761ce9011a 100644 --- a/lib/shared/utils/password.dart +++ b/lib/shared/utils/password.dart @@ -27,7 +27,9 @@ String generatePassword() { if (tab.contains(lowerCase) && tab.contains(upperCase) && tab.contains(digit) && - tab.contains(punctuation)) break; + tab.contains(punctuation)) { + break; + } } for (int i = 0; i < tab.length; i++) { diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 8b1005f1ac..ece4cc2aeb 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -7,6 +7,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:rational/rational.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web_dex/common/screen.dart'; @@ -648,3 +650,12 @@ enum HashExplorerType { address, tx, } + +Asset getSdkAsset(KomodoDefiSdk? sdk, String abbr) { + if (sdk == null) { + throw Exception('getSdkAsset: SDK is null'); + } + + // ignore: deprecated_member_use + return sdk.assets.assetsFromTicker(abbr).single; +} diff --git a/lib/shared/widgets/auto_scroll_text.dart b/lib/shared/widgets/auto_scroll_text.dart index 87523f1a1a..364fd12c16 100644 --- a/lib/shared/widgets/auto_scroll_text.dart +++ b/lib/shared/widgets/auto_scroll_text.dart @@ -128,7 +128,7 @@ class _AutoScrollTextState extends State clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: isTextAnimatable && animationDebugMode - ? Colors.purple.withOpacity(0.5) + ? Colors.purple.withValues(alpha: 0.5) : null, ), width: double.infinity, @@ -267,7 +267,8 @@ class _AutoScrollTextState extends State ); if (mustHighlightBackground) { - style = style.copyWith(backgroundColor: Colors.red.withOpacity(0.3)); + style = + style.copyWith(backgroundColor: Colors.red.withValues(alpha: 0.3)); } return _renderedTextStyle = style; diff --git a/lib/shared/widgets/coin_item/coin_logo.dart b/lib/shared/widgets/coin_item/coin_logo.dart index 993599053c..60511ed439 100644 --- a/lib/shared/widgets/coin_item/coin_logo.dart +++ b/lib/shared/widgets/coin_item/coin_logo.dart @@ -59,7 +59,7 @@ class _ProtocolIcon extends StatelessWidget { color: Colors.white, shape: BoxShape.circle, boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 2) + BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 2) ], ), child: Container( diff --git a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart index 2b86d63944..6bb41cefec 100644 --- a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart +++ b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart @@ -110,7 +110,7 @@ class _ConnectWalletButtonState extends State { onSuccess: (_) async { takerBloc.add(TakerReInit()); bridgeBloc.add(const BridgeReInit()); - await reInitTradingForms(); + await reInitTradingForms(context); _popupDispatcher?.close(); }, ), diff --git a/lib/shared/widgets/hidden_with_wallet.dart b/lib/shared/widgets/hidden_with_wallet.dart index 87e9a7efc8..77005a3d7f 100644 --- a/lib/shared/widgets/hidden_with_wallet.dart +++ b/lib/shared/widgets/hidden_with_wallet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; -import 'package:web_dex/model/wallet.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; class HiddenWithWallet extends StatelessWidget { const HiddenWithWallet({Key? key, required this.child}) : super(key: key); @@ -8,14 +8,8 @@ class HiddenWithWallet extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - initialData: currentWalletBloc.wallet, - stream: currentWalletBloc.outWallet, - builder: (BuildContext context, - AsyncSnapshot currentWalletSnapshot) { - return currentWalletSnapshot.data == null - ? child - : const SizedBox.shrink(); - }); + return BlocBuilder(builder: (context, state) { + return state.currentUser == null ? child : const SizedBox.shrink(); + }); } } diff --git a/lib/shared/widgets/hidden_without_wallet.dart b/lib/shared/widgets/hidden_without_wallet.dart index 1dd431d229..acdcd9b1b0 100644 --- a/lib/shared/widgets/hidden_without_wallet.dart +++ b/lib/shared/widgets/hidden_without_wallet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/model/wallet.dart'; class HiddenWithoutWallet extends StatelessWidget { @@ -11,21 +12,17 @@ class HiddenWithoutWallet extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - initialData: currentWalletBloc.wallet, - stream: currentWalletBloc.outWallet, - builder: (BuildContext context, - AsyncSnapshot currentWalletSnapshot) { - final Wallet? currentWallet = currentWalletSnapshot.data; - if (currentWallet == null) { - return const SizedBox.shrink(); - } + return BlocBuilder(builder: (context, state) { + final Wallet? currentWallet = state.currentUser?.wallet; + if (currentWallet == null) { + return const SizedBox.shrink(); + } - if (isHiddenForHw && currentWallet.isHW) { - return const SizedBox.shrink(); - } + if (isHiddenForHw && currentWallet.isHW) { + return const SizedBox.shrink(); + } - return child; - }); + return child; + }); } } diff --git a/lib/shared/widgets/logout_popup.dart b/lib/shared/widgets/logout_popup.dart index 2d7ec935e0..50eab6c528 100644 --- a/lib/shared/widgets/logout_popup.dart +++ b/lib/shared/widgets/logout_popup.dart @@ -1,12 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class LogOutPopup extends StatelessWidget { const LogOutPopup({ @@ -19,54 +18,63 @@ class LogOutPopup extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxWidth: 300), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SelectableText( - LocaleKeys.logoutPopupTitle.tr(), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 12), - if (currentWalletBloc.wallet?.config.type == WalletType.iguana) - SelectableText( - LocaleKeys.logoutPopupDescription.tr(), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 25), - Row( + return BlocBuilder( + builder: (context, state) { + final currentWallet = state.currentUser?.wallet; + return Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - UiUnderlineTextButton( - key: const Key('popup-cancel-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.cancel.tr(), - onPressed: onCancel, + SelectableText( + LocaleKeys.logoutPopupTitle.tr(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), ), - const SizedBox(width: 12), - UiPrimaryButton( - key: const Key('popup-confirm-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.logOut.tr(), - onPressed: () { - context.read().add(const AuthLogOutEvent()); - onConfirm(); - }, + const SizedBox(height: 12), + if (currentWallet?.config.type == WalletType.iguana) + SelectableText( + LocaleKeys.logoutPopupDescription.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 25), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + UiUnderlineTextButton( + key: const Key('popup-cancel-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.cancel.tr(), + onPressed: onCancel, + ), + const SizedBox(width: 12), + UiPrimaryButton( + key: const Key('popup-confirm-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.logOut.tr(), + onPressed: () => _onConfirmLogout(context), + ), + ], ), ], ), - ], - ), + ); + }, ); } + + void _onConfirmLogout(BuildContext context) { + // stop listening to balance updates before logging out + context.read().add(CoinsSessionEnded()); + context.read().add(const AuthSignOutRequested()); + onConfirm(); + } } diff --git a/lib/shared/widgets/password_visibility_control.dart b/lib/shared/widgets/password_visibility_control.dart index e6e3ba9bae..3145ebe572 100644 --- a/lib/shared/widgets/password_visibility_control.dart +++ b/lib/shared/widgets/password_visibility_control.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; + import 'package:flutter/material.dart'; /// #644: We want the password to be obscured most of the time @@ -87,7 +88,7 @@ class _PasswordVisibilityControlState extends State { .textTheme .bodyMedium ?.color - ?.withOpacity(0.7)), + ?.withValues(alpha: 0.7)), ), ), ); diff --git a/lib/views/bridge/bridge_confirmation.dart b/lib/views/bridge/bridge_confirmation.dart index f4b8803277..10b3d338c9 100644 --- a/lib/views/bridge/bridge_confirmation.dart +++ b/lib/views/bridge/bridge_confirmation.dart @@ -7,7 +7,8 @@ import 'package:rational/rational.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -41,14 +42,17 @@ class _BridgeOrderConfirmationState extends State { context.read().add(const BridgeClear()); routingState.bridgeState.setDetailsAction(swapUuid); + final tradingEntitiesBloc = + RepositoryProvider.of(context); await tradingEntitiesBloc.fetch(); }, builder: (BuildContext context, BridgeState state) { final TradePreimage? preimage = state.preimageData?.data; if (preimage == null) return const UiSpinner(); + final coinsRepo = RepositoryProvider.of(context); - final Coin? sellCoin = coinsBloc.getCoin(preimage.request.base); - final Coin? buyCoin = coinsBloc.getCoin(preimage.request.rel); + final Coin? sellCoin = coinsRepo.getCoin(preimage.request.base); + final Coin? buyCoin = coinsRepo.getCoin(preimage.request.rel); final Rational? sellAmount = preimage.request.volume; final Rational buyAmount = (sellAmount ?? Rational.zero) * preimage.request.price; @@ -241,6 +245,7 @@ class _SendGroup extends StatelessWidget { fontSize: 14.0, fontWeight: FontWeight.w500, ); + final coinsBloc = RepositoryProvider.of(context); final Coin? coin = coinsBloc.getCoin(dto.sellCoin.abbr); if (coin == null) return const SizedBox.shrink(); diff --git a/lib/views/bridge/bridge_exchange_form.dart b/lib/views/bridge/bridge_exchange_form.dart index 2739b12f44..d4f4d13479 100644 --- a/lib/views/bridge/bridge_exchange_form.dart +++ b/lib/views/bridge/bridge_exchange_form.dart @@ -1,16 +1,15 @@ -import 'dart:async'; - import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/widgets/connect_wallet/connect_wallet_wrapper.dart'; import 'package:web_dex/views/bridge/bridge_group.dart'; @@ -25,7 +24,6 @@ import 'package:web_dex/views/bridge/view/bridge_target_amount_row.dart'; import 'package:web_dex/views/bridge/view/bridge_target_protocol_row.dart'; import 'package:web_dex/views/bridge/view/error_list/bridge_form_error_list.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class BridgeExchangeForm extends StatefulWidget { const BridgeExchangeForm({Key? key}) : super(key: key); @@ -35,53 +33,49 @@ class BridgeExchangeForm extends StatefulWidget { } class _BridgeExchangeFormState extends State { - StreamSubscription? _coinsListener; - @override void initState() { final bridgeBloc = context.read(); + final coinsBlocState = context.read().state; bridgeBloc.add(const BridgeInit(ticker: defaultDexCoin)); - bridgeBloc.add(BridgeSetWalletIsReady(coinsBloc.loginActivationFinished)); - _coinsListener = coinsBloc.outLoginActivationFinished.listen((value) { - bridgeBloc.add(BridgeSetWalletIsReady(value)); - }); - + bridgeBloc + .add(BridgeSetWalletIsReady(coinsBlocState.loginActivationFinished)); super.initState(); } - @override - void dispose() { - _coinsListener?.cancel(); - - super.dispose(); - } - @override Widget build(BuildContext context) { - return const Column( - mainAxisSize: MainAxisSize.max, - children: [ - BridgeTickerSelector(), - SizedBox(height: 30), - BridgeGroup( - header: SourceProtocolHeader(), - child: SourceProtocol(), - ), - SizedBox(height: 19), - BridgeGroup( - header: TargetProtocolHeader(), - child: TargetProtocol(), - ), - SizedBox(height: 12), - BridgeFormErrorList(), - SizedBox(height: 12), - BridgeExchangeRate(), - SizedBox(height: 12), - BridgeTotalFees(), - SizedBox(height: 24), - _ExchangeButton(), - SizedBox(height: 12), - ], + final bridgeBloc = context.read(); + return BlocListener( + listenWhen: (previous, current) => + previous.loginActivationFinished != current.loginActivationFinished, + listener: (context, state) => + bridgeBloc.add(BridgeSetWalletIsReady(state.loginActivationFinished)), + child: const Column( + mainAxisSize: MainAxisSize.max, + children: [ + BridgeTickerSelector(), + SizedBox(height: 30), + BridgeGroup( + header: SourceProtocolHeader(), + child: SourceProtocol(), + ), + SizedBox(height: 19), + BridgeGroup( + header: TargetProtocolHeader(), + child: TargetProtocol(), + ), + SizedBox(height: 12), + BridgeFormErrorList(), + SizedBox(height: 12), + BridgeExchangeRate(), + SizedBox(height: 12), + BridgeTotalFees(), + SizedBox(height: 24), + _ExchangeButton(), + SizedBox(height: 12), + ], + ), ); } } diff --git a/lib/views/bridge/bridge_page.dart b/lib/views/bridge/bridge_page.dart index 5726152e2f..b4cb4a7f49 100644 --- a/lib/views/bridge/bridge_page.dart +++ b/lib/views/bridge/bridge_page.dart @@ -2,7 +2,6 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/swap.dart'; diff --git a/lib/views/bridge/bridge_tab_bar.dart b/lib/views/bridge/bridge_tab_bar.dart index 1d02c553a0..0b111b047e 100644 --- a/lib/views/bridge/bridge_tab_bar.dart +++ b/lib/views/bridge/bridge_tab_bar.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab.dart'; import 'package:web_dex/shared/ui/ui_tab_bar/ui_tab_bar.dart'; @@ -27,6 +28,8 @@ class _BridgeTabBarState extends State { void initState() { _onDataChange(null); + final tradingEntitiesBloc = + RepositoryProvider.of(context); _listeners.add(tradingEntitiesBloc.outMyOrders.listen(_onDataChange)); _listeners.add(tradingEntitiesBloc.outSwaps.listen(_onDataChange)); @@ -70,6 +73,8 @@ class _BridgeTabBarState extends State { void _onDataChange(dynamic _) { if (!mounted) return; + final tradingEntitiesBloc = + RepositoryProvider.of(context); setState(() { _inProgressCount = tradingEntitiesBloc.swaps .where((swap) => !swap.isCompleted && swap.isTheSameTicker) diff --git a/lib/views/bridge/bridge_target_protocol_selector_tile.dart b/lib/views/bridge/bridge_target_protocol_selector_tile.dart index ccfff94ac1..baf11cc817 100644 --- a/lib/views/bridge/bridge_target_protocol_selector_tile.dart +++ b/lib/views/bridge/bridge_target_protocol_selector_tile.dart @@ -2,7 +2,7 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/bridge/bridge_protocol_label.dart'; @@ -34,10 +34,11 @@ class _BridgeTargetProtocolSelectorTileState bool get noSelected => widget.coin == null && widget.bestOrder == null; Coin? get coin { + final coinsRepository = RepositoryProvider.of(context); final widgetCoin = widget.coin; if (widgetCoin != null) return widgetCoin; final bestOrder = widget.bestOrder; - if (bestOrder != null) return coinsBloc.getCoin(bestOrder.coin); + if (bestOrder != null) return coinsRepository.getCoin(bestOrder.coin); return null; } diff --git a/lib/views/bridge/view/bridge_target_amount_row.dart b/lib/views/bridge/view/bridge_target_amount_row.dart index ef0d50d2a4..6077678363 100644 --- a/lib/views/bridge/view/bridge_target_amount_row.dart +++ b/lib/views/bridge/view/bridge_target_amount_row.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/views/dex/common/trading_amount_field.dart'; @@ -66,6 +66,7 @@ class _FiatAmount extends StatelessWidget { @override Widget build(BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); return BlocBuilder( buildWhen: (prev, cur) { return prev.bestOrder != cur.bestOrder || @@ -73,7 +74,7 @@ class _FiatAmount extends StatelessWidget { }, builder: (context, state) { final String? abbr = state.bestOrder?.coin; - final Coin? coin = abbr == null ? null : coinsBloc.getCoin(abbr); + final Coin? coin = abbr == null ? null : coinsRepository.getCoin(abbr); return DexFiatAmount( coin: coin, diff --git a/lib/views/bridge/view/table/bridge_target_protocols_table.dart b/lib/views/bridge/view/table/bridge_target_protocols_table.dart index 09024c1351..0505c5f8da 100644 --- a/lib/views/bridge/view/table/bridge_target_protocols_table.dart +++ b/lib/views/bridge/view/table/bridge_target_protocols_table.dart @@ -5,7 +5,7 @@ import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; @@ -112,6 +112,7 @@ class _TargetProtocolItems extends StatelessWidget { if (targetsList.isEmpty) return BridgeNothingFound(); final scrollController = ScrollController(); + final coinsRepository = RepositoryProvider.of(context); return Column( mainAxisSize: MainAxisSize.min, @@ -128,7 +129,7 @@ class _TargetProtocolItems extends StatelessWidget { shrinkWrap: true, itemBuilder: (BuildContext context, int index) { final BestOrder order = targetsList[index]; - final Coin coin = coinsBloc.getCoin(order.coin)!; + final Coin coin = coinsRepository.getCoin(order.coin)!; return BridgeProtocolTableOrderItem( index: index, diff --git a/lib/views/bridge/view/table/bridge_tickers_list.dart b/lib/views/bridge/view/table/bridge_tickers_list.dart index 449a0eed25..7ae3e0a1f9 100644 --- a/lib/views/bridge/view/table/bridge_tickers_list.dart +++ b/lib/views/bridge/view/table/bridge_tickers_list.dart @@ -49,7 +49,7 @@ class _BridgeTickersListState extends State { border: Border.all(width: 1, color: theme.currentGlobal.primaryColor), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.25), + color: Colors.black.withValues(alpha: 0.25), spreadRadius: 0, blurRadius: 4, offset: const Offset(0, 4), diff --git a/lib/views/common/header/actions/account_switcher.dart b/lib/views/common/header/actions/account_switcher.dart index 3275cb528b..466a99d75a 100644 --- a/lib/views/common/header/actions/account_switcher.dart +++ b/lib/views/common/header/actions/account_switcher.dart @@ -1,10 +1,11 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; @@ -77,6 +78,7 @@ class _AccountSwitcher extends StatelessWidget { @override Widget build(BuildContext context) { + final currentWallet = context.read().state.currentUser?.wallet; return Container( constraints: const BoxConstraints(minWidth: minWidth), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), @@ -84,24 +86,24 @@ class _AccountSwitcher extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - StreamBuilder( - stream: currentWalletBloc.outWallet, - builder: (context, snapshot) { - return Container( - constraints: const BoxConstraints(maxWidth: maxWidth), - child: Text( - currentWalletBloc.wallet?.name ?? '', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Theme.of(context).textTheme.labelLarge?.color, - ), - textAlign: TextAlign.end, - maxLines: 1, - overflow: TextOverflow.ellipsis, + BlocBuilder( + builder: (context, state) { + return Container( + constraints: const BoxConstraints(maxWidth: maxWidth), + child: Text( + currentWallet?.name ?? '', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Theme.of(context).textTheme.labelLarge?.color, ), - ); - }), + textAlign: TextAlign.end, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), const SizedBox(width: 6), const _AccountIcon(), ], diff --git a/lib/views/common/header/actions/header_actions.dart b/lib/views/common/header/actions/header_actions.dart index dfd8ad3ae9..b8a6b576ae 100644 --- a/lib/views/common/header/actions/header_actions.dart +++ b/lib/views/common/header/actions/header_actions.dart @@ -1,9 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -32,13 +33,12 @@ List? getHeaderActions(BuildContext context) { ), Padding( padding: headerActionsPadding, - child: StreamBuilder>( - initialData: coinsBloc.walletCoinsMap.values, - stream: coinsBloc.outWalletCoins, - builder: (context, snapshot) { + child: BlocBuilder( + builder: (context, state) { return ActionTextButton( text: LocaleKeys.balance.tr(), - secondaryText: '\$${formatAmt(_getTotalBalance(snapshot.data!))}', + secondaryText: + '\$${formatAmt(_getTotalBalance(state.walletCoins.values))}', onTap: null, ); }, diff --git a/lib/views/common/header/app_header.dart b/lib/views/common/header/app_header.dart index 0285826f23..fdcf792da1 100644 --- a/lib/views/common/header/app_header.dart +++ b/lib/views/common/header/app_header.dart @@ -8,15 +8,6 @@ import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/common/header/actions/header_actions.dart'; -PreferredSize? buildAppHeader() { - return isMobile - ? null - : const PreferredSize( - preferredSize: Size.fromHeight(appBarHeight), - child: AppHeader(), - ); -} - class AppHeader extends StatefulWidget { const AppHeader({Key? key}) : super(key: key); diff --git a/lib/views/common/hw_wallet_dialog/hw_dialog_init.dart b/lib/views/common/hw_wallet_dialog/hw_dialog_init.dart index 7d11f5001f..c11b86e292 100644 --- a/lib/views/common/hw_wallet_dialog/hw_dialog_init.dart +++ b/lib/views/common/hw_wallet_dialog/hw_dialog_init.dart @@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_bloc.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/hw_wallet/hw_wallet.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; diff --git a/lib/views/common/hw_wallet_dialog/hw_dialog_wallet_select.dart b/lib/views/common/hw_wallet_dialog/hw_dialog_wallet_select.dart index 76bb0489b8..9fca825397 100644 --- a/lib/views/common/hw_wallet_dialog/hw_dialog_wallet_select.dart +++ b/lib/views/common/hw_wallet_dialog/hw_dialog_wallet_select.dart @@ -5,7 +5,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_bloc.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/hw_wallet/hw_wallet.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/constants.dart'; diff --git a/lib/views/common/hw_wallet_dialog/show_trezor_passphrase_dialog.dart b/lib/views/common/hw_wallet_dialog/show_trezor_passphrase_dialog.dart index f0e187749a..fe3c08f2ad 100644 --- a/lib/views/common/hw_wallet_dialog/show_trezor_passphrase_dialog.dart +++ b/lib/views/common/hw_wallet_dialog/show_trezor_passphrase_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; @@ -24,6 +25,7 @@ Future showTrezorPassphraseDialog(TrezorTask task) async { onDismiss: close, popupContent: TrezorDialogSelectWallet( onComplete: (String passphrase) async { + final trezorRepo = RepositoryProvider.of(context); await trezorRepo.sendPassphrase(passphrase, task); // todo(yurii): handle invalid pin close(); diff --git a/lib/views/common/hw_wallet_dialog/show_trezor_pin_dialog.dart b/lib/views/common/hw_wallet_dialog/show_trezor_pin_dialog.dart index f783aac804..af6b3a7ea8 100644 --- a/lib/views/common/hw_wallet_dialog/show_trezor_pin_dialog.dart +++ b/lib/views/common/hw_wallet_dialog/show_trezor_pin_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; @@ -23,6 +24,7 @@ Future showTrezorPinDialog(TrezorTask task) async { onDismiss: close, popupContent: TrezorDialogPinPad( onComplete: (String pin) async { + final trezorRepo = RepositoryProvider.of(context); await trezorRepo.sendPin(pin, task); // todo(yurii): handle invalid pin close(); diff --git a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart index 419d24899f..56efd5bbd3 100644 --- a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart +++ b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart @@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_bloc.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/hw_wallet/trezor_status_error.dart'; @@ -53,7 +52,7 @@ class TrezorDialogError extends StatelessWidget { return _parseErrorMessage(error); } if (error is BaseError) { - return error.text; + return error.message; } return error.toString(); diff --git a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_pin_pad.dart b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_pin_pad.dart index 3fbd455c18..8173cfbe9e 100644 --- a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_pin_pad.dart +++ b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_pin_pad.dart @@ -2,10 +2,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/constants.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; const List> _keys = [ [7, 8, 9], @@ -75,7 +75,7 @@ class _TrezorDialogPinPadState extends State { Widget _buildObscuredPin() { final Color? backspaceColor = _pinController.text.isEmpty - ? Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7) + ? Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.7) : Theme.of(context).textTheme.bodyMedium?.color; return UiTextFormField( 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 a7924e7e28..9fbe70cf16 100644 --- a/lib/views/common/main_menu/main_menu_bar_mobile.dart +++ b/lib/views/common/main_menu/main_menu_bar_mobile.dart @@ -1,10 +1,11 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/main_menu_value.dart'; +import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/views/common/main_menu/main_menu_bar_mobile_item.dart'; @@ -12,6 +13,7 @@ class MainMenuBarMobile extends StatelessWidget { @override Widget build(BuildContext context) { final MainMenuValue selected = routingState.selectedMenu; + final currentWallet = context.watch().state.currentUser?.wallet; return BlocBuilder( builder: (context, state) { @@ -21,7 +23,7 @@ class MainMenuBarMobile extends StatelessWidget { color: theme.currentGlobal.cardColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), offset: const Offset(0, -10), blurRadius: 10, ), @@ -40,30 +42,28 @@ class MainMenuBarMobile extends StatelessWidget { ), MainMenuBarMobileItem( value: MainMenuValue.fiat, - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, isActive: selected == MainMenuValue.fiat, ), MainMenuBarMobileItem( value: MainMenuValue.dex, - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, isActive: selected == MainMenuValue.dex, ), MainMenuBarMobileItem( value: MainMenuValue.bridge, - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, isActive: selected == MainMenuValue.bridge, ), - // TODO(Francois): consider moving into sub-menu somewhere to - // avoid cluttering the main menu (and text wrapping) if (isMMBotEnabled) MainMenuBarMobileItem( - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, value: MainMenuValue.marketMakerBot, isActive: selected == MainMenuValue.marketMakerBot, ), MainMenuBarMobileItem( value: MainMenuValue.nft, - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, isActive: selected == MainMenuValue.nft, ), MainMenuBarMobileItem( diff --git a/lib/views/common/main_menu/main_menu_desktop.dart b/lib/views/common/main_menu/main_menu_desktop.dart index bb285d7e1f..1238113e44 100644 --- a/lib/views/common/main_menu/main_menu_desktop.dart +++ b/lib/views/common/main_menu/main_menu_desktop.dart @@ -8,7 +8,6 @@ import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_event.dart'; import 'package:web_dex/bloc/settings/settings_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/authorize_mode.dart'; @@ -28,15 +27,15 @@ class _MainMenuDesktopState extends State { final isAuthenticated = context .select((AuthBloc bloc) => bloc.state.mode == AuthorizeMode.logIn); - return StreamBuilder( - stream: currentWalletBloc.outWallet, - builder: (context, currentWalletSnapshot) { + return BlocBuilder( + builder: (context, state) { return BlocBuilder( builder: (context, settingsState) { final bool isDarkTheme = settingsState.themeMode == ThemeMode.dark; final bool isMMBotEnabled = settingsState.mmBotSettings.isMMBotEnabled; final SettingsBloc settings = context.read(); + final currentWallet = state.currentUser?.wallet; return Container( margin: isWideScreen ? const EdgeInsets.fromLTRB(0, mainLayoutPadding + 12, 24, 0) @@ -59,21 +58,21 @@ class _MainMenuDesktopState extends State { ), DesktopMenuDesktopItem( key: const Key('main-menu-fiat'), - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, menu: MainMenuValue.fiat, onTap: onTapItem, isSelected: _checkSelectedItem(MainMenuValue.fiat), ), DesktopMenuDesktopItem( key: const Key('main-menu-dex'), - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, menu: MainMenuValue.dex, onTap: onTapItem, isSelected: _checkSelectedItem(MainMenuValue.dex), ), DesktopMenuDesktopItem( key: const Key('main-menu-bridge'), - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, menu: MainMenuValue.bridge, onTap: onTapItem, isSelected: _checkSelectedItem(MainMenuValue.bridge), @@ -81,7 +80,7 @@ class _MainMenuDesktopState extends State { if (isMMBotEnabled && isAuthenticated) DesktopMenuDesktopItem( key: const Key('main-menu-market-maker-bot'), - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, menu: MainMenuValue.marketMakerBot, onTap: onTapItem, isSelected: @@ -89,7 +88,7 @@ class _MainMenuDesktopState extends State { ), DesktopMenuDesktopItem( key: const Key('main-menu-nft'), - enabled: currentWalletBloc.wallet?.isHW != true, + enabled: currentWallet?.isHW != true, menu: MainMenuValue.nft, onTap: onTapItem, isSelected: _checkSelectedItem(MainMenuValue.nft)), @@ -98,8 +97,7 @@ class _MainMenuDesktopState extends State { key: const Key('main-menu-settings'), menu: MainMenuValue.settings, onTap: onTapItem, - needAttention: - currentWalletBloc.wallet?.config.hasBackup == false, + needAttention: currentWallet?.config.hasBackup == false, isSelected: _checkSelectedItem(MainMenuValue.settings), ), Theme( diff --git a/lib/views/common/wallet_password_dialog/password_dialog_content.dart b/lib/views/common/wallet_password_dialog/password_dialog_content.dart index 6df1fa11d1..26dea60251 100644 --- a/lib/views/common/wallet_password_dialog/password_dialog_content.dart +++ b/lib/views/common/wallet_password_dialog/password_dialog_content.dart @@ -1,11 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; class PasswordDialogContent extends StatefulWidget { @@ -98,25 +99,13 @@ class _PasswordDialogContentState extends State { } Future _onContinue() async { - final Wallet? wallet = widget.wallet ?? currentWalletBloc.wallet; - if (wallet == null) return; + final currentWallet = context.read().state.currentUser?.wallet; + if (currentWallet == null) return; final String password = _passwordController.text; setState(() => _inProgress = true); WidgetsBinding.instance.addPostFrameCallback((_) async { - final String seed = await wallet.getSeed(password); - if (seed.isEmpty) { - if (mounted) { - setState(() { - _error = LocaleKeys.invalidPasswordError.tr(); - _inProgress = false; - }); - } - - return; - } - widget.onSuccess(password); if (mounted) setState(() => _inProgress = false); diff --git a/lib/views/common/wallet_password_dialog/wallet_password_dialog.dart b/lib/views/common/wallet_password_dialog/wallet_password_dialog.dart index 757d70a2e5..c6f7caa660 100644 --- a/lib/views/common/wallet_password_dialog/wallet_password_dialog.dart +++ b/lib/views/common/wallet_password_dialog/wallet_password_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/wallet_password_dialog/password_dialog_content.dart'; @@ -10,7 +11,8 @@ Future walletPasswordDialog( BuildContext context, { Wallet? wallet, }) async { - wallet ??= currentWalletBloc.wallet; + final currentWallet = context.read().state.currentUser?.wallet; + wallet ??= currentWallet; late PopupDispatcher popupManager; bool isOpen = false; String? password; diff --git a/lib/views/dex/common/fiat_amount.dart b/lib/views/dex/common/fiat_amount.dart index 1e0b5da951..fe064eecf6 100644 --- a/lib/views/dex/common/fiat_amount.dart +++ b/lib/views/dex/common/fiat_amount.dart @@ -16,7 +16,7 @@ class DexFiatAmountText extends StatelessWidget { Theme.of(context).textTheme.bodySmall?.merge(style); return Text( - getFormattedFiatAmount(coin.abbr, amount), + getFormattedFiatAmount(context, coin.abbr, amount), style: textStyle, ); } diff --git a/lib/views/dex/common/front_plate.dart b/lib/views/dex/common/front_plate.dart index f377bf26ca..f19d4e97a6 100644 --- a/lib/views/dex/common/front_plate.dart +++ b/lib/views/dex/common/front_plate.dart @@ -11,7 +11,7 @@ class FrontPlate extends StatelessWidget { Widget build(BuildContext context) { final borderRadius = BorderRadius.circular(18); final shadow = BoxShadow( - color: Colors.black.withOpacity(0.25), + color: Colors.black.withValues(alpha: 0.25), spreadRadius: 0, blurRadius: 4, offset: const Offset(0, 4), diff --git a/lib/views/dex/dex_helpers.dart b/lib/views/dex/dex_helpers.dart index 22fa2a7b22..f11ad3548e 100644 --- a/lib/views/dex/dex_helpers.dart +++ b/lib/views/dex/dex_helpers.dart @@ -1,10 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.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/model/coin.dart'; import 'package:web_dex/model/dex_form_error.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; @@ -33,15 +34,20 @@ class FiatAmount extends StatelessWidget { Theme.of(context).textTheme.bodySmall?.merge(style); return Text( - getFormattedFiatAmount(coin.abbr, amount), + getFormattedFiatAmount(context, coin.abbr, amount), style: textStyle, ); } } -String getFormattedFiatAmount(String coinAbbr, Rational amount, - [int digits = 8]) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); +String getFormattedFiatAmount( + BuildContext context, + String coinAbbr, + Rational amount, [ + int digits = 8, +]) { + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(coinAbbr); if (coin == null) return ''; return '≈\$${formatAmt(getFiatAmount(coin, amount))}'; } @@ -67,7 +73,9 @@ List applyFiltersForSwap( } if (statuses != null && statuses.isNotEmpty) { if (statuses.contains(TradingStatus.successful) && - statuses.contains(TradingStatus.failed)) return true; + statuses.contains(TradingStatus.failed)) { + return true; + } if (statuses.contains(TradingStatus.successful)) { return swap.isSuccessful; } @@ -76,7 +84,9 @@ List applyFiltersForSwap( if (shownSides != null && shownSides.isNotEmpty && - !shownSides.contains(swap.type)) return false; + !shownSides.contains(swap.type)) { + return false; + } return true; }).toList(); @@ -95,9 +105,13 @@ List applyFiltersForOrders( if (buyCoin != null && order.rel != buyCoin) return false; if (startDate != null && order.createdAt < startDate / 1000) return false; if (endDate != null && - order.createdAt > (endDate + millisecondsIn24H) / 1000) return false; + order.createdAt > (endDate + millisecondsIn24H) / 1000) { + return false; + } if ((shownSides != null && shownSides.isNotEmpty) && - !shownSides.contains(order.orderType)) return false; + !shownSides.contains(order.orderType)) { + return false; + } return true; }).toList(); @@ -149,35 +163,6 @@ int getCoinPairsCountFromCoinAbbrMap(Map> coinAbbrMap, .length; } -void removeTestCoinOrders(List orders) { - orders.removeWhere((BestOrder order) { - final Coin? coin = coinsBloc.getCoin(order.coin); - if (coin == null) return true; - - return coin.isTestCoin; - }); -} - -void removeSuspendedCoinOrders( - List orders, AuthorizeMode authorizeMode) { - if (authorizeMode == AuthorizeMode.noLogin) return; - orders.removeWhere((BestOrder order) { - final Coin? coin = coinsBloc.getCoin(order.coin); - if (coin == null) return true; - - return coin.isSuspended; - }); -} - -void removeWalletOnlyCoinOrders(List orders) { - orders.removeWhere((BestOrder order) { - final Coin? coin = coinsBloc.getCoin(order.coin); - if (coin == null) return true; - - return coin.walletOnly; - }); -} - /// Compares the rate of a decentralized exchange (DEX) with a centralized exchange (CEX) in percentage. /// /// The comparison is based on the provided exchange rates and a given [rate] of the DEX. @@ -223,16 +208,19 @@ double compareToCex(double baseUsdPrice, double relUsdPrice, Rational rate) { return (dexRate - cexRate) * 100 / cexRate; } -Future> activateCoinIfNeeded(String? abbr) async { +Future> activateCoinIfNeeded( + String? abbr, + CoinsRepo coinsRepository, +) async { final List errors = []; if (abbr == null) return errors; - final Coin? coin = coinsBloc.getCoin(abbr); + final Coin? coin = coinsRepository.getCoin(abbr); if (coin == null) return errors; if (!coin.isActive) { try { - await coinsBloc.activateCoins([coin]); + await coinsRepository.activateCoinsSync([coin]); } catch (e) { errors.add(DexFormError( error: '${LocaleKeys.unableToActiveCoin.tr(args: [coin.abbr])}: $e')); @@ -241,7 +229,7 @@ Future> activateCoinIfNeeded(String? abbr) async { final Coin? parentCoin = coin.parentCoin; if (parentCoin != null && !parentCoin.isActive) { try { - await coinsBloc.activateCoins([parentCoin]); + await coinsRepository.activateCoinsSync([parentCoin]); } catch (e) { errors.add(DexFormError( error: @@ -253,13 +241,14 @@ Future> activateCoinIfNeeded(String? abbr) async { return errors; } -Future reInitTradingForms() async { +Future reInitTradingForms(BuildContext context) async { // If some of the DEX or Bridge forms were modified by user during // interaction in 'no-login' mode, their blocs may link to special // instances of [Coin], initialized in that mode. // After login to iguana wallet, // we must replace them with regular [Coin] instances, and // auto-activate corresponding coins if needed + final makerFormBloc = RepositoryProvider.of(context); await makerFormBloc.reInitForm(); } diff --git a/lib/views/dex/dex_list_filter/desktop/dex_list_filter_coin_desktop.dart b/lib/views/dex/dex_list_filter/desktop/dex_list_filter_coin_desktop.dart index 2d94e3d297..d26593e2f7 100644 --- a/lib/views/dex/dex_list_filter/desktop/dex_list_filter_coin_desktop.dart +++ b/lib/views/dex/dex_list_filter/desktop/dex_list_filter_coin_desktop.dart @@ -1,13 +1,15 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/model/swap.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; -import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; class DexListFilterCoinDesktop extends StatelessWidget { const DexListFilterCoinDesktop({ @@ -28,6 +30,8 @@ class DexListFilterCoinDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); switch (listType) { case DexListType.orders: return StreamBuilder>( @@ -42,8 +46,9 @@ class DexListFilterCoinDesktop extends StatelessWidget { label: label, onCoinSelect: onCoinSelect, value: coinAbbr, - items: _getItems(coinAbbrMap), + items: _getItems(context, coinAbbrMap), selectedItemBuilder: (context) => _getItems( + context, coinAbbrMap, selected: true, ), @@ -67,8 +72,9 @@ class DexListFilterCoinDesktop extends StatelessWidget { label: label, onCoinSelect: onCoinSelect, value: coinAbbr, - items: _getItems(coinAbbrMap), + items: _getItems(context, coinAbbrMap), selectedItemBuilder: (context) => _getItems( + context, coinAbbrMap, selected: true, ), @@ -81,6 +87,7 @@ class DexListFilterCoinDesktop extends StatelessWidget { } List> _getItems( + BuildContext context, Map> coinAbbrMap, { bool selected = false, }) { @@ -89,7 +96,7 @@ class DexListFilterCoinDesktop extends StatelessWidget { return selected ? coinAbbrList.map((abbr) { - return _buildSelectedItem(abbr); + return _buildSelectedItem(context, abbr); }).toList() : coinAbbrList.map((abbr) { final int pairsCount = getCoinPairsCountFromCoinAbbrMap( @@ -97,15 +104,17 @@ class DexListFilterCoinDesktop extends StatelessWidget { abbr, anotherCoinAbbr, ); - return _buildItem(abbr, pairsCount); + return _buildItem(context, abbr, pairsCount); }).toList(); } DropdownMenuItem _buildItem( + BuildContext context, String coinAbbr, int pairsCount, ) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(coinAbbr); if (coin == null) return const DropdownMenuItem(child: SizedBox()); return DropdownMenuItem( @@ -129,8 +138,10 @@ class DexListFilterCoinDesktop extends StatelessWidget { ); } - DropdownMenuItem _buildSelectedItem(String coinAbbr) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); + DropdownMenuItem _buildSelectedItem( + BuildContext context, String coinAbbr) { + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(coinAbbr); if (coin == null) return const DropdownMenuItem(child: SizedBox()); return DropdownMenuItem( diff --git a/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart b/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart index 98f1e936a1..dbda89c9ed 100644 --- a/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart +++ b/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -64,6 +65,8 @@ class _DexListFilterCoinsListState extends State { } Widget _buildSwapCoinList() { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( stream: tradingEntitiesBloc.outSwaps, initialData: tradingEntitiesBloc.swaps, @@ -80,6 +83,8 @@ class _DexListFilterCoinsListState extends State { } Widget _buildOrderCoinList() { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( stream: tradingEntitiesBloc.outMyOrders, initialData: tradingEntitiesBloc.myOrders, diff --git a/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart b/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart index 95e5ce8ccb..eedde8512f 100644 --- a/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart +++ b/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart @@ -1,9 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; @@ -27,6 +28,8 @@ class DexListHeaderMobile extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); final List filterElements = _getFilterElements(context); final filterData = entitiesFilterData; return Column( diff --git a/lib/views/dex/dex_page.dart b/lib/views/dex/dex_page.dart index 38398469be..816ed4d156 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -1,12 +1,12 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_bot_order_list_repository.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/router/state/routing_state.dart'; @@ -42,22 +42,24 @@ class _DexPageState extends State { @override Widget build(BuildContext context) { - if (kIsWalletOnly) { - return const Placeholder(child: Text('You should not see this page')); - } + final tradingEntitiesBloc = + RepositoryProvider.of(context); + final coinsRepository = RepositoryProvider.of(context); + final myOrdersService = RepositoryProvider.of(context); + return MultiBlocProvider( providers: [ BlocProvider( key: const Key('dex-page'), create: (BuildContext context) => DexTabBarBloc( - authRepo, + RepositoryProvider.of(context), tradingEntitiesBloc, MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), - coinsBloc, + coinsRepository, ), - )..add(const StartListening()), + )..add(const ListenToOrdersRequested()), ), ], child: isTradingDetails diff --git a/lib/views/dex/entities_list/common/buy_price_mobile.dart b/lib/views/dex/entities_list/common/buy_price_mobile.dart index cf06c3077e..3f8d5fda50 100644 --- a/lib/views/dex/entities_list/common/buy_price_mobile.dart +++ b/lib/views/dex/entities_list/common/buy_price_mobile.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/formatters.dart'; @@ -19,6 +20,8 @@ class BuyPriceMobile extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Container( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 17), decoration: BoxDecoration( diff --git a/lib/views/dex/entities_list/common/coin_amount_mobile.dart b/lib/views/dex/entities_list/common/coin_amount_mobile.dart index 5b53d95a74..979b9f3230 100644 --- a/lib/views/dex/entities_list/common/coin_amount_mobile.dart +++ b/lib/views/dex/entities_list/common/coin_amount_mobile.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; @@ -13,7 +14,8 @@ class CoinAmountMobile extends StatelessWidget { @override Widget build(BuildContext context) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(coinAbbr); if (coin == null) return const SizedBox.shrink(); diff --git a/lib/views/dex/entities_list/common/trade_amount_desktop.dart b/lib/views/dex/entities_list/common/trade_amount_desktop.dart index cf8c2d9d07..9dad6aff12 100644 --- a/lib/views/dex/entities_list/common/trade_amount_desktop.dart +++ b/lib/views/dex/entities_list/common/trade_amount_desktop.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; @@ -15,7 +16,8 @@ class TradeAmountDesktop extends StatelessWidget { @override Widget build(BuildContext context) { - final Coin? coin = coinsBloc.getCoin(coinAbbr); + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(coinAbbr); if (coin == null) return const SizedBox.shrink(); return Padding( diff --git a/lib/views/dex/entities_list/history/history_item.dart b/lib/views/dex/entities_list/history/history_item.dart index 58e9e3f538..037db7b628 100644 --- a/lib/views/dex/entities_list/history/history_item.dart +++ b/lib/views/dex/entities_list/history/history_item.dart @@ -1,8 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/swap.dart'; @@ -12,7 +14,6 @@ import 'package:web_dex/shared/widgets/focusable_widget.dart'; import 'package:web_dex/views/dex/entities_list/common/coin_amount_mobile.dart'; import 'package:web_dex/views/dex/entities_list/common/entity_item_status_wrapper.dart'; import 'package:web_dex/views/dex/entities_list/common/trade_amount_desktop.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class HistoryItem extends StatefulWidget { const HistoryItem(this.swap, {Key? key, required this.onClick}) @@ -39,6 +40,8 @@ class _HistoryItemState extends State { final bool isSuccessful = !widget.swap.isFailed; final bool isTaker = widget.swap.isTaker; final bool isRecoverable = widget.swap.recoverable; + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -101,6 +104,8 @@ class _HistoryItemState extends State { setState(() { _isRecovering = true; }); + final tradingEntitiesBloc = + RepositoryProvider.of(context); await tradingEntitiesBloc.recoverFundsOfSwap(widget.swap.uuid); setState(() { _isRecovering = false; @@ -142,6 +147,8 @@ class _HistoryItemDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/views/dex/entities_list/history/history_list.dart b/lib/views/dex/entities_list/history/history_list.dart index e6a3d99d4d..aab661823c 100644 --- a/lib/views/dex/entities_list/history/history_list.dart +++ b/lib/views/dex/entities_list/history/history_list.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/model/trading_entities_filter.dart'; @@ -10,7 +12,6 @@ import 'package:web_dex/views/dex/entities_list/common/dex_empty_list.dart'; import 'package:web_dex/views/dex/entities_list/common/dex_error_message.dart'; import 'package:web_dex/views/dex/entities_list/history/history_item.dart'; import 'package:web_dex/views/dex/entities_list/history/history_list_header.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'swap_history_sort_mixin.dart'; @@ -108,6 +109,8 @@ class _HistoryListState extends State } StreamSubscription> listenForSwaps() { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return tradingEntitiesBloc.outSwaps.where((swaps) { final didSwapsChange = !areSwapsSame(swaps, _unprocessedSwaps); @@ -145,7 +148,7 @@ class _HistoryListState extends State setState(() { clearErrorIfExists(); - _processedSwaps = sortSwaps(filteredSwaps, sortData: _sortData); + _processedSwaps = sortSwaps(context, filteredSwaps, sortData: _sortData); }); } diff --git a/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart b/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart index fdfcdae571..2e6821021f 100644 --- a/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart +++ b/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart @@ -1,5 +1,7 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/shared/utils/sorting.dart'; import 'package:web_dex/views/dex/entities_list/history/history_list_header.dart'; @@ -14,7 +16,7 @@ mixin SwapHistorySortingMixin { } List sortSwaps( - List swaps, { + BuildContext context, List swaps, { required SortData sortData, }) { final direction = sortData.sortDirection; @@ -25,7 +27,7 @@ mixin SwapHistorySortingMixin { case HistoryListSortType.receive: return _sortByAmount(swaps, false, direction); case HistoryListSortType.price: - return _sortByPrice(swaps, sortDirection: direction); + return _sortByPrice(context, swaps, sortDirection: direction); case HistoryListSortType.date: return _sortByDate(swaps, sortDirection: direction); case HistoryListSortType.orderType: @@ -85,9 +87,10 @@ mixin SwapHistorySortingMixin { } List _sortByPrice( - List swaps, { + BuildContext context, List swaps, { required SortDirection sortDirection, }) { + final tradingEntitiesBloc = RepositoryProvider.of(context); swaps.sort( (first, second) => sortByDouble( tradingEntitiesBloc.getPriceFromAmount( diff --git a/lib/views/dex/entities_list/in_progress/in_progress_item.dart b/lib/views/dex/entities_list/in_progress/in_progress_item.dart index 643ade609b..6675ad9064 100644 --- a/lib/views/dex/entities_list/in_progress/in_progress_item.dart +++ b/lib/views/dex/entities_list/in_progress/in_progress_item.dart @@ -1,9 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/swap.dart'; @@ -28,6 +29,8 @@ class InProgressItem extends StatelessWidget { final Rational buyAmount = swap.buyAmount; final String date = getFormattedDate(swap.myInfo.startedAt); final bool isTaker = swap.isTaker; + final tradingEntitiesBloc = + RepositoryProvider.of(context); final String typeText = tradingEntitiesBloc.getTypeString(isTaker); return Column( @@ -109,6 +112,8 @@ class _InProgressItemDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Row( mainAxisSize: MainAxisSize.max, children: [ diff --git a/lib/views/dex/entities_list/in_progress/in_progress_list.dart b/lib/views/dex/entities_list/in_progress/in_progress_list.dart index b14176d85b..b313f36329 100644 --- a/lib/views/dex/entities_list/in_progress/in_progress_list.dart +++ b/lib/views/dex/entities_list/in_progress/in_progress_list.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/model/trading_entities_filter.dart'; @@ -9,7 +11,6 @@ import 'package:web_dex/views/dex/entities_list/common/dex_empty_list.dart'; import 'package:web_dex/views/dex/entities_list/common/dex_error_message.dart'; import 'package:web_dex/views/dex/entities_list/in_progress/in_progress_item.dart'; import 'package:web_dex/views/dex/entities_list/in_progress/in_progress_list_header.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class InProgressList extends StatefulWidget { const InProgressList({ @@ -36,6 +37,8 @@ class _InProgressListState extends State { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( initialData: tradingEntitiesBloc.swaps, stream: tradingEntitiesBloc.outSwaps, @@ -151,6 +154,8 @@ class _InProgressListState extends State { } List _sortByPrice(List swaps) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); swaps.sort((first, second) => sortByDouble( tradingEntitiesBloc.getPriceFromAmount( first.sellAmount, diff --git a/lib/views/dex/entities_list/orders/order_cancel_button.dart b/lib/views/dex/entities_list/orders/order_cancel_button.dart index d6c4d3095d..b318c2b2d5 100644 --- a/lib/views/dex/entities_list/orders/order_cancel_button.dart +++ b/lib/views/dex/entities_list/orders/order_cancel_button.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; @@ -45,6 +46,8 @@ class _OrderCancelButtonState extends State { setState(() { _isCancelling = true; }); + final tradingEntitiesBloc = + RepositoryProvider.of(context); final String? error = await tradingEntitiesBloc.cancelOrder(order.uuid); setState(() { _isCancelling = false; diff --git a/lib/views/dex/entities_list/orders/order_item.dart b/lib/views/dex/entities_list/orders/order_item.dart index cf72bee01c..78efe2bc40 100644 --- a/lib/views/dex/entities_list/orders/order_item.dart +++ b/lib/views/dex/entities_list/orders/order_item.dart @@ -1,8 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; import 'package:vector_math/vector_math_64.dart' as vector_math; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; @@ -35,6 +36,8 @@ class _OrderItemState extends State { final bool isTaker = order.orderType == TradeSide.taker; final String date = getFormattedDate(order.createdAt); final int orderMatchingTime = order.orderMatchingTime; + final tradingEntitiesBloc = + RepositoryProvider.of(context); final double fillProgress = tradingEntitiesBloc.getProgressFillSwap(order); return Column( @@ -119,6 +122,8 @@ class _OrderItemDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Row( mainAxisSize: MainAxisSize.max, children: [ diff --git a/lib/views/dex/entities_list/orders/orders_list.dart b/lib/views/dex/entities_list/orders/orders_list.dart index e6b47283e1..b14af27a79 100644 --- a/lib/views/dex/entities_list/orders/orders_list.dart +++ b/lib/views/dex/entities_list/orders/orders_list.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; @@ -33,6 +34,8 @@ class _OrdersListState extends State { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( initialData: tradingEntitiesBloc.myOrders, stream: tradingEntitiesBloc.outMyOrders, @@ -149,6 +152,8 @@ class _OrdersListState extends State { } List _sortByPrice(List orders) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); orders.sort((first, second) => sortByDouble( tradingEntitiesBloc.getPriceFromAmount( first.baseAmount, diff --git a/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart b/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart index 0422458ecb..e8ee9f41d1 100644 --- a/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart +++ b/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/order_status/cancellation_reason.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; @@ -12,7 +14,6 @@ import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/dex/entity_details/trading_details_coin_pair.dart'; import 'package:web_dex/views/dex/entity_details/trading_details_header.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class MakerOrderDetailsPage extends StatefulWidget { const MakerOrderDetailsPage(this.makerOrderStatus, {Key? key}) @@ -219,6 +220,7 @@ class _MakerOrderDetailsPageState extends State { _inProgress = true; }); + final tradingEntitiesBloc = RepositoryProvider.of(context); final String? error = await tradingEntitiesBloc .cancelOrder(widget.makerOrderStatus.order.uuid); diff --git a/lib/views/dex/entity_details/swap/swap_details_step.dart b/lib/views/dex/entity_details/swap/swap_details_step.dart index 33c4375041..5cafd31577 100644 --- a/lib/views/dex/entity_details/swap/swap_details_step.dart +++ b/lib/views/dex/entity_details/swap/swap_details_step.dart @@ -91,7 +91,8 @@ class SwapDetailsStep extends StatelessWidget { width: 1, color: isProcessedStep ? theme.custom.progressBarPassedColor - : themeData.textTheme.bodyMedium?.color?.withOpacity(0.3) ?? + : themeData.textTheme.bodyMedium?.color + ?.withValues(alpha: 0.3) ?? Colors.transparent, ), ], diff --git a/lib/views/dex/entity_details/swap/swap_details_step_list.dart b/lib/views/dex/entity_details/swap/swap_details_step_list.dart index a369279ed4..b6c0965c40 100644 --- a/lib/views/dex/entity_details/swap/swap_details_step_list.dart +++ b/lib/views/dex/entity_details/swap/swap_details_step_list.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/swap.dart'; import 'package:web_dex/views/dex/entity_details/swap/swap_details_step.dart'; @@ -39,7 +40,7 @@ class SwapDetailsStepList extends StatelessWidget { (isLastStep && isFailedSwap); final SwapEventItem? eventData = swapStatus.events.firstWhereOrNull((e) => e.event.type == event); - final Coin? coin = _getCoinForTransaction(event, swapStatus); + final Coin? coin = _getCoinForTransaction(context, event, swapStatus); return SwapDetailsStep( key: Key('swap-details-step-$event'), @@ -88,7 +89,12 @@ class SwapDetailsStepList extends StatelessWidget { return currentEvent.timestamp - previousEvent.timestamp; } - Coin? _getCoinForTransaction(String event, Swap swapStatus) { + Coin? _getCoinForTransaction( + BuildContext context, + String event, + Swap swapStatus, + ) { + final coinsRepository = RepositoryProvider.of(context); final List takerEvents = [ 'TakerPaymentSent', 'TakerPaymentSpent', @@ -103,10 +109,10 @@ class SwapDetailsStepList extends StatelessWidget { 'MakerPaymentSent', ]; if (takerEvents.contains(event)) { - return coinsBloc.getCoin(swapStatus.takerCoin); + return coinsRepository.getCoin(swapStatus.takerCoin); } if (makerEvents.contains(event)) { - return coinsBloc.getCoin(swapStatus.makerCoin); + return coinsRepository.getCoin(swapStatus.makerCoin); } return null; } diff --git a/lib/views/dex/entity_details/swap/swap_recover_button.dart b/lib/views/dex/entity_details/swap/swap_recover_button.dart index 955ab32f22..c2ecb991bc 100644 --- a/lib/views/dex/entity_details/swap/swap_recover_button.dart +++ b/lib/views/dex/entity_details/swap/swap_recover_button.dart @@ -1,8 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/recover_funds_of_swap/recover_funds_of_swap_response.dart'; import 'package:web_dex/model/coin.dart'; @@ -52,6 +54,8 @@ class _SwapRecoverButtonState extends State { _recoverResponse = null; _message = ''; }); + final tradingEntitiesBloc = + RepositoryProvider.of(context); final response = await tradingEntitiesBloc .recoverFundsOfSwap(widget.uuid); await Future.delayed(const Duration(seconds: 1)); @@ -95,7 +99,8 @@ class _SwapRecoverButtonState extends State { ), ); } - final Coin? coin = coinsBloc.getCoin(response?.result.coin ?? ''); + final coinsRepository = RepositoryProvider.of(context); + final Coin? coin = coinsRepository.getCoin(response?.result.coin ?? ''); if (coin == null || response == null) { return const SizedBox(); } diff --git a/lib/views/dex/entity_details/trading_details.dart b/lib/views/dex/entity_details/trading_details.dart index 7d65db1f58..7d9641238a 100644 --- a/lib/views/dex/entity_details/trading_details.dart +++ b/lib/views/dex/entity_details/trading_details.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/swap.dart'; @@ -10,7 +12,6 @@ import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/dex/entity_details/maker_order/maker_order_details_page.dart'; import 'package:web_dex/views/dex/entity_details/swap/swap_details_page.dart'; import 'package:web_dex/views/dex/entity_details/taker_order/taker_order_details_page.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class TradingDetails extends StatefulWidget { const TradingDetails({Key? key, required this.uuid}) : super(key: key); @@ -28,8 +29,11 @@ class _TradingDetailsState extends State { @override void initState() { + final myOrdersService = RepositoryProvider.of(context); + final dexRepository = RepositoryProvider.of(context); + _statusTimer = Timer.periodic(const Duration(seconds: 1), (_) { - _updateStatus(); + _updateStatus(dexRepository, myOrdersService); }); super.initState(); @@ -79,7 +83,10 @@ class _TradingDetailsState extends State { return const SizedBox.shrink(); } - Future _updateStatus() async { + Future _updateStatus( + DexRepository dexRepository, + MyOrdersService myOrdersService, + ) async { Swap? swapStatus; try { swapStatus = await dexRepository.getSwapStatus(widget.uuid); diff --git a/lib/views/dex/entity_details/trading_details_coin_pair.dart b/lib/views/dex/entity_details/trading_details_coin_pair.dart index 620babc95d..e4b199fce3 100644 --- a/lib/views/dex/entity_details/trading_details_coin_pair.dart +++ b/lib/views/dex/entity_details/trading_details_coin_pair.dart @@ -1,9 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; @@ -22,8 +23,9 @@ class TradingDetailsCoinPair extends StatelessWidget { final Rational relAmount; @override Widget build(BuildContext context) { - final Coin? coinBase = coinsBloc.getCoin(baseCoin); - final Coin? coinRel = coinsBloc.getCoin(relCoin); + final coinsRepository = RepositoryProvider.of(context); + final Coin? coinBase = coinsRepository.getCoin(baseCoin); + final Coin? coinRel = coinsRepository.getCoin(relCoin); if (coinBase == null || coinRel == null) return const SizedBox.shrink(); diff --git a/lib/views/dex/orderbook/orderbook_table.dart b/lib/views/dex/orderbook/orderbook_table.dart index 9ad1afc71c..4d12b231ac 100644 --- a/lib/views/dex/orderbook/orderbook_table.dart +++ b/lib/views/dex/orderbook/orderbook_table.dart @@ -1,14 +1,15 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/orderbook/order.dart'; import 'package:web_dex/model/orderbook/orderbook.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_table_item.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_table_title.dart'; @@ -46,7 +47,7 @@ class OrderbookTable extends StatelessWidget { Container( height: 30, alignment: Alignment.centerLeft, - child: _buildSpotPrice(), + child: _buildSpotPrice(context), ), Flexible(child: _buildBids(highestVolume)), ], @@ -55,10 +56,11 @@ class OrderbookTable extends StatelessWidget { ); } - Widget _buildSpotPrice() { + Widget _buildSpotPrice(BuildContext context) { const TextStyle style = TextStyle(fontSize: 11); - final Coin? baseCoin = coinsBloc.getCoin(orderbook.base); - final Coin? relCoin = coinsBloc.getCoin(orderbook.rel); + final coinsRepository = RepositoryProvider.of(context); + final Coin? baseCoin = coinsRepository.getCoin(orderbook.base); + final Coin? relCoin = coinsRepository.getCoin(orderbook.rel); if (baseCoin == null || relCoin == null) return const SizedBox.shrink(); final double? baseUsdPrice = baseCoin.usdPrice?.price; diff --git a/lib/views/dex/orderbook/orderbook_table_item.dart b/lib/views/dex/orderbook/orderbook_table_item.dart index af743b8d5e..d7bbc27370 100644 --- a/lib/views/dex/orderbook/orderbook_table_item.dart +++ b/lib/views/dex/orderbook/orderbook_table_item.dart @@ -1,6 +1,7 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/model/orderbook/order.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; @@ -32,9 +33,10 @@ class _OrderbookTableItemState extends State { @override void initState() { + final coinsRepository = RepositoryProvider.of(context); _isPreview = widget.order.uuid == orderPreviewUuid; - _isTradeWithSelf = - widget.order.address == coinsBloc.getCoin(widget.order.rel)?.address; + _isTradeWithSelf = widget.order.address == + coinsRepository.getCoin(widget.order.rel)?.address; _style = const TextStyle(fontSize: 11, fontWeight: FontWeight.w500); _color = _isPreview ? theme.custom.targetColor @@ -119,7 +121,7 @@ class _OrderbookTableItemState extends State { child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 21), child: Container( - color: _color.withOpacity(0.1), + color: _color.withValues(alpha: 0.1), ), ), ); @@ -132,11 +134,11 @@ class _OrderbookTableItemState extends State { ? Border( bottom: BorderSide( width: 0.5, - color: _color.withOpacity(0.3), + color: _color.withValues(alpha: 0.3), ), top: BorderSide( width: 0.5, - color: _color.withOpacity(0.3), + color: _color.withValues(alpha: 0.3), ), ) : null, diff --git a/lib/views/dex/orderbook/orderbook_view.dart b/lib/views/dex/orderbook/orderbook_view.dart index 78d696c24e..e9ae5ddba8 100644 --- a/lib/views/dex/orderbook/orderbook_view.dart +++ b/lib/views/dex/orderbook/orderbook_view.dart @@ -1,6 +1,9 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/blocs/orderbook_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/orderbook/orderbook_response.dart'; import 'package:web_dex/model/coin.dart'; @@ -11,7 +14,6 @@ import 'package:web_dex/shared/ui/gradient_border.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_error_message.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_table.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_table_title.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class OrderbookView extends StatefulWidget { const OrderbookView({ @@ -42,6 +44,7 @@ class _OrderbookViewState extends State { _model = OrderbookModel( base: widget.base, rel: widget.rel, + orderBookRepository: RepositoryProvider.of(context), ); super.initState(); diff --git a/lib/views/dex/simple/confirm/maker_order_confirmation.dart b/lib/views/dex/simple/confirm/maker_order_confirmation.dart index 0aa72126aa..b8358642ee 100644 --- a/lib/views/dex/simple/confirm/maker_order_confirmation.dart +++ b/lib/views/dex/simple/confirm/maker_order_confirmation.dart @@ -1,9 +1,12 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -37,6 +40,9 @@ class _MakerOrderConfirmationState extends State { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); + final coinsRepository = RepositoryProvider.of(context); + return Container( padding: isMobile ? const EdgeInsets.only(top: 18.0) @@ -50,8 +56,9 @@ class _MakerOrderConfirmationState extends State { final preimage = preimageSnapshot.data; if (preimage == null) return const UiSpinner(); - final Coin? sellCoin = coinsBloc.getCoin(preimage.request.base); - final Coin? buyCoin = coinsBloc.getCoin(preimage.request.rel); + final Coin? sellCoin = + coinsRepository.getCoin(preimage.request.base); + final Coin? buyCoin = coinsRepository.getCoin(preimage.request.rel); final Rational? sellAmount = preimage.request.volume; final Rational buyAmount = (sellAmount ?? Rational.zero) * preimage.request.price; @@ -294,8 +301,12 @@ class _MakerOrderConfirmationState extends State { _inProgress = true; }); + final makerFormBloc = RepositoryProvider.of(context); final TextError? error = await makerFormBloc.makeOrder(); + final tradingEntitiesBloc = + // ignore: use_build_context_synchronously + RepositoryProvider.of(context); await tradingEntitiesBloc.fetch(); // Delay helps to avoid buttons enabled/disabled state blinking diff --git a/lib/views/dex/simple/confirm/taker_order_confirmation.dart b/lib/views/dex/simple/confirm/taker_order_confirmation.dart index 1ea65ad0a3..cba15a0122 100644 --- a/lib/views/dex/simple/confirm/taker_order_confirmation.dart +++ b/lib/views/dex/simple/confirm/taker_order_confirmation.dart @@ -2,11 +2,13 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -16,13 +18,12 @@ import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; import 'package:web_dex/shared/utils/balances_formatter.dart'; import 'package:web_dex/shared/utils/formatters.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; import 'package:web_dex/shared/widgets/segwit_icon.dart'; -import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/views/dex/simple/form/taker/taker_form_exchange_rate.dart'; import 'package:web_dex/views/dex/simple/form/taker/taker_form_total_fees.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class TakerOrderConfirmation extends StatefulWidget { const TakerOrderConfirmation({Key? key}) : super(key: key); @@ -34,6 +35,7 @@ class TakerOrderConfirmation extends StatefulWidget { class _TakerOrderConfirmationState extends State { @override Widget build(BuildContext context) { + final coinsBloc = RepositoryProvider.of(context); return Container( padding: EdgeInsets.only(top: isMobile ? 18.0 : 9.00), constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), @@ -317,6 +319,8 @@ class _TakerOrderConfirmationState extends State { context.read().add(TakerClear()); routingState.dexState.setDetailsAction(uuid); + final tradingEntitiesBloc = + RepositoryProvider.of(context); await tradingEntitiesBloc.fetch(); } } diff --git a/lib/views/dex/simple/form/exchange_info/exchange_rate.dart b/lib/views/dex/simple/form/exchange_info/exchange_rate.dart index e01dd332b9..be5afce3a7 100644 --- a/lib/views/dex/simple/form/exchange_info/exchange_rate.dart +++ b/lib/views/dex/simple/form/exchange_info/exchange_rate.dart @@ -81,7 +81,7 @@ class _Rates extends StatelessWidget { ), ), Text( - showDetails ? '($baseFiat)' : '', + showDetails ? '(${baseFiat(context)})' : '', style: theme.custom.tradingFormDetailsContent, ), ], @@ -91,7 +91,7 @@ class _Rates extends StatelessWidget { '1 ${Coin.normalizeAbbr(rel ?? '')} =' ' $quotePrice' ' ${Coin.normalizeAbbr(base ?? '')}' - ' ($relFiat)', + ' (${relFiat(context)})', style: TextStyle( fontSize: 12, color: theme.custom.subBalanceColor, @@ -102,15 +102,16 @@ class _Rates extends StatelessWidget { ); } - String get baseFiat { - return getFormattedFiatAmount(rel ?? '', rate ?? Rational.zero); + String baseFiat(BuildContext context) { + return getFormattedFiatAmount(context, rel ?? '', rate ?? Rational.zero); } - String get relFiat { + String relFiat(BuildContext context) { if (rate == Rational.zero) { - return getFormattedFiatAmount(base ?? '', Rational.zero); + return getFormattedFiatAmount(context, base ?? '', Rational.zero); } - return getFormattedFiatAmount(base ?? '', rate?.inverse ?? Rational.zero); + return getFormattedFiatAmount( + context, base ?? '', rate?.inverse ?? Rational.zero); } String get price { diff --git a/lib/views/dex/simple/form/exchange_info/total_fees.dart b/lib/views/dex/simple/form/exchange_info/total_fees.dart index 44be1f6dad..a3baae48c6 100644 --- a/lib/views/dex/simple/form/exchange_info/total_fees.dart +++ b/lib/views/dex/simple/form/exchange_info/total_fees.dart @@ -3,9 +3,10 @@ import 'dart:math'; import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/trade_preimage.dart'; @@ -30,6 +31,7 @@ class TotalFees extends StatefulWidget { class _TotalFeesState extends State { @override Widget build(BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); return Row( children: [ Text(LocaleKeys.totalFees.tr(), @@ -53,7 +55,8 @@ class _TotalFeesState extends State { child: Container( alignment: Alignment.centerRight, child: AutoScrollText( - text: getTotalFee(widget.preimage?.totalFees, coinsBloc.getCoin), + text: getTotalFee( + widget.preimage?.totalFees, coinsRepository.getCoin), style: theme.custom.tradingFormDetailsContent, ), ), @@ -100,7 +103,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(preimage.baseCoinFee.amount) ?? 0))} ' '${preimage.baseCoinFee.coin} ' - '(${getFormattedFiatAmount(preimage.baseCoinFee.coin, preimage.baseCoinFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, preimage.baseCoinFee.coin, preimage.baseCoinFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsSendCoinTxFee.tr(args: [ preimage.baseCoinFee.coin ])}', @@ -113,7 +116,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(preimage.relCoinFee.amount) ?? 0))} ' '${preimage.relCoinFee.coin} ' - '(${getFormattedFiatAmount(preimage.relCoinFee.coin, preimage.relCoinFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, preimage.relCoinFee.coin, preimage.relCoinFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsReceiveCoinTxFee.tr(args: [ preimage.relCoinFee.coin ])}', @@ -126,7 +129,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(takerFee.amount) ?? 0))} ' '${takerFee.coin} ' - '(${getFormattedFiatAmount(takerFee.coin, takerFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, takerFee.coin, takerFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsTradingFee.tr()}', style: Theme.of(context).textTheme.bodySmall, ), @@ -137,7 +140,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(feeToSendTakerFee.amount) ?? 0))} ' '${feeToSendTakerFee.coin} ' - '(${getFormattedFiatAmount(feeToSendTakerFee.coin, feeToSendTakerFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, feeToSendTakerFee.coin, feeToSendTakerFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsSendTradingFeeTxFee.tr()}', style: Theme.of(context).textTheme.bodySmall, ), @@ -161,7 +164,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(preimage.baseCoinFee.amount) ?? 0))} ' '${preimage.baseCoinFee.coin} ' - '(${getFormattedFiatAmount(preimage.baseCoinFee.coin, preimage.baseCoinFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, preimage.baseCoinFee.coin, preimage.baseCoinFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsSendCoinTxFee.tr(args: [ preimage.baseCoinFee.coin ])}', @@ -176,7 +179,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(preimage.relCoinFee.amount) ?? 0))} ' '${preimage.relCoinFee.coin} ' - '(${getFormattedFiatAmount(preimage.relCoinFee.coin, preimage.relCoinFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, preimage.relCoinFee.coin, preimage.relCoinFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsReceiveCoinTxFee.tr(args: [ preimage.relCoinFee.coin ])}', @@ -191,7 +194,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(takerFee.amount) ?? 0))} ' '${takerFee.coin} ' - '(${getFormattedFiatAmount(takerFee.coin, takerFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, takerFee.coin, takerFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsTradingFee.tr()}', style: Theme.of(context).textTheme.bodySmall, ), @@ -204,7 +207,7 @@ class _TotalFeesState extends State { child: SelectableText( '• ${cutTrailingZeros(formatAmt(double.tryParse(feeToSendTakerFee.amount) ?? 0))} ' '${feeToSendTakerFee.coin} ' - '(${getFormattedFiatAmount(feeToSendTakerFee.coin, feeToSendTakerFee.amountRational, 8)}): ' + '(${getFormattedFiatAmount(context, feeToSendTakerFee.coin, feeToSendTakerFee.amountRational, 8)}): ' '${LocaleKeys.swapFeeDetailsSendTradingFeeTxFee.tr()}', style: Theme.of(context).textTheme.bodySmall, ), diff --git a/lib/views/dex/simple/form/maker/maker_form_buy_amount.dart b/lib/views/dex/simple/form/maker/maker_form_buy_amount.dart index 9951487d31..9992d38f71 100644 --- a/lib/views/dex/simple/form/maker/maker_form_buy_amount.dart +++ b/lib/views/dex/simple/form/maker/maker_form_buy_amount.dart @@ -1,7 +1,8 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; @@ -41,6 +42,7 @@ class _BuyAmountFiat extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); final TextStyle? textStyle = Theme.of(context).textTheme.bodySmall; return StreamBuilder( initialData: makerFormBloc.buyAmount, @@ -51,7 +53,7 @@ class _BuyAmountFiat extends StatelessWidget { final amount = snapshot.data ?? Rational.zero; return Text( - getFormattedFiatAmount(coin.abbr, amount), + getFormattedFiatAmount(context, coin.abbr, amount), style: textStyle, ); }, @@ -71,6 +73,7 @@ class _BuyAmountInput extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.buyAmount, stream: makerFormBloc.outBuyAmount, diff --git a/lib/views/dex/simple/form/maker/maker_form_buy_coin_table.dart b/lib/views/dex/simple/form/maker/maker_form_buy_coin_table.dart index 5dedaad698..e08d4dbb70 100644 --- a/lib/views/dex/simple/form/maker/maker_form_buy_coin_table.dart +++ b/lib/views/dex/simple/form/maker/maker_form_buy_coin_table.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_buy_switcher.dart'; import 'package:web_dex/views/dex/simple/form/tables/coins_table/coins_table.dart'; @@ -10,6 +11,7 @@ class MakerFormBuyCoinTable extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.showBuyCoinSelect, stream: makerFormBloc.outShowBuyCoinSelect, diff --git a/lib/views/dex/simple/form/maker/maker_form_buy_item.dart b/lib/views/dex/simple/form/maker/maker_form_buy_item.dart index 315f4417ac..9c517e3f0d 100644 --- a/lib/views/dex/simple/form/maker/maker_form_buy_item.dart +++ b/lib/views/dex/simple/form/maker/maker_form_buy_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_buy_switcher.dart'; import 'package:web_dex/views/dex/simple/form/taker/coin_item/trade_controller.dart'; @@ -15,6 +16,7 @@ class MakerFormBuyItem extends StatefulWidget { class _MakerFormBuyItemState extends State { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.buyCoin, stream: makerFormBloc.outBuyCoin, diff --git a/lib/views/dex/simple/form/maker/maker_form_compare_to_cex.dart b/lib/views/dex/simple/form/maker/maker_form_compare_to_cex.dart index ea6eb27975..caed424d2a 100644 --- a/lib/views/dex/simple/form/maker/maker_form_compare_to_cex.dart +++ b/lib/views/dex/simple/form/maker/maker_form_compare_to_cex.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/views/dex/simple/form/exchange_info/dex_compared_to_cex.dart'; class MakerFormCompareToCex extends StatelessWidget { @@ -8,6 +9,7 @@ class MakerFormCompareToCex extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.price, stream: makerFormBloc.outPrice, diff --git a/lib/views/dex/simple/form/maker/maker_form_content.dart b/lib/views/dex/simple/form/maker/maker_form_content.dart index 727825bca1..f595a8bd17 100644 --- a/lib/views/dex/simple/form/maker/maker_form_content.dart +++ b/lib/views/dex/simple/form/maker/maker_form_content.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; import 'package:web_dex/shared/widgets/connect_wallet/connect_wallet_wrapper.dart'; @@ -25,6 +26,8 @@ class MakerFormContent extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); + return FormPlate( child: Padding( padding: const EdgeInsets.fromLTRB(0, 12, 0, 20), @@ -98,6 +101,7 @@ class _ClearButton extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return UiLightButton( text: LocaleKeys.clear.tr(), onPressed: () { diff --git a/lib/views/dex/simple/form/maker/maker_form_error_list.dart b/lib/views/dex/simple/form/maker/maker_form_error_list.dart index 5c7135f9aa..8e4b95b51e 100644 --- a/lib/views/dex/simple/form/maker/maker_form_error_list.dart +++ b/lib/views/dex/simple/form/maker/maker_form_error_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/dex_form_error.dart'; import 'package:web_dex/views/dex/simple/form/error_list/dex_form_error_list.dart'; @@ -8,6 +9,7 @@ class MakerFormErrorList extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder>( initialData: makerFormBloc.getFormErrors(), stream: makerFormBloc.outFormErrors, diff --git a/lib/views/dex/simple/form/maker/maker_form_exchange_rate.dart b/lib/views/dex/simple/form/maker/maker_form_exchange_rate.dart index 13f0f1582b..e8c4f4c463 100644 --- a/lib/views/dex/simple/form/maker/maker_form_exchange_rate.dart +++ b/lib/views/dex/simple/form/maker/maker_form_exchange_rate.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/views/dex/simple/form/exchange_info/exchange_rate.dart'; class MakerFormExchangeRate extends StatelessWidget { @@ -8,6 +9,7 @@ class MakerFormExchangeRate extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.price, stream: makerFormBloc.outPrice, diff --git a/lib/views/dex/simple/form/maker/maker_form_layout.dart b/lib/views/dex/simple/form/maker/maker_form_layout.dart index 921c3f73d7..6d764c4c36 100644 --- a/lib/views/dex/simple/form/maker/maker_form_layout.dart +++ b/lib/views/dex/simple/form/maker/maker_form_layout.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/coin.dart'; @@ -26,6 +26,7 @@ class MakerFormLayout extends StatefulWidget { class _MakerFormLayoutState extends State { @override void initState() { + final makerFormBloc = RepositoryProvider.of(context); makerFormBloc.setDefaultSellCoin(); _consumeRouteParameters(); @@ -33,10 +34,13 @@ class _MakerFormLayoutState extends State { } void _consumeRouteParameters() { + final makerFormBloc = RepositoryProvider.of(context); + final coinsRepository = RepositoryProvider.of(context); + if (routingState.dexState.orderType != 'taker') { if (routingState.dexState.fromCurrency.isNotEmpty) { final Coin? sellCoin = - coinsBloc.getCoin(routingState.dexState.fromCurrency); + coinsRepository.getCoin(routingState.dexState.fromCurrency); if (sellCoin != null) { makerFormBloc.sellCoin = sellCoin; @@ -49,7 +53,7 @@ class _MakerFormLayoutState extends State { if (routingState.dexState.toCurrency.isNotEmpty) { final Coin? buyCoin = - coinsBloc.getCoin(routingState.dexState.toCurrency); + coinsRepository.getCoin(routingState.dexState.toCurrency); if (buyCoin != null) { makerFormBloc.buyCoin = buyCoin; @@ -67,6 +71,8 @@ class _MakerFormLayoutState extends State { @override Widget build(BuildContext context) { final DexTabBarBloc bloc = context.read(); + final makerFormBloc = RepositoryProvider.of(context); + return BlocListener( listener: (context, state) { if (state.mode == AuthorizeMode.noLogin) { diff --git a/lib/views/dex/simple/form/maker/maker_form_orderbook.dart b/lib/views/dex/simple/form/maker/maker_form_orderbook.dart index 44b811766f..27dd1162a5 100644 --- a/lib/views/dex/simple/form/maker/maker_form_orderbook.dart +++ b/lib/views/dex/simple/form/maker/maker_form_orderbook.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/orderbook/order.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_view.dart'; @@ -10,6 +11,7 @@ class MakerFormOrderbook extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.sellCoin, stream: makerFormBloc.outSellCoin, @@ -22,10 +24,11 @@ class MakerFormOrderbook extends StatelessWidget { initialData: makerFormBloc.price, stream: makerFormBloc.outPrice, builder: (context, price) { - return _buildOrderbook( - base: sellCoin.data, - rel: buyCoin.data, - price: price.data, + return OrderbookView( + base: makerFormBloc.sellCoin, + rel: makerFormBloc.buyCoin, + myOrder: _getMyOrder(context, price.data), + onAskClick: (Order order) => _onAskClick(context, order), ); }, ); @@ -35,20 +38,8 @@ class MakerFormOrderbook extends StatelessWidget { ); } - Widget _buildOrderbook({ - required Coin? base, - required Coin? rel, - required Rational? price, - }) { - return OrderbookView( - base: makerFormBloc.sellCoin, - rel: makerFormBloc.buyCoin, - myOrder: _getMyOrder(price), - onAskClick: _onAskClick, - ); - } - - Order? _getMyOrder(Rational? price) { + Order? _getMyOrder(BuildContext context, Rational? price) { + final makerFormBloc = RepositoryProvider.of(context); final Coin? sellCoin = makerFormBloc.sellCoin; final Coin? buyCoin = makerFormBloc.buyCoin; final Rational? sellAmount = makerFormBloc.sellAmount; @@ -68,7 +59,8 @@ class MakerFormOrderbook extends StatelessWidget { ); } - void _onAskClick(Order order) { + void _onAskClick(BuildContext context, Order order) { + final makerFormBloc = RepositoryProvider.of(context); if (makerFormBloc.sellAmount == null) makerFormBloc.setMaxSellAmount(); makerFormBloc.setPriceValue(order.price.toDouble().toStringAsFixed(8)); } diff --git a/lib/views/dex/simple/form/maker/maker_form_price_item.dart b/lib/views/dex/simple/form/maker/maker_form_price_item.dart index 6af460eb7b..88dea0d619 100644 --- a/lib/views/dex/simple/form/maker/maker_form_price_item.dart +++ b/lib/views/dex/simple/form/maker/maker_form_price_item.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/amount_input_field.dart'; @@ -17,11 +18,16 @@ class MakerFormPriceItem extends StatefulWidget { class _MakerFormPriceItemState extends State { final List _listeners = []; - Coin? _sellCoin = makerFormBloc.sellCoin; - Coin? _buyCoin = makerFormBloc.buyCoin; + Coin? _sellCoin; + Coin? _buyCoin; @override void initState() { + final makerFormBloc = RepositoryProvider.of(context); + + _sellCoin = makerFormBloc.sellCoin; + _buyCoin = makerFormBloc.buyCoin; + _listeners.add(makerFormBloc.outSellCoin.listen(_onFormStateChange)); _listeners.add(makerFormBloc.outBuyCoin.listen(_onFormStateChange)); super.initState(); @@ -73,6 +79,7 @@ class _MakerFormPriceItemState extends State { } Widget _buildPriceField() { + final makerFormBloc = RepositoryProvider.of(context); return AmountInputField( hint: '', stream: makerFormBloc.outPrice, @@ -103,6 +110,7 @@ class _MakerFormPriceItemState extends State { void _onFormStateChange(dynamic _) { if (!mounted) return; + final makerFormBloc = RepositoryProvider.of(context); setState(() { _sellCoin = makerFormBloc.sellCoin; _buyCoin = makerFormBloc.buyCoin; diff --git a/lib/views/dex/simple/form/maker/maker_form_sell_amount.dart b/lib/views/dex/simple/form/maker/maker_form_sell_amount.dart index 4db7234a04..176dbe3a74 100644 --- a/lib/views/dex/simple/form/maker/maker_form_sell_amount.dart +++ b/lib/views/dex/simple/form/maker/maker_form_sell_amount.dart @@ -1,7 +1,8 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; @@ -41,6 +42,7 @@ class _SellAmountFiat extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); final TextStyle? textStyle = Theme.of(context).textTheme.bodySmall; return StreamBuilder( initialData: makerFormBloc.sellAmount, @@ -56,7 +58,7 @@ class _SellAmountFiat extends StatelessWidget { if (coin == null) return const SizedBox(); return Text( - getFormattedFiatAmount(coin.abbr, amount), + getFormattedFiatAmount(context, coin.abbr, amount), style: textStyle, ); }); @@ -77,6 +79,7 @@ class _SellAmountInput extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.sellAmount, stream: makerFormBloc.outSellAmount, diff --git a/lib/views/dex/simple/form/maker/maker_form_sell_coin_table.dart b/lib/views/dex/simple/form/maker/maker_form_sell_coin_table.dart index b7def2597f..21b87f2b0e 100644 --- a/lib/views/dex/simple/form/maker/maker_form_sell_coin_table.dart +++ b/lib/views/dex/simple/form/maker/maker_form_sell_coin_table.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_sell_switcher.dart'; import 'package:web_dex/views/dex/simple/form/tables/coins_table/coins_table.dart'; @@ -10,6 +11,7 @@ class MakerFormSellCoinTable extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.showSellCoinSelect, stream: makerFormBloc.outShowSellCoinSelect, diff --git a/lib/views/dex/simple/form/maker/maker_form_sell_header.dart b/lib/views/dex/simple/form/maker/maker_form_sell_header.dart index e90d29baae..279a8c2022 100644 --- a/lib/views/dex/simple/form/maker/maker_form_sell_header.dart +++ b/lib/views/dex/simple/form/maker/maker_form_sell_header.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/available_balance_state.dart'; import 'package:web_dex/views/dex/simple/form/common/dex_form_group_header.dart'; @@ -29,6 +30,7 @@ class _AvailableBalance extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.maxSellAmount, stream: makerFormBloc.outMaxSellAmount, @@ -51,6 +53,7 @@ class _HalfMaxButtons extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.maxSellAmount, stream: makerFormBloc.outMaxSellAmount, @@ -69,11 +72,15 @@ class _HalfMaxButtons extends StatelessWidget { class _MaxButton extends DexSmallButton { _MaxButton() : super( - LocaleKeys.max.tr(), (context) => makerFormBloc.setMaxSellAmount()); + LocaleKeys.max.tr(), + (context) => RepositoryProvider.of(context) + .setMaxSellAmount()); } class _HalfButton extends DexSmallButton { _HalfButton() - : super(LocaleKeys.half.tr(), - (context) => makerFormBloc.setHalfSellAmount()); + : super( + LocaleKeys.half.tr(), + (context) => RepositoryProvider.of(context) + .setHalfSellAmount()); } diff --git a/lib/views/dex/simple/form/maker/maker_form_sell_item.dart b/lib/views/dex/simple/form/maker/maker_form_sell_item.dart index cbf622bcd1..0116b3b720 100644 --- a/lib/views/dex/simple/form/maker/maker_form_sell_item.dart +++ b/lib/views/dex/simple/form/maker/maker_form_sell_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/common/front_plate.dart'; import 'package:web_dex/views/dex/simple/form/maker/maker_form_sell_header.dart'; @@ -17,6 +18,7 @@ class MakerFormSellItem extends StatefulWidget { class _MakerFormSellItemState extends State { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return FrontPlate( child: StreamBuilder( initialData: makerFormBloc.sellCoin, diff --git a/lib/views/dex/simple/form/maker/maker_form_total_fees.dart b/lib/views/dex/simple/form/maker/maker_form_total_fees.dart index f53371af12..7cafdbfe52 100644 --- a/lib/views/dex/simple/form/maker/maker_form_total_fees.dart +++ b/lib/views/dex/simple/form/maker/maker_form_total_fees.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/model/trade_preimage.dart'; import 'package:web_dex/views/dex/simple/form/exchange_info/total_fees.dart'; @@ -8,6 +9,7 @@ class MakerFormTotalFees extends StatelessWidget { @override Widget build(BuildContext context) { + final makerFormBloc = RepositoryProvider.of(context); return StreamBuilder( initialData: makerFormBloc.preimage, stream: makerFormBloc.outPreimage, diff --git a/lib/views/dex/simple/form/maker/maker_form_trade_button.dart b/lib/views/dex/simple/form/maker/maker_form_trade_button.dart index 23a1db2300..da679e25f3 100644 --- a/lib/views/dex/simple/form/maker/maker_form_trade_button.dart +++ b/lib/views/dex/simple/form/maker/maker_form_trade_button.dart @@ -2,11 +2,12 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class MakerFormTradeButton extends StatelessWidget { const MakerFormTradeButton({Key? key}) : super(key: key); @@ -20,6 +21,9 @@ class MakerFormTradeButton extends StatelessWidget { systemHealthState is SystemHealthLoadSuccess && systemHealthState.isValid; + final makerFormBloc = RepositoryProvider.of(context); + final coinsBloc = context.watch(); + return StreamBuilder( initialData: makerFormBloc.inProgress, stream: makerFormBloc.outInProgress, @@ -46,7 +50,7 @@ class MakerFormTradeButton extends StatelessWidget { onPressed: disabled ? null : () async { - while (!coinsBloc.loginActivationFinished) { + while (!coinsBloc.state.loginActivationFinished) { await Future.delayed( const Duration(milliseconds: 300)); } diff --git a/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart b/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart index 805c760efa..c44648c4ab 100644 --- a/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart +++ b/lib/views/dex/simple/form/tables/coins_table/coins_table_content.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.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'; @@ -20,12 +20,11 @@ class CoinsTableContent extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder>( - stream: coinsBloc.outKnownCoins, - initialData: coinsBloc.knownCoins, - builder: (context, snapshot) { + return BlocBuilder( + builder: (context, state) { final coins = prepareCoinsForTable( - coinsBloc.knownCoins, + context, + state.coins.values.toList(), searchString, testCoinsEnabled: context.read().state.testCoinsEnabled, ); diff --git a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart index c8aac36e71..ded6723482 100644 --- a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart +++ b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; @@ -25,7 +26,7 @@ class GroupedListView extends StatelessWidget { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - final groupedItems = _groupList(items); + final groupedItems = _groupList(context, items); // Add right padding to the last column if there are grouped items // to align the grouped and non-grouped @@ -57,7 +58,7 @@ class GroupedListView extends StatelessWidget { initiallyExpanded: false, title: CoinsTableItem( data: group.value.first, - coin: _createHeaderCoinData(group.value), + coin: _createHeaderCoinData(context, group.value), onSelect: onSelect, isGroupHeader: true, subtitleText: LocaleKeys.nNetworks @@ -90,42 +91,42 @@ class GroupedListView extends StatelessWidget { padding: padding, child: CoinsTableItem( data: item, - coin: getCoin(item), + coin: getCoin(context, item), onSelect: onSelect, ), ); } - Coin _createHeaderCoinData(List list) { - final firstCoin = getCoin(list.first); + Coin _createHeaderCoinData(BuildContext context, List list) { + final firstCoin = getCoin(context, list.first); double totalBalance = list.fold(0, (sum, item) { - final coin = getCoin(item); + final coin = getCoin(context, item); return sum + coin.balance; }); final coin = firstCoin.dummyCopyWithoutProtocolData(); - - coin.balance = totalBalance; - - return coin; + return coin.copyWith(balance: totalBalance); } - Map> _groupList(List list) { + Map> _groupList(BuildContext context, List list) { Map> grouped = {}; for (final item in list) { - final coin = getCoin(item); + final coin = getCoin(context, item); grouped.putIfAbsent(coin.name, () => []).add(item); } return grouped; } - Coin getCoin(T item) { + Coin getCoin(BuildContext context, T item) { + final coinsState = RepositoryProvider.of(context).state; if (item is Coin) { return item as Coin; } else if (item is coin_dropdown.CoinSelectItem) { - return coinsBloc.getCoin(item.coinId)!; + return (coinsState.walletCoins[item.coinId] ?? + coinsState.coins[item.coinId])!; } else { - return coinsBloc.getCoin((item as BestOrder).coin)!; + final String coinId = (item as BestOrder).coin; + return (coinsState.walletCoins[coinId] ?? coinsState.coins[coinId])!; } } } diff --git a/lib/views/dex/simple/form/tables/orders_table/orders_table.dart b/lib/views/dex/simple/form/tables/orders_table/orders_table.dart index 952ee539f1..96a26cd2e0 100644 --- a/lib/views/dex/simple/form/tables/orders_table/orders_table.dart +++ b/lib/views/dex/simple/form/tables/orders_table/orders_table.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/views/dex/common/front_plate.dart'; import 'package:web_dex/views/dex/simple/form/tables/orders_table/orders_table_content.dart'; @@ -26,7 +26,8 @@ class _OrdersTableState extends State { return BlocSelector( selector: (state) => state.selectedOrder, builder: (context, selectedOrder) { - final coin = coinsBloc.getCoin(selectedOrder?.coin ?? ''); + final coinsRepository = RepositoryProvider.of(context); + final coin = coinsRepository.getCoin(selectedOrder?.coin ?? ''); final controller = TradeOrderController( order: selectedOrder, coin: coin, 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 f499f449dd..a1d321403f 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 @@ -45,6 +45,7 @@ class OrdersTableContent extends StatelessWidget { final Map> ordersMap = bestOrders.result!; final AuthorizeMode mode = context.watch().state.mode; final List orders = prepareOrdersForTable( + context, ordersMap, searchString, mode, diff --git a/lib/views/dex/simple/form/tables/table_utils.dart b/lib/views/dex/simple/form/tables/table_utils.dart index 99ccc67152..5078398b89 100644 --- a/lib/views/dex/simple/form/tables/table_utils.dart +++ b/lib/views/dex/simple/form/tables/table_utils.dart @@ -1,52 +1,59 @@ -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.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/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/shared/utils/balances_formatter.dart'; -import 'package:web_dex/views/dex/dex_helpers.dart'; List prepareCoinsForTable( + BuildContext context, List coins, String? searchString, { bool testCoinsEnabled = true, }) { + final authBloc = RepositoryProvider.of(context); coins = List.from(coins); if (!testCoinsEnabled) coins = removeTestCoins(coins); coins = removeWalletOnly(coins); - coins = removeSuspended(coins); + coins = removeSuspended(coins, authBloc.state.isSignedIn); coins = sortFiatBalance(coins); coins = filterCoinsByPhrase(coins, searchString ?? '').toList(); return coins; } List prepareOrdersForTable( + BuildContext context, Map>? orders, String? searchString, AuthorizeMode mode, { bool testCoinsEnabled = true, }) { if (orders == null) return []; - final List sorted = _sortBestOrders(orders); + final List sorted = _sortBestOrders(context, orders); if (sorted.isEmpty) return []; if (!testCoinsEnabled) { - removeTestCoinOrders(sorted); + removeTestCoinOrders(sorted, context); if (sorted.isEmpty) return []; } - removeSuspendedCoinOrders(sorted, mode); + removeSuspendedCoinOrders(sorted, mode, context); if (sorted.isEmpty) return []; - removeWalletOnlyCoinOrders(sorted); + removeWalletOnlyCoinOrders(sorted, context); if (sorted.isEmpty) return []; final String? filter = searchString?.toLowerCase(); if (filter == null || filter.isEmpty) { return sorted; } + + final coinsRepository = RepositoryProvider.of(context); final List filtered = sorted.where((order) { - final Coin? coin = coinsBloc.getCoin(order.coin); + final Coin? coin = coinsRepository.getCoin(order.coin); if (coin == null) return false; return compareCoinByPhrase(coin, filter); }).toList(); @@ -54,18 +61,20 @@ List prepareOrdersForTable( return filtered; } -List _sortBestOrders(Map> unsorted) { +List _sortBestOrders( + BuildContext context, Map> unsorted) { if (unsorted.isEmpty) return []; + final coinsRepository = RepositoryProvider.of(context); final List sorted = []; unsorted.forEach((ticker, list) { - if (coinsBloc.getCoin(list[0].coin) == null) return; + if (coinsRepository.getCoin(list[0].coin) == null) return; sorted.add(list[0]); }); sorted.sort((a, b) { - final Coin? coinA = coinsBloc.getCoin(a.coin); - final Coin? coinB = coinsBloc.getCoin(b.coin); + final Coin? coinA = coinsRepository.getCoin(a.coin); + final Coin? coinB = coinsRepository.getCoin(b.coin); if (coinA == null || coinB == null) return 0; final double fiatPriceA = getFiatAmount(coinA, a.price); @@ -79,3 +88,38 @@ List _sortBestOrders(Map> unsorted) { return sorted; } + +void removeSuspendedCoinOrders( + List orders, + AuthorizeMode authorizeMode, + BuildContext context, +) { + if (authorizeMode == AuthorizeMode.noLogin) return; + final coinsRepository = RepositoryProvider.of(context); + orders.removeWhere((BestOrder order) { + final Coin? coin = coinsRepository.getCoin(order.coin); + if (coin == null) return true; + + return coin.isSuspended; + }); +} + +void removeWalletOnlyCoinOrders(List orders, BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); + orders.removeWhere((BestOrder order) { + final Coin? coin = coinsRepository.getCoin(order.coin); + if (coin == null) return true; + + return coin.walletOnly; + }); +} + +void removeTestCoinOrders(List orders, BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); + orders.removeWhere((BestOrder order) { + final Coin? coin = coinsRepository.getCoin(order.coin); + if (coin == null) return true; + + return coin.isTestCoin; + }); +} diff --git a/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_amount.dart b/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_amount.dart index 529234f0ed..e5df4da679 100644 --- a/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_amount.dart +++ b/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_amount.dart @@ -55,7 +55,7 @@ class _BuyPriceField extends StatelessWidget { final amount = state.buyAmount ?? Rational.zero; return Text( - getFormattedFiatAmount(order.coin, amount), + getFormattedFiatAmount(context, order.coin, amount), style: textStyle, ); }, diff --git a/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_item.dart b/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_item.dart index f0ac9dfa0e..72d54f4f91 100644 --- a/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_item.dart +++ b/lib/views/dex/simple/form/taker/coin_item/taker_form_buy_item.dart @@ -1,15 +1,15 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/views/dex/common/front_plate.dart'; import 'package:web_dex/views/dex/simple/form/common/dex_form_group_header.dart'; import 'package:web_dex/views/dex/simple/form/taker/coin_item/taker_form_buy_switcher.dart'; import 'package:web_dex/views/dex/simple/form/taker/coin_item/trade_controller.dart'; -import 'package:easy_localization/easy_localization.dart'; class TakerFormBuyItem extends StatelessWidget { const TakerFormBuyItem({super.key}); @@ -24,7 +24,8 @@ class TakerFormBuyItem extends StatelessWidget { return false; }, builder: (context, state) { - final coin = coinsBloc.getCoin(state.selectedOrder?.coin ?? ''); + final coinsRepository = RepositoryProvider.of(context); + final coin = coinsRepository.getCoin(state.selectedOrder?.coin ?? ''); final controller = TradeOrderController( order: state.selectedOrder, diff --git a/lib/views/dex/simple/form/taker/coin_item/taker_form_sell_amount.dart b/lib/views/dex/simple/form/taker/coin_item/taker_form_sell_amount.dart index 7c2717181b..19f00f1c3d 100644 --- a/lib/views/dex/simple/form/taker/coin_item/taker_form_sell_amount.dart +++ b/lib/views/dex/simple/form/taker/coin_item/taker_form_sell_amount.dart @@ -53,7 +53,7 @@ class _SellPriceField extends StatelessWidget { final amount = state.sellAmount ?? Rational.zero; return Text( - getFormattedFiatAmount(coin.abbr, amount), + getFormattedFiatAmount(context, coin.abbr, amount), style: textStyle, ); }); diff --git a/lib/views/dex/simple/form/taker/taker_form.dart b/lib/views/dex/simple/form/taker/taker_form.dart index 3afddf36ca..e95b679c01 100644 --- a/lib/views/dex/simple/form/taker/taker_form.dart +++ b/lib/views/dex/simple/form/taker/taker_form.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/views/dex/simple/form/taker/taker_form_layout.dart'; @@ -23,18 +24,19 @@ class _TakerFormState extends State { @override void initState() { + final coinsBlocState = context.read().state; final takerBloc = context.read(); takerBloc.add(TakerSetDefaults()); - takerBloc.add(TakerSetWalletIsReady(coinsBloc.loginActivationFinished)); - _coinsListener = coinsBloc.outLoginActivationFinished.listen((value) { - takerBloc.add(TakerSetWalletIsReady(value)); - }); - + takerBloc + .add(TakerSetWalletIsReady(coinsBlocState.loginActivationFinished)); routingState.dexState.addListener(_consumeRouteParameters); super.initState(); } void _consumeRouteParameters() async { + final coinsRepository = RepositoryProvider.of(context); + final dexRepository = RepositoryProvider.of(context); + if (routingState.dexState.orderType == 'taker') { final fromCurrency = routingState.dexState.fromCurrency; final toCurrency = routingState.dexState.toCurrency; @@ -45,10 +47,11 @@ class _TakerFormState extends State { if (mounted) { final takerBloc = context.read(); - Coin? sellCoin = - fromCurrency.isNotEmpty ? coinsBloc.getCoin(fromCurrency) : null; + Coin? sellCoin = fromCurrency.isNotEmpty + ? coinsRepository.getCoin(fromCurrency) + : null; Coin? buyCoin = - toCurrency.isNotEmpty ? coinsBloc.getCoin(toCurrency) : null; + toCurrency.isNotEmpty ? coinsRepository.getCoin(toCurrency) : null; if (sellCoin != null || buyCoin != null) { takerBloc.add( @@ -79,6 +82,14 @@ class _TakerFormState extends State { @override Widget build(BuildContext context) { - return const TakerFormLayout(); + return BlocListener( + listenWhen: (previous, current) => + previous.loginActivationFinished != current.loginActivationFinished, + listener: (context, state) { + final takerBloc = context.read(); + takerBloc.add(TakerSetWalletIsReady(state.loginActivationFinished)); + }, + child: const TakerFormLayout(), + ); } } diff --git a/lib/views/dex/simple/form/taker/taker_form_content.dart b/lib/views/dex/simple/form/taker/taker_form_content.dart index 373f17f99b..f46cd60803 100644 --- a/lib/views/dex/simple/form/taker/taker_form_content.dart +++ b/lib/views/dex/simple/form/taker/taker_form_content.dart @@ -1,7 +1,9 @@ import 'package:app_theme/app_theme.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_state.dart'; @@ -20,8 +22,6 @@ import 'package:web_dex/views/dex/simple/form/taker/coin_item/taker_form_sell_it import 'package:web_dex/views/dex/simple/form/taker/taker_form_error_list.dart'; import 'package:web_dex/views/dex/simple/form/taker/taker_form_exchange_info.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:collection/collection.dart'; class TakerFormContent extends StatelessWidget { @override @@ -40,13 +40,14 @@ class TakerFormContent extends StatelessWidget { final selectedOrder = takerBloc.state.selectedOrder; if (selectedOrder == null) return false; - final knownCoins = await coinsRepo.getKnownCoins(); + final coinsRepo = RepositoryProvider.of(context); + final knownCoins = coinsRepo.getKnownCoins(); final buyCoin = knownCoins.firstWhereOrNull( (element) => element.abbr == selectedOrder.coin); if (buyCoin == null) return false; takerBloc.add(TakerSetSellCoin(buyCoin, - autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr)); + autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr),); return true; }, topWidget: const TakerFormSellItem(), diff --git a/lib/views/dex/simple/form/taker/taker_form_exchange_info.dart b/lib/views/dex/simple/form/taker/taker_form_exchange_info.dart index 4167e6ca27..0e02f9b834 100644 --- a/lib/views/dex/simple/form/taker/taker_form_exchange_info.dart +++ b/lib/views/dex/simple/form/taker/taker_form_exchange_info.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/common/dex_info_container.dart'; @@ -42,6 +42,7 @@ class _TakerComparedToCex extends StatelessWidget { builder: (context, state) { final BestOrder? bestOrder = state.selectedOrder; final Coin? sellCoin = state.sellCoin; + final coinsBloc = RepositoryProvider.of(context); final Coin? buyCoin = bestOrder == null ? null : coinsBloc.getCoin(bestOrder.coin); diff --git a/lib/views/dex/simple/form/taker/taker_order_book.dart b/lib/views/dex/simple/form/taker/taker_order_book.dart index 7e4a617a51..af47c1608d 100644 --- a/lib/views/dex/simple/form/taker/taker_order_book.dart +++ b/lib/views/dex/simple/form/taker/taker_order_book.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/model/orderbook/order.dart'; import 'package:web_dex/views/dex/orderbook/orderbook_view.dart'; @@ -13,6 +13,7 @@ class TakerOrderbook extends StatelessWidget { @override Widget build(BuildContext context) { + final coinsBloc = RepositoryProvider.of(context); return BlocBuilder( buildWhen: (prev, cur) { if (prev.sellCoin?.abbr != cur.sellCoin?.abbr) return true; @@ -27,7 +28,7 @@ class TakerOrderbook extends StatelessWidget { base: state.sellCoin, rel: selectedOrder == null ? null - : coinsBloc.getKnownCoin(selectedOrder.coin), + : coinsBloc.getCoin(selectedOrder.coin), selectedOrderUuid: state.selectedOrder?.uuid, onBidClick: (Order order) { if (state.selectedOrder?.uuid == order.uuid) return; diff --git a/lib/views/fiat/fiat_form.dart b/lib/views/fiat/fiat_form.dart index 86d9af0ede..bb1c35fde8 100644 --- a/lib/views/fiat/fiat_form.dart +++ b/lib/views/fiat/fiat_form.dart @@ -6,12 +6,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/fiat/base_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; import 'package:web_dex/bloc/fiat/models/fiat_mode.dart'; import 'package:web_dex/bloc/fiat/models/i_currency.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -32,7 +33,7 @@ class FiatForm extends StatefulWidget { } class _FiatFormState extends State { - bool _isLoggedIn = currentWalletBloc.wallet != null; + bool _isLoggedIn = false; StreamSubscription>? _walletCoinsListener; StreamSubscription? _loginActivationListener; @@ -66,14 +67,8 @@ class _FiatFormState extends State { void initState() { super.initState(); - _walletCoinsListener = coinsBloc.outWalletCoins.listen((walletCoins) async { - await _handleAccountStatusChange(walletCoins.isNotEmpty); - }); - - _loginActivationListener = - coinsBloc.outLoginActivationFinished.listen((isLoggedIn) async { - await _handleAccountStatusChange(isLoggedIn); - }); + _isLoggedIn = + RepositoryProvider.of(context).wallet != null; context.read() ..add(const LoadCurrencyListsRequested()) @@ -211,80 +206,85 @@ class _FiatFormState extends State { // orders that were never completed. final scrollController = ScrollController(); - return BlocConsumer( - listenWhen: (previous, current) => - previous.fiatOrderStatus != current.fiatOrderStatus, - listener: (context, state) => _handlePaymentStatusUpdate(state), - builder: (context, state) => DexScrollbar( - isMobile: isMobile, - scrollController: scrollController, - child: SingleChildScrollView( - key: const Key('fiat-form-scroll'), - controller: scrollController, - child: Column( - children: [ - FiatActionTabBar( - currentTabIndex: state.fiatMode.tabIndex, - onTabClick: _setActiveTab, - ), - const SizedBox(height: 16), - if (state.fiatMode == FiatMode.offramp) - Center(child: Text(LocaleKeys.comingSoon.tr())) - else - GradientBorder( - innerColor: dexPageColors.frontPlate, - gradient: dexPageColors.formPlateGradient, - child: Container( - padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), - child: Column( - children: [ - FiatInputs( - onFiatCurrencyChanged: _onFiatChanged, - onCoinChanged: _onCoinChanged, - onFiatAmountUpdate: _onFiatAmountChanged, - initialFiat: state.selectedFiat.value!, - initialCoin: state.selectedCoin.value!, - initialFiatAmount: state.fiatAmount.valueAsDouble, - fiatList: state.fiatList, - coinList: state.coinList, - selectedPaymentMethodPrice: - state.selectedPaymentMethod.priceInfo, - receiveAddress: state.coinReceiveAddress, - isLoggedIn: _isLoggedIn, - fiatMinAmount: state.minFiatAmount, - fiatMaxAmount: state.maxFiatAmount, - boundariesError: state.fiatAmount.error?.text(state), - ), - const SizedBox(height: 16), - FiatPaymentMethodsGrid(state: state), - const SizedBox(height: 16), - ConnectWalletWrapper( - key: const Key('connect-wallet-fiat-form'), - eventType: WalletsManagerEventType.fiat, - child: UiPrimaryButton( - key: const Key('fiat-onramp-submit-button'), - height: 40, - text: state.fiatOrderStatus.isSubmitting - ? '${LocaleKeys.submitting.tr()}...' - : LocaleKeys.buyNow.tr(), - onPressed: state.canSubmit ? completeOrder : null, + return BlocListener( + listener: (context, state) => _handleAccountStatusChange( + state.loginActivationFinished || state.walletCoins.isNotEmpty), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.fiatOrderStatus != current.fiatOrderStatus, + listener: (context, state) => _handlePaymentStatusUpdate(state), + builder: (context, state) => DexScrollbar( + isMobile: isMobile, + scrollController: scrollController, + child: SingleChildScrollView( + key: const Key('fiat-form-scroll'), + controller: scrollController, + child: Column( + children: [ + FiatActionTabBar( + currentTabIndex: state.fiatMode.tabIndex, + onTabClick: _setActiveTab, + ), + const SizedBox(height: 16), + if (state.fiatMode == FiatMode.offramp) + Center(child: Text(LocaleKeys.comingSoon.tr())) + else + GradientBorder( + innerColor: dexPageColors.frontPlate, + gradient: dexPageColors.formPlateGradient, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 32, 16, 16), + child: Column( + children: [ + FiatInputs( + onFiatCurrencyChanged: _onFiatChanged, + onCoinChanged: _onCoinChanged, + onFiatAmountUpdate: _onFiatAmountChanged, + initialFiat: state.selectedFiat.value!, + initialCoin: state.selectedCoin.value!, + initialFiatAmount: state.fiatAmount.valueAsDouble, + fiatList: state.fiatList, + coinList: state.coinList, + selectedPaymentMethodPrice: + state.selectedPaymentMethod.priceInfo, + receiveAddress: state.coinReceiveAddress, + isLoggedIn: _isLoggedIn, + fiatMinAmount: state.minFiatAmount, + fiatMaxAmount: state.maxFiatAmount, + boundariesError: + state.fiatAmount.error?.text(state), + ), + const SizedBox(height: 16), + FiatPaymentMethodsGrid(state: state), + const SizedBox(height: 16), + ConnectWalletWrapper( + key: const Key('connect-wallet-fiat-form'), + eventType: WalletsManagerEventType.fiat, + child: UiPrimaryButton( + key: const Key('fiat-onramp-submit-button'), + height: 40, + text: state.fiatOrderStatus.isSubmitting + ? '${LocaleKeys.submitting.tr()}...' + : LocaleKeys.buyNow.tr(), + onPressed: state.canSubmit ? completeOrder : null, + ), ), - ), - const SizedBox(height: 16), - Text( - _isLoggedIn - ? state.fiatOrderStatus.isFailed - ? LocaleKeys.fiatCantCompleteOrder.tr() - : LocaleKeys.fiatPriceCanChange.tr() - : LocaleKeys.fiatConnectWallet.tr(), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + const SizedBox(height: 16), + Text( + _isLoggedIn + ? state.fiatOrderStatus.isFailed + ? LocaleKeys.fiatCantCompleteOrder.tr() + : LocaleKeys.fiatPriceCanChange.tr() + : LocaleKeys.fiatConnectWallet.tr(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/views/fiat/fiat_page.dart b/lib/views/fiat/fiat_page.dart index f79cb9e1cc..e59e94616b 100644 --- a/lib/views/fiat/fiat_page.dart +++ b/lib/views/fiat/fiat_page.dart @@ -2,9 +2,12 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/bloc/fiat/banxa_fiat_provider.dart'; import 'package:web_dex/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart'; import 'package:web_dex/bloc/fiat/fiat_order_status.dart'; +import 'package:web_dex/bloc/fiat/fiat_repository.dart'; +import 'package:web_dex/bloc/fiat/ramp/ramp_fiat_provider.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/swap.dart'; @@ -42,8 +45,16 @@ class _FiatPageState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); + final fiatRepository = FiatRepository( + [BanxaFiatProvider(), RampFiatProvider()], + coinsRepository, + ); return BlocProvider( - create: (_) => FiatFormBloc(), + create: (_) => FiatFormBloc( + repository: fiatRepository, + coinsRepository: coinsRepository, + ), child: MultiBlocListener( listeners: [ BlocListener( diff --git a/lib/views/fiat/fiat_payment_method_card.dart b/lib/views/fiat/fiat_payment_method_card.dart index 5162077b70..cf1af166e7 100644 --- a/lib/views/fiat/fiat_payment_method_card.dart +++ b/lib/views/fiat/fiat_payment_method_card.dart @@ -45,7 +45,7 @@ class FiatPaymentMethodCardState extends State { side: BorderSide( color: Theme.of(context) .primaryColor - .withOpacity(isSelected ? 1 : 0.25), + .withValues(alpha: isSelected ? 1 : 0.25), ), ), child: Padding( diff --git a/lib/views/fiat/fiat_payment_method_group.dart b/lib/views/fiat/fiat_payment_method_group.dart index 403abf25bd..38c7e1a131 100644 --- a/lib/views/fiat/fiat_payment_method_group.dart +++ b/lib/views/fiat/fiat_payment_method_group.dart @@ -30,8 +30,8 @@ class FiatPaymentMethodGroup extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( - color: Theme.of(context).primaryColor.withOpacity( - selectedPaymentMethod != null && + color: Theme.of(context).primaryColor.withValues( + alpha: selectedPaymentMethod != null && selectedPaymentMethod!.providerId == providerId ? 1 : 0.25, diff --git a/lib/views/fiat/fiat_select_button.dart b/lib/views/fiat/fiat_select_button.dart index 74ccb37798..586f00ce16 100644 --- a/lib/views/fiat/fiat_select_button.dart +++ b/lib/views/fiat/fiat_select_button.dart @@ -46,7 +46,7 @@ class FiatSelectButton extends StatelessWidget { fontWeight: FontWeight.w500, color: enabled ? foregroundColor - : foregroundColor.withOpacity(0.5), + : foregroundColor.withValues(alpha: 0.5), ), ), if (!isFiat && currency != null) @@ -56,8 +56,8 @@ class FiatSelectButton extends StatelessWidget { : '', style: DefaultTextStyle.of(context).style.copyWith( color: enabled - ? foregroundColor.withOpacity(0.5) - : foregroundColor.withOpacity(0.25), + ? foregroundColor.withValues(alpha: 0.5) + : foregroundColor.withValues(alpha: 0.25), ), ), ], @@ -66,7 +66,7 @@ class FiatSelectButton extends StatelessWidget { Icon( Icons.keyboard_arrow_down, size: 28, - color: foregroundColor.withOpacity(enabled ? 1 : 0.5), + color: foregroundColor.withValues(alpha: enabled ? 1 : 0.5), ), ], ), diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index b22deb72c6..353e7c4d18 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -4,19 +4,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; -import 'package:web_dex/blocs/startup_bloc.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/blocs/update_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; +import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/router/navigators/main_layout/main_layout_router.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/services/alpha_version_alert_service/alpha_version_alert_service.dart'; -import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/utils/window/window.dart'; import 'package:web_dex/views/common/header/app_header.dart'; import 'package:web_dex/views/common/main_menu/main_menu_bar_mobile.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class MainLayout extends StatefulWidget { @override @@ -48,66 +46,24 @@ class _MainLayoutState extends State { if (state.mode == AuthorizeMode.noLogin) { routingState.resetOnLogOut(); } + // This is necessary until current wallet bloc can be phased out + // completely. AuthBloc adds metadata to the current user & wallet + // after the sign-in/register events, so current wallet bloc has to be + // updated to have the metadata reflect where it needs to + context.read().wallet = state.currentUser?.wallet; }, child: Scaffold( key: scaffoldKey, floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, - appBar: buildAppHeader(), - body: SafeArea(child: _buildAppBody()), + appBar: isMobile + ? null + : const PreferredSize( + preferredSize: Size.fromHeight(appBarHeight), + child: AppHeader(), + ), + body: SafeArea(child: MainLayoutRouter()), bottomNavigationBar: !isDesktop ? MainMenuBarMobile() : null, ), ); } - - Widget _buildAppBody() { - return StreamBuilder( - initialData: startUpBloc.running, - stream: startUpBloc.outRunning, - builder: (context, snapshot) { - log('_LayoutWrapperState.build([context]) StreamBuilder: $snapshot'); - if (!snapshot.hasData) { - return const Center(child: UiSpinner()); - } - - return MainLayoutRouter(); - }); - } - - // Method to show an alert dialog with an option to agree if the app is in - // debug mode stating that trading features may not be used for actual trading - // and that only test assets/networks may be used. - Future _showDebugModeDialog() async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return AlertDialog( - title: const Text('Debug mode'), - content: const Text( - 'This app is in debug mode. Trading features may not be used for ' - 'actual trading. Only test assets/networks may be used.', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - _saveAgreedState().ignore(); - }, - child: const Text('I agree'), - ), - ], - ); - }, - ); - } - - Future _saveAgreedState() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool('wallet_only_agreed', true); - } - - Future _hasAgreedNoTrading() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('wallet_only_agreed') ?? false; - } } diff --git a/lib/views/market_maker_bot/animated_bot_status_indicator.dart b/lib/views/market_maker_bot/animated_bot_status_indicator.dart index 2845f5a02a..5a6dba2fac 100644 --- a/lib/views/market_maker_bot/animated_bot_status_indicator.dart +++ b/lib/views/market_maker_bot/animated_bot_status_indicator.dart @@ -56,7 +56,7 @@ class _AnimatedBotStatusIndicatorState extends State decoration: BoxDecoration( shape: BoxShape.circle, color: _getStatusColor(widget.status) - .withOpacity(_getOpacity(widget.status)), + .withValues(alpha: _getOpacity(widget.status)), ), ); }, diff --git a/lib/views/market_maker_bot/coin_search_dropdown.dart b/lib/views/market_maker_bot/coin_search_dropdown.dart index ff7c5d807c..f40491b7f1 100644 --- a/lib/views/market_maker_bot/coin_search_dropdown.dart +++ b/lib/views/market_maker_bot/coin_search_dropdown.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/widgets/coin_icon.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_body.dart'; @@ -382,8 +383,10 @@ class _CoinDropdownState extends State { @override Widget build(BuildContext context) { - final coin = - selectedItem == null ? null : coinsBloc.getCoin(selectedItem!.coinId); + final coinsRepository = RepositoryProvider.of(context); + final coin = selectedItem == null + ? null + : coinsRepository.getCoin(selectedItem!.coinId); return CompositedTransformTarget( link: _layerLink, diff --git a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart index 51256524c0..1488c619ed 100644 --- a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart +++ b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_icon.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_body.dart'; @@ -57,6 +57,7 @@ class _CoinSelectionAndAmountInputState void _prepareItems() { _items = prepareCoinsForTable( + context, widget.coins, null, testCoinsEnabled: context.read().state.testCoinsEnabled, @@ -115,10 +116,11 @@ class _CoinSelectionAndAmountInputState content = FrontPlate(child: content); } + final coinsRepository = RepositoryProvider.of(context); return CoinDropdown( items: _items, - onItemSelected: (item) => - widget.onItemSelected?.call(coinsBloc.getCoin(item.coinId)), + onItemSelected: (item) async => widget.onItemSelected + ?.call(await coinsRepository.getEnabledCoin(item.coinId)), child: content, ); } diff --git a/lib/views/market_maker_bot/coin_trade_amount_form_field.dart b/lib/views/market_maker_bot/coin_trade_amount_form_field.dart index f9bfc78443..153048c51f 100644 --- a/lib/views/market_maker_bot/coin_trade_amount_form_field.dart +++ b/lib/views/market_maker_bot/coin_trade_amount_form_field.dart @@ -44,8 +44,9 @@ class _CoinTradeAmountFormFieldState extends State { @override void dispose() { - _controller..removeListener(_inputChangedListener) - ..dispose(); + _controller + ..removeListener(_inputChangedListener) + ..dispose(); super.dispose(); } @@ -127,7 +128,8 @@ class TradeAmountFiatPriceText extends StatelessWidget { return Text( coin == null ? r'≈$0' - : getFormattedFiatAmount(coin!.abbr, amount ?? Rational.zero), + : getFormattedFiatAmount( + context, coin!.abbr, amount ?? Rational.zero), style: Theme.of(context).textTheme.bodySmall, overflow: TextOverflow.ellipsis, ); @@ -136,7 +138,7 @@ class TradeAmountFiatPriceText extends StatelessWidget { class TradeAmountTextFormField extends StatelessWidget { const TradeAmountTextFormField({ - required this.controller, + required this.controller, super.key, this.enabled = true, }); diff --git a/lib/views/market_maker_bot/market_maker_bot_form.dart b/lib/views/market_maker_bot/market_maker_bot_form.dart index f14e48c4b1..04387265ce 100644 --- a/lib/views/market_maker_bot/market_maker_bot_form.dart +++ b/lib/views/market_maker_bot/market_maker_bot_form.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/orderbook/order.dart'; @@ -81,20 +81,14 @@ class _MakerFormDesktopLayout extends StatelessWidget { child: ConstrainedBox( constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), - child: StreamBuilder>( - initialData: coinsBloc.walletCoins, - stream: coinsBloc.outWalletCoins, - builder: (context, snapshot) { - final coins = snapshot.data - ?.where( - (e) => - e != null && - e.usdPrice != null && - e.usdPrice!.price > 0, - ) - .cast() - .toList() ?? - []; + child: BlocBuilder( + builder: (context, state) { + final coins = state.walletCoins.values + .where( + (e) => e.usdPrice != null && e.usdPrice!.price > 0, + ) + .cast() + .toList(); return MarketMakerBotFormContent(coins: coins); }, ), @@ -130,20 +124,14 @@ class _MakerFormMobileLayout extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - StreamBuilder>( - initialData: coinsBloc.walletCoins, - stream: coinsBloc.outWalletCoins, - builder: (context, snapshot) { - final coins = snapshot.data - ?.where( - (e) => - e != null && - e.usdPrice != null && - e.usdPrice!.price > 0, - ) - .cast() - .toList() ?? - []; + BlocBuilder( + builder: (context, state) { + final coins = state.walletCoins.values + .where( + (e) => e.usdPrice != null && e.usdPrice!.price > 0, + ) + .cast() + .toList(); return MarketMakerBotFormContent(coins: coins); }, ), 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 6edeb75866..3bd4daa136 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 @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; @@ -178,7 +178,8 @@ class _MarketMakerBotFormContentState extends State { } void _setSellCoinToDefaultCoin() { - final defaultCoin = coinsBloc.getCoin(defaultDexCoin); + final coinsRepository = RepositoryProvider.of(context); + final defaultCoin = coinsRepository.getCoin(defaultDexCoin); final tradeFormBloc = context.read(); if (defaultCoin != null && tradeFormBloc.state.sellCoin.value == null) { tradeFormBloc.add(MarketMakerTradeFormSellCoinChanged(defaultCoin)); diff --git a/lib/views/market_maker_bot/market_maker_bot_page.dart b/lib/views/market_maker_bot/market_maker_bot_page.dart index 812ccbd9ee..7648e0d9e6 100644 --- a/lib/views/market_maker_bot/market_maker_bot_page.dart +++ b/lib/views/market_maker_bot/market_maker_bot_page.dart @@ -1,9 +1,8 @@ -// TODO(Francois): delete once the migration is done import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_repository.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_bot/market_maker_bot_bloc.dart'; @@ -11,7 +10,7 @@ import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_mak import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/market_maker_order_list_bloc.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/services/orders_service/my_orders_service.dart'; @@ -42,25 +41,30 @@ class _MarketMakerBotPageState extends State { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); + final coinsRepository = RepositoryProvider.of(context); + final myOrdersService = RepositoryProvider.of(context); + final orderListRepository = MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), - coinsBloc, + coinsRepository, ); return MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => DexTabBarBloc( - authRepo, + RepositoryProvider.of(context), tradingEntitiesBloc, orderListRepository, - )..add(const StartListening()), + )..add(const ListenToOrdersRequested()), ), BlocProvider( create: (BuildContext context) => MarketMakerTradeFormBloc( - dexRepo: DexRepository(), - coinsRepo: coinsBloc, + dexRepo: RepositoryProvider.of(context), + coinsRepo: coinsRepository, ), ), BlocProvider( @@ -68,7 +72,7 @@ class _MarketMakerBotPageState extends State { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), - coinsBloc, + coinsRepository, ), ), ), 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 67af2d1b0b..84408b3a94 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 @@ -18,9 +18,7 @@ extension TradeMarginValidationErrorText on TradeMarginValidationError { return LocaleKeys.postitiveNumberRequired.tr(); case TradeMarginValidationError.greaterThanMaximum: return LocaleKeys.mustBeLessThan.tr(args: [maxValue.toString()]); - default: - return null; - } + } } } @@ -56,9 +54,7 @@ extension AmountValidationErrorText on AmountValidationError { .tr(args: [coin?.balance.toString() ?? '0', coin?.abbr ?? '']); case AmountValidationError.lessThanMinimum: return LocaleKeys.mmBotMinimumTradeVolume.tr(args: ["0.00000001"]); - default: - return null; - } + } } } diff --git a/lib/views/market_maker_bot/trade_pair_list_item.dart b/lib/views/market_maker_bot/trade_pair_list_item.dart index 9d2614bcf5..65c4660109 100644 --- a/lib/views/market_maker_bot/trade_pair_list_item.dart +++ b/lib/views/market_maker_bot/trade_pair_list_item.dart @@ -1,9 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rational/rational.dart'; import 'package:vector_math/vector_math_64.dart' as vector_math; import 'package:web_dex/bloc/market_maker_bot/market_maker_order_list/trade_pair.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/utils/formatters.dart'; @@ -35,6 +36,7 @@ class TradePairListItem extends StatelessWidget { final buyCoin = config.relCoinId; final buyAmount = order?.relAmountAvailable ?? pair.relCoinAmount; final String date = order != null ? getFormattedDate(order.createdAt) : '-'; + final tradingEntitiesBloc = RepositoryProvider.of(context); final double fillProgress = order != null ? tradingEntitiesBloc.getProgressFillSwap(pair.order!) : 0; @@ -108,6 +110,8 @@ class _OrderItemDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Row( mainAxisSize: MainAxisSize.max, children: [ diff --git a/lib/views/nfts/details_page/nft_details_page.dart b/lib/views/nfts/details_page/nft_details_page.dart index 3d3f58f66c..0e3fd70b62 100644 --- a/lib/views/nfts/details_page/nft_details_page.dart +++ b/lib/views/nfts/details_page/nft_details_page.dart @@ -4,10 +4,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/nft_withdraw/nft_withdraw_bloc.dart'; import 'package:web_dex/bloc/nft_withdraw/nft_withdraw_repo.dart'; import 'package:web_dex/bloc/nfts/nft_main_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; @@ -43,6 +43,7 @@ class NftDetailsPage extends StatelessWidget { .firstWhereOrNull((list) => list?.firstWhereOrNull((token) => token.uuid == uuid) != null) ?.firstWhereOrNull((token) => token.uuid == uuid); + final mm2Api = RepositoryProvider.of(context); if (nft == null) { return Column( @@ -64,8 +65,9 @@ class NftDetailsPage extends StatelessWidget { key: Key('nft-withdraw-bloc-provider-${nft.uuid}'), create: (context) => NftWithdrawBloc( nft: nft, - repo: NftWithdrawRepo(api: mm2Api.nft), - coinsBloc: coinsBloc, + repo: NftWithdrawRepo(api: mm2Api), + mm2Api: mm2Api, + coinsRepository: RepositoryProvider.of(context), ), child: isMobile ? NftDetailsPageMobile(isRouterSend: isSend) diff --git a/lib/views/nfts/nft_list/nft_list_item.dart b/lib/views/nfts/nft_list/nft_list_item.dart index faa7be1919..2155b16dd0 100644 --- a/lib/views/nfts/nft_list/nft_list_item.dart +++ b/lib/views/nfts/nft_list/nft_list_item.dart @@ -113,7 +113,7 @@ class _NftAmount extends StatelessWidget { return const SizedBox.shrink(); } return Card( - color: Theme.of(context).cardColor.withOpacity(0.8), + color: Theme.of(context).cardColor.withValues(alpha: 0.8), shape: const CircleBorder(), child: Padding( padding: const EdgeInsets.all(12), @@ -147,7 +147,7 @@ class _NftData extends StatelessWidget { nft.collectionName != null && nft.name != nft.collectionName; return GridTileBar( - backgroundColor: Theme.of(context).cardColor.withOpacity(0.9), + backgroundColor: Theme.of(context).cardColor.withValues(alpha: 0.9), title: _tileText(nft.name), subtitle: !mustShowSubtitle ? null : _tileText(nft.collectionName!), trailing: const Icon(Icons.more_vert), diff --git a/lib/views/nfts/nft_page.dart b/lib/views/nfts/nft_page.dart index 1c0db1d9e1..d09ff0339d 100644 --- a/lib/views/nfts/nft_page.dart +++ b/lib/views/nfts/nft_page.dart @@ -35,8 +35,8 @@ class NftPage extends StatelessWidget { providers: [ RepositoryProvider( create: (context) => NftTxnRepository( - api: mm2Api.nft, - coinsRepo: coinsRepo, + api: RepositoryProvider.of(context).nft, + coinsRepo: RepositoryProvider.of(context), ), ), ], diff --git a/lib/views/nfts/nft_receive/nft_receive_page.dart b/lib/views/nfts/nft_receive/nft_receive_page.dart index 947494cc57..558a816084 100644 --- a/lib/views/nfts/nft_receive/nft_receive_page.dart +++ b/lib/views/nfts/nft_receive/nft_receive_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/nft_receive/bloc/nft_receive_bloc.dart'; import 'package:web_dex/bloc/nfts/nft_main_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/views/nfts/nft_receive/nft_receive_view.dart'; class NftReceivePage extends StatelessWidget { @@ -14,8 +15,9 @@ class NftReceivePage extends StatelessWidget { builder: (context, state) { return BlocProvider( create: (context) => NftReceiveBloc( - coinsRepo: coinsBloc, - currentWalletBloc: currentWalletBloc, + coinsRepo: RepositoryProvider.of(context), + currentWalletBloc: + RepositoryProvider.of(context), )..add(NftReceiveEventInitial(chain: state.selectedChain)), child: NftReceiveView(), ); diff --git a/lib/views/nfts/nft_transactions/common/widgets/nft_txn_hash.dart b/lib/views/nfts/nft_transactions/common/widgets/nft_txn_hash.dart index 785b93a48b..fa8894ba08 100644 --- a/lib/views/nfts/nft_transactions/common/widgets/nft_txn_hash.dart +++ b/lib/views/nfts/nft_transactions/common/widgets/nft_txn_hash.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/rpc/nft_transaction/nft_transactions_response.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/hash_explorer_link.dart'; @@ -10,7 +11,8 @@ class NftTxnHash extends StatelessWidget { @override Widget build(BuildContext context) { - final coin = coinsBloc.getCoin(transaction.chain.coinAbbr()); + final coinsRepository = RepositoryProvider.of(context); + final coin = coinsRepository.getCoin(transaction.chain.coinAbbr()); if (coin == null) return const SizedBox.shrink(); return HashExplorerLink( coin: coin, diff --git a/lib/views/nfts/nft_transactions/mobile/widgets/nft_txn_copied_text.dart b/lib/views/nfts/nft_transactions/mobile/widgets/nft_txn_copied_text.dart index 3e575d26c0..6e7f82b1c4 100644 --- a/lib/views/nfts/nft_transactions/mobile/widgets/nft_txn_copied_text.dart +++ b/lib/views/nfts/nft_transactions/mobile/widgets/nft_txn_copied_text.dart @@ -1,6 +1,7 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/rpc/nft_transaction/nft_transactions_response.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -22,7 +23,7 @@ class NftTxnCopiedText extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).extension()!; final textScheme = Theme.of(context).extension()!; - final coin = _coin; + final coin = _coin(context); final textStyle = textScheme.bodyXS.copyWith(color: colorScheme.s70); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -42,8 +43,9 @@ class NftTxnCopiedText extends StatelessWidget { ); } - Coin? get _coin { - return coinsBloc.getCoin(transaction.chain.coinAbbr()); + Coin? _coin(BuildContext context) { + final coinsRepository = RepositoryProvider.of(context); + return coinsRepository.getCoin(transaction.chain.coinAbbr()); } String get _exploreValue { diff --git a/lib/views/nfts/nft_transactions/nft_txn_page.dart b/lib/views/nfts/nft_transactions/nft_txn_page.dart index e69db31fe1..3fa18e0026 100644 --- a/lib/views/nfts/nft_transactions/nft_txn_page.dart +++ b/lib/views/nfts/nft_transactions/nft_txn_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/nft_transactions/bloc/nft_transactions_bloc.dart'; import 'package:web_dex/bloc/nft_transactions/nft_txn_repository.dart'; import 'package:web_dex/bloc/nfts/nft_main_repo.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/views/nfts/nft_transactions/desktop/nft_txn_desktop_page.dart.dart'; @@ -20,8 +20,8 @@ class NftListOfTransactionsPage extends StatelessWidget { create: (context) => NftTransactionsBloc( nftTxnRepository: context.read(), nftsRepository: context.read(), - coinsBloc: coinsBloc, - authRepo: authRepo, + coinsRepository: RepositoryProvider.of(context), + kdfSdk: RepositoryProvider.of(context), isLoggedIn: context.read().state.mode == AuthorizeMode.logIn, )..add(const NftTxnReceiveEvent()), child: isMobile ? const NftTxnMobilePage() : const NftTxnDesktopPage(), diff --git a/lib/views/settings/widgets/general_settings/app_version_number.dart b/lib/views/settings/widgets/general_settings/app_version_number.dart index 12349e2670..fb863036fa 100644 --- a/lib/views/settings/widgets/general_settings/app_version_number.dart +++ b/lib/views/settings/widgets/general_settings/app_version_number.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/app_config/package_information.dart'; -import 'package:web_dex/bloc/runtime_coin_updates/coin_config_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; @@ -60,11 +59,6 @@ class _BundledCoinsCommitConfig extends StatelessWidget { @override Widget build(BuildContext context) { - final configBlocState = context.watch().state; - - final runtimeCoinUpdatesCommit = (configBlocState is CoinConfigLoadSuccess) - ? configBlocState.updatedCommitHash - : null; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -83,7 +77,8 @@ class _BundledCoinsCommitConfig extends StatelessWidget { }, ), SelectableText( - '${LocaleKeys.updated.tr()}: ${_tryParseCommitHash(runtimeCoinUpdatesCommit) ?? LocaleKeys.notUpdated.tr()}', + // TODO!: add sdk getter for updated commit hash + '${LocaleKeys.updated.tr()}: ${LocaleKeys.updated.tr()}', style: _textStyle, ), ], @@ -96,6 +91,8 @@ class _ApiVersion extends StatelessWidget { @override Widget build(BuildContext context) { + final mm2Api = RepositoryProvider.of(context); + return Row( children: [ Flexible( diff --git a/lib/views/settings/widgets/general_settings/import_swaps.dart b/lib/views/settings/widgets/general_settings/import_swaps.dart index 5cb9354ce6..ad4eceebf3 100644 --- a/lib/views/settings/widgets/general_settings/import_swaps.dart +++ b/lib/views/settings/widgets/general_settings/import_swaps.dart @@ -4,7 +4,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; import 'package:web_dex/shared/utils/debug_utils.dart'; @@ -142,7 +145,9 @@ class _ImportSwapsState extends State { } try { - await importSwapsData(swaps); + final mm2Api = RepositoryProvider.of(context); + final ImportSwapsRequest request = ImportSwapsRequest(swaps: swaps); + await mm2Api.importSwaps(request); } catch (e) { setState(() { _inProgress = false; diff --git a/lib/views/settings/widgets/general_settings/settings_reset_activated_coins.dart b/lib/views/settings/widgets/general_settings/settings_reset_activated_coins.dart index 360c70a7f2..c17c8d70d6 100644 --- a/lib/views/settings/widgets/general_settings/settings_reset_activated_coins.dart +++ b/lib/views/settings/widgets/general_settings/settings_reset_activated_coins.dart @@ -1,8 +1,9 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -37,19 +38,20 @@ class _SettingsResetActivatedCoinsState ); } - void _showResetPopup() async { - await walletsBloc.fetchSavedWallets(); - PopupDispatcher popupDispatcher = _createPopupDispatcher(); + Future _showResetPopup() async { + final walletsBloc = RepositoryProvider.of(context); + final wallets = await walletsBloc.getWallets(); + PopupDispatcher popupDispatcher = _createPopupDispatcher(wallets); popupDispatcher.show(); } - PopupDispatcher _createPopupDispatcher() { + PopupDispatcher _createPopupDispatcher(List wallets) { final textStyle = Theme.of(context).textTheme.bodyMedium; return PopupDispatcher( borderColor: theme.custom.specificButtonBorderColor, barrierColor: isMobile ? Theme.of(context).colorScheme.onSurface : null, width: 320, - popupContent: walletsBloc.wallets.isEmpty + popupContent: wallets.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(16.0), @@ -66,14 +68,13 @@ class _SettingsResetActivatedCoinsState style: textStyle, ), const SizedBox(height: 8), - ...List.generate(walletsBloc.wallets.length, (index) { + ...List.generate(wallets.length, (index) { return ListTile( title: AutoScrollText( - text: walletsBloc.wallets[index].name, + text: wallets[index].name, style: textStyle, ), - onTap: () => - _showConfirmationDialog(walletsBloc.wallets[index]), + onTap: () => _showConfirmationDialog(wallets[index]), ); }), ]), @@ -112,6 +113,7 @@ class _SettingsResetActivatedCoinsState } Future _resetSpecificWallet(Wallet wallet) async { + final walletsBloc = RepositoryProvider.of(context); await walletsBloc.resetSpecificWallet(wallet); if (!mounted) return; diff --git a/lib/views/settings/widgets/general_settings/show_swap_data.dart b/lib/views/settings/widgets/general_settings/show_swap_data.dart index d91a2d45ed..86f1a43386 100644 --- a/lib/views/settings/widgets/general_settings/show_swap_data.dart +++ b/lib/views/settings/widgets/general_settings/show_swap_data.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request.dart'; @@ -87,6 +88,7 @@ class _ShowSwapDataState extends State { setState(() => _inProgress = true); try { + final mm2Api = RepositoryProvider.of(context); final response = await mm2Api.getRawSwapData(MyRecentSwapsRequest()); final Map data = jsonDecode(response); _controller.text = jsonEncode(data['result']['swaps']).toString(); diff --git a/lib/views/settings/widgets/security_settings/password_update_page.dart b/lib/views/settings/widgets/security_settings/password_update_page.dart index 815bec0f33..3911f53817 100644 --- a/lib/views/settings/widgets/security_settings/password_update_page.dart +++ b/lib/views/settings/widgets/security_settings/password_update_page.dart @@ -3,16 +3,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/validators.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class PasswordUpdatePage extends StatefulWidget { const PasswordUpdatePage({Key? key}) : super(key: key); @@ -44,7 +45,7 @@ class _PasswordUpdatePageState extends State { return Container( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(.3), + color: Theme.of(context).colorScheme.surface.withValues(alpha: .3), borderRadius: BorderRadius.circular(18.0)), child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -173,6 +174,7 @@ class _FormViewState extends State<_FormView> { if (!(_formKey.currentState?.validate() ?? false)) { return; } + final currentWalletBloc = RepositoryProvider.of(context); final Wallet? wallet = currentWalletBloc.wallet; if (wallet == null) return; final String password = _newController.text; @@ -233,6 +235,7 @@ class _CurrentFieldState extends State<_CurrentField> { @override Widget build(BuildContext context) { + final currentWallet = context.read().state.currentUser?.wallet; return _PasswordField( hintText: LocaleKeys.currentPassword.tr(), controller: widget.controller, @@ -248,7 +251,6 @@ class _CurrentFieldState extends State<_CurrentField> { return result; } - final Wallet? currentWallet = currentWalletBloc.wallet; if (currentWallet == null) return LocaleKeys.walletNotFound.tr(); _validateSeed(currentWallet, password); @@ -261,11 +263,13 @@ class _CurrentFieldState extends State<_CurrentField> { } Future _validateSeed(Wallet currentWallet, String password) async { - _seedError = ''; - final seed = await currentWallet.getSeed(password); - if (seed.isNotEmpty) return; - _seedError = LocaleKeys.invalidPasswordError.tr(); - widget.formKey.currentState?.validate(); + // TODO!: determine if this needs to be reimplemented in the sdk or if it + // can be removed entirely. + // _seedError = ''; + // final seed = await currentWallet.getSeed(password); + // if (seed.isNotEmpty) return; + // _seedError = LocaleKeys.invalidPasswordError.tr(); + // widget.formKey.currentState?.validate(); } } diff --git a/lib/views/settings/widgets/security_settings/plate_seed_backup.dart b/lib/views/settings/widgets/security_settings/plate_seed_backup.dart index 8b1772c140..fe7f4ef918 100644 --- a/lib/views/settings/widgets/security_settings/plate_seed_backup.dart +++ b/lib/views/settings/widgets/security_settings/plate_seed_backup.dart @@ -1,11 +1,14 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/wallet_password_dialog/wallet_password_dialog.dart'; class PlateSeedBackup extends StatelessWidget { @@ -101,7 +104,8 @@ class _AtomicIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final hasBackup = currentWalletBloc.wallet?.config.hasBackup ?? false; + final currentWallet = context.read().state.currentUser?.wallet; + final hasBackup = currentWallet?.config.hasBackup ?? false; return DexSvgImage( path: hasBackup ? Assets.seedBackedUp : Assets.seedNotBackedUp, size: 50, @@ -114,7 +118,8 @@ class _SaveAndRememberTitle extends StatelessWidget { @override Widget build(BuildContext context) { - final hasBackup = currentWalletBloc.wallet?.config.hasBackup ?? false; + final currentWallet = context.read().state.currentUser?.wallet; + final hasBackup = currentWallet?.config.hasBackup ?? false; return Row( mainAxisSize: MainAxisSize.min, @@ -171,7 +176,9 @@ class _SaveAndRememberButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final hasBackup = currentWalletBloc.wallet?.config.hasBackup == true; + final currentWallet = context.read().state.currentUser?.wallet; + final currentWalletBloc = context.read(); + final hasBackup = currentWallet?.config.hasBackup == true; final text = hasBackup ? LocaleKeys.viewSeedPhrase.tr() : LocaleKeys.backupSeedPhrase.tr(); diff --git a/lib/views/settings/widgets/security_settings/security_settings_main_page.dart b/lib/views/settings/widgets/security_settings/security_settings_main_page.dart index 4cbcd311a5..95078ecbd3 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_main_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_main_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/views/settings/widgets/security_settings/change_password_section.dart'; import 'package:web_dex/views/settings/widgets/security_settings/plate_seed_backup.dart'; class SecuritySettingsMainPage extends StatelessWidget { @@ -13,8 +12,9 @@ class SecuritySettingsMainPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PlateSeedBackup(onViewSeedPressed: onViewSeedPressed), - const SizedBox(height: 12), - const ChangePasswordSection(), + // TODO!: re-enable once implemented + // const SizedBox(height: 12), + // const ChangePasswordSection(), ], ); } 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 7557f4bc2c..faf75b3613 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -1,29 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; import 'package:web_dex/bloc/security_settings/security_settings_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/settings_menu_value.dart'; -import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/common/wallet_password_dialog/wallet_password_dialog.dart'; import 'package:web_dex/views/settings/widgets/common/settings_content_wrapper.dart'; -import 'package:web_dex/views/settings/widgets/security_settings/password_update_page.dart'; import 'package:web_dex/views/settings/widgets/security_settings/security_settings_main_page.dart'; import 'package:web_dex/views/settings/widgets/security_settings/seed_settings/seed_confirm_success.dart'; +import 'package:web_dex/views/settings/widgets/security_settings/seed_settings/seed_confirmation/seed_confirmation.dart'; import 'package:web_dex/views/settings/widgets/security_settings/seed_settings/seed_show.dart'; -import 'seed_settings/seed_confirmation/seed_confirmation.dart'; - class SecuritySettingsPage extends StatefulWidget { // ignore: prefer_const_constructors_in_immutables - SecuritySettingsPage({super.key, required this.onBackPressed}); + SecuritySettingsPage({required this.onBackPressed, super.key}); final VoidCallback onBackPressed; @override State createState() => _SecuritySettingsPageState(); @@ -36,7 +35,10 @@ class _SecuritySettingsPageState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SecuritySettingsBloc(SecuritySettingsState.initialState()), + create: (_) => SecuritySettingsBloc( + SecuritySettingsState.initialState(), + RepositoryProvider.of(context), + ), child: BlocBuilder( builder: (BuildContext context, SecuritySettingsState state) { final Widget content = _buildContent(state.step); @@ -47,19 +49,17 @@ class _SecuritySettingsPageState extends State { switch (state.step) { case SecuritySettingsStep.securityMain: widget.onBackPressed(); - break; case SecuritySettingsStep.seedConfirm: context .read() .add(const ShowSeedEvent()); - break; case SecuritySettingsStep.seedShow: case SecuritySettingsStep.seedSuccess: - case SecuritySettingsStep.passwordUpdate: - context - .read() - .add(const ResetEvent()); - break; + // case SecuritySettingsStep.passwordUpdate: + // context + // .read() + // .add(const ResetEvent()); + // break; } }, ); @@ -88,10 +88,10 @@ class _SecuritySettingsPageState extends State { _privKeys.clear(); return const SeedConfirmSuccess(); - case SecuritySettingsStep.passwordUpdate: - _seed = ''; - _privKeys.clear(); - return const PasswordUpdatePage(); + // case SecuritySettingsStep.passwordUpdate: + // _seed = ''; + // _privKeys.clear(); + // return const PasswordUpdatePage(); } } @@ -101,13 +101,19 @@ class _SecuritySettingsPageState extends State { final String? pass = await walletPasswordDialog(context); if (pass == null) return; - final Wallet? wallet = currentWalletBloc.wallet; - if (wallet == null) return; - _seed = await wallet.getSeed(pass); - if (_seed.isEmpty) return; + + // ignore: use_build_context_synchronously + final coinsBloc = context.read(); + // ignore: use_build_context_synchronously + final mm2Api = RepositoryProvider.of(context); + // ignore: use_build_context_synchronously + final kdfSdk = RepositoryProvider.of(context); + + final mnemonic = await kdfSdk.auth.getMnemonicPlainText(pass); + _seed = mnemonic.plaintextMnemonic ?? ''; _privKeys.clear(); - for (final coin in coinsBloc.walletCoins) { + for (final coin in coinsBloc.state.walletCoins.values) { final result = await mm2Api.showPrivKey(ShowPrivKeyRequest(coin: coin.abbr)); if (result != null) { @@ -120,8 +126,10 @@ class _SecuritySettingsPageState extends State { } class _SecuritySettingsPageMobile extends StatelessWidget { - const _SecuritySettingsPageMobile( - {required this.onBackButtonPressed, required this.content}); + const _SecuritySettingsPageMobile({ + required this.onBackButtonPressed, + required this.content, + }); final VoidCallback onBackButtonPressed; final Widget content; diff --git a/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart b/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart index d84c1a64fc..c9db206cb5 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart @@ -1,7 +1,8 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; @@ -39,10 +40,9 @@ class _BackupSeedNotificationState extends State { final String description = widget.description ?? LocaleKeys.backupSeedNotificationDescription.tr(); - return StreamBuilder( - stream: currentWalletBloc.outWallet, - builder: (context, snapshot) { - final currentWallet = currentWalletBloc.wallet; + return BlocBuilder( + builder: (context, state) { + final currentWallet = state.currentUser?.wallet; if (currentWallet == null || currentWallet.config.hasBackup) { return const SizedBox(); } @@ -221,7 +221,11 @@ class BackupNotification extends StatelessWidget { @override Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7), + color: Theme.of(context) + .textTheme + .bodyLarge + ?.color + ?.withValues(alpha: 0.7), fontWeight: FontWeight.w600, ); return BackupSeedNotification( diff --git a/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart b/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart index 219c799a26..deda992b70 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/seed_show.dart @@ -3,6 +3,7 @@ import 'package:bip39/bip39.dart' show validateMnemonic; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/security_settings/security_settings_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; import 'package:web_dex/bloc/security_settings/security_settings_state.dart'; @@ -14,7 +15,6 @@ import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/dry_intrinsic.dart'; import 'package:web_dex/views/settings/widgets/security_settings/seed_settings/seed_back_button.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/views/wallet/coin_details/receive/qr_code_address.dart'; class SeedShow extends StatelessWidget { @@ -215,7 +215,7 @@ class _TitleRow extends StatelessWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: theme.custom.warningColor.withOpacity(0.1), + color: theme.custom.warningColor.withValues(alpha: 0.1), border: Border.all(color: theme.custom.warningColor), borderRadius: BorderRadius.circular(8), ), @@ -412,7 +412,8 @@ class _SelectableSeedWord extends StatelessWidget { final numStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.4), + color: + Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.4), ); final TextEditingController seedWordController = TextEditingController() ..text = isSeedShown ? initialValue : '••••••'; @@ -472,7 +473,7 @@ class _SeedPhraseConfirmButton extends StatelessWidget { final isCustom = !validateMnemonic(seedPhrase); if (isCustom) return const SizedBox.shrink(); - onPressed() => bloc.add(const SeedConfirmEvent()); + void onPressed() => bloc.add(const SeedConfirmEvent()); final text = LocaleKeys.seedPhraseShowingSavedPhraseButton.tr(); final contentWidth = screenWidth - 80; diff --git a/lib/views/settings/widgets/security_settings/seed_settings/seed_word_button.dart b/lib/views/settings/widgets/security_settings/seed_settings/seed_word_button.dart index 23d7432fd0..377a9795c6 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/seed_word_button.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/seed_word_button.dart @@ -32,10 +32,10 @@ class SeedWordButton extends StatelessWidget { child: InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(15), - hoverColor: color.withOpacity(0.05), - highlightColor: color.withOpacity(0.1), - focusColor: color.withOpacity(0.2), - splashColor: color.withOpacity(0.4), + hoverColor: color.withValues(alpha: 0.05), + highlightColor: color.withValues(alpha: 0.1), + focusColor: color.withValues(alpha: 0.2), + splashColor: color.withValues(alpha: 0.4), child: Stack( children: [ Container( diff --git a/lib/views/settings/widgets/settings_menu/settings_menu.dart b/lib/views/settings/widgets/settings_menu/settings_menu.dart index fbe42cbcfc..12b093e0fb 100644 --- a/lib/views/settings/widgets/settings_menu/settings_menu.dart +++ b/lib/views/settings/widgets/settings_menu/settings_menu.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/settings_menu_value.dart'; import 'package:web_dex/model/wallet.dart'; @@ -21,11 +22,9 @@ class SettingsMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: currentWalletBloc.outWallet, - initialData: currentWalletBloc.wallet, - builder: (context, snapshot) { - final showSecurity = snapshot.data?.isHW == false; + return BlocBuilder( + builder: (context, state) { + final showSecurity = state.currentUser?.wallet.isHW == false; final Set menuItems = { SettingsMenuValue.general, diff --git a/lib/views/wallet/coin_details/coin_details.dart b/lib/views/wallet/coin_details/coin_details.dart index 04779ae0d7..4b1a751d86 100644 --- a/lib/views/wallet/coin_details/coin_details.dart +++ b/lib/views/wallet/coin_details/coin_details.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_details_info.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; @@ -52,10 +52,8 @@ class _CoinDetailsState extends State { @override Widget build(BuildContext context) { - return StreamBuilder>( - initialData: coinsBloc.walletCoinsMap.values, - stream: coinsBloc.outWalletCoins, - builder: (context, AsyncSnapshot> snapshot) { + return BlocBuilder( + builder: (context, state) { return _buildContent(); }, ); diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart index 7dc9309335..904c51100c 100644 --- a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart +++ b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart @@ -3,10 +3,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; @@ -96,16 +97,19 @@ class _PortfolioGrowthChartState extends State { percentageIncrease: percentageIncrease, selectedPeriod: state.selectedPeriod, onPeriodChanged: (selected) { - if (selected != null) { - final walletId = currentWalletBloc.wallet!.id; - context.read().add( - PortfolioGrowthPeriodChanged( - selectedPeriod: selected, - coins: _selectedCoins, - walletId: walletId, - ), - ); + if (selected == null) { + return; } + + final user = context.read().state.currentUser; + final walletId = user!.wallet.id; + context.read().add( + PortfolioGrowthPeriodChanged( + selectedPeriod: selected, + coins: _selectedCoins, + walletId: walletId, + ), + ); }, ), const Gap(16), @@ -179,12 +183,13 @@ class _PortfolioGrowthChartState extends State { } void _showSpecificCoin(String? coinId) { + final currentWallet = context.read().state.currentUser?.wallet; final coin = coinId == null ? null : widget.initialCoins.firstWhere((coin) => coin.abbr == coinId); final newCoins = coin == null ? widget.initialCoins : [coin]; - final walletId = currentWalletBloc.wallet!.id; + final walletId = currentWallet!.id; context.read().add( PortfolioGrowthPeriodChanged( selectedPeriod: diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart index a18c4875f7..f7bb543db5 100644 --- a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart +++ b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/prominent_colors.dart'; @@ -37,7 +37,8 @@ class PortfolioProfitLossChartState extends State { // TODO: Handle this. And for other charts. This } - String? get walletId => currentWalletBloc.wallet?.id; + String? get walletId => + RepositoryProvider.of(context).wallet?.id; @override Widget build(BuildContext context) { diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart new file mode 100644 index 0000000000..22ca7af244 --- /dev/null +++ b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart @@ -0,0 +1,489 @@ +import 'package:app_theme/app_theme.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_state.dart'; +import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_type_tag.dart'; +import 'package:web_dex/views/wallet/common/address_copy_button.dart'; +import 'package:web_dex/views/wallet/common/address_icon.dart'; +import 'package:web_dex/views/wallet/common/address_text.dart'; + +class CoinAddresses extends StatelessWidget { + const CoinAddresses({ + super.key, + required this.coin, + }); + + final Coin coin; + + @override + Widget build(BuildContext context) { + final kdfSdk = RepositoryProvider.of(context); + return BlocBuilder(builder: (context, state) { + return BlocProvider( + create: (context) => CoinAddressesBloc( + kdfSdk, + coin.abbr, + )..add(const LoadAddressesEvent()), + child: BlocBuilder( + builder: (context, state) { + return SliverToBoxAdapter( + child: Column( + children: [ + Card( + margin: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + color: theme.custom.dexPageTheme.frontPlate, + child: Padding( + padding: EdgeInsets.all(isMobile ? 16.0 : 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Header( + status: state.status, + createAddressStatus: state.createAddressStatus, + hideZeroBalance: state.hideZeroBalance, + cantCreateNewAddressReasons: + state.cantCreateNewAddressReasons, + ), + const SizedBox(height: 12), + ...state.addresses.asMap().entries.map( + (entry) { + final index = entry.key; + final address = entry.value; + if (state.hideZeroBalance && + !address.balance.hasBalance) { + return const SizedBox(); + } + + return AddressCard( + address: address, + index: index, + coin: coin, + ); + }, + ).toList(), + if (state.status == FormStatus.submitting) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: Center(child: CircularProgressIndicator()), + ), + if (state.status == FormStatus.failure || + state.createAddressStatus == FormStatus.failure) + Padding( + padding: + const EdgeInsets.symmetric(vertical: 20.0), + child: Center( + child: Text( + state.errorMessage ?? + LocaleKeys.somethingWrong.tr(), + style: TextStyle( + color: theme.currentGlobal.colorScheme + .error))), + ), + ], + ), + ), + ), + if (isMobile) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CreateButton( + status: state.status, + createAddressStatus: state.createAddressStatus, + cantCreateNewAddressReasons: + state.cantCreateNewAddressReasons, + ), + ), + ], + ), + ); + }, + ), + ); + }); + } +} + +class _Header extends StatelessWidget { + const _Header({ + Key? key, + required this.status, + required this.createAddressStatus, + required this.hideZeroBalance, + required this.cantCreateNewAddressReasons, + }) : super(key: key); + + final FormStatus status; + final FormStatus createAddressStatus; + final bool hideZeroBalance; + final Set? cantCreateNewAddressReasons; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const AddressesTitle(), + const Spacer(), + HideZeroBalanceCheckbox( + hideZeroBalance: hideZeroBalance, + ), + if (!isMobile) + Padding( + padding: const EdgeInsets.only(left: 24.0), + child: SizedBox( + width: 200, + child: CreateButton( + status: status, + createAddressStatus: createAddressStatus, + cantCreateNewAddressReasons: cantCreateNewAddressReasons, + ), + ), + ), + ], + ); + } +} + +class AddressCard extends StatelessWidget { + const AddressCard({ + super.key, + required this.address, + required this.index, + required this.coin, + }); + + final PubkeyInfo address; + final int index; + final Coin coin; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + color: theme.custom.dexPageTheme.emptyPlace, + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + leading: isMobile ? null : AddressIcon(address: address.address), + title: isMobile + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AddressIcon(address: address.address), + const SizedBox(width: 8), + AddressText(address: address.address), + const SizedBox(width: 8), + SwapAddressTag(address: address), + const Spacer(), + AddressCopyButton(address: address.address), + QrButton( + coin: coin, + address: address, + ), + ], + ), + const SizedBox(height: 12), + _Balance(address: address, coin: coin), + const SizedBox(height: 4), + ], + ) + : Row( + children: [ + AddressText(address: address.address), + const SizedBox(width: 8), + AddressCopyButton(address: address.address), + QrButton(coin: coin, address: address), + SwapAddressTag(address: address), + ], + ), + trailing: isMobile ? null : _Balance(address: address, coin: coin), + ), + ); + } +} + +class _Balance extends StatelessWidget { + const _Balance({ + required this.address, + required this.coin, + }); + + final PubkeyInfo address; + final Coin coin; + + @override + Widget build(BuildContext context) { + return Text( + '${doubleToString(address.balance.total.toDouble())} ${abbr2Ticker(coin.abbr)} (${coin.amountToFormattedUsd(address.balance.total.toDouble())})', + style: TextStyle(fontSize: isMobile ? 12 : 14), + ); + } +} + +class QrButton extends StatelessWidget { + const QrButton({ + super.key, + required this.address, + required this.coin, + }); + + final PubkeyInfo address; + final Coin coin; + + @override + Widget build(BuildContext context) { + return IconButton( + splashRadius: 18, + icon: const Icon(Icons.qr_code, size: 16), + color: Theme.of(context).textTheme.bodyMedium!.color, + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.receive.tr(), + style: const TextStyle(fontSize: 16), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + content: SizedBox( + width: 450, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocaleKeys.onlySendToThisAddress + .tr(args: [abbr2Ticker(coin.abbr)]), + style: const TextStyle(fontSize: 14), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + LocaleKeys.network.tr(), + style: const TextStyle(fontSize: 14), + ), + CoinTypeTag(coin), + ], + ), + ), + QrCode( + address: address.address, + coinAbbr: coin.abbr, + ), + const SizedBox(height: 16), + Text( + LocaleKeys.scanTheQrCode.tr(), + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ); + }, + ); + } +} + +class SwapAddressTag extends StatelessWidget { + const SwapAddressTag({ + super.key, + required this.address, + }); + + final PubkeyInfo address; + + @override + Widget build(BuildContext context) { + return address.isActiveForSwap + ? Padding( + padding: EdgeInsets.only(left: isMobile ? 4 : 8), + child: Container( + padding: EdgeInsets.symmetric( + vertical: isMobile ? 6 : 8, + horizontal: isMobile ? 8 : 12.0, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.tertiary, + borderRadius: BorderRadius.circular(16.0), + ), + child: Text( + LocaleKeys.swapAddress.tr(), + style: TextStyle(fontSize: isMobile ? 9 : 12), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class AddressesTitle extends StatelessWidget { + const AddressesTitle({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Text( + LocaleKeys.addresses.tr(), + style: + TextStyle(fontSize: isMobile ? 14 : 24, fontWeight: FontWeight.bold), + ); + } +} + +class HideZeroBalanceCheckbox extends StatelessWidget { + final bool hideZeroBalance; + + const HideZeroBalanceCheckbox({ + super.key, + required this.hideZeroBalance, + }); + + @override + Widget build(BuildContext context) { + return UiCheckbox( + key: const Key('addresses-with-balance-checkbox'), + text: LocaleKeys.hideZeroBalanceAddresses.tr(), + value: hideZeroBalance, + onChanged: (value) { + context + .read() + .add(UpdateHideZeroBalanceEvent(value)); + }, + ); + } +} + +class CreateButton extends StatelessWidget { + const CreateButton({ + Key? key, + required this.status, + required this.createAddressStatus, + required this.cantCreateNewAddressReasons, + }) : super(key: key); + + final FormStatus status; + final FormStatus createAddressStatus; + final Set? cantCreateNewAddressReasons; + + @override + Widget build(BuildContext context) { + final tooltipMessage = _getTooltipMessage(); + + return Tooltip( + message: tooltipMessage, + child: UiPrimaryButton( + backgroundColor: isMobile ? theme.custom.dexPageTheme.emptyPlace : null, + text: createAddressStatus == FormStatus.submitting + ? '${LocaleKeys.creating.tr()}...' + : LocaleKeys.createAddress.tr(), + prefix: createAddressStatus == FormStatus.submitting + ? null + : const Icon(Icons.add, size: 16), + onPressed: canCreateNewAddress && + status != FormStatus.submitting && + createAddressStatus != FormStatus.submitting + ? () { + context + .read() + .add(const SubmitCreateAddressEvent()); + } + : null, + ), + ); + } + + bool get canCreateNewAddress => cantCreateNewAddressReasons?.isEmpty ?? true; + + String _getTooltipMessage() { + if (cantCreateNewAddressReasons?.isEmpty ?? true) { + return ''; + } + + return cantCreateNewAddressReasons!.map((reason) { + return switch (reason) { + CantCreateNewAddressReason.maxGapLimitReached => + LocaleKeys.maxGapLimitReached.tr(), + CantCreateNewAddressReason.maxAddressesReached => + LocaleKeys.maxAddressesReached.tr(), + CantCreateNewAddressReason.missingDerivationPath => + LocaleKeys.missingDerivationPath.tr(), + CantCreateNewAddressReason.protocolNotSupported => + LocaleKeys.protocolNotSupported.tr(), + CantCreateNewAddressReason.derivationModeNotSupported => + LocaleKeys.derivationModeNotSupported.tr(), + CantCreateNewAddressReason.noActiveWallet => + LocaleKeys.noActiveWallet.tr(), + }; + }).join('\n'); + } +} + +class QrCode extends StatelessWidget { + final String address; + final String coinAbbr; + + const QrCode({ + Key? key, + required this.address, + required this.coinAbbr, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: QrImage( + data: address, + backgroundColor: Theme.of(context).textTheme.bodyMedium!.color!, + foregroundColor: theme.custom.dexPageTheme.emptyPlace, + version: QrVersions.auto, + size: 200.0, + errorCorrectionLevel: QrErrorCorrectLevel.H, + ), + ), + Positioned( + child: CoinIcon(coinAbbr, size: 40), + ), + ], + ); + } +} diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index 2405e6b4b9..e18c169bd6 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -1,8 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/wallet.dart'; @@ -302,7 +303,8 @@ class CoinDetailsSwapButton extends StatelessWidget { @override Widget build(BuildContext context) { - if (currentWalletBloc.wallet?.config.type != WalletType.iguana) { + final currentWallet = context.watch().state.currentUser?.wallet; + if (currentWallet?.config.type != WalletType.iguana) { return const SizedBox.shrink(); } diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index d7b0b681e7..3d75910997 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -3,16 +3,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/wallet.dart'; @@ -26,6 +28,7 @@ import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_details_info_fiat.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; @@ -50,14 +53,13 @@ class CoinDetailsInfo extends StatefulWidget { class _CoinDetailsInfoState extends State with SingleTickerProviderStateMixin { Transaction? _selectedTransaction; - late TabController _tabController; - String? get _walletId => currentWalletBloc.wallet?.id; + String? get _walletId => + RepositoryProvider.of(context).wallet?.id; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); const selectedDurationInitial = Duration(hours: 1); final growthBloc = context.read(); @@ -82,12 +84,6 @@ class _CoinDetailsInfoState extends State ); } - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return PageLayout( @@ -116,7 +112,6 @@ class _CoinDetailsInfoState extends State selectedTransaction: _selectedTransaction, setPageType: widget.setPageType, setTransaction: _selectTransaction, - tabController: _tabController, ); } return _DesktopContent( @@ -124,7 +119,6 @@ class _CoinDetailsInfoState extends State selectedTransaction: _selectedTransaction, setPageType: widget.setPageType, setTransaction: _selectTransaction, - tabController: _tabController, ); } @@ -133,7 +127,8 @@ class _CoinDetailsInfoState extends State return DisableCoinButton( onClick: () async { - await coinsBloc.deactivateCoin(widget.coin); + final coinsBloc = context.read(); + coinsBloc.add(CoinsDeactivated([widget.coin.abbr])); widget.onBackButtonPressed(); }, ); @@ -167,14 +162,12 @@ class _DesktopContent extends StatelessWidget { required this.selectedTransaction, required this.setPageType, required this.setTransaction, - required this.tabController, }); final Coin coin; final Transaction? selectedTransaction; final void Function(CoinPageType) setPageType; final Function(Transaction?) setTransaction; - final TabController tabController; @override Widget build(BuildContext context) { @@ -191,12 +184,15 @@ class _DesktopContent extends StatelessWidget { child: _DesktopCoinDetails( coin: coin, setPageType: setPageType, - tabController: tabController, ), ), const SliverToBoxAdapter( child: SizedBox(height: 20), ), + if (selectedTransaction == null) CoinAddresses(coin: coin), + const SliverToBoxAdapter( + child: SizedBox(height: 20), + ), TransactionTable( coin: coin, selectedTransaction: selectedTransaction, @@ -212,24 +208,13 @@ class _DesktopCoinDetails extends StatelessWidget { const _DesktopCoinDetails({ required this.coin, required this.setPageType, - required this.tabController, }); final Coin coin; final void Function(CoinPageType) setPageType; - final TabController tabController; @override Widget build(BuildContext context) { - final portfolioGrowthState = context.watch().state; - final profitLossState = context.watch().state; - final isPortfolioGrowthSupported = - portfolioGrowthState is! PortfolioGrowthChartUnsupported; - final isProfitLossSupported = - profitLossState is! PortfolioProfitLossChartUnsupported; - final areChartsSupported = - isPortfolioGrowthSupported || isProfitLossSupported; - return Padding( padding: const EdgeInsets.only(right: 8.0), child: Column( @@ -272,39 +257,7 @@ class _DesktopCoinDetails extends StatelessWidget { ), ), const Gap(16), - if (areChartsSupported) - Card( - child: TabBar( - controller: tabController, - tabs: [ - if (isPortfolioGrowthSupported) - Tab(text: LocaleKeys.growth.tr()), - if (isProfitLossSupported) - Tab(text: LocaleKeys.profitAndLoss.tr()), - ], - ), - ), - if (areChartsSupported) - SizedBox( - height: 340, - child: TabBarView( - controller: tabController, - children: [ - if (isPortfolioGrowthSupported) - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioGrowthChart(initialCoins: [coin]), - ), - if (isProfitLossSupported) - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart(initialCoins: [coin]), - ), - ], - ), - ), + _CoinDetailsMarketMetricsTabBar(coin: coin), ], ), ); @@ -317,14 +270,12 @@ class _MobileContent extends StatelessWidget { required this.selectedTransaction, required this.setPageType, required this.setTransaction, - required this.tabController, }); final Coin coin; final Transaction? selectedTransaction; final void Function(CoinPageType) setPageType; final Function(Transaction?) setTransaction; - final TabController tabController; @override Widget build(BuildContext context) { @@ -332,11 +283,19 @@ class _MobileContent extends StatelessWidget { slivers: [ if (selectedTransaction == null) SliverToBoxAdapter( - child: _buildMobileTopContent(context), + child: _CoinDetailsInfoHeader( + coin: coin, + setPageType: setPageType, + context: context, + ), ), const SliverToBoxAdapter( child: SizedBox(height: 20), ), + if (selectedTransaction == null) CoinAddresses(coin: coin), + const SliverToBoxAdapter( + child: SizedBox(height: 20), + ), TransactionTable( coin: coin, selectedTransaction: selectedTransaction, @@ -345,8 +304,21 @@ class _MobileContent extends StatelessWidget { ], ); } +} - Widget _buildMobileTopContent(BuildContext context) { +class _CoinDetailsInfoHeader extends StatelessWidget { + const _CoinDetailsInfoHeader({ + required this.coin, + required this.setPageType, + required this.context, + }); + + final Coin coin; + final void Function(CoinPageType p1) setPageType; + final BuildContext context; + + @override + Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(15, 18, 15, 16), decoration: BoxDecoration( @@ -385,39 +357,85 @@ class _MobileContent extends StatelessWidget { FaucetButton( onPressed: () => setPageType(CoinPageType.faucet), ), - Card( - child: TabBar( - controller: tabController, - tabs: [ - Tab(text: LocaleKeys.growth.tr()), - Tab(text: LocaleKeys.profitAndLoss.tr()), - ], - ), - ), - SizedBox( - height: 340, - child: TabBarView( - controller: tabController, - children: [ - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioGrowthChart(initialCoins: [coin]), - ), - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart(initialCoins: [coin]), - ), - ], - ), - ), + _CoinDetailsMarketMetricsTabBar(coin: coin), ], ), ); } } +class _CoinDetailsMarketMetricsTabBar extends StatelessWidget { + const _CoinDetailsMarketMetricsTabBar({required this.coin}); + + final Coin coin; + + @override + Widget build(BuildContext context) { + final portfolioGrowthState = context.watch().state; + final profitLossState = context.watch().state; + final isPortfolioGrowthSupported = + portfolioGrowthState is! PortfolioGrowthChartUnsupported; + final isProfitLossSupported = + profitLossState is! PortfolioProfitLossChartUnsupported; + final areChartsSupported = + isPortfolioGrowthSupported || isProfitLossSupported; + final numChartsSupported = 0 + + (isPortfolioGrowthSupported ? 1 : 0) + + (isProfitLossSupported ? 1 : 0); + + if (!areChartsSupported) { + return const SizedBox.shrink(); + } + + final TabController tabController = TabController( + length: numChartsSupported, + vsync: Navigator.of(context), + ); + + return Column( + children: [ + Card( + child: TabBar( + controller: tabController, + tabs: [ + // spread operator used to ensure that tabs and views are + // in sync + ...([ + if (isPortfolioGrowthSupported) + Tab(text: LocaleKeys.growth.tr()), + if (isProfitLossSupported) + Tab(text: LocaleKeys.profitAndLoss.tr()), + ]), + ], + ), + ), + SizedBox( + height: 340, + child: TabBarView( + controller: tabController, + children: [ + ...([ + if (isPortfolioGrowthSupported) + SizedBox( + width: double.infinity, + height: 340, + child: PortfolioGrowthChart(initialCoins: [coin]), + ), + if (isProfitLossSupported) + SizedBox( + width: double.infinity, + height: 340, + child: PortfolioProfitLossChart(initialCoins: [coin]), + ), + ]), + ], + ), + ) + ], + ); + } +} + class _FaucetButton extends StatelessWidget { const _FaucetButton({ required this.coin, @@ -564,7 +582,8 @@ class _SpecificButton extends StatelessWidget { @override Widget build(BuildContext context) { - final walletType = currentWalletBloc.wallet?.config.type; + final currentWallet = context.watch().state.currentUser?.wallet; + final walletType = currentWallet?.config.type; if (coin.abbr == 'KMD' && walletType == WalletType.iguana) { return _GetRewardsButton( diff --git a/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart b/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart index e09035e4c8..ff5161e1f8 100644 --- a/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart +++ b/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart @@ -176,8 +176,11 @@ class _ContractAddressTitle extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall!.copyWith( fontSize: 9, fontWeight: FontWeight.w500, - color: - Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(.45), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: .45), ), ); } diff --git a/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart b/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart index 2c1a18263a..4e04b16062 100644 --- a/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart +++ b/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart @@ -1,25 +1,38 @@ import 'package:bloc/bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/3p_api/faucet/faucet.dart' as api; import 'package:web_dex/3p_api/faucet/faucet_response.dart'; import 'package:web_dex/views/wallet/coin_details/faucet/cubit/faucet_state.dart'; class FaucetCubit extends Cubit { final String coinAbbr; - final String? coinAddress; + final KomodoDefiSdk kdfSdk; FaucetCubit({ required this.coinAbbr, - required this.coinAddress, + required this.kdfSdk, }) : super(const FaucetInitial()); Future callFaucet() async { emit(const FaucetLoading()); try { - final FaucetResponse response = - await api.callFaucet(coinAbbr, coinAddress!); - if (response.status == FaucetStatus.error) { - return emit(FaucetError(response.message)); + // Temporary band-aid fix to faucet to support HD wallet - currently + // defaults to calling faucet on all addresses + // TODO: maybe add faucet button per address, or ask user if they want + // to faucet all addresses at once (or offer both options) + final asset = kdfSdk.assets.assetsFromTicker(coinAbbr).single; + final addresses = (await asset.getPubkeys(kdfSdk)).keys; + final faucetFutures = addresses.map((address) async { + return await api.callFaucet(coinAbbr, address.address); + }).toList(); + final responses = await Future.wait(faucetFutures); + if (!responses + .any((response) => response.status == FaucetStatus.success)) { + return emit(FaucetError(responses.first.message)); } else { + final response = responses.firstWhere( + (response) => response.status == FaucetStatus.success, + ); return emit(FaucetSuccess(response)); } } catch (error) { diff --git a/lib/views/wallet/coin_details/faucet/faucet_page.dart b/lib/views/wallet/coin_details/faucet/faucet_page.dart index d874eb00f4..34c9fa9964 100644 --- a/lib/views/wallet/coin_details/faucet/faucet_page.dart +++ b/lib/views/wallet/coin_details/faucet/faucet_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/views/wallet/coin_details/faucet/faucet_view.dart'; import 'cubit/faucet_cubit.dart'; @@ -18,9 +19,9 @@ class FaucetPage extends StatelessWidget { @override Widget build(BuildContext context) { + final kdfSdk = RepositoryProvider.of(context); return BlocProvider( - create: (context) => - FaucetCubit(coinAbbr: coinAbbr, coinAddress: coinAddress), + create: (context) => FaucetCubit(coinAbbr: coinAbbr, kdfSdk: kdfSdk), child: FaucetView( onBackButtonPressed: onBackButtonPressed, ), diff --git a/lib/views/wallet/coin_details/faucet/widgets/faucet_message.dart b/lib/views/wallet/coin_details/faucet/widgets/faucet_message.dart index 4a02333441..ee3dd27237 100644 --- a/lib/views/wallet/coin_details/faucet/widgets/faucet_message.dart +++ b/lib/views/wallet/coin_details/faucet/widgets/faucet_message.dart @@ -16,7 +16,11 @@ class FaucetMessage extends StatelessWidget { final textStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w700, - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6)); + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.6)); return Center( child: Container( padding: const EdgeInsets.all(20), diff --git a/lib/views/wallet/coin_details/receive/receive_address.dart b/lib/views/wallet/coin_details/receive/receive_address.dart index 606c0c64b6..f4b4e5657d 100644 --- a/lib/views/wallet/coin_details/receive/receive_address.dart +++ b/lib/views/wallet/coin_details/receive/receive_address.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -25,7 +26,8 @@ class ReceiveAddress extends StatelessWidget { @override Widget build(BuildContext context) { - if (currentWalletBloc.wallet?.config.type == WalletType.trezor) { + final currentWallet = context.watch().state.currentUser?.wallet; + if (currentWallet?.config.type == WalletType.trezor) { return ReceiveAddressTrezor( coin: coin, selectedAddress: selectedAddress, diff --git a/lib/views/wallet/coin_details/receive/receive_details.dart b/lib/views/wallet/coin_details/receive/receive_details.dart index b5976d7691..b5ac87dd79 100644 --- a/lib/views/wallet/coin_details/receive/receive_details.dart +++ b/lib/views/wallet/coin_details/receive/receive_details.dart @@ -1,8 +1,9 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -29,9 +30,8 @@ class ReceiveDetails extends StatelessWidget { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return StreamBuilder( - stream: currentWalletBloc.outWallet, - builder: (context, snapshot) { + return BlocBuilder( + builder: (context, state) { return PageLayout( header: PageHeader( title: LocaleKeys.receive.tr(), @@ -81,8 +81,8 @@ class _ReceiveDetailsContentState extends State<_ReceiveDetailsContent> { Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); - if (currentWalletBloc.wallet?.config.hasBackup == false && - !widget.coin.isTestCoin) { + final currentWallet = context.read().state.currentUser?.wallet; + if (currentWallet?.config.hasBackup == false && !widget.coin.isTestCoin) { return const BackupNotification(); } diff --git a/lib/views/wallet/coin_details/receive/request_address_button.dart b/lib/views/wallet/coin_details/receive/request_address_button.dart index 7c1e3fd5e9..9b6a56035e 100644 --- a/lib/views/wallet/coin_details/receive/request_address_button.dart +++ b/lib/views/wallet/coin_details/receive/request_address_button.dart @@ -3,8 +3,9 @@ import 'dart:async'; import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/get_new_address/get_new_address_response.dart'; @@ -34,11 +35,12 @@ class _RequestAddressButtonState extends State { @override void dispose() { + final coinsRepository = RepositoryProvider.of(context); _message = null; _inProgress = false; _confirmAddressDispatcher?.close(); _confirmAddressDispatcher = null; - coinsBloc.trezor.unsubscribeFromNewAddressStatus(); + coinsRepository.trezor.unsubscribeFromNewAddressStatus(); super.dispose(); } @@ -88,15 +90,16 @@ class _RequestAddressButtonState extends State { } Future _getAddress() async { + final coinsRepository = RepositoryProvider.of(context); setState(() { _inProgress = true; _message = null; }); - final taskId = await coinsBloc.trezor.initNewAddress(widget.coin); + final taskId = await coinsRepository.trezor.initNewAddress(widget.coin); if (taskId == null) return; routingState.isBrowserNavigationBlocked = true; - coinsBloc.trezor + coinsRepository.trezor .subscribeOnNewAddressStatus(taskId, widget.coin, _onStatusUpdate); } @@ -142,7 +145,8 @@ class _RequestAddressButtonState extends State { } void _onOkStatus(GetNewAddressResultOkDetails details) { - coinsBloc.trezor.unsubscribeFromNewAddressStatus(); + final coinsRepository = RepositoryProvider.of(context); + coinsRepository.trezor.unsubscribeFromNewAddressStatus(); _confirmAddressDispatcher?.close(); _confirmAddressDispatcher = null; routingState.isBrowserNavigationBlocked = false; diff --git a/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart b/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart index adc3f62efc..4beee093d5 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart @@ -43,7 +43,8 @@ class KmdRewardClaimSuccess extends StatelessWidget { Text( LocaleKeys.youClaimed.tr(), style: TextStyle( - color: themeData.textTheme.bodyMedium?.color?.withOpacity(0.4), + color: + themeData.textTheme.bodyMedium?.color?.withValues(alpha: 0.4), fontWeight: FontWeight.w700, fontSize: 14, ), @@ -60,7 +61,8 @@ class KmdRewardClaimSuccess extends StatelessWidget { SelectableText( '\$$formattedUsd', style: TextStyle( - color: themeData.textTheme.bodyMedium?.color?.withOpacity(0.7), + color: + themeData.textTheme.bodyMedium?.color?.withValues(alpha: 0.7), fontWeight: FontWeight.w500, fontSize: 14, ), diff --git a/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart b/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart index 3df0c76374..7f46a60386 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart @@ -1,7 +1,10 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/blocs/kmd_rewards_bloc.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/common/screen.dart'; @@ -16,7 +19,6 @@ import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/wallet/coin_details/rewards/kmd_reward_info_header.dart'; import 'package:web_dex/views/wallet/coin_details/rewards/kmd_reward_list_item.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class KmdRewardsInfo extends StatefulWidget { const KmdRewardsInfo({ @@ -161,7 +163,7 @@ class _KmdRewardsInfoState extends State { .textTheme .bodyMedium ?.color - ?.withOpacity(0.4), + ?.withValues(alpha: 0.4), ), ), const SizedBox( @@ -263,7 +265,7 @@ class _KmdRewardsInfoState extends State { .textTheme .bodyMedium ?.color - ?.withOpacity(0.3), + ?.withValues(alpha: 0.3), ), ), const SizedBox(height: 24.0), @@ -403,6 +405,8 @@ class _KmdRewardsInfoState extends State { _successMessage = ''; }); + final coinsRepository = RepositoryProvider.of(context); + final kmdRewardsBloc = RepositoryProvider.of(context); final BlocResponse response = await kmdRewardsBloc.claim(context); final BaseError? error = response.error; @@ -414,13 +418,14 @@ class _KmdRewardsInfoState extends State { return; } - await coinsBloc.updateBalances(); // consider refactoring (add timeout?) + // ignore: use_build_context_synchronously + context.read().add(CoinsBalancesRefreshed()); await _updateInfoUntilSuccessOrTimeOut(30000); final String reward = doubleToString(double.tryParse(response.result!) ?? 0); final double? usdPrice = - coinsBloc.getUsdPriceByAmount(response.result!, 'KMD'); + coinsRepository.getUsdPriceByAmount(response.result!, 'KMD'); final String formattedUsdPrice = cutTrailingZeros(formatAmt(usdPrice ?? 0)); setState(() { _isClaiming = false; @@ -460,10 +465,12 @@ class _KmdRewardsInfoState extends State { } Future _updateRewardsInfo() async { + final coinsRepository = RepositoryProvider.of(context); + final kmdRewardsBloc = RepositoryProvider.of(context); final double? total = await kmdRewardsBloc.getTotal(context); final List currentRewards = await kmdRewardsBloc.getInfo(); final double? totalUsd = - coinsBloc.getUsdPriceByAmount((total ?? 0).toString(), 'KMD'); + coinsRepository.getUsdPriceByAmount((total ?? 0).toString(), 'KMD'); if (!mounted) return; setState(() { diff --git a/lib/views/wallet/coin_details/transactions/transaction_details.dart b/lib/views/wallet/coin_details/transactions/transaction_details.dart index 1e7ae6fcc7..de0e753e0d 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_details.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_details.dart @@ -1,13 +1,14 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; import 'package:web_dex/model/coin.dart'; - import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/copied_text.dart'; @@ -88,7 +89,7 @@ class TransactionDetails extends StatelessWidget { _buildSimpleData( context, title: LocaleKeys.date.tr(), - value: transaction.formattedTime, + value: formatTransactionDateTime(transaction), hasBackground: true, ), _buildFee(context), @@ -107,7 +108,7 @@ class TransactionDetails extends StatelessWidget { _buildSimpleData( context, title: LocaleKeys.transactionHash.tr(), - value: transaction.txHash, + value: transaction.txHash ?? '', isCopied: true, ), const SizedBox(height: 20), @@ -169,7 +170,7 @@ class TransactionDetails extends StatelessWidget { _buildAddress( context, title: LocaleKeys.to.tr(), - address: transaction.toAddress, + address: transaction.to.first, ), ], ) @@ -192,7 +193,7 @@ class TransactionDetails extends StatelessWidget { child: _buildAddress( context, title: LocaleKeys.to.tr(), - address: transaction.toAddress, + address: transaction.to.first, ), ), ), @@ -202,14 +203,14 @@ class TransactionDetails extends StatelessWidget { } Widget _buildBalanceChanges(BuildContext context) { - final String formatted = - formatDexAmt(double.parse(transaction.myBalanceChange).abs()); - final String sign = transaction.isReceived ? '+' : '-'; + final String formatted = formatDexAmt(transaction.amount.toDouble().abs()); + final String sign = transaction.amount.toDouble() > 0 ? '+' : '-'; + final coinsBloc = RepositoryProvider.of(context); final double? usd = - coinsBloc.getUsdPriceByAmount(formatted, transaction.coin); + coinsBloc.getUsdPriceByAmount(formatted, transaction.assetId.id); final String formattedUsd = formatAmt(usd ?? 0); final String value = - '$sign $formatted ${Coin.normalizeAbbr(transaction.coin)} (\$$formattedUsd)'; + '$sign $formatted ${Coin.normalizeAbbr(transaction.assetId.id)} (\$$formattedUsd)'; return SelectableText( value, @@ -237,7 +238,7 @@ class TransactionDetails extends StatelessWidget { color: theme.custom.defaultGradientButtonTextColor, ), onPressed: () { - launchURL(getTxExplorerUrl(coin, transaction.txHash)); + launchURL(getTxExplorerUrl(coin, transaction.txHash ?? '')); }, text: LocaleKeys.viewOnExplorer.tr(), ), @@ -258,10 +259,12 @@ class TransactionDetails extends StatelessWidget { } Widget _buildFee(BuildContext context) { - final String? fee = transaction.feeDetails.feeValue; + final String? fee = transaction.fee?.amount.toString(); final String formattedFee = getNumberWithoutExponent(double.parse(fee ?? '').abs().toString()); - final double? usd = coinsBloc.getUsdPriceByAmount(formattedFee, _feeCoin); + final coinsBloc = context.read(); + final double? usd = + coinsBloc.state.getUsdPriceByAmount(formattedFee, _feeCoin); final String formattedUsd = formatAmt(usd ?? 0); final String title = LocaleKeys.fees.tr(); @@ -392,8 +395,8 @@ class TransactionDetails extends StatelessWidget { } String get _feeCoin { - return transaction.feeDetails.coin.isNotEmpty - ? transaction.feeDetails.coin - : transaction.coin; + return transaction.fee != null && transaction.fee!.coin.isNotEmpty + ? transaction.fee!.coin + : transaction.assetId.id; } } diff --git a/lib/views/wallet/coin_details/transactions/transaction_list.dart b/lib/views/wallet/coin_details/transactions/transaction_list.dart index 902cdb153a..c7d96e3ee6 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_list.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_list.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/views/wallet/coin_details/transactions/transaction_list_item.dart'; class TransactionList extends StatelessWidget { @@ -47,40 +47,109 @@ class _List extends StatelessWidget { @override Widget build(BuildContext context) { - final hasTitle = transactions.isNotEmpty || !isMobile; - final indexOffset = hasTitle ? 1 : 0; + return SliverToBoxAdapter( + child: Column( + children: [ + if (transactions.isNotEmpty && !isMobile) const HistoryTitle(), + Card( + margin: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + color: theme.custom.dexPageTheme.frontPlate, + child: Padding( + padding: EdgeInsets.all(isMobile ? 16.0 : 24.0), + child: isMobile + ? HistoryListContent( + transactions: transactions, + coinAbbr: coinAbbr, + setTransaction: setTransaction, + isInProgress: isInProgress, + ) + : Card( + margin: const EdgeInsets.symmetric(vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + color: Theme.of(context).colorScheme.onSurface, + child: HistoryListContent( + transactions: transactions, + coinAbbr: coinAbbr, + setTransaction: setTransaction, + isInProgress: isInProgress, + ), + ), + ), + ), + ], + ), + ); + } +} + +class HistoryListContent extends StatelessWidget { + const HistoryListContent({ + super.key, + required this.transactions, + required this.coinAbbr, + required this.setTransaction, + required this.isInProgress, + }); + + final List transactions; + final String coinAbbr; + final void Function(Transaction tx) setTransaction; + final bool isInProgress; - return SliverList( - key: const Key('coin-details-transaction-list'), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - LocaleKeys.history.tr(), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: isMobile ? 16 : 18, + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (transactions.isNotEmpty && isMobile) const HistoryTitle(), + if (transactions.isNotEmpty && isMobile) const SizedBox(height: 12), + ...transactions.asMap().entries.map( + (entry) { + final index = entry.key; + final transaction = entry.value; + + return Column( + children: [ + TransactionListRow( + transaction: transaction, + coinAbbr: coinAbbr, + setTransaction: setTransaction, ), - ), + if (isMobile && index < transactions.length - 1) + const SizedBox(height: 12), + ], ); - } - - final adjustedIndex = index - indexOffset; + }, + ).toList(), + if (isInProgress) const UiSpinnerList(height: 50), + ], + ); + } +} - if (adjustedIndex + 1 == transactions.length && isInProgress) { - return const UiSpinnerList(height: 50); - } +class HistoryTitle extends StatelessWidget { + const HistoryTitle({ + super.key, + }); - final Transaction tx = transactions[adjustedIndex]; - return TransactionListRow( - transaction: tx, - coinAbbr: coinAbbr, - setTransaction: setTransaction, - ); - }, - childCount: transactions.length + indexOffset, + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + LocaleKeys.lastTransactions.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: isMobile ? 16 : 24, + ), + ), ), ); } diff --git a/lib/views/wallet/coin_details/transactions/transaction_list_item.dart b/lib/views/wallet/coin_details/transactions/transaction_list_item.dart index 27af03f82b..786cd3e9da 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_list_item.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_list_item.dart @@ -1,13 +1,17 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/custom_tooltip.dart'; import 'package:web_dex/shared/utils/formatters.dart'; +import 'package:web_dex/views/wallet/common/address_copy_button.dart'; +import 'package:web_dex/views/wallet/common/address_icon.dart'; +import 'package:web_dex/views/wallet/common/address_text.dart'; class TransactionListRow extends StatefulWidget { const TransactionListRow({ @@ -30,7 +34,7 @@ class _TransactionListRowState extends State { return _isReceived ? Icons.arrow_circle_down : Icons.arrow_circle_up; } - bool get _isReceived => widget.transaction.isReceived; + bool get _isReceived => widget.transaction.amount.toDouble() > 0; String get _sign { return _isReceived ? '+' : '-'; @@ -40,12 +44,16 @@ class _TransactionListRowState extends State { @override Widget build(BuildContext context) { + final borderRadius = BorderRadius.circular(16); return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface, - borderRadius: BorderRadius.circular(4), + color: isMobile + ? Theme.of(context).colorScheme.onSurface + : Colors.transparent, + borderRadius: borderRadius, ), child: InkWell( + borderRadius: borderRadius, onFocusChange: (value) { setState(() { _hasFocus = value; @@ -56,10 +64,10 @@ class _TransactionListRowState extends State { color: _hasFocus ? Theme.of(context).colorScheme.tertiary : Colors.transparent, - margin: const EdgeInsets.symmetric(vertical: 5), + margin: EdgeInsets.symmetric(vertical: isMobile ? 5 : 0), padding: isMobile - ? const EdgeInsets.fromLTRB(0, 12, 0, 12) - : const EdgeInsets.fromLTRB(12, 12, 12, 12), + ? const EdgeInsets.only(bottom: 12) + : const EdgeInsets.all(6), child: isMobile ? _buildMobileRow(context) : _buildNormalRow(context)), onTap: () => widget.setTransaction(widget.transaction), @@ -69,17 +77,17 @@ class _TransactionListRowState extends State { Widget _buildAmountChangesMobile(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildBalanceChanges(), _buildUsdChanges(), - _buildBalanceMobile(), ], ); } Widget _buildBalanceChanges() { final String formatted = - formatDexAmt(double.parse(widget.transaction.myBalanceChange).abs()); + formatDexAmt(widget.transaction.amount.toDouble().abs()); return Row( children: [ @@ -92,7 +100,7 @@ class _TransactionListRowState extends State { ), const SizedBox(width: 4), Text( - '${Coin.normalizeAbbr(widget.transaction.coin)} $formatted', + '${Coin.normalizeAbbr(widget.transaction.assetId.id)} $formatted', style: TextStyle( color: _isReceived ? theme.custom.increaseColor @@ -112,16 +120,11 @@ class _TransactionListRowState extends State { children: [ Text( _isReceived ? LocaleKeys.receive.tr() : LocaleKeys.send.tr(), - style: TextStyle( - color: _isReceived - ? theme.custom.increaseColor - : theme.custom.decreaseColor, - fontSize: 14, - fontWeight: FontWeight.w500), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800), ), Text( - widget.transaction.formattedTime, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + formatTransactionDateTime(widget.transaction), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400), ), ], ) @@ -129,16 +132,6 @@ class _TransactionListRowState extends State { ); } - Widget _buildBalanceMobile() { - final String formatted = - formatDexAmt(double.parse(widget.transaction.myBalanceChange).abs()); - - return Text( - '${Coin.normalizeAbbr(widget.transaction.coin)} $formatted', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - ); - } - Widget _buildMemoAndDate() { return Align( alignment: isMobile ? const Alignment(-1, 0) : const Alignment(1, 0), @@ -149,11 +142,12 @@ class _TransactionListRowState extends State { _buildMemo(), const SizedBox(width: 6), Text( - widget.transaction.formattedTime, + formatTransactionDateTime(widget.transaction), style: isMobile ? TextStyle(color: Colors.grey[400]) : const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), + const SizedBox(width: 4), ], ), ); @@ -162,42 +156,31 @@ class _TransactionListRowState extends State { Widget _buildMobileRow(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: Row( - mainAxisSize: MainAxisSize.max, + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 35, - height: 35, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.tertiary, - borderRadius: BorderRadius.circular(20)), - child: Center( - child: Icon( - _isReceived ? Icons.arrow_downward : Icons.arrow_upward, - color: _isReceived - ? theme.custom.increaseColor - : theme.custom.decreaseColor, - size: 15, + _buildAddress(), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildBalanceChangesMobile(context), + ], + ), ), - ), - ), - const SizedBox(width: 10), - Expanded( - flex: 5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildBalanceChangesMobile(context), - ], - ), - ), - Expanded( - flex: 5, - child: Align( - alignment: const Alignment(1, 0), - child: _buildAmountChangesMobile(context), - ), + Expanded( + flex: 5, + child: Align( + alignment: const Alignment(1, 0), + child: _buildAmountChangesMobile(context), + ), + ), + ], ), ], ), @@ -208,7 +191,7 @@ class _TransactionListRowState extends State { return Row( mainAxisSize: MainAxisSize.max, children: [ - const SizedBox(width: 4), + Expanded(flex: 4, child: _buildAddress()), Expanded( flex: 4, child: Text( @@ -259,8 +242,9 @@ class _TransactionListRowState extends State { } Widget _buildUsdChanges() { - final double? usdChanges = coinsBloc.getUsdPriceByAmount( - widget.transaction.myBalanceChange, + final coinsBloc = context.read(); + final double? usdChanges = coinsBloc.state.getUsdPriceByAmount( + widget.transaction.amount.toString(), widget.coinAbbr, ); return Text( @@ -273,4 +257,20 @@ class _TransactionListRowState extends State { fontWeight: FontWeight.w500), ); } + + Widget _buildAddress() { + final myAddress = widget.transaction.isIncoming + ? widget.transaction.to.first + : widget.transaction.from.first; + + return Row( + children: [ + const SizedBox(width: 8), + AddressIcon(address: myAddress), + const SizedBox(width: 8), + AddressText(address: myAddress), + AddressCopyButton(address: myAddress), + ], + ); + } } diff --git a/lib/views/wallet/coin_details/transactions/transaction_table.dart b/lib/views/wallet/coin_details/transactions/transaction_table.dart index d2b536e82d..2b541c7131 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_table.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_table.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; +import 'package:komodo_defi_types/types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/launch_native_explorer_button.dart'; @@ -68,40 +68,28 @@ class TransactionTable extends StatelessWidget { Widget _buildTransactionList(BuildContext context) { return BlocBuilder( builder: (BuildContext ctx, TransactionHistoryState state) { - if (coin.isActivating || state is TransactionHistoryInitialState) { - return const SliverToBoxAdapter( - child: UiSpinnerList(), - ); - } - - if (state is TransactionHistoryFailureState) { - return SliverToBoxAdapter( - child: _ErrorMessage( - text: state.error.message, - textColor: theme.currentGlobal.colorScheme.error, - ), - ); - } - - if (state is TransactionHistoryInProgressState) { - return _TransactionsListWrapper( - coinAbbr: coin.abbr, - setTransaction: setTransaction, - transactions: state.transactions, - isInProgress: true, - ); + if (state.transactions.isEmpty) { + if (coin.isActivating || state.loading) { + return const SliverToBoxAdapter( + child: UiSpinnerList(), + ); + } + + if (state.error != null) { + return SliverToBoxAdapter( + child: _ErrorMessage( + text: state.error!.message, + textColor: theme.currentGlobal.colorScheme.error, + ), + ); + } } - if (state is TransactionHistoryLoadedState) { - return _TransactionsListWrapper( - coinAbbr: coin.abbr, - setTransaction: setTransaction, - transactions: state.transactions, - isInProgress: false, - ); - } - return const SliverToBoxAdapter( - child: SizedBox(), + return _TransactionsListWrapper( + coinAbbr: coin.abbr, + setTransaction: setTransaction, + transactions: state.transactions, + isInProgress: state.loading, ); }, ); diff --git a/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart b/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart index 18463ab64a..91fd802e92 100644 --- a/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart +++ b/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart @@ -75,8 +75,11 @@ class _SendErrorBody extends StatelessWidget { return BlocSelector( selector: (state) => state.sendError.message, builder: (BuildContext context, String errorText) { - final iconColor = - Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(.7); + final iconColor = Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: .7); return Material( color: theme.custom.buttonColorDefault, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart index 08f810bc0f..ffda432136 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart @@ -22,7 +22,7 @@ class FillFormTitle extends StatelessWidget { .textTheme .bodyMedium ?.color - ?.withOpacity(.4), + ?.withValues(alpha: .4), ), children: [ TextSpan( diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart index 95349ebe0f..612408a036 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/widgets/copied_text.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class SendConfirmItem extends StatelessWidget { const SendConfirmItem({ @@ -44,7 +44,7 @@ class SendConfirmItem extends StatelessWidget { .textTheme .titleLarge ?.color - ?.withOpacity(.6)), + ?.withValues(alpha: .6)), ), ), const SizedBox(height: 8), diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index 53c2aab3c3..a7639f5d64 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -1,14 +1,15 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/bitrefill/bitrefill_transaction_completed_dialog.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; class WithdrawForm extends StatelessWidget { const WithdrawForm({ @@ -26,7 +27,8 @@ class WithdrawForm extends StatelessWidget { return BlocProvider( create: (BuildContext context) => WithdrawFormBloc( coin: coin, - coinsBloc: coinsBloc, + coinsRepository: RepositoryProvider.of(context), + api: RepositoryProvider.of(context), goBack: onBackButtonPressed, ), child: isBitrefillIntegrationEnabled diff --git a/lib/views/wallet/coins_manager/coins_manager_controls.dart b/lib/views/wallet/coins_manager/coins_manager_controls.dart index 893fb61cb0..11f324a4b6 100644 --- a/lib/views/wallet/coins_manager/coins_manager_controls.dart +++ b/lib/views/wallet/coins_manager/coins_manager_controls.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_filters_dropdown.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_select_all_button.dart'; diff --git a/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart b/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart index 93195ccae3..0043664410 100644 --- a/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart +++ b/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart @@ -6,10 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/coin_utils.dart'; @@ -101,8 +100,9 @@ class _Dropdown extends StatelessWidget { bloc: bloc, builder: (context, state) { final List selectedCoinTypes = bloc.state.selectedCoinTypes; - final List listTypes = - CoinType.values.where(_filterTypes).toList(); + final List listTypes = CoinType.values + .where((CoinType type) => _filterTypes(context, type)) + .toList(); onTap(CoinType type) => bloc.add(CoinsManagerCoinTypeSelect(type: type)); @@ -139,14 +139,16 @@ class _Dropdown extends StatelessWidget { ); } - bool _filterTypes(CoinType type) { - switch (currentWalletBloc.wallet?.config.type) { + bool _filterTypes(BuildContext context, CoinType type) { + final coinsBloc = context.read(); + final currentWallet = context.read().state.currentUser?.wallet; + switch (currentWallet?.config.type) { case WalletType.iguana: - return coinsBloc.knownCoins + return coinsBloc.state.coins.values .firstWhereOrNull((coin) => coin.type == type) != null; case WalletType.trezor: - return coinsBloc.knownCoins.firstWhereOrNull( + return coinsBloc.state.coins.values.firstWhereOrNull( (coin) => coin.type == type && coin.hasTrezorSupport) != null; case WalletType.metamask: diff --git a/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart b/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart index 7210cefbde..b4e8533a13 100644 --- a/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart +++ b/lib/views/wallet/coins_manager/coins_manager_list_wrapper.dart @@ -1,11 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_state.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -18,7 +17,6 @@ import 'package:web_dex/views/wallet/coins_manager/coins_manager_helpers.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_list.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_list_header.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_selected_types_list.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class CoinsManagerListWrapper extends StatefulWidget { const CoinsManagerListWrapper({Key? key}) : super(key: key); @@ -116,6 +114,8 @@ class _CoinsManagerListWrapperState extends State { } void _onCoinSelect(Coin coin) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); final bloc = context.read(); if (bloc.state.action == CoinsManagerAction.remove && tradingEntitiesBloc.isCoinBusy(coin.abbr)) { diff --git a/lib/views/wallet/coins_manager/coins_manager_page.dart b/lib/views/wallet/coins_manager/coins_manager_page.dart index 2622722bfd..78519be0b6 100644 --- a/lib/views/wallet/coins_manager/coins_manager_page.dart +++ b/lib/views/wallet/coins_manager/coins_manager_page.dart @@ -1,14 +1,16 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/router/state/wallet_state.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_list_wrapper.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class CoinsManagerPage extends StatelessWidget { const CoinsManagerPage({ @@ -38,28 +40,34 @@ class CoinsManagerPage extends StatelessWidget { content: Flexible( child: Padding( padding: const EdgeInsets.only(top: 20.0), - child: StreamBuilder( - initialData: coinsBloc.loginActivationFinished, - stream: coinsBloc.outLoginActivationFinished, - builder: - (context, AsyncSnapshot walletCoinsEnabledSnapshot) { - if (!(walletCoinsEnabledSnapshot.data ?? false)) { - return const Center( - child: Padding( - padding: EdgeInsets.fromLTRB(0, 100, 0, 100), - child: UiSpinner(), - ), - ); - } - return BlocProvider( - key: Key('coins-manager-page-${action.toString()}'), - create: (context) => CoinsManagerBloc( - action: action, - coinsRepo: coinsBloc, - ), - child: const CoinsManagerListWrapper(), - ); - })), + child: + BlocBuilder(builder: (context, state) { + if (!state.loginActivationFinished) { + return const Center( + child: Padding( + padding: EdgeInsets.fromLTRB(0, 100, 0, 100), + child: UiSpinner(), + ), + ); + } + return BlocProvider( + key: Key('coins-manager-page-${action.toString()}'), + create: (context) => CoinsManagerBloc( + action: action, + coinsRepo: RepositoryProvider.of(context), + currentWalletBloc: + RepositoryProvider.of(context), + )..add(const CoinsManagerCoinsUpdate()), + child: BlocListener( + listenWhen: (previous, current) => + previous.walletCoins != current.walletCoins, + listener: (context, state) => context + .read() + .add(const CoinsManagerCoinsUpdate()), + child: const CoinsManagerListWrapper(), + ), + ); + })), ), ); } diff --git a/lib/views/wallet/coins_manager/coins_manager_select_all_button.dart b/lib/views/wallet/coins_manager/coins_manager_select_all_button.dart index 29cd2da0a5..d030b04163 100644 --- a/lib/views/wallet/coins_manager/coins_manager_select_all_button.dart +++ b/lib/views/wallet/coins_manager/coins_manager_select_all_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; class CoinsManagerSelectAllButton extends StatelessWidget { const CoinsManagerSelectAllButton({ diff --git a/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart b/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart index 7cf638459a..a10cb84bb0 100644 --- a/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart +++ b/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_state.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin_type.dart'; diff --git a/lib/views/wallet/coins_manager/coins_manager_switch_button.dart b/lib/views/wallet/coins_manager/coins_manager_switch_button.dart index 9f433da806..7fecd3e406 100644 --- a/lib/views/wallet/coins_manager/coins_manager_switch_button.dart +++ b/lib/views/wallet/coins_manager/coins_manager_switch_button.dart @@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; -import 'package:web_dex/bloc/coins_manager/coins_manager_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/router/state/wallet_state.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; diff --git a/lib/views/wallet/common/address_copy_button.dart b/lib/views/wallet/common/address_copy_button.dart new file mode 100644 index 0000000000..b9b2f50dff --- /dev/null +++ b/lib/views/wallet/common/address_copy_button.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class AddressCopyButton extends StatelessWidget { + final String address; + + const AddressCopyButton({Key? key, required this.address}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + splashRadius: 18, + icon: const Icon(Icons.copy, size: 16), + color: Theme.of(context).textTheme.bodyMedium!.color, + onPressed: () { + copyToClipBoard(context, address); + }, + ); + } +} diff --git a/lib/views/wallet/common/address_icon.dart b/lib/views/wallet/common/address_icon.dart new file mode 100644 index 0000000000..803aa05c60 --- /dev/null +++ b/lib/views/wallet/common/address_icon.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/common/screen.dart'; + +Color _generateColorFromString(String input) { + final hash = input.hashCode; + final r = (hash & 0xFF0000) >> 16; + final g = (hash & 0x00FF00) >> 8; + final b = (hash & 0x0000FF); + return Color.fromARGB(255, r, g, b); +} + +class AddressIcon extends StatelessWidget { + const AddressIcon({ + super.key, + required this.address, + this.radius = 16, + }); + + final String address; + final double radius; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: radius * (isMobile ? 0.5 : 1), + backgroundColor: _generateColorFromString(address), + ); + } +} diff --git a/lib/views/wallet/common/address_text.dart b/lib/views/wallet/common/address_text.dart new file mode 100644 index 0000000000..d382db45dd --- /dev/null +++ b/lib/views/wallet/common/address_text.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; + +class AddressText extends StatelessWidget { + const AddressText({ + required this.address, + }); + + final String address; + + @override + Widget build(BuildContext context) { + return Text( + truncateMiddleSymbols(address, 5, 4), + style: const TextStyle(fontSize: 14), + ); + } +} diff --git a/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart b/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart index 5a7a478cbc..27d9153859 100644 --- a/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart +++ b/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart @@ -34,10 +34,12 @@ class PriceChartPage extends StatelessWidget { children: [ MarketChartHeaderControls( title: const Text('Statistics'), - leadingIcon: CoinIcon( - state.data.firstOrNull?.info.ticker ?? '???', - size: 22, - ), + leadingIcon: state.data.firstOrNull?.info.ticker == null + ? const Icon(Icons.attach_money, size: 22) + : CoinIcon( + state.data.firstOrNull?.info.ticker ?? '', + size: 22, + ), leadingText: Text( NumberFormat.currency(symbol: '\$', decimalDigits: 4) .format( diff --git a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart index 407a243fb8..6b18678b7f 100644 --- a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart +++ b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart @@ -1,8 +1,9 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/ui_simple_border_button.dart'; @@ -11,9 +12,9 @@ import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; import 'package:web_dex/shared/widgets/coin_fiat_change.dart'; import 'package:web_dex/shared/widgets/coin_fiat_price.dart'; +import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; import 'package:web_dex/shared/widgets/need_attention_mark.dart'; -import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart'; class CoinListItemDesktop extends StatelessWidget { @@ -134,6 +135,7 @@ class _CoinBalance extends StatelessWidget { Flexible( flex: 2, child: AutoScrollText( + key: Key('coin-balance-asset-${coin.abbr.toLowerCase()}'), text: doubleToString(coin.balance), style: const TextStyle( fontSize: _fontSize, @@ -175,11 +177,13 @@ class _SuspendedMessage extends StatelessWidget { required this.coin, required this.isReEnabling, }); + final Coin coin; final bool isReEnabling; @override Widget build(BuildContext context) { + final coinsBloc = context.read(); return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -200,9 +204,7 @@ class _SuspendedMessage extends StatelessWidget { key: Key('retry-suspended-asset-${(coin.abbr)}'), onPressed: isReEnabling ? null - : () async { - await coinsBloc.activateCoins([coin]); - }, + : () => coinsBloc.add(CoinsActivated([coin.abbr])), inProgress: isReEnabling, child: const Text(LocaleKeys.retryButtonText).tr(), ), diff --git a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart index 83911d93f6..58d479a863 100644 --- a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; @@ -21,42 +21,34 @@ class ActiveCoinsList extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - initialData: coinsBloc.loginActivationFinished, - stream: coinsBloc.outLoginActivationFinished, - builder: (context, AsyncSnapshot walletCoinsEnabledSnapshot) { - return StreamBuilder>( - initialData: coinsBloc.walletCoinsMap.values, - stream: coinsBloc.outWalletCoins, - builder: (context, AsyncSnapshot> snapshot) { - final List coins = List.from(snapshot.data ?? []); - final Iterable displayedCoins = _getDisplayedCoins(coins); + return BlocBuilder( + builder: (context, state) { + final coins = state.walletCoins.values.toList(); + final Iterable displayedCoins = _getDisplayedCoins(coins); - if (displayedCoins.isEmpty && - (searchPhrase.isNotEmpty || withBalance)) { - return SliverToBoxAdapter( - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(8.0), - child: - SelectableText(LocaleKeys.walletPageNoSuchAsset.tr()), - ), - ); - } + if (displayedCoins.isEmpty && + (searchPhrase.isNotEmpty || withBalance)) { + return SliverToBoxAdapter( + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(8.0), + child: SelectableText(LocaleKeys.walletPageNoSuchAsset.tr()), + ), + ); + } - List sorted = sortFiatBalance(displayedCoins.toList()); + List sorted = sortFiatBalance(displayedCoins.toList()); - if (!context.read().state.testCoinsEnabled) { - sorted = removeTestCoins(sorted); - } + if (!context.read().state.testCoinsEnabled) { + sorted = removeTestCoins(sorted); + } - return WalletCoinsList( - coins: sorted, - onCoinItemTap: onCoinItemTap, - ); - }, - ); - }); + return WalletCoinsList( + coins: sorted, + onCoinItemTap: onCoinItemTap, + ); + }, + ); } Iterable _getDisplayedCoins(Iterable coins) => diff --git a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart index a8cad9a5ef..aa28400562 100644 --- a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/views/wallet/wallet_page/common/wallet_coins_list.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class AllCoinsList extends StatelessWidget { const AllCoinsList({ @@ -20,27 +20,24 @@ class AllCoinsList extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder>( - initialData: coinsBloc.knownCoins, - stream: coinsBloc.outKnownCoins, - builder: (context, AsyncSnapshot> snapshot) { - final List coins = snapshot.data ?? []; + return BlocBuilder(builder: (context, state) { + final List coins = state.coins.values.toList(); - if (coins.isEmpty) { - return const SliverToBoxAdapter(child: UiSpinner()); - } + if (coins.isEmpty) { + return const SliverToBoxAdapter(child: UiSpinner()); + } - List displayedCoins = - sortByPriority(filterCoinsByPhrase(coins, searchPhrase)).toList(); + List displayedCoins = + sortByPriority(filterCoinsByPhrase(coins, searchPhrase)).toList(); - if (!context.read().state.testCoinsEnabled) { - displayedCoins = removeTestCoins(displayedCoins); - } + if (!context.read().state.testCoinsEnabled) { + displayedCoins = removeTestCoins(displayedCoins); + } - return WalletCoinsList( - coins: displayedCoins, - onCoinItemTap: onCoinItemTap, - ); - }); + return WalletCoinsList( + coins: displayedCoins, + onCoinItemTap: onCoinItemTap, + ); + }); } } diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart index c3b0696464..e736fbf6e6 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -12,9 +12,10 @@ import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -55,18 +56,11 @@ class _WalletMainState extends State void initState() { super.initState(); - if (currentWalletBloc.wallet != null) { - _loadWalletData(currentWalletBloc.wallet!.id); + final authBloc = context.read(); + if (authBloc.state.currentUser != null) { + _loadWalletData(authBloc.state.currentUser!.wallet.id).ignore(); } - _walletSubscription = currentWalletBloc.outWallet.listen((wallet) { - if (wallet != null) { - _loadWalletData(wallet.id); - } else { - _clearWalletData(); - } - }); - _tabController = TabController(length: 2, vsync: this); } @@ -81,95 +75,118 @@ class _WalletMainState extends State @override Widget build(BuildContext context) { - final walletCoinsFiltered = coinsBloc.walletCoinsMap.values; + return BlocConsumer( + // This should only load / refresh wallet data if the user changes + listenWhen: (previous, current) => + previous.currentUser != current.currentUser, + listener: (context, state) { + if (state.currentUser?.wallet != null) { + _loadWalletData(state.currentUser!.wallet.id).ignore(); + } else { + _clearWalletData(); + } + }, + builder: (authContext, authState) { + final authStateMode = authState.currentUser == null + ? AuthorizeMode.noLogin + : AuthorizeMode.logIn; + return BlocBuilder( + builder: (context, state) { + final walletCoinsFiltered = state.walletCoins.values.toList(); - final authState = context.select((AuthBloc bloc) => bloc.state.mode); - - return PageLayout( - noBackground: true, - header: isMobile ? PageHeader(title: LocaleKeys.wallet.tr()) : null, - content: Expanded( - child: CustomScrollView( - key: const Key('wallet-page-scroll-view'), - slivers: [ - SliverToBoxAdapter( - child: Column( - children: [ - if (authState == AuthorizeMode.logIn) ...[ - WalletOverview( - onPortfolioGrowthPressed: () => - _tabController.animateTo(0), - onPortfolioProfitLossPressed: () => - _tabController.animateTo(1), - ), - const Gap(8), - ], - if (authState != AuthorizeMode.logIn) - const SizedBox( - width: double.infinity, - height: 340, - child: PriceChartPage(key: Key('price-chart')), - ) - else ...[ - Card( - child: TabBar( - controller: _tabController, - tabs: [ - Tab(text: LocaleKeys.portfolioGrowth.tr()), - Tab(text: LocaleKeys.profitAndLoss.tr()), - ], - ), - ), - SizedBox( - height: 340, - child: TabBarView( - controller: _tabController, + return PageLayout( + noBackground: true, + header: + isMobile ? PageHeader(title: LocaleKeys.wallet.tr()) : null, + content: Expanded( + child: CustomScrollView( + key: const Key('wallet-page-scroll-view'), + slivers: [ + SliverToBoxAdapter( + child: Column( children: [ - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioGrowthChart( - initialCoins: walletCoinsFiltered.toList(), + if (authStateMode == AuthorizeMode.logIn) ...[ + WalletOverview( + onPortfolioGrowthPressed: () => + _tabController.animateTo(0), + onPortfolioProfitLossPressed: () => + _tabController.animateTo(1), + ), + const Gap(8), + ], + if (authStateMode != AuthorizeMode.logIn) + const SizedBox( + width: double.infinity, + height: 340, + child: PriceChartPage(key: Key('price-chart')), + ) + else ...[ + Card( + child: TabBar( + controller: _tabController, + tabs: [ + Tab(text: LocaleKeys.portfolioGrowth.tr()), + Tab(text: LocaleKeys.profitAndLoss.tr()), + ], + ), ), - ), - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart( - initialCoins: walletCoinsFiltered.toList(), + SizedBox( + height: 340, + child: TabBarView( + controller: _tabController, + children: [ + SizedBox( + width: double.infinity, + height: 340, + child: PortfolioGrowthChart( + initialCoins: walletCoinsFiltered, + ), + ), + SizedBox( + width: double.infinity, + height: 340, + child: PortfolioProfitLossChart( + initialCoins: walletCoinsFiltered, + ), + ), + ], + ), ), - ), + ], + const Gap(8), ], ), ), + SliverPersistentHeader( + pinned: true, + delegate: _SliverSearchBarDelegate( + withBalance: _showCoinWithBalance, + onSearchChange: _onSearchChange, + onWithBalanceChange: _onShowCoinsWithBalanceClick, + mode: authStateMode, + ), + ), + _buildCoinList(authStateMode), ], - const Gap(8), - ], + ), ), - ), - SliverPersistentHeader( - pinned: true, - delegate: _SliverSearchBarDelegate( - withBalance: _showCoinWithBalance, - onSearchChange: _onSearchChange, - onWithBalanceChange: _onShowCoinsWithBalanceClick, - mode: authState, - ), - ), - _buildCoinList(authState), - ], - ), - )); + ); + }, + ); + }, + ); } - void _loadWalletData(String walletId) { + Future _loadWalletData(String walletId) async { final portfolioGrowthBloc = context.read(); final profitLossBloc = context.read(); final assetOverviewBloc = context.read(); + final coinsRepository = RepositoryProvider.of(context); + final walletCoins = await coinsRepository.getWalletCoins(); portfolioGrowthBloc.add( PortfolioGrowthLoadRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, fiatCoinId: 'USDT', updateFrequency: const Duration(minutes: 1), selectedPeriod: portfolioGrowthBloc.state.selectedPeriod, @@ -179,7 +196,7 @@ class _WalletMainState extends State profitLossBloc.add( ProfitLossPortfolioChartLoadRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, selectedPeriod: profitLossBloc.state.selectedPeriod, fiatCoinId: 'USDT', walletId: walletId, @@ -189,13 +206,13 @@ class _WalletMainState extends State assetOverviewBloc ..add( PortfolioAssetsOverviewLoadRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, walletId: walletId, ), ) ..add( PortfolioAssetsOverviewSubscriptionRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, walletId: walletId, updateFrequency: const Duration(minutes: 1), ), @@ -266,7 +283,7 @@ class _WalletMainState extends State onSuccess: (_) async { takerBloc.add(TakerReInit()); bridgeBloc.add(const BridgeReInit()); - await reInitTradingForms(); + await reInitTradingForms(context); _popupDispatcher?.close(); }, ), diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart index 92170cc0e4..5eda95e631 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart @@ -84,7 +84,7 @@ class WalletManageSection extends StatelessWidget { backgroundColor: themeData.colorScheme.surface, textStyle: TextStyle( color: themeData.textTheme.labelLarge?.color - ?.withOpacity(0.7), + ?.withValues(alpha: 0.7), fontSize: 12, fontWeight: FontWeight.w700, ), @@ -169,7 +169,7 @@ class WalletManageSection extends StatelessWidget { backgroundColor: themeData.colorScheme.onSurface, textStyle: TextStyle( color: themeData.textTheme.labelLarge?.color - ?.withOpacity(0.7), + ?.withValues(alpha: 0.7), fontSize: 12, fontWeight: FontWeight.w700, ), diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart index 19c64ebe76..505f7396a9 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -3,10 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/assets_overview/bloc/asset_overview_bloc.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin.dart'; class WalletOverview extends StatelessWidget { const WalletOverview({ @@ -20,17 +19,12 @@ class WalletOverview extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder>( - initialData: coinsBloc.walletCoinsMap.values.toList(), - stream: coinsBloc.outWalletCoins, - builder: (context, snapshot) { - final List? coins = snapshot.data; - if (!snapshot.hasData || coins == null) return _buildSpinner(); + return BlocBuilder( + builder: (context, state) { + if (state.coins.isEmpty) return _buildSpinner(); final portfolioAssetsOverviewBloc = context.watch(); - - final int assetCount = coins.length; - + final int assetCount = state.walletCoins.length; final stateWithData = portfolioAssetsOverviewBloc.state is PortfolioAssetsOverviewLoadSuccess ? portfolioAssetsOverviewBloc.state diff --git a/lib/views/wallet/wallet_page/wallet_page.dart b/lib/views/wallet/wallet_page/wallet_page.dart index 384f5920ed..e0505a96aa 100644 --- a/lib/views/wallet/wallet_page/wallet_page.dart +++ b/lib/views/wallet/wallet_page/wallet_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/router/state/wallet_state.dart'; @@ -17,25 +18,26 @@ class WalletPage extends StatelessWidget { @override Widget build(BuildContext context) { - final Coin? coin = coinsBloc.getWalletCoin(coinAbbr ?? ''); - if (coin != null && coin.enabledType != null) { - return CoinDetails( - key: Key(coin.abbr), - coin: coin, - onBackButtonPressed: _onBackButtonPressed, - ); - } + return BlocBuilder(builder: (context, state) { + final Coin? coin = state.walletCoins[coinAbbr ?? '']; + if (coin != null && coin.enabledType != null) { + return CoinDetails( + key: Key(coin.abbr), + coin: coin, + onBackButtonPressed: _onBackButtonPressed, + ); + } - final action = this.action; + final action = this.action; + if (action != CoinsManagerAction.none) { + return CoinsManagerPage( + action: action, + closePage: _onBackButtonPressed, + ); + } - if (action != CoinsManagerAction.none) { - return CoinsManagerPage( - action: action, - closePage: _onBackButtonPressed, - ); - } - - return const WalletMain(); + return const WalletMain(); + }); } void _onBackButtonPressed() { diff --git a/lib/views/wallets_manager/wallets_manager_wrapper.dart b/lib/views/wallets_manager/wallets_manager_wrapper.dart index 3e14a4c1a2..03dd833f98 100644 --- a/lib/views/wallets_manager/wallets_manager_wrapper.dart +++ b/lib/views/wallets_manager/wallets_manager_wrapper.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; @@ -11,8 +10,8 @@ class WalletsManagerWrapper extends StatefulWidget { const WalletsManagerWrapper({ required this.eventType, this.onSuccess, - Key? key = const Key('wallets-manager-wrapper'), - }) : super(key: key); + super.key = const Key('wallets-manager-wrapper'), + }); final Function(Wallet)? onSuccess; final WalletsManagerEventType eventType; @@ -25,7 +24,6 @@ class _WalletsManagerWrapperState extends State { WalletType? _selectedWalletType; @override void initState() { - walletsBloc.fetchSavedWallets(); super.initState(); } diff --git a/lib/views/wallets_manager/widgets/hardware_wallets_manager.dart b/lib/views/wallets_manager/widgets/hardware_wallets_manager.dart index e891130fe5..f6ea1623c8 100644 --- a/lib/views/wallets_manager/widgets/hardware_wallets_manager.dart +++ b/lib/views/wallets_manager/widgets/hardware_wallets_manager.dart @@ -1,17 +1,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/analytics/analytics_event.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_repository.dart'; -import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_bloc.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_event.dart'; -import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/hw_wallet/init_trezor.dart'; import 'package:web_dex/model/hw_wallet/trezor_status.dart'; +import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/text_error.dart'; +import 'package:web_dex/router/state/routing_state.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/hw_dialog_init.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_in_progress.dart'; @@ -30,13 +32,9 @@ class HardwareWalletsManager extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - TrezorInitBloc(authRepo: authRepo, trezorRepo: trezorRepo), - child: HardwareWalletsManagerView( - close: close, - eventType: eventType, - ), + return HardwareWalletsManagerView( + close: close, + eventType: eventType, ); } } @@ -69,9 +67,7 @@ class _HardwareWalletsManagerViewState listener: (context, state) { final status = state.status; if (status?.trezorStatus == InitTrezorStatus.ok) { - context.read().add(AnalyticsSendDataEvent( - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.hardware))); + _successfulTrezorLogin(context, state.kdfUser!); } }, child: BlocSelector( @@ -89,6 +85,22 @@ class _HardwareWalletsManagerViewState ); } + void _successfulTrezorLogin(BuildContext context, KdfUser kdfUser) { + context.read().add( + AuthModeChanged(mode: AuthorizeMode.logIn, currentUser: kdfUser), + ); + context.read().add(CoinsSessionStarted(kdfUser)); + context.read().add( + AnalyticsSendDataEvent( + walletsManagerEventsFactory.createEvent( + widget.eventType, WalletsManagerEventMethod.hardware), + ), + ); + + routingState.selectedMenu = MainMenuValue.wallet; + widget.close(); + } + Widget _buildContent() { return BlocSelector( selector: (state) { diff --git a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart index b08a04b37d..d62bb9341d 100644 --- a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart +++ b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart @@ -4,11 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/analytics/analytics_event.dart'; -import 'package:web_dex/bloc/analytics/analytics_repo.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_event.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_state.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/authorize_mode.dart'; @@ -24,14 +21,15 @@ import 'package:web_dex/views/wallets_manager/widgets/wallets_manager_controls.d class IguanaWalletsManager extends StatefulWidget { const IguanaWalletsManager({ - Key? key, required this.eventType, required this.close, required this.onSuccess, - }) : super(key: key); + super.key, + }); + final WalletsManagerEventType eventType; final VoidCallback close; - final Function(Wallet) onSuccess; + final void Function(Wallet) onSuccess; @override State createState() => _IguanaWalletsManagerState(); @@ -63,8 +61,10 @@ class _IguanaWalletsManagerState extends State { children: [ WalletsList( walletType: WalletType.iguana, - onWalletClick: (Wallet wallet, - WalletsManagerExistWalletAction existWalletAction) { + onWalletClick: ( + Wallet wallet, + WalletsManagerExistWalletAction existWalletAction, + ) { setState(() { _selectedWallet = wallet; _existWalletAction = existWalletAction; @@ -73,11 +73,13 @@ class _IguanaWalletsManagerState extends State { ), Padding( padding: const EdgeInsets.only(top: 15.0), - child: WalletsManagerControls(onTap: (newAction) { - setState(() { - _action = newAction; - }); - }), + child: WalletsManagerControls( + onTap: (newAction) { + setState(() { + _action = newAction; + }); + }, + ), ), Padding( padding: const EdgeInsets.only(top: 12), @@ -85,7 +87,7 @@ class _IguanaWalletsManagerState extends State { text: LocaleKeys.cancel.tr(), onPressed: widget.close, ), - ) + ), ], ), ); @@ -185,68 +187,43 @@ class _IguanaWalletsManagerState extends State { }); } - Future _createWallet({ + void _createWallet({ required String name, required String password, required String seed, - }) async { + }) { setState(() { _isLoading = true; }); - final Wallet? newWallet = await walletsBloc.createNewWallet( - name: name, - password: password, - seed: seed, - ); - - if (newWallet == null) { - setState(() { - _errorText = - LocaleKeys.walletsManagerStepBuilderCreationWalletError.tr(); - }); + final Wallet newWallet = Wallet.fromName(name: name); - return; - } - - await _reLogin( - seed, - newWallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.create), - password, - ); + context.read().add( + AuthRestoreRequested( + wallet: newWallet, + password: password, + seed: seed, + ), + ); } - Future _importWallet({ + void _importWallet({ required String name, required String password, required WalletConfig walletConfig, - }) async { + }) { setState(() { _isLoading = true; }); - final Wallet? newWallet = await walletsBloc.importWallet( - name: name, - password: password, - walletConfig: walletConfig, - ); + final Wallet newWallet = + Wallet.fromConfig(name: name, config: walletConfig); - if (newWallet == null) { - setState(() { - _errorText = - LocaleKeys.walletsManagerStepBuilderCreationWalletError.tr(); - }); - - return; - } - - await _reLogin( - walletConfig.seedPhrase, - newWallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.import), - password, - ); + context.read().add( + AuthRestoreRequested( + wallet: newWallet, + password: password, + seed: walletConfig.seedPhrase, + ), + ); } Future _logInToWallet(String password, Wallet wallet) async { @@ -255,30 +232,16 @@ class _IguanaWalletsManagerState extends State { _errorText = null; }); - final String seed = await wallet.getSeed(password); - if (seed.isEmpty) { - setState(() { - _isLoading = false; - _errorText = LocaleKeys.invalidPasswordError.tr(); - }); - - return; - } - await _reLogin( - seed, - wallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.loginExisting), - password, + final AnalyticsBloc analyticsBloc = context.read(); + final analyticsEvent = walletsManagerEventsFactory.createEvent( + widget.eventType, + WalletsManagerEventMethod.loginExisting, ); - } + analyticsBloc.add(AnalyticsSendDataEvent(analyticsEvent)); - void _onLogIn() { - final wallet = currentWalletBloc.wallet; - _action = WalletsManagerAction.none; - if (wallet != null) { - widget.onSuccess(wallet); - } + context + .read() + .add(AuthSignInRequested(wallet: wallet, password: password)); if (mounted) { setState(() { @@ -287,22 +250,15 @@ class _IguanaWalletsManagerState extends State { } } - Future _reLogin( - String seed, - Wallet wallet, - AnalyticsEventData analyticsEventData, - String password, - ) async { - final AnalyticsBloc analyticsBloc = context.read(); - final AuthBloc authBloc = context.read(); - if (await authBloc.isLoginAllowed(wallet)) { - analyticsBloc.add(AnalyticsSendDataEvent(analyticsEventData)); - authBloc.add(AuthReLogInEvent( - seed: seed, - wallet: wallet, - password: password, - )); + void _onLogIn() { + final currentUser = context.read().state.currentUser; + final currentWallet = currentUser?.wallet; + _action = WalletsManagerAction.none; + if (currentUser != null && currentWallet != null) { + context.read().add(CoinsSessionStarted(currentUser)); + widget.onSuccess(currentWallet); } + if (mounted) { setState(() { _isLoading = false; diff --git a/lib/views/wallets_manager/widgets/wallet_creation.dart b/lib/views/wallets_manager/widgets/wallet_creation.dart index aa9173fb45..da192a5b02 100644 --- a/lib/views/wallets_manager/widgets/wallet_creation.dart +++ b/lib/views/wallets_manager/widgets/wallet_creation.dart @@ -1,7 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallets_manager_models.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; @@ -114,6 +115,7 @@ class _WalletCreationState extends State { } Widget _buildNameField() { + final walletsRepository = RepositoryProvider.of(context); return UiTextFormField( key: const Key('name-wallet-field'), controller: _nameController, @@ -122,7 +124,7 @@ class _WalletCreationState extends State { textInputAction: TextInputAction.next, enableInteractiveSelection: true, validator: (String? name) => - _inProgress ? null : walletsBloc.validateWalletName(name ?? ''), + _inProgress ? null : walletsRepository.validateWalletName(name ?? ''), inputFormatters: [LengthLimitingTextInputFormatter(40)], hintText: LocaleKeys.walletCreationNameHint.tr(), ); diff --git a/lib/views/wallets_manager/widgets/wallet_deleting.dart b/lib/views/wallets_manager/widgets/wallet_deleting.dart index 74b643a9e2..e3051ee0cf 100644 --- a/lib/views/wallets_manager/widgets/wallet_deleting.dart +++ b/lib/views/wallets_manager/widgets/wallet_deleting.dart @@ -1,10 +1,9 @@ import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class WalletDeleting extends StatefulWidget { const WalletDeleting({ @@ -20,7 +19,7 @@ class WalletDeleting extends StatefulWidget { } class _WalletDeletingState extends State { - bool _isDeleting = false; + // final bool _isDeleting = false; @override Widget build(BuildContext context) { @@ -93,28 +92,31 @@ class _WalletDeletingState extends State { borderColor: theme.custom.specificButtonBorderColor, ), ), - const SizedBox(width: 8.0), - Flexible( - child: UiPrimaryButton( - text: LocaleKeys.delete.tr(), - onPressed: _isDeleting ? null : _deleteWallet, - prefix: _isDeleting ? const UiSpinner() : null, - height: 40, - width: 150, - ), - ) + // TODO!: uncomment once re-implemented + // const SizedBox(width: 8.0), + // Flexible( + // child: UiPrimaryButton( + // text: LocaleKeys.delete.tr(), + // onPressed: _isDeleting ? null : _deleteWallet, + // prefix: _isDeleting ? const UiSpinner() : null, + // height: 40, + // width: 150, + // ), + // ) ], ); } - Future _deleteWallet() async { - setState(() { - _isDeleting = true; - }); - await walletsBloc.deleteWallet(widget.wallet); - setState(() { - _isDeleting = false; - }); - widget.close(); - } + // TODO!: uncomment once re-implemented + // Future _deleteWallet() async { + // setState(() { + // _isDeleting = true; + // }); + // final walletsRepository = RepositoryProvider.of(context); + // await walletsRepository.deleteWallet(widget.wallet); + // setState(() { + // _isDeleting = false; + // }); + // widget.close(); + // } } diff --git a/lib/views/wallets_manager/widgets/wallet_import_by_file.dart b/lib/views/wallets_manager/widgets/wallet_import_by_file.dart index 8803ab1eec..af3d1f817e 100644 --- a/lib/views/wallets_manager/widgets/wallet_import_by_file.dart +++ b/lib/views/wallets_manager/widgets/wallet_import_by_file.dart @@ -3,12 +3,12 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/ui/ui_gradient_icon.dart'; - import 'package:web_dex/shared/utils/encryption_tool.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; @@ -170,10 +170,11 @@ class _WalletImportByFileState extends State { if (!_isValidData) return; walletConfig.seedPhrase = decryptedSeed; - final String name = widget.fileData.name.split('.').first; + // ignore: use_build_context_synchronously + final walletsBloc = RepositoryProvider.of(context); final bool isNameExisted = - walletsBloc.wallets.firstWhereOrNull((w) => w.name == name) != null; + walletsBloc.wallets!.firstWhereOrNull((w) => w.name == name) != null; if (isNameExisted) { setState(() { _commonError = LocaleKeys.walletCreationExistNameError.tr(); diff --git a/lib/views/wallets_manager/widgets/wallet_list_item.dart b/lib/views/wallets_manager/widgets/wallet_list_item.dart index e1f31bacee..94fa5cd0d7 100644 --- a/lib/views/wallets_manager/widgets/wallet_list_item.dart +++ b/lib/views/wallets_manager/widgets/wallet_list_item.dart @@ -38,10 +38,12 @@ class WalletListItem extends StatelessWidget { ), ), ), - IconButton( - onPressed: () => - onClick(wallet, WalletsManagerExistWalletAction.delete), - icon: const Icon(Icons.close)) + // TODO: enable delete for sdk wallets as well when supported + if (wallet.isLegacyWallet) + IconButton( + onPressed: () => + onClick(wallet, WalletsManagerExistWalletAction.delete), + icon: const Icon(Icons.close)), ], ), ); diff --git a/lib/views/wallets_manager/widgets/wallet_login.dart b/lib/views/wallets_manager/widgets/wallet_login.dart index dcb7f3d4f9..1b7497ca9c 100644 --- a/lib/views/wallets_manager/widgets/wallet_login.dart +++ b/lib/views/wallets_manager/widgets/wallet_login.dart @@ -1,12 +1,9 @@ -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; - -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; class WalletLogIn extends StatefulWidget { @@ -41,10 +38,6 @@ class _WalletLogInState extends State { } void _submitLogin() async { - final Wallet? wallet = - walletsBloc.wallets.firstWhereOrNull((w) => w.id == widget.wallet.id); - if (wallet == null) return; - setState(() { _errorDisplay = true; _inProgress = true; @@ -53,7 +46,7 @@ class _WalletLogInState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onLogin( _passwordController.text, - wallet, + widget.wallet, ); if (mounted) setState(() => _inProgress = false); diff --git a/lib/views/wallets_manager/widgets/wallet_simple_import.dart b/lib/views/wallets_manager/widgets/wallet_simple_import.dart index c3d39047f6..0aa846a3a2 100644 --- a/lib/views/wallets_manager/widgets/wallet_simple_import.dart +++ b/lib/views/wallets_manager/widgets/wallet_simple_import.dart @@ -2,9 +2,10 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/services/file_loader/file_loader.dart'; @@ -207,6 +208,7 @@ class _WalletImportWrapperState extends State { } Widget _buildNameField() { + final walletsRepository = RepositoryProvider.of(context); return UiTextFormField( key: const Key('name-wallet-field'), controller: _nameController, @@ -214,7 +216,7 @@ class _WalletImportWrapperState extends State { autocorrect: false, textInputAction: TextInputAction.next, validator: (String? name) => - _inProgress ? null : walletsBloc.validateWalletName(name ?? ''), + _inProgress ? null : walletsRepository.validateWalletName(name ?? ''), inputFormatters: [LengthLimitingTextInputFormatter(40)], hintText: LocaleKeys.walletCreationNameHint.tr(), ); diff --git a/lib/views/wallets_manager/widgets/wallets_list.dart b/lib/views/wallets_manager/widgets/wallets_list.dart index f8b2d6d0e7..f1266b785a 100644 --- a/lib/views/wallets_manager/widgets/wallets_list.dart +++ b/lib/views/wallets_manager/widgets/wallets_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/model/wallets_manager_models.dart'; @@ -14,9 +15,10 @@ class WalletsList extends StatelessWidget { final void Function(Wallet, WalletsManagerExistWalletAction) onWalletClick; @override Widget build(BuildContext context) { + final walletsRepository = RepositoryProvider.of(context); return StreamBuilder>( - initialData: walletsBloc.wallets, - stream: walletsBloc.outWallets, + initialData: walletsRepository.wallets, + stream: walletsRepository.getWallets().asStream(), builder: (BuildContext context, AsyncSnapshot> snapshot) { final List wallets = snapshot.data ?? []; final List filteredWallets = diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 34ca498fed..0352206d21 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,15 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 0FECB4522D11D6AF00F11CB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + CE34F2E95A5E72AAE9A202B0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BC509CF635E02824A671F07F /* GoogleService-Info.plist */; }; D60A10D52711A1B300EB58E3 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A10D42711A1B300EB58E3 /* CoreFoundation.framework */; platformFilter = maccatalyst; }; D60A10D72711A1D000EB58E3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A10D62711A1D000EB58E3 /* SystemConfiguration.framework */; platformFilter = maccatalyst; }; - D68B8E5A2710401800D6C7D1 /* mm2.m in Sources */ = {isa = PBXBuildFile; fileRef = D68B8E592710401800D6C7D1 /* mm2.m */; }; - D68B8E5C2710416100D6C7D1 /* libmm2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D68B8E5B2710416000D6C7D1 /* libmm2.a */; }; D6B034F02711A360007FC221 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D6B034EF2711A360007FC221 /* libz.tbd */; }; D6F2739D2710691C005CC4F3 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D6F2739C2710690C005CC4F3 /* libc++.tbd */; platformFilter = maccatalyst; }; D6F2739F27106934005CC4F3 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D6F2739E2710692B005CC4F3 /* libresolv.tbd */; platformFilter = maccatalyst; }; @@ -78,13 +77,10 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; AC3362C2E5FD3245C1DF46DE /* 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 = ""; }; + BC509CF635E02824A671F07F /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; CBED5C6C4A1CA4CE9B9F2193 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D60A10D42711A1B300EB58E3 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; D60A10D62711A1D000EB58E3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; - D68B8E572710401800D6C7D1 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - D68B8E582710401800D6C7D1 /* mm2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mm2.h; sourceTree = ""; }; - D68B8E592710401800D6C7D1 /* mm2.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = mm2.m; sourceTree = ""; }; - D68B8E5B2710416000D6C7D1 /* libmm2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libmm2.a; sourceTree = ""; }; D6B034EF2711A360007FC221 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D6F2739C2710690C005CC4F3 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; D6F2739E2710692B005CC4F3 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; @@ -101,7 +97,6 @@ D6B034F02711A360007FC221 /* libz.tbd in Frameworks */, D6F2739D2710691C005CC4F3 /* libc++.tbd in Frameworks */, D60A10D72711A1D000EB58E3 /* SystemConfiguration.framework in Frameworks */, - D68B8E5C2710416100D6C7D1 /* libmm2.a in Frameworks */, D60A10D52711A1B300EB58E3 /* CoreFoundation.framework in Frameworks */, F0C41ACB9674358D4A6C7838 /* Pods_Runner.framework in Frameworks */, D6F273A12710694D005CC4F3 /* libSystem.tbd in Frameworks */, @@ -131,6 +126,7 @@ 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 3931C7C5835EE32D50936E8A /* Pods */, + BC509CF635E02824A671F07F /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -167,15 +163,12 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( - D68B8E582710401800D6C7D1 /* mm2.h */, - D68B8E592710401800D6C7D1 /* mm2.m */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, - D68B8E572710401800D6C7D1 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; @@ -193,7 +186,6 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - D68B8E5B2710416000D6C7D1 /* libmm2.a */, D6B034EF2711A360007FC221 /* libz.tbd */, D60A10D62711A1D000EB58E3 /* SystemConfiguration.framework */, D60A10D42711A1B300EB58E3 /* CoreFoundation.framework */, @@ -282,6 +274,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + CE34F2E95A5E72AAE9A202B0 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -324,7 +317,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\n"; }; A4BFE8B57517ABC4F933089B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -409,9 +402,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D68B8E5A2710401800D6C7D1 /* mm2.m in Sources */, 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 0FECB4522D11D6AF00F11CB7 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -493,11 +485,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -512,7 +504,6 @@ "$(PROJECT_DIR)", ); PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -632,10 +623,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = G3VBBBMD8T; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -651,7 +643,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.komodo.komodowallet; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -665,10 +657,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = G3VBBBMD8T; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter/ephemeral", @@ -684,7 +677,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.komodo.komodowallet; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index bc841d0522..b3c1761412 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,73 +1,13 @@ import Cocoa import FlutterMacOS -import os.log - -var mm2StartArgs: String? -var eventSink: FlutterEventSink? - -func mm2Callback(line: UnsafePointer?) { - if let lineStr = line, let sink = eventSink { - let logMessage = String(cString: lineStr) - sink(logMessage) - } -} - -@available(macOS 10.12, *) -func performMM2Start() -> Int32 { - eventSink?("START MM2 --------------------------------") - let error = Int32(mm2_main(mm2StartArgs, mm2Callback)) - eventSink?("START MM2 RESULT: \(error) ---------------") - - return error; -} -func performMM2Stop() -> Int32 { - eventSink?("STOP MM2 --------------------------------"); - let error = Int32(mm2_stop()); - eventSink?("STOP MM2 RESULT: \(error) ---------------"); - return error; -} - -@available(macOS 10.12, *) @main -class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { - - override func applicationDidFinishLaunching(_ notification: Notification) { - let controller : FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController - let channelMain = FlutterMethodChannel.init(name: "komodo-web-dex", binaryMessenger: controller.engine.binaryMessenger) - - let eventChannel = FlutterEventChannel(name: "komodo-web-dex/event", binaryMessenger: controller.engine.binaryMessenger) - eventChannel.setStreamHandler(self) - - channelMain.setMethodCallHandler({ - (_ call: FlutterMethodCall, _ result: FlutterResult) -> Void in - if ("start" == call.method) { - guard let arg = (call.arguments as! Dictionary)["params"] else { result(0); return } - mm2StartArgs = arg; - let error: Int32 = performMM2Start(); - - result(error) - } else if ("status" == call.method) { - let ret = Int32(mm2_main_status()); - result(ret) - } else if ("stop" == call.method) { - let error: Int32 = performMM2Stop() - result(error) - } - }); - } - - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - eventSink = nil - return nil - } +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 304a169252..d2f442ec99 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -12,5 +12,7 @@ com.apple.security.network.server + keychain-access-groups + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 3e75d9ad24..3cc05eb234 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -2,9 +2,11 @@ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index ee95ab7e58..225aa48bc8 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.client + keychain-access-groups + diff --git a/macos/Runner/Runner-Bridging-Header.h b/macos/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 88601cc4bc..0000000000 --- a/macos/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "mm2.h" \ No newline at end of file diff --git a/macos/Runner/mm2.h b/macos/Runner/mm2.h deleted file mode 100644 index a132efafa8..0000000000 --- a/macos/Runner/mm2.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef mm2_h -#define mm2_h - -#include - -char* writeable_dir (void); - -void start_mm2 (const char* mm2_conf); - -/// Checks if the MM2 singleton thread is currently running or not. -/// 0 .. not running. -/// 1 .. running, but no context yet. -/// 2 .. context, but no RPC yet. -/// 3 .. RPC is up. -int8_t mm2_main_status (void); - -/// Defined in "common/for_c.rs". -uint8_t is_loopback_ip (const char* ip); -/// Defined in "mm2_lib.rs". -int8_t mm2_main (const char* conf, void (*log_cb) (const char* line)); - -/// Defined in "mm2_lib.rs". -/// 0 .. MM2 has been stopped successfully. -/// 1 .. not running. -/// 2 .. error stopping an MM2 instance. -int8_t mm2_stop (void); - -void lsof (void); - -/// Measurement of application metrics: network traffic, CPU usage, etc. -const char* metrics (void); - -/// Corresponds to the `applicationDocumentsDirectory` used in Dart. -const char* documentDirectory (void); - -#endif /* mm2_h */ diff --git a/macos/Runner/mm2.m b/macos/Runner/mm2.m deleted file mode 100644 index d2469813e0..0000000000 --- a/macos/Runner/mm2.m +++ /dev/null @@ -1,235 +0,0 @@ -#include "mm2.h" - -#import -#import -#import -#import -#import -#import // os_log -#import // NSException - -#include -#include - -#include // task_info, mach_task_self - -#include // strcpy -#include -#include -#include - -// Note that the network interface traffic is not the same as the application traffic. -// Might still be useful with picking some trends in how the application is using the network, -// and for troubleshooting. -void network (NSMutableDictionary* ret) { - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/getifaddrs.3.html - struct ifaddrs *addrs = NULL; - int rc = getifaddrs (&addrs); - if (rc != 0) return; - - for (struct ifaddrs *addr = addrs; addr != NULL; addr = addr->ifa_next) { - if (addr->ifa_addr->sa_family != AF_LINK) continue; - - // Known aliases: “en0” is wi-fi, “pdp_ip0” is mobile. - // AG: “lo0” on my iPhone 5s seems to be measuring the Wi-Fi traffic. - const char* name = addr->ifa_name; - - struct if_data *stats = (struct if_data*) addr->ifa_data; - if (name == NULL || stats == NULL) continue; - if (stats->ifi_ipackets == 0 || stats->ifi_opackets == 0) continue; - - int8_t log = 0; - if (log == 1) os_log (OS_LOG_DEFAULT, - "network] if %{public}s ipackets %lld ibytes %lld opackets %lld obytes %lld", - name, - (int64_t) stats->ifi_ipackets, - (int64_t) stats->ifi_ibytes, - (int64_t) stats->ifi_opackets, - (int64_t) stats->ifi_obytes); - - NSDictionary* readings = @{ - @"ipackets": @((int64_t) stats->ifi_ipackets), - @"ibytes": @((int64_t) stats->ifi_ibytes), - @"opackets": @((int64_t) stats->ifi_opackets), - @"obytes": @((int64_t) stats->ifi_obytes)}; - NSString* key = [[NSString alloc] initWithUTF8String:name]; - [ret setObject:readings forKey:key];} - - freeifaddrs (addrs);} - -// Results in a `EXC_CRASH (SIGABRT)` crash log. -void throw_example (void) { - @throw [NSException exceptionWithName:@"exceptionName" reason:@"throw_example" userInfo:nil];} - -const char* documentDirectory (void) { - NSFileManager* sharedFM = [NSFileManager defaultManager]; - NSArray* urls = [sharedFM URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; - //for (NSURL* url in urls) os_log (OS_LOG_DEFAULT, "documentDirectory] supp dir: %{public}s\n", url.fileSystemRepresentation); - if (urls.count < 1) {os_log (OS_LOG_DEFAULT, "documentDirectory] Can't get a NSApplicationSupportDirectory"); return NULL;} - const char* wr_dir = urls[0].fileSystemRepresentation; - return wr_dir; -} - -// “in_use” stops at 256. -void file_example (void) { - const char* documents = documentDirectory(); - NSString* dir = [[NSString alloc] initWithUTF8String:documents]; - NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:NULL]; - static int32_t total = 0; - [files enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) { - NSString* filename = (NSString*) obj; - os_log (OS_LOG_DEFAULT, "file_example] filename: %{public}s", filename.UTF8String); - - NSString* path = [NSString stringWithFormat:@"%@/%@", dir, filename]; - int fd = open (path.UTF8String, O_RDWR); - if (fd > 0) ++total;}]; - - int32_t in_use = 0; - for (int fd = 0; fd < (int) FD_SETSIZE; ++fd) if (fcntl (fd, F_GETFD, 0) != -1) ++in_use; - - os_log (OS_LOG_DEFAULT, "file_example] leaked %d; in_use %d / %d", total, in_use, (int32_t) FD_SETSIZE);} - -// On iPhone 5s the app stopped at “phys_footprint 646 MiB; rs 19 MiB”. -// It didn't get to a memory allocation failure but was killed by Jetsam instead -// (“JetsamEvent-2020-04-03-175018.ips” was generated in the iTunes crash logs directory). -void leak_example (void) { - static int8_t* leaks[9999]; // Preserve the pointers for GC - static int32_t next_leak = 0; - int32_t size = 9 * 1024 * 1024; - os_log (OS_LOG_DEFAULT, "leak_example] Leaking %d MiB…", size / 1024 / 1024); - int8_t* leak = malloc (size); - if (leak == NULL) {os_log (OS_LOG_DEFAULT, "leak_example] Allocation failed"); return;} - leaks[next_leak++] = leak; - // Fill with random junk to workaround memory compression - for (int ix = 0; ix < size; ++ix) leak[ix] = (int8_t) rand(); - os_log (OS_LOG_DEFAULT, "leak_example] Leak %d, allocated %d MiB", next_leak, size / 1024 / 1024);} - -int32_t fds_simple (void) { - int32_t fds = 0; - for (int fd = 0; fd < (int) FD_SETSIZE; ++fd) if (fcntl (fd, F_GETFD, 0) != -1) ++fds; - return fds;} - -int32_t fds (void) { - // fds_simple is likely to generate a number of interrupts - // (FD_SETSIZE of 1024 would likely mean 1024 interrupts). - // We should actually check it: maybe it will help us with reproducing the high number of `wakeups`. - // But for production use we want to reduce the number of `fcntl` invocations. - - // We'll skip the first portion of file descriptors because most of the time we have them opened anyway. - int fd = 66; - int32_t fds = 66; - int32_t gap = 0; - - while (fd < (int) FD_SETSIZE && fd < 333) { - if (fcntl (fd, F_GETFD, 0) != -1) { // If file descriptor exists - gap = 0; - if (fd < 220) { - // We will count the files by ten, hoping that iOS traditionally fills the gaps. - fd += 10; - fds += 10; - } else { - // Unless we're close to the limit, where we want more precision. - ++fd; ++fds;} - continue;} - // Sample with increasing step while inside the gap. - int step = 1 + gap / 3; - fd += step; - gap += step;} - - return fds;} - -const char* metrics (void) { - //file_example(); - //leak_example(); - - mach_port_t self = mach_task_self(); - if (self == MACH_PORT_NULL || self == MACH_PORT_DEAD) return "{}"; - - // cf. https://forums.developer.apple.com/thread/105088#357415 - int32_t footprint = 0, rs = 0; - task_vm_info_data_t vmInfo; - mach_msg_type_number_t count = TASK_VM_INFO_COUNT; - kern_return_t rc = task_info (self, TASK_VM_INFO, (task_info_t) &vmInfo, &count); - if (rc == KERN_SUCCESS) { - footprint = (int32_t) vmInfo.phys_footprint / (1024 * 1024); - rs = (int32_t) vmInfo.resident_size / (1024 * 1024);} - - // iOS applications are in danger of being killed if the number of iterrupts is too high, - // so it might be interesting to maintain some statistics on the number of interrupts. - int64_t wakeups = 0; - task_power_info_data_t powInfo; - count = TASK_POWER_INFO_COUNT; - rc = task_info (self, TASK_POWER_INFO, (task_info_t) &powInfo, &count); - if (rc == KERN_SUCCESS) wakeups = (int64_t) powInfo.task_interrupt_wakeups; - - int32_t files = fds(); - - NSMutableDictionary* ret = [NSMutableDictionary new]; - - //os_log (OS_LOG_DEFAULT, - // "metrics] phys_footprint %d MiB; rs %d MiB; wakeups %lld; files %d", footprint, rs, wakeups, files); - ret[@"footprint"] = @(footprint); - ret[@"rs"] = @(rs); - ret[@"wakeups"] = @(wakeups); - ret[@"files"] = @(files); - - network (ret); - - NSError *err; - NSData *js = [NSJSONSerialization dataWithJSONObject:ret options:0 error: &err]; - if (js == NULL) {os_log (OS_LOG_DEFAULT, "metrics] !json: %@", err); return "{}";} - NSString *jss = [[NSString alloc] initWithData:js encoding:NSUTF8StringEncoding]; - const char *cs = [jss UTF8String]; - return cs;} - -void lsof (void) -{ - // AG: For now `os_log` allows me to see the information in the logs, - // but in the future we might want to return the information to Flutter - // in order to gather statistics on the use of file descriptors in the app, etc. - - int flags; - int fd; - char buf[MAXPATHLEN+1] ; - int n = 1 ; - - for (fd = 0; fd < (int) FD_SETSIZE; fd++) { - errno = 0; - flags = fcntl(fd, F_GETFD, 0); - if (flags == -1 && errno) { - if (errno != EBADF) { - return ; - } - else - continue; - } - if (fcntl(fd , F_GETPATH, buf ) >= 0) - { - printf("File Descriptor %d number %d in use for: %s\n", fd, n, buf); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d number %d in use for: %{public}s", fd, n, buf); - } - else - { - //[...] - - struct sockaddr_in addr; - socklen_t addr_size = sizeof(struct sockaddr); - int res = getpeername(fd, (struct sockaddr*)&addr, &addr_size); - if (res >= 0) - { - char clientip[20]; - strcpy(clientip, inet_ntoa(addr.sin_addr)); - uint16_t port = \ - (uint16_t)((((uint16_t)(addr.sin_port) & 0xff00) >> 8) | \ - (((uint16_t)(addr.sin_port) & 0x00ff) << 8)); - printf("File Descriptor %d, %s:%d \n", fd, clientip, port); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d, %{public}s:%d", fd, clientip, port); - } - else { - printf("File Descriptor %d number %d couldn't get path or socket\n", fd, n); - os_log (OS_LOG_DEFAULT, "lsof] File Descriptor %d number %d couldn't get path or socket", fd, n); - } - } - ++n ; - } -} diff --git a/packages/komodo_cex_market_data/pubspec.lock b/packages/komodo_cex_market_data/pubspec.lock index 631f00c1eb..a6901b0140 100644 --- a/packages/komodo_cex_market_data/pubspec.lock +++ b/packages/komodo_cex_market_data/pubspec.lock @@ -82,10 +82,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" file: dependency: transitive description: diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index 890a72ccd2..0dda81d60f 100644 --- a/packages/komodo_cex_market_data/pubspec.yaml +++ b/packages/komodo_cex_market_data/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: http: 1.2.2 # dart.dev - equatable: 2.0.5 + equatable: 2.0.7 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: diff --git a/packages/komodo_coin_updates/.gitignore b/packages/komodo_coin_updates/.gitignore deleted file mode 100644 index 3cceda5578..0000000000 --- a/packages/komodo_coin_updates/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ - -# Avoid committing pubspec.lock for library packages; see -# https://dart.dev/guides/libraries/private-files#pubspeclock. -pubspec.lock diff --git a/packages/komodo_coin_updates/CHANGELOG.md b/packages/komodo_coin_updates/CHANGELOG.md deleted file mode 100644 index b66cbecb5a..0000000000 --- a/packages/komodo_coin_updates/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Changelog - -## 0.0.1 - -- Initial version. diff --git a/packages/komodo_coin_updates/README.md b/packages/komodo_coin_updates/README.md deleted file mode 100644 index 49fc6bc602..0000000000 --- a/packages/komodo_coin_updates/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Komodo Coin Updater - -This package provides the funcionality to update the coins list and configuration files for the Komodo Platform at runtime. - -## Usage - -To use this package, you need to add `komodo_coin_updater` to your `pubspec.yaml` file. - -```yaml -dependencies: - komodo_coin_updater: ^1.0.0 -``` - -### Initialize the package - -Then you can use the `KomodoCoinUpdater` class to initialize the package. - -```dart -import 'package:komodo_coin_updater/komodo_coin_updater.dart'; - -void main() async { - await KomodoCoinUpdater.ensureInitialized("path/to/komodo/coin/files"); -} -``` - -### Provider - -The coins provider is responsible for fetching the coins list and configuration files from GitHub. - -```dart -import 'package:komodo_coin_updater/komodo_coin_updater.dart'; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await KomodoCoinUpdater.ensureInitialized("path/to/komodo/coin/files"); - - final provider = const CoinConfigProvider(); - final coins = await provider.getLatestCoins(); - final coinsConfigs = await provider.getLatestCoinConfigs(); -} -``` - -### Repository - -The repository is responsible for managing the coins list and configuration files, fetching from GitHub and persisting to storage. - -```dart -import 'package:komodo_coin_updater/komodo_coin_updater.dart'; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await KomodoCoinUpdater.ensureInitialized("path/to/komodo/coin/files"); - - final repository = CoinConfigRepository( - api: const CoinConfigProvider(), - storageProvider: CoinConfigStorageProvider.withDefaults(), - ); - - // Load the coin configuration if it is saved, otherwise update it - if(await repository.coinConfigExists()) { - if (await repository.isLatestCommit()) { - await repository.loadCoinConfigs(); - } else { - await repository.updateCoinConfig(); - } - } - else { - await repository.updateCoinConfig(); - } -} -``` diff --git a/packages/komodo_coin_updates/analysis_options.yaml b/packages/komodo_coin_updates/analysis_options.yaml deleted file mode 100644 index 0f9ee263df..0000000000 --- a/packages/komodo_coin_updates/analysis_options.yaml +++ /dev/null @@ -1,250 +0,0 @@ -# Specify analysis options. -# -# For a list of lints, see: https://dart.dev/lints -# For guidelines on configuring static analysis, see: -# https://dart.dev/guides/language/analysis-options -# -# There are other similar analysis options files in the flutter repos, -# which should be kept in sync with this file: -# -# - analysis_options.yaml (this file) -# - https://github.com/flutter/engine/blob/main/analysis_options.yaml -# - https://github.com/flutter/packages/blob/main/analysis_options.yaml -# -# This file contains the analysis options used for code in the flutter/flutter -# repository. - -analyzer: - language: - strict-casts: true - strict-inference: true - strict-raw-types: true - errors: - # allow self-reference to deprecated members (we do this because otherwise we have - # to annotate every member in every test, assert, etc, when we deprecate something) - deprecated_member_use_from_same_package: ignore - exclude: - - "bin/cache/**" - # Ignore protoc generated files - - "dev/conductor/lib/proto/*" - -linter: - rules: - # This list is derived from the list of all available lints located at - # https://github.com/dart-lang/linter/blob/main/example/all.yaml - - always_declare_return_types - - always_put_control_body_on_new_line - # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 - - always_specify_types - # - always_use_package_imports # we do this commonly - - annotate_overrides - # - avoid_annotating_with_dynamic # conflicts with always_specify_types - - avoid_bool_literals_in_conditional_expressions - # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 - # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 - # - avoid_classes_with_only_static_members # we do this commonly for `abstract final class`es - - avoid_double_and_int_checks - - avoid_dynamic_calls - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - # - avoid_final_parameters # incompatible with prefer_final_parameters - - avoid_function_literals_in_foreach_calls - # - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558 - - avoid_init_to_null - - avoid_js_rounded_ints - # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it - - avoid_print - # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null_for_void - # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_type_to_string - - avoid_types_as_parameter_names - # - avoid_types_on_closure_parameters # conflicts with always_specify_types - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere - - await_only_futures - - camel_case_extensions - - camel_case_types - - cancel_subscriptions - # - cascade_invocations # doesn't match the typical style of this repo - - cast_nullable_to_non_nullable - # - close_sinks # not reliable enough - - collection_methods_unrelated_type - - combinators_ordering - # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 - - conditional_uri_does_not_exist - # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - - control_flow_in_finally - - curly_braces_in_flow_control_structures - - dangling_library_doc_comments - - depend_on_referenced_packages - - deprecated_consistency - # - deprecated_member_use_from_same_package # we allow self-references to deprecated members - # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) - - directives_ordering - # - discarded_futures # too many false positives, similar to unawaited_futures - # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic - - empty_catches - - empty_constructor_bodies - - empty_statements - - eol_at_end_of_file - - exhaustive_cases - - file_names - - flutter_style_todos - - hash_and_equals - - implementation_imports - - implicit_call_tearoffs - - implicit_reopen - - invalid_case_patterns - # - join_return_with_assignment # not required by flutter style - - leading_newlines_in_multiline_strings - - library_annotations - - library_names - - library_prefixes - - library_private_types_in_public_api - # - lines_longer_than_80_chars # not required by flutter style - - literal_only_boolean_expressions - # - matching_super_parameters # blocked on https://github.com/dart-lang/language/issues/2509 - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_default_cases - - no_duplicate_case_values - - no_leading_underscores_for_library_prefixes - - no_leading_underscores_for_local_identifiers - - no_literal_bool_comparisons - - no_logic_in_create_state - # - no_runtimeType_toString # ok in tests; we enable this only in packages/ - - no_self_assignments - - no_wildcard_variable_uses - - non_constant_identifier_names - - noop_primitive_operations - - null_check_on_nullable_type_parameter - - null_closures - # - omit_local_variable_types # opposite of always_specify_types - # - one_member_abstracts # too many false positives - - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - # - parameter_assignments # we do this commonly - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - # - prefer_asserts_with_message # not required by flutter style - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - # - prefer_constructors_over_static_methods # far too many false positives - - prefer_contains - # - prefer_double_quotes # opposite of prefer_single_quotes - # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - # - prefer_final_parameters # adds too much verbosity - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin - # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere - - prefer_null_aware_operators - - prefer_relative_imports - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - - recursive_getters - - require_trailing_commas # would be nice, but requires a lot of manual work: 10,000+ code locations would need to be reformatted by hand after bulk fix is applied - - secure_pubspec_urls - - sized_box_for_whitespace - - sized_box_shrink_expand - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - # - sort_pub_dependencies # prevents separating pinned transitive dependencies - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - tighten_type_of_initializing_formals - # - type_annotate_public_apis # subset of always_specify_types - - type_init_formals - - type_literal_in_constant_pattern - # - unawaited_futures # too many false positives, especially with the way AnimationController works - - unnecessary_await_in_return - - unnecessary_brace_in_string_interps - - unnecessary_breaks - - unnecessary_const - - unnecessary_constructor_name - # - unnecessary_final # conflicts with prefer_final_locals - - unnecessary_getters_setters - # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 - - unnecessary_late - - unnecessary_library_directive - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_aware_operator_on_extension_on_nullable - - unnecessary_null_checks - - unnecessary_null_in_if_null_operators - - unnecessary_nullable_for_final_variable_declarations - - unnecessary_overrides - - unnecessary_parenthesis - # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unnecessary_to_list_in_spreads - - unreachable_from_main - - unrelated_type_equality_checks - - unsafe_html - - use_build_context_synchronously - - use_colored_box - # - use_decorated_box # leads to bugs: DecoratedBox and Container are not equivalent (Container inserts extra padding) - - use_enums - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_if_null_to_convert_nulls_to_bools - - use_is_even_rather_than_modulo - - use_key_in_widget_constructors - - use_late_for_private_fields_and_variables - - use_named_constants - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 - - use_string_in_part_of_directives - - use_super_parameters - - use_test_throws_matchers - # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - - valid_regexps - - void_checks \ No newline at end of file diff --git a/packages/komodo_coin_updates/example/komodo_coin_updates_example.dart b/packages/komodo_coin_updates/example/komodo_coin_updates_example.dart deleted file mode 100644 index 337c98cd8f..0000000000 --- a/packages/komodo_coin_updates/example/komodo_coin_updates_example.dart +++ /dev/null @@ -1,4 +0,0 @@ -void main() { - // TODO(Francois): implement this - throw UnimplementedError(); -} diff --git a/packages/komodo_coin_updates/lib/komodo_coin_updates.dart b/packages/komodo_coin_updates/lib/komodo_coin_updates.dart deleted file mode 100644 index 76f6f27d05..0000000000 --- a/packages/komodo_coin_updates/lib/komodo_coin_updates.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. -library; - -export 'src/data/data.dart'; -export 'src/komodo_coin_updater.dart'; -export 'src/models/models.dart'; diff --git a/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart b/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart deleted file mode 100644 index 8290cd54ed..0000000000 --- a/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; - -import '../models/models.dart'; - -/// A provider that fetches the coins and coin configs from the repository. -/// The repository is hosted on GitHub. -/// The repository contains a list of coins and a map of coin configs. -class CoinConfigProvider { - CoinConfigProvider({ - this.branch = 'master', - this.coinsGithubContentUrl = - 'https://raw.githubusercontent.com/KomodoPlatform/coins', - this.coinsGithubApiUrl = - 'https://api.github.com/repos/KomodoPlatform/coins', - this.coinsPath = 'coins', - this.coinsConfigPath = 'utils/coins_config_unfiltered.json', - }); - - factory CoinConfigProvider.fromConfig(RuntimeUpdateConfig config) { - // TODO(Francois): derive all the values from the config - return CoinConfigProvider( - branch: config.coinsRepoBranch, - ); - } - - final String branch; - final String coinsGithubContentUrl; - final String coinsGithubApiUrl; - final String coinsPath; - final String coinsConfigPath; - - /// Fetches the coins from the repository. - /// [commit] is the commit hash to fetch the coins from. - /// If [commit] is not provided, it will fetch the coins from the latest commit. - /// Returns a list of [Coin] objects. - /// Throws an [Exception] if the request fails. - Future> getCoins(String commit) async { - final Uri url = _contentUri(coinsPath, branchOrCommit: commit); - final http.Response response = await http.get(url); - final List items = jsonDecode(response.body) as List; - return items - .map((dynamic e) => Coin.fromJson(e as Map)) - .toList(); - } - - /// Fetches the coins from the repository. - /// Returns a list of [Coin] objects. - /// Throws an [Exception] if the request fails. - Future> getLatestCoins() async { - return getCoins(branch); - } - - /// Fetches the coin configs from the repository. - /// [commit] is the commit hash to fetch the coin configs from. - /// If [commit] is not provided, it will fetch the coin configs - /// from the latest commit. - /// Returns a map of [CoinConfig] objects. - /// Throws an [Exception] if the request fails. - /// The key of the map is the coin symbol. - Future> getCoinConfigs(String commit) async { - final Uri url = _contentUri(coinsConfigPath, branchOrCommit: commit); - final http.Response response = await http.get(url); - final Map items = - jsonDecode(response.body) as Map; - return { - for (final String key in items.keys) - key: CoinConfig.fromJson(items[key] as Map), - }; - } - - /// Fetches the latest coin configs from the repository. - /// Returns a map of [CoinConfig] objects. - /// Throws an [Exception] if the request fails. - Future> getLatestCoinConfigs() async { - return getCoinConfigs(branch); - } - - /// Fetches the latest commit hash from the repository. - /// Returns the latest commit hash. - /// Throws an [Exception] if the request fails. - Future getLatestCommit() async { - final http.Client client = http.Client(); - final Uri url = Uri.parse('$coinsGithubApiUrl/branches/$branch'); - final Map header = { - 'Accept': 'application/vnd.github+json', - }; - final http.Response response = await client.get(url, headers: header); - - final Map json = - jsonDecode(response.body) as Map; - final Map commit = json['commit'] as Map; - final String latestCommitHash = commit['sha'] as String; - return latestCommitHash; - } - - Uri _contentUri(String path, {String? branchOrCommit}) { - branchOrCommit ??= branch; - return Uri.parse('$coinsGithubContentUrl/$branch/$path'); - } -} diff --git a/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart b/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart deleted file mode 100644 index b62d6e2fc7..0000000000 --- a/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; - -import '../../komodo_coin_updates.dart'; -import '../models/coin_info.dart'; - -/// A repository that fetches the coins and coin configs from the provider and -/// stores them in the storage provider. -class CoinConfigRepository implements CoinConfigStorage { - /// Creates a coin config repository. - /// [coinConfigProvider] is the provider that fetches the coins and coin configs. - /// [coinsDatabase] is the database that stores the coins and their configs. - /// [coinSettingsDatabase] is the database that stores the coin settings - /// (i.e. current commit hash). - CoinConfigRepository({ - required this.coinConfigProvider, - required this.coinsDatabase, - required this.coinSettingsDatabase, - }); - - /// Creates a coin config storage provider with default databases. - /// The default databases are HiveLazyBoxProvider. - /// The default databases are named 'coins' and 'coins_settings'. - CoinConfigRepository.withDefaults(RuntimeUpdateConfig config) - : coinConfigProvider = CoinConfigProvider.fromConfig(config), - coinsDatabase = HiveLazyBoxProvider( - name: 'coins', - ), - coinSettingsDatabase = HiveBoxProvider( - name: 'coins_settings', - ); - - /// The provider that fetches the coins and coin configs. - final CoinConfigProvider coinConfigProvider; - - /// The database that stores the coins. The key is the coin id. - final PersistenceProvider coinsDatabase; - - /// The database that stores the coin settings. The key is the coin settings key. - final PersistenceProvider coinSettingsDatabase; - - /// The key for the coins commit. The value is the commit hash. - final String coinsCommitKey = 'coins_commit'; - - String? _latestCommit; - - /// Updates the coin configs from the provider and stores them in the storage provider. - /// Throws an [Exception] if the request fails. - Future updateCoinConfig({ - List excludedAssets = const [], - }) async { - final List coins = await coinConfigProvider.getLatestCoins(); - final Map coinConfig = - await coinConfigProvider.getLatestCoinConfigs(); - - await saveCoinData(coins, coinConfig, _latestCommit ?? ''); - } - - @override - Future isLatestCommit() async { - final String? commit = await getCurrentCommit(); - if (commit != null) { - _latestCommit = await coinConfigProvider.getLatestCommit(); - return commit == _latestCommit; - } - return false; - } - - @override - Future?> getCoins({ - List excludedAssets = const [], - }) async { - final List result = await coinsDatabase.getAll(); - return result - .where( - (CoinInfo? coin) => - coin != null && !excludedAssets.contains(coin.coin.coin), - ) - .map((CoinInfo? coin) => coin!.coin) - .toList(); - } - - @override - Future getCoin(String coinId) async { - return (await coinsDatabase.get(coinId))!.coin; - } - - @override - Future?> getCoinConfigs({ - List excludedAssets = const [], - }) async { - final List coinConfigs = (await coinsDatabase.getAll()) - .where((CoinInfo? e) => e != null && e.coinConfig != null) - .cast() - .map((CoinInfo e) => e.coinConfig) - .cast() - .toList(); - - return { - for (final CoinConfig coinConfig in coinConfigs) - coinConfig.primaryKey: coinConfig, - }; - } - - @override - Future getCoinConfig(String coinId) async { - return (await coinsDatabase.get(coinId))!.coinConfig; - } - - @override - Future getCurrentCommit() async { - return coinSettingsDatabase - .get(coinsCommitKey) - .then((PersistedString? persistedString) { - return persistedString?.value; - }); - } - - @override - Future saveCoinData( - List coins, - Map coinConfig, - String commit, - ) async { - final Map combinedCoins = {}; - for (final Coin coin in coins) { - combinedCoins[coin.coin] = CoinInfo( - coin: coin, - coinConfig: coinConfig[coin.coin], - ); - } - - await coinsDatabase.insertAll(combinedCoins.values.toList()); - await coinSettingsDatabase.insert(PersistedString(coinsCommitKey, commit)); - _latestCommit = _latestCommit ?? await coinConfigProvider.getLatestCommit(); - } - - @override - Future coinConfigExists() async { - return await coinsDatabase.exists() && await coinSettingsDatabase.exists(); - } - - @override - Future saveRawCoinData( - List coins, - Map coinConfig, - String commit, - ) async { - final Map combinedCoins = {}; - for (final dynamic coin in coins) { - // ignore: avoid_dynamic_calls - final String coinAbbr = coin['coin'] as String; - final CoinConfig? config = coinConfig[coinAbbr] != null - ? CoinConfig.fromJson(coinConfig[coinAbbr] as Map) - : null; - combinedCoins[coinAbbr] = CoinInfo( - coin: Coin.fromJson(coin as Map), - coinConfig: config, - ); - } - - await coinsDatabase.insertAll(combinedCoins.values.toList()); - await coinSettingsDatabase.insert(PersistedString(coinsCommitKey, commit)); - } -} diff --git a/packages/komodo_coin_updates/lib/src/data/coin_config_storage.dart b/packages/komodo_coin_updates/lib/src/data/coin_config_storage.dart deleted file mode 100644 index 3e71671a74..0000000000 --- a/packages/komodo_coin_updates/lib/src/data/coin_config_storage.dart +++ /dev/null @@ -1,67 +0,0 @@ -import '../models/coin.dart'; -import '../models/coin_config.dart'; - -/// A storage provider that fetches the coins and coin configs from the storage. -/// The storage provider is responsible for fetching the coins and coin configs -/// from the storage and saving the coins and coin configs to the storage. -abstract class CoinConfigStorage { - /// Fetches the coins from the storage provider. - /// Returns a list of [Coin] objects. - /// Throws an [Exception] if the request fails. - Future?> getCoins(); - - /// Fetches the specified coin from the storage provider. - /// [coinId] is the coin symbol. - /// Returns a [Coin] object. - /// Throws an [Exception] if the request fails. - Future getCoin(String coinId); - - /// Fetches the coin configs from the storage provider. - /// Returns a map of [CoinConfig] objects. - /// Throws an [Exception] if the request fails. - Future?> getCoinConfigs(); - - /// Fetches the specified coin config from the storage provider. - /// [coinId] is the coin symbol. - /// Returns a [CoinConfig] object. - /// Throws an [Exception] if the request fails. - Future getCoinConfig(String coinId); - - /// Checks if the latest commit is the same as the current commit. - /// Returns `true` if the latest commit is the same as the current commit, - /// otherwise `false`. - /// Throws an [Exception] if the request fails. - Future isLatestCommit(); - - /// Fetches the current commit hash. - /// Returns the commit hash as a [String]. - /// Throws an [Exception] if the request fails. - Future getCurrentCommit(); - - /// Checks if the coin configs are saved in the storage provider. - /// Returns `true` if the coin configs are saved, otherwise `false`. - /// Throws an [Exception] if the request fails. - Future coinConfigExists(); - - /// Saves the coin data to the storage provider. - /// [coins] is a list of [Coin] objects. - /// [coinConfig] is a map of [CoinConfig] objects. - /// [commit] is the commit hash. - /// Throws an [Exception] if the request fails. - Future saveCoinData( - List coins, - Map coinConfig, - String commit, - ); - - /// Saves the raw coin data to the storage provider. - /// [coins] is a list of [Coin] objects in raw JSON `dynamic` form. - /// [coinConfig] is a map of [CoinConfig] objects in raw JSON `dynamic` form. - /// [commit] is the commit hash. - /// Throws an [Exception] if the request fails. - Future saveRawCoinData( - List coins, - Map coinConfig, - String commit, - ); -} diff --git a/packages/komodo_coin_updates/lib/src/data/data.dart b/packages/komodo_coin_updates/lib/src/data/data.dart deleted file mode 100644 index aea56ef55b..0000000000 --- a/packages/komodo_coin_updates/lib/src/data/data.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'coin_config_provider.dart'; -export 'coin_config_repository.dart'; -export 'coin_config_storage.dart'; diff --git a/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart b/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart deleted file mode 100644 index ed4cd11261..0000000000 --- a/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:io'; - -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; - -import 'models/coin_info.dart'; -import 'models/models.dart'; - -class KomodoCoinUpdater { - /// Initialises Hive with the path to the app folder. This should be called - /// before any other operations with this package. If [isWeb] is true, then - /// [Hive.initFlutter] is called instead of [Hive.init]. - static Future ensureInitialized( - String appFolder, { - bool isWeb = false, - }) async { - if (isWeb) { - await Hive.initFlutter(appFolder); - } else { - final Directory appDir = await getApplicationDocumentsDirectory(); - final String path = p.join(appDir.path, appFolder); - Hive.init(path); - } - initializeAdapters(); - } - - static void ensureInitializedIsolate(String fullAppFolderPath) { - Hive.init(fullAppFolderPath); - initializeAdapters(); - } - - static void initializeAdapters() { - Hive.registerAdapter(AddressFormatAdapter()); - Hive.registerAdapter(CheckPointBlockAdapter()); - Hive.registerAdapter(CoinAdapter()); - Hive.registerAdapter(CoinConfigAdapter()); - Hive.registerAdapter(CoinInfoAdapter()); - Hive.registerAdapter(ConsensusParamsAdapter()); - Hive.registerAdapter(ContactAdapter()); - Hive.registerAdapter(ElectrumAdapter()); - Hive.registerAdapter(LinksAdapter()); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(PersistedStringAdapter()); - Hive.registerAdapter(ProtocolAdapter()); - Hive.registerAdapter(ProtocolDataAdapter()); - Hive.registerAdapter(RpcUrlAdapter()); - } -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/address_format_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/address_format_adapter.dart deleted file mode 100644 index 59b451cde0..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/address_format_adapter.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of '../address_format.dart'; - -class AddressFormatAdapter extends TypeAdapter { - @override - final int typeId = 3; - - @override - AddressFormat read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return AddressFormat( - format: fields[0] as String?, - network: fields[1] as String?, - ); - } - - @override - void write(BinaryWriter writer, AddressFormat obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.format) - ..writeByte(1) - ..write(obj.network); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AddressFormatAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/checkpoint_block_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/checkpoint_block_adapter.dart deleted file mode 100644 index 923c3409c8..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/checkpoint_block_adapter.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of '../checkpoint_block.dart'; - -class CheckPointBlockAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - CheckPointBlock read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return CheckPointBlock( - height: fields[0] as num?, - time: fields[1] as num?, - hash: fields[2] as String?, - saplingTree: fields[3] as String?, - ); - } - - @override - void write(BinaryWriter writer, CheckPointBlock obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.height) - ..writeByte(1) - ..write(obj.time) - ..writeByte(2) - ..write(obj.hash) - ..writeByte(3) - ..write(obj.saplingTree); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CheckPointBlockAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/coin_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/coin_adapter.dart deleted file mode 100644 index 6099fb802c..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/coin_adapter.dart +++ /dev/null @@ -1,167 +0,0 @@ -part of '../coin.dart'; - -class CoinAdapter extends TypeAdapter { - @override - final int typeId = 0; - - @override - Coin read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Coin( - coin: fields[0] as String, - name: fields[1] as String?, - fname: fields[2] as String?, - rpcport: fields[3] as num?, - mm2: fields[4] as num?, - chainId: fields[5] as num?, - requiredConfirmations: fields[6] as num?, - avgBlocktime: fields[7] as num?, - decimals: fields[8] as num?, - protocol: fields[9] as Protocol?, - derivationPath: fields[10] as String?, - trezorCoin: fields[11] as String?, - links: fields[12] as Links?, - isPoS: fields[13] as num?, - pubtype: fields[14] as num?, - p2shtype: fields[15] as num?, - wiftype: fields[16] as num?, - txfee: fields[17] as num?, - dust: fields[18] as num?, - matureConfirmations: fields[19] as num?, - segwit: fields[20] as bool?, - signMessagePrefix: fields[21] as String?, - asset: fields[22] as String?, - txversion: fields[23] as num?, - overwintered: fields[24] as num?, - requiresNotarization: fields[25] as bool?, - walletOnly: fields[26] as bool?, - bech32Hrp: fields[27] as String?, - isTestnet: fields[28] as bool?, - forkId: fields[29] as String?, - signatureVersion: fields[30] as String?, - confpath: fields[31] as String?, - addressFormat: fields[32] as AddressFormat?, - aliasTicker: fields[33] as String?, - estimateFeeMode: fields[34] as String?, - orderbookTicker: fields[35] as String?, - taddr: fields[36] as num?, - forceMinRelayFee: fields[37] as bool?, - p2p: fields[38] as num?, - magic: fields[39] as String?, - nSPV: fields[40] as String?, - isPoSV: fields[41] as num?, - versionGroupId: fields[42] as String?, - consensusBranchId: fields[43] as String?, - estimateFeeBlocks: fields[44] as num?, - ); - } - - @override - void write(BinaryWriter writer, Coin obj) { - writer - ..writeByte(45) - ..writeByte(0) - ..write(obj.coin) - ..writeByte(1) - ..write(obj.name) - ..writeByte(2) - ..write(obj.fname) - ..writeByte(3) - ..write(obj.rpcport) - ..writeByte(4) - ..write(obj.mm2) - ..writeByte(5) - ..write(obj.chainId) - ..writeByte(6) - ..write(obj.requiredConfirmations) - ..writeByte(7) - ..write(obj.avgBlocktime) - ..writeByte(8) - ..write(obj.decimals) - ..writeByte(9) - ..write(obj.protocol) - ..writeByte(10) - ..write(obj.derivationPath) - ..writeByte(11) - ..write(obj.trezorCoin) - ..writeByte(12) - ..write(obj.links) - ..writeByte(13) - ..write(obj.isPoS) - ..writeByte(14) - ..write(obj.pubtype) - ..writeByte(15) - ..write(obj.p2shtype) - ..writeByte(16) - ..write(obj.wiftype) - ..writeByte(17) - ..write(obj.txfee) - ..writeByte(18) - ..write(obj.dust) - ..writeByte(19) - ..write(obj.matureConfirmations) - ..writeByte(20) - ..write(obj.segwit) - ..writeByte(21) - ..write(obj.signMessagePrefix) - ..writeByte(22) - ..write(obj.asset) - ..writeByte(23) - ..write(obj.txversion) - ..writeByte(24) - ..write(obj.overwintered) - ..writeByte(25) - ..write(obj.requiresNotarization) - ..writeByte(26) - ..write(obj.walletOnly) - ..writeByte(27) - ..write(obj.bech32Hrp) - ..writeByte(28) - ..write(obj.isTestnet) - ..writeByte(29) - ..write(obj.forkId) - ..writeByte(30) - ..write(obj.signatureVersion) - ..writeByte(31) - ..write(obj.confpath) - ..writeByte(32) - ..write(obj.addressFormat) - ..writeByte(33) - ..write(obj.aliasTicker) - ..writeByte(34) - ..write(obj.estimateFeeMode) - ..writeByte(35) - ..write(obj.orderbookTicker) - ..writeByte(36) - ..write(obj.taddr) - ..writeByte(37) - ..write(obj.forceMinRelayFee) - ..writeByte(38) - ..write(obj.p2p) - ..writeByte(39) - ..write(obj.magic) - ..writeByte(40) - ..write(obj.nSPV) - ..writeByte(41) - ..write(obj.isPoSV) - ..writeByte(42) - ..write(obj.versionGroupId) - ..writeByte(43) - ..write(obj.consensusBranchId) - ..writeByte(44) - ..write(obj.estimateFeeBlocks); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CoinAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/coin_config_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/coin_config_adapter.dart deleted file mode 100644 index 3138e02028..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/coin_config_adapter.dart +++ /dev/null @@ -1,248 +0,0 @@ -part of '../coin_config.dart'; - -class CoinConfigAdapter extends TypeAdapter { - @override - final int typeId = 7; - - @override - CoinConfig read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return CoinConfig( - coin: fields[0] as String, - type: fields[1] as String?, - name: fields[2] as String?, - coingeckoId: fields[3] as String?, - livecoinwatchId: fields[4] as String?, - explorerUrl: fields[5] as String?, - explorerTxUrl: fields[6] as String?, - explorerAddressUrl: fields[7] as String?, - supported: (fields[8] as List?)?.cast(), - active: fields[9] as bool?, - isTestnet: fields[10] as bool?, - currentlyEnabled: fields[11] as bool?, - walletOnly: fields[12] as bool?, - fname: fields[13] as String?, - rpcport: fields[14] as num?, - mm2: fields[15] as num?, - chainId: fields[16] as num?, - requiredConfirmations: fields[17] as num?, - avgBlocktime: fields[18] as num?, - decimals: fields[19] as num?, - protocol: fields[20] as Protocol?, - derivationPath: fields[21] as String?, - contractAddress: fields[22] as String?, - parentCoin: fields[23] as String?, - swapContractAddress: fields[24] as String?, - fallbackSwapContract: fields[25] as String?, - nodes: (fields[26] as List?)?.cast(), - explorerBlockUrl: fields[27] as String?, - tokenAddressUrl: fields[28] as String?, - trezorCoin: fields[29] as String?, - links: fields[30] as Links?, - pubtype: fields[31] as num?, - p2shtype: fields[32] as num?, - wiftype: fields[33] as num?, - txfee: fields[34] as num?, - dust: fields[35] as num?, - segwit: fields[36] as bool?, - electrum: (fields[37] as List?)?.cast(), - signMessagePrefix: fields[38] as String?, - lightWalletDServers: (fields[39] as List?)?.cast(), - asset: fields[40] as String?, - txversion: fields[41] as num?, - overwintered: fields[42] as num?, - requiresNotarization: fields[43] as bool?, - checkpointHeight: fields[44] as num?, - checkpointBlocktime: fields[45] as num?, - binanceId: fields[46] as String?, - bech32Hrp: fields[47] as String?, - forkId: fields[48] as String?, - signatureVersion: fields[49] as String?, - confpath: fields[50] as String?, - matureConfirmations: fields[51] as num?, - bchdUrls: (fields[52] as List?)?.cast(), - otherTypes: (fields[53] as List?)?.cast(), - addressFormat: fields[54] as AddressFormat?, - allowSlpUnsafeConf: fields[55] as bool?, - slpPrefix: fields[56] as String?, - tokenId: fields[57] as String?, - forexId: fields[58] as String?, - isPoS: fields[59] as num?, - aliasTicker: fields[60] as String?, - estimateFeeMode: fields[61] as String?, - orderbookTicker: fields[62] as String?, - taddr: fields[63] as num?, - forceMinRelayFee: fields[64] as bool?, - isClaimable: fields[65] as bool?, - minimalClaimAmount: fields[66] as String?, - isPoSV: fields[67] as num?, - versionGroupId: fields[68] as String?, - consensusBranchId: fields[69] as String?, - estimateFeeBlocks: fields[70] as num?, - rpcUrls: (fields[71] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, CoinConfig obj) { - writer - ..writeByte(72) - ..writeByte(0) - ..write(obj.coin) - ..writeByte(1) - ..write(obj.type) - ..writeByte(2) - ..write(obj.name) - ..writeByte(3) - ..write(obj.coingeckoId) - ..writeByte(4) - ..write(obj.livecoinwatchId) - ..writeByte(5) - ..write(obj.explorerUrl) - ..writeByte(6) - ..write(obj.explorerTxUrl) - ..writeByte(7) - ..write(obj.explorerAddressUrl) - ..writeByte(8) - ..write(obj.supported) - ..writeByte(9) - ..write(obj.active) - ..writeByte(10) - ..write(obj.isTestnet) - ..writeByte(11) - ..write(obj.currentlyEnabled) - ..writeByte(12) - ..write(obj.walletOnly) - ..writeByte(13) - ..write(obj.fname) - ..writeByte(14) - ..write(obj.rpcport) - ..writeByte(15) - ..write(obj.mm2) - ..writeByte(16) - ..write(obj.chainId) - ..writeByte(17) - ..write(obj.requiredConfirmations) - ..writeByte(18) - ..write(obj.avgBlocktime) - ..writeByte(19) - ..write(obj.decimals) - ..writeByte(20) - ..write(obj.protocol) - ..writeByte(21) - ..write(obj.derivationPath) - ..writeByte(22) - ..write(obj.contractAddress) - ..writeByte(23) - ..write(obj.parentCoin) - ..writeByte(24) - ..write(obj.swapContractAddress) - ..writeByte(25) - ..write(obj.fallbackSwapContract) - ..writeByte(26) - ..write(obj.nodes) - ..writeByte(27) - ..write(obj.explorerBlockUrl) - ..writeByte(28) - ..write(obj.tokenAddressUrl) - ..writeByte(29) - ..write(obj.trezorCoin) - ..writeByte(30) - ..write(obj.links) - ..writeByte(31) - ..write(obj.pubtype) - ..writeByte(32) - ..write(obj.p2shtype) - ..writeByte(33) - ..write(obj.wiftype) - ..writeByte(34) - ..write(obj.txfee) - ..writeByte(35) - ..write(obj.dust) - ..writeByte(36) - ..write(obj.segwit) - ..writeByte(37) - ..write(obj.electrum) - ..writeByte(38) - ..write(obj.signMessagePrefix) - ..writeByte(39) - ..write(obj.lightWalletDServers) - ..writeByte(40) - ..write(obj.asset) - ..writeByte(41) - ..write(obj.txversion) - ..writeByte(42) - ..write(obj.overwintered) - ..writeByte(43) - ..write(obj.requiresNotarization) - ..writeByte(44) - ..write(obj.checkpointHeight) - ..writeByte(45) - ..write(obj.checkpointBlocktime) - ..writeByte(46) - ..write(obj.binanceId) - ..writeByte(47) - ..write(obj.bech32Hrp) - ..writeByte(48) - ..write(obj.forkId) - ..writeByte(49) - ..write(obj.signatureVersion) - ..writeByte(50) - ..write(obj.confpath) - ..writeByte(51) - ..write(obj.matureConfirmations) - ..writeByte(52) - ..write(obj.bchdUrls) - ..writeByte(53) - ..write(obj.otherTypes) - ..writeByte(54) - ..write(obj.addressFormat) - ..writeByte(55) - ..write(obj.allowSlpUnsafeConf) - ..writeByte(56) - ..write(obj.slpPrefix) - ..writeByte(57) - ..write(obj.tokenId) - ..writeByte(58) - ..write(obj.forexId) - ..writeByte(59) - ..write(obj.isPoS) - ..writeByte(60) - ..write(obj.aliasTicker) - ..writeByte(61) - ..write(obj.estimateFeeMode) - ..writeByte(62) - ..write(obj.orderbookTicker) - ..writeByte(63) - ..write(obj.taddr) - ..writeByte(64) - ..write(obj.forceMinRelayFee) - ..writeByte(65) - ..write(obj.isClaimable) - ..writeByte(66) - ..write(obj.minimalClaimAmount) - ..writeByte(67) - ..write(obj.isPoSV) - ..writeByte(68) - ..write(obj.versionGroupId) - ..writeByte(69) - ..write(obj.consensusBranchId) - ..writeByte(70) - ..write(obj.estimateFeeBlocks) - ..writeByte(71) - ..write(obj.rpcUrls); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CoinConfigAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/coin_info_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/coin_info_adapter.dart deleted file mode 100644 index a661914d4a..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/coin_info_adapter.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of '../coin_info.dart'; - -class CoinInfoAdapter extends TypeAdapter { - @override - final int typeId = 13; - - @override - CoinInfo read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return CoinInfo( - coin: fields[0] as Coin, - coinConfig: fields[1] as CoinConfig?, - ); - } - - @override - void write(BinaryWriter writer, CoinInfo obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.coin) - ..writeByte(1) - ..write(obj.coinConfig); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NodeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/consensus_params_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/consensus_params_adapter.dart deleted file mode 100644 index ea3714aa3e..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/consensus_params_adapter.dart +++ /dev/null @@ -1,65 +0,0 @@ -part of '../consensus_params.dart'; - -class ConsensusParamsAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - ConsensusParams read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ConsensusParams( - overwinterActivationHeight: fields[0] as num?, - saplingActivationHeight: fields[1] as num?, - blossomActivationHeight: fields[2] as num?, - heartwoodActivationHeight: fields[3] as num?, - canopyActivationHeight: fields[4] as num?, - coinType: fields[5] as num?, - hrpSaplingExtendedSpendingKey: fields[6] as String?, - hrpSaplingExtendedFullViewingKey: fields[7] as String?, - hrpSaplingPaymentAddress: fields[8] as String?, - b58PubkeyAddressPrefix: (fields[9] as List?)?.cast(), - b58ScriptAddressPrefix: (fields[10] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, ConsensusParams obj) { - writer - ..writeByte(11) - ..writeByte(0) - ..write(obj.overwinterActivationHeight) - ..writeByte(1) - ..write(obj.saplingActivationHeight) - ..writeByte(2) - ..write(obj.blossomActivationHeight) - ..writeByte(3) - ..write(obj.heartwoodActivationHeight) - ..writeByte(4) - ..write(obj.canopyActivationHeight) - ..writeByte(5) - ..write(obj.coinType) - ..writeByte(6) - ..write(obj.hrpSaplingExtendedSpendingKey) - ..writeByte(7) - ..write(obj.hrpSaplingExtendedFullViewingKey) - ..writeByte(8) - ..write(obj.hrpSaplingPaymentAddress) - ..writeByte(9) - ..write(obj.b58PubkeyAddressPrefix) - ..writeByte(10) - ..write(obj.b58ScriptAddressPrefix); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ConsensusParamsAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/contact_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/contact_adapter.dart deleted file mode 100644 index f93c7118d5..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/contact_adapter.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of '../contact.dart'; - -class ContactAdapter extends TypeAdapter { - @override - final int typeId = 10; - - @override - Contact read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Contact( - email: fields[0] as String?, - github: fields[1] as String?, - ); - } - - @override - void write(BinaryWriter writer, Contact obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.email) - ..writeByte(1) - ..write(obj.github); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ContactAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/electrum_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/electrum_adapter.dart deleted file mode 100644 index 3de6a608b9..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/electrum_adapter.dart +++ /dev/null @@ -1,41 +0,0 @@ -part of '../electrum.dart'; - -class ElectrumAdapter extends TypeAdapter { - @override - final int typeId = 8; - - @override - Electrum read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Electrum( - url: fields[0] as String?, - protocol: fields[1] as String?, - contact: (fields[2] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, Electrum obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.url) - ..writeByte(1) - ..write(obj.protocol) - ..writeByte(2) - ..write(obj.contact); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ElectrumAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/links_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/links_adapter.dart deleted file mode 100644 index 1fc2666af4..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/links_adapter.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of '../links.dart'; - -class LinksAdapter extends TypeAdapter { - @override - final int typeId = 4; - - @override - Links read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Links( - github: fields[0] as String?, - homepage: fields[1] as String?, - ); - } - - @override - void write(BinaryWriter writer, Links obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.github) - ..writeByte(1) - ..write(obj.homepage); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LinksAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/node_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/node_adapter.dart deleted file mode 100644 index 9e968302d2..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/node_adapter.dart +++ /dev/null @@ -1,38 +0,0 @@ -part of '../node.dart'; - -class NodeAdapter extends TypeAdapter { - @override - final int typeId = 9; - - @override - Node read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Node( - url: fields[0] as String?, - guiAuth: fields[1] as bool?, - ); - } - - @override - void write(BinaryWriter writer, Node obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.url) - ..writeByte(1) - ..write(obj.guiAuth); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NodeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/protocol_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/protocol_adapter.dart deleted file mode 100644 index 807c4292c3..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/protocol_adapter.dart +++ /dev/null @@ -1,41 +0,0 @@ -part of '../protocol.dart'; - -class ProtocolAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - Protocol read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Protocol( - type: fields[0] as String?, - protocolData: fields[1] as ProtocolData?, - bip44: fields[2] as String?, - ); - } - - @override - void write(BinaryWriter writer, Protocol obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.type) - ..writeByte(1) - ..write(obj.protocolData) - ..writeByte(2) - ..write(obj.bip44); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProtocolAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/protocol_data_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/protocol_data_adapter.dart deleted file mode 100644 index 683b5d14c3..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/protocol_data_adapter.dart +++ /dev/null @@ -1,68 +0,0 @@ -part of '../protocol_data.dart'; - -class ProtocolDataAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - ProtocolData read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ProtocolData( - platform: fields[0] as String?, - contractAddress: fields[1] as String?, - consensusParams: fields[2] as ConsensusParams?, - checkPointBlock: fields[3] as CheckPointBlock?, - slpPrefix: fields[4] as String?, - decimals: fields[5] as num?, - tokenId: fields[6] as String?, - requiredConfirmations: fields[7] as num?, - denom: fields[8] as String?, - accountPrefix: fields[9] as String?, - chainId: fields[10] as String?, - gasPrice: fields[11] as num?, - ); - } - - @override - void write(BinaryWriter writer, ProtocolData obj) { - writer - ..writeByte(12) - ..writeByte(0) - ..write(obj.platform) - ..writeByte(1) - ..write(obj.contractAddress) - ..writeByte(2) - ..write(obj.consensusParams ?? const ConsensusParams()) - ..writeByte(3) - ..write(obj.checkPointBlock ?? const CheckPointBlock()) - ..writeByte(4) - ..write(obj.slpPrefix) - ..writeByte(5) - ..write(obj.decimals) - ..writeByte(6) - ..write(obj.tokenId) - ..writeByte(7) - ..write(obj.requiredConfirmations) - ..writeByte(8) - ..write(obj.denom) - ..writeByte(9) - ..write(obj.accountPrefix) - ..writeByte(10) - ..write(obj.chainId) - ..writeByte(11) - ..write(obj.gasPrice); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProtocolDataAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/adapters/rpc_url_adapter.dart b/packages/komodo_coin_updates/lib/src/models/adapters/rpc_url_adapter.dart deleted file mode 100644 index 14460c1281..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/adapters/rpc_url_adapter.dart +++ /dev/null @@ -1,35 +0,0 @@ -part of '../rpc_url.dart'; - -class RpcUrlAdapter extends TypeAdapter { - @override - final int typeId = 11; - - @override - RpcUrl read(BinaryReader reader) { - final int numOfFields = reader.readByte(); - final Map fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return RpcUrl( - url: fields[0] as String?, - ); - } - - @override - void write(BinaryWriter writer, RpcUrl obj) { - writer - ..writeByte(1) - ..writeByte(0) - ..write(obj.url); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RpcUrlAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/packages/komodo_coin_updates/lib/src/models/address_format.dart b/packages/komodo_coin_updates/lib/src/models/address_format.dart deleted file mode 100644 index 4b50241087..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/address_format.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/address_format_adapter.dart'; - -class AddressFormat extends Equatable { - const AddressFormat({ - this.format, - this.network, - }); - - factory AddressFormat.fromJson(Map json) { - return AddressFormat( - format: json['format'] as String?, - network: json['network'] as String?, - ); - } - - final String? format; - final String? network; - - Map toJson() { - return { - 'format': format, - 'network': network, - }; - } - - @override - List get props => [format, network]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/checkpoint_block.dart b/packages/komodo_coin_updates/lib/src/models/checkpoint_block.dart deleted file mode 100644 index 921c431e27..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/checkpoint_block.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/checkpoint_block_adapter.dart'; - -class CheckPointBlock extends Equatable { - const CheckPointBlock({ - this.height, - this.time, - this.hash, - this.saplingTree, - }); - - factory CheckPointBlock.fromJson(Map json) { - return CheckPointBlock( - height: json['height'] as num?, - time: json['time'] as num?, - hash: json['hash'] as String?, - saplingTree: json['saplingTree'] as String?, - ); - } - - final num? height; - final num? time; - final String? hash; - final String? saplingTree; - - Map toJson() { - return { - 'height': height, - 'time': time, - 'hash': hash, - 'saplingTree': saplingTree, - }; - } - - @override - List get props => [height, time, hash, saplingTree]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/coin.dart b/packages/komodo_coin_updates/lib/src/models/coin.dart deleted file mode 100644 index 7c778e779f..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/coin.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; - -import 'address_format.dart'; -import 'links.dart'; -import 'protocol.dart'; - -part 'adapters/coin_adapter.dart'; - -class Coin extends Equatable implements ObjectWithPrimaryKey { - const Coin({ - required this.coin, - this.name, - this.fname, - this.rpcport, - this.mm2, - this.chainId, - this.requiredConfirmations, - this.avgBlocktime, - this.decimals, - this.protocol, - this.derivationPath, - this.trezorCoin, - this.links, - this.isPoS, - this.pubtype, - this.p2shtype, - this.wiftype, - this.txfee, - this.dust, - this.matureConfirmations, - this.segwit, - this.signMessagePrefix, - this.asset, - this.txversion, - this.overwintered, - this.requiresNotarization, - this.walletOnly, - this.bech32Hrp, - this.isTestnet, - this.forkId, - this.signatureVersion, - this.confpath, - this.addressFormat, - this.aliasTicker, - this.estimateFeeMode, - this.orderbookTicker, - this.taddr, - this.forceMinRelayFee, - this.p2p, - this.magic, - this.nSPV, - this.isPoSV, - this.versionGroupId, - this.consensusBranchId, - this.estimateFeeBlocks, - }); - - factory Coin.fromJson(Map json) { - return Coin( - coin: json['coin'] as String, - name: json['name'] as String?, - fname: json['fname'] as String?, - rpcport: json['rpcport'] as num?, - mm2: json['mm2'] as num?, - chainId: json['chain_id'] as num?, - requiredConfirmations: json['required_confirmations'] as num?, - avgBlocktime: json['avg_blocktime'] as num?, - decimals: json['decimals'] as num?, - protocol: json['protocol'] != null - ? Protocol.fromJson(json['protocol'] as Map) - : null, - derivationPath: json['derivation_path'] as String?, - trezorCoin: json['trezor_coin'] as String?, - links: json['links'] != null - ? Links.fromJson(json['links'] as Map) - : null, - isPoS: json['isPoS'] as num?, - pubtype: json['pubtype'] as num?, - p2shtype: json['p2shtype'] as num?, - wiftype: json['wiftype'] as num?, - txfee: json['txfee'] as num?, - dust: json['dust'] as num?, - matureConfirmations: json['mature_confirmations'] as num?, - segwit: json['segwit'] as bool?, - signMessagePrefix: json['sign_message_prefix'] as String?, - asset: json['asset'] as String?, - txversion: json['txversion'] as num?, - overwintered: json['overwintered'] as num?, - requiresNotarization: json['requires_notarization'] as bool?, - walletOnly: json['wallet_only'] as bool?, - bech32Hrp: json['bech32_hrp'] as String?, - isTestnet: json['is_testnet'] as bool?, - forkId: json['fork_id'] as String?, - signatureVersion: json['signature_version'] as String?, - confpath: json['confpath'] as String?, - addressFormat: json['address_format'] != null - ? AddressFormat.fromJson( - json['address_format'] as Map, - ) - : null, - aliasTicker: json['alias_ticker'] as String?, - estimateFeeMode: json['estimate_fee_mode'] as String?, - orderbookTicker: json['orderbook_ticker'] as String?, - taddr: json['taddr'] as num?, - forceMinRelayFee: json['force_min_relay_fee'] as bool?, - p2p: json['p2p'] as num?, - magic: json['magic'] as String?, - nSPV: json['nSPV'] as String?, - isPoSV: json['isPoSV'] as num?, - versionGroupId: json['version_group_id'] as String?, - consensusBranchId: json['consensus_branch_id'] as String?, - estimateFeeBlocks: json['estimate_fee_blocks'] as num?, - ); - } - - final String coin; - final String? name; - final String? fname; - final num? rpcport; - final num? mm2; - final num? chainId; - final num? requiredConfirmations; - final num? avgBlocktime; - final num? decimals; - final Protocol? protocol; - final String? derivationPath; - final String? trezorCoin; - final Links? links; - final num? isPoS; - final num? pubtype; - final num? p2shtype; - final num? wiftype; - final num? txfee; - final num? dust; - final num? matureConfirmations; - final bool? segwit; - final String? signMessagePrefix; - final String? asset; - final num? txversion; - final num? overwintered; - final bool? requiresNotarization; - final bool? walletOnly; - final String? bech32Hrp; - final bool? isTestnet; - final String? forkId; - final String? signatureVersion; - final String? confpath; - final AddressFormat? addressFormat; - final String? aliasTicker; - final String? estimateFeeMode; - final String? orderbookTicker; - final num? taddr; - final bool? forceMinRelayFee; - final num? p2p; - final String? magic; - final String? nSPV; - final num? isPoSV; - final String? versionGroupId; - final String? consensusBranchId; - final num? estimateFeeBlocks; - - Map toJson() { - return { - 'coin': coin, - 'name': name, - 'fname': fname, - 'rpcport': rpcport, - 'mm2': mm2, - 'chain_id': chainId, - 'required_confirmations': requiredConfirmations, - 'avg_blocktime': avgBlocktime, - 'decimals': decimals, - 'protocol': protocol?.toJson(), - 'derivation_path': derivationPath, - 'trezor_coin': trezorCoin, - 'links': links?.toJson(), - 'isPoS': isPoS, - 'pubtype': pubtype, - 'p2shtype': p2shtype, - 'wiftype': wiftype, - 'txfee': txfee, - 'dust': dust, - 'mature_confirmations': matureConfirmations, - 'segwit': segwit, - 'sign_message_prefix': signMessagePrefix, - 'asset': asset, - 'txversion': txversion, - 'overwintered': overwintered, - 'requires_notarization': requiresNotarization, - 'wallet_only': walletOnly, - 'bech32_hrp': bech32Hrp, - 'is_testnet': isTestnet, - 'fork_id': forkId, - 'signature_version': signatureVersion, - 'confpath': confpath, - 'address_format': addressFormat?.toJson(), - 'alias_ticker': aliasTicker, - 'estimate_fee_mode': estimateFeeMode, - 'orderbook_ticker': orderbookTicker, - 'taddr': taddr, - 'force_min_relay_fee': forceMinRelayFee, - 'p2p': p2p, - 'magic': magic, - 'nSPV': nSPV, - 'isPoSV': isPoSV, - 'version_group_id': versionGroupId, - 'consensus_branch_id': consensusBranchId, - 'estimate_fee_blocks': estimateFeeBlocks, - }; - } - - @override - List get props => [coin]; - - @override - String get primaryKey => coin; -} diff --git a/packages/komodo_coin_updates/lib/src/models/coin_config.dart b/packages/komodo_coin_updates/lib/src/models/coin_config.dart deleted file mode 100644 index f2d3a2de3b..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/coin_config.dart +++ /dev/null @@ -1,417 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; - -import 'address_format.dart'; -import 'electrum.dart'; -import 'links.dart'; -import 'node.dart'; -import 'protocol.dart'; -import 'rpc_url.dart'; - -part 'adapters/coin_config_adapter.dart'; - -class CoinConfig extends Equatable implements ObjectWithPrimaryKey { - const CoinConfig({ - required this.coin, - this.type, - this.name, - this.coingeckoId, - this.livecoinwatchId, - this.explorerUrl, - this.explorerTxUrl, - this.explorerAddressUrl, - this.supported, - this.active, - this.isTestnet, - this.currentlyEnabled, - this.walletOnly, - this.fname, - this.rpcport, - this.mm2, - this.chainId, - this.requiredConfirmations, - this.avgBlocktime, - this.decimals, - this.protocol, - this.derivationPath, - this.contractAddress, - this.parentCoin, - this.swapContractAddress, - this.fallbackSwapContract, - this.nodes, - this.explorerBlockUrl, - this.tokenAddressUrl, - this.trezorCoin, - this.links, - this.pubtype, - this.p2shtype, - this.wiftype, - this.txfee, - this.dust, - this.segwit, - this.electrum, - this.signMessagePrefix, - this.lightWalletDServers, - this.asset, - this.txversion, - this.overwintered, - this.requiresNotarization, - this.checkpointHeight, - this.checkpointBlocktime, - this.binanceId, - this.bech32Hrp, - this.forkId, - this.signatureVersion, - this.confpath, - this.matureConfirmations, - this.bchdUrls, - this.otherTypes, - this.addressFormat, - this.allowSlpUnsafeConf, - this.slpPrefix, - this.tokenId, - this.forexId, - this.isPoS, - this.aliasTicker, - this.estimateFeeMode, - this.orderbookTicker, - this.taddr, - this.forceMinRelayFee, - this.isClaimable, - this.minimalClaimAmount, - this.isPoSV, - this.versionGroupId, - this.consensusBranchId, - this.estimateFeeBlocks, - this.rpcUrls, - }); - - factory CoinConfig.fromJson(Map json) { - return CoinConfig( - coin: json['coin'] as String, - type: json['type'] as String?, - name: json['name'] as String?, - coingeckoId: json['coingecko_id'] as String?, - livecoinwatchId: json['livecoinwatch_id'] as String?, - explorerUrl: json['explorer_url'] as String?, - explorerTxUrl: json['explorer_tx_url'] as String?, - explorerAddressUrl: json['explorer_address_url'] as String?, - supported: (json['supported'] as List?) - ?.map((dynamic e) => e as String) - .toList(), - active: json['active'] as bool?, - isTestnet: json['is_testnet'] as bool?, - currentlyEnabled: json['currently_enabled'] as bool?, - walletOnly: json['wallet_only'] as bool?, - fname: json['fname'] as String?, - rpcport: json['rpcport'] as num?, - mm2: json['mm2'] as num?, - chainId: json['chain_id'] as num?, - requiredConfirmations: json['required_confirmations'] as num?, - avgBlocktime: json['avg_blocktime'] as num?, - decimals: json['decimals'] as num?, - protocol: json['protocol'] == null - ? null - : Protocol.fromJson(json['protocol'] as Map), - derivationPath: json['derivation_path'] as String?, - contractAddress: json['contractAddress'] as String?, - parentCoin: json['parent_coin'] as String?, - swapContractAddress: json['swap_contract_address'] as String?, - fallbackSwapContract: json['fallback_swap_contract'] as String?, - nodes: (json['nodes'] as List?) - ?.map((dynamic e) => Node.fromJson(e as Map)) - .toList(), - explorerBlockUrl: json['explorer_block_url'] as String?, - tokenAddressUrl: json['token_address_url'] as String?, - trezorCoin: json['trezor_coin'] as String?, - links: json['links'] == null - ? null - : Links.fromJson(json['links'] as Map), - pubtype: json['pubtype'] as num?, - p2shtype: json['p2shtype'] as num?, - wiftype: json['wiftype'] as num?, - txfee: json['txfee'] as num?, - dust: json['dust'] as num?, - segwit: json['segwit'] as bool?, - electrum: (json['electrum'] as List?) - ?.map((dynamic e) => Electrum.fromJson(e as Map)) - .toList(), - signMessagePrefix: json['sign_message_refix'] as String?, - lightWalletDServers: (json['light_wallet_d_servers'] as List?) - ?.map((dynamic e) => e as String) - .toList(), - asset: json['asset'] as String?, - txversion: json['txversion'] as num?, - overwintered: json['overwintered'] as num?, - requiresNotarization: json['requires_notarization'] as bool?, - checkpointHeight: json['checkpoint_height'] as num?, - checkpointBlocktime: json['checkpoint_blocktime'] as num?, - binanceId: json['binance_id'] as String?, - bech32Hrp: json['bech32_hrp'] as String?, - forkId: json['forkId'] as String?, - signatureVersion: json['signature_version'] as String?, - confpath: json['confpath'] as String?, - matureConfirmations: json['mature_confirmations'] as num?, - bchdUrls: (json['bchd_urls'] as List?) - ?.map((dynamic e) => e as String) - .toList(), - otherTypes: (json['other_types'] as List?) - ?.map((dynamic e) => e as String) - .toList(), - addressFormat: json['address_format'] == null - ? null - : AddressFormat.fromJson( - json['address_format'] as Map, - ), - allowSlpUnsafeConf: json['allow_slp_unsafe_conf'] as bool?, - slpPrefix: json['slp_prefix'] as String?, - tokenId: json['token_id'] as String?, - forexId: json['forex_id'] as String?, - isPoS: json['isPoS'] as num?, - aliasTicker: json['alias_ticker'] as String?, - estimateFeeMode: json['estimate_fee_mode'] as String?, - orderbookTicker: json['orderbook_ticker'] as String?, - taddr: json['taddr'] as num?, - forceMinRelayFee: json['force_min_relay_fee'] as bool?, - isClaimable: json['is_claimable'] as bool?, - minimalClaimAmount: json['minimal_claim_amount'] as String?, - isPoSV: json['isPoSV'] as num?, - versionGroupId: json['version_group_id'] as String?, - consensusBranchId: json['consensus_branch_id'] as String?, - estimateFeeBlocks: json['estimate_fee_blocks'] as num?, - rpcUrls: (json['rpc_urls'] as List?) - ?.map((dynamic e) => RpcUrl.fromJson(e as Map)) - .toList(), - ); - } - - final String coin; - final String? type; - final String? name; - final String? coingeckoId; - final String? livecoinwatchId; - final String? explorerUrl; - final String? explorerTxUrl; - final String? explorerAddressUrl; - final List? supported; - final bool? active; - final bool? isTestnet; - final bool? currentlyEnabled; - final bool? walletOnly; - final String? fname; - final num? rpcport; - final num? mm2; - final num? chainId; - final num? requiredConfirmations; - final num? avgBlocktime; - final num? decimals; - final Protocol? protocol; - final String? derivationPath; - final String? contractAddress; - final String? parentCoin; - final String? swapContractAddress; - final String? fallbackSwapContract; - final List? nodes; - final String? explorerBlockUrl; - final String? tokenAddressUrl; - final String? trezorCoin; - final Links? links; - final num? pubtype; - final num? p2shtype; - final num? wiftype; - final num? txfee; - final num? dust; - final bool? segwit; - final List? electrum; - final String? signMessagePrefix; - final List? lightWalletDServers; - final String? asset; - final num? txversion; - final num? overwintered; - final bool? requiresNotarization; - final num? checkpointHeight; - final num? checkpointBlocktime; - final String? binanceId; - final String? bech32Hrp; - final String? forkId; - final String? signatureVersion; - final String? confpath; - final num? matureConfirmations; - final List? bchdUrls; - final List? otherTypes; - final AddressFormat? addressFormat; - final bool? allowSlpUnsafeConf; - final String? slpPrefix; - final String? tokenId; - final String? forexId; - final num? isPoS; - final String? aliasTicker; - final String? estimateFeeMode; - final String? orderbookTicker; - final num? taddr; - final bool? forceMinRelayFee; - final bool? isClaimable; - final String? minimalClaimAmount; - final num? isPoSV; - final String? versionGroupId; - final String? consensusBranchId; - final num? estimateFeeBlocks; - final List? rpcUrls; - - Map toJson() { - return { - 'coin': coin, - 'type': type, - 'name': name, - 'coingecko_id': coingeckoId, - 'livecoinwatch_id': livecoinwatchId, - 'explorer_url': explorerUrl, - 'explorer_tx_url': explorerTxUrl, - 'explorer_address_url': explorerAddressUrl, - 'supported': supported, - 'active': active, - 'is_testnet': isTestnet, - 'currently_enabled': currentlyEnabled, - 'wallet_only': walletOnly, - 'fname': fname, - 'rpcport': rpcport, - 'mm2': mm2, - 'chain_id': chainId, - 'required_confirmations': requiredConfirmations, - 'avg_blocktime': avgBlocktime, - 'decimals': decimals, - 'protocol': protocol?.toJson(), - 'derivation_path': derivationPath, - 'contractAddress': contractAddress, - 'parent_coin': parentCoin, - 'swap_contract_address': swapContractAddress, - 'fallback_swap_contract': fallbackSwapContract, - 'nodes': nodes?.map((Node e) => e.toJson()).toList(), - 'explorer_block_url': explorerBlockUrl, - 'token_address_url': tokenAddressUrl, - 'trezor_coin': trezorCoin, - 'links': links?.toJson(), - 'pubtype': pubtype, - 'p2shtype': p2shtype, - 'wiftype': wiftype, - 'txfee': txfee, - 'dust': dust, - 'segwit': segwit, - 'electrum': electrum?.map((Electrum e) => e.toJson()).toList(), - 'sign_message_refix': signMessagePrefix, - 'light_wallet_d_servers': lightWalletDServers, - 'asset': asset, - 'txversion': txversion, - 'overwintered': overwintered, - 'requires_notarization': requiresNotarization, - 'checkpoint_height': checkpointHeight, - 'checkpoint_blocktime': checkpointBlocktime, - 'binance_id': binanceId, - 'bech32_hrp': bech32Hrp, - 'forkId': forkId, - 'signature_version': signatureVersion, - 'confpath': confpath, - 'mature_confirmations': matureConfirmations, - 'bchd_urls': bchdUrls, - 'other_types': otherTypes, - 'address_format': addressFormat?.toJson(), - 'allow_slp_unsafe_conf': allowSlpUnsafeConf, - 'slp_prefix': slpPrefix, - 'token_id': tokenId, - 'forex_id': forexId, - 'isPoS': isPoS, - 'alias_ticker': aliasTicker, - 'estimate_fee_mode': estimateFeeMode, - 'orderbook_ticker': orderbookTicker, - 'taddr': taddr, - 'force_min_relay_fee': forceMinRelayFee, - 'is_claimable': isClaimable, - 'minimal_claim_amount': minimalClaimAmount, - 'isPoSV': isPoSV, - 'version_group_id': versionGroupId, - 'consensus_branch_id': consensusBranchId, - 'estimate_fee_blocks': estimateFeeBlocks, - 'rpc_urls': rpcUrls?.map((RpcUrl e) => e.toJson()).toList(), - }; - } - - @override - List get props => [ - coin, - type, - name, - coingeckoId, - livecoinwatchId, - explorerUrl, - explorerTxUrl, - explorerAddressUrl, - supported, - active, - isTestnet, - currentlyEnabled, - walletOnly, - fname, - rpcport, - mm2, - chainId, - requiredConfirmations, - avgBlocktime, - decimals, - protocol, - derivationPath, - contractAddress, - parentCoin, - swapContractAddress, - fallbackSwapContract, - nodes, - explorerBlockUrl, - tokenAddressUrl, - trezorCoin, - links, - pubtype, - p2shtype, - wiftype, - txfee, - dust, - segwit, - electrum, - signMessagePrefix, - lightWalletDServers, - asset, - txversion, - overwintered, - requiresNotarization, - checkpointHeight, - checkpointBlocktime, - binanceId, - bech32Hrp, - forkId, - signatureVersion, - confpath, - matureConfirmations, - bchdUrls, - otherTypes, - addressFormat, - allowSlpUnsafeConf, - slpPrefix, - tokenId, - forexId, - isPoS, - aliasTicker, - estimateFeeMode, - orderbookTicker, - taddr, - forceMinRelayFee, - isClaimable, - minimalClaimAmount, - isPoSV, - versionGroupId, - consensusBranchId, - estimateFeeBlocks, - rpcUrls, - ]; - - @override - String get primaryKey => coin; -} diff --git a/packages/komodo_coin_updates/lib/src/models/coin_info.dart b/packages/komodo_coin_updates/lib/src/models/coin_info.dart deleted file mode 100644 index 2030d57863..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/coin_info.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; - -import '../../komodo_coin_updates.dart'; - -part 'adapters/coin_info_adapter.dart'; - -class CoinInfo extends Equatable implements ObjectWithPrimaryKey { - const CoinInfo({ - required this.coin, - required this.coinConfig, - }); - - final Coin coin; - final CoinConfig? coinConfig; - - @override - String get primaryKey => coin.coin; - - @override - // TODO(Francois): optimize for comparisons - decide on fields to use when comparing - List get props => [coin, coinConfig]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/consensus_params.dart b/packages/komodo_coin_updates/lib/src/models/consensus_params.dart deleted file mode 100644 index ddd41d64a5..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/consensus_params.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/consensus_params_adapter.dart'; - -class ConsensusParams extends Equatable { - const ConsensusParams({ - this.overwinterActivationHeight, - this.saplingActivationHeight, - this.blossomActivationHeight, - this.heartwoodActivationHeight, - this.canopyActivationHeight, - this.coinType, - this.hrpSaplingExtendedSpendingKey, - this.hrpSaplingExtendedFullViewingKey, - this.hrpSaplingPaymentAddress, - this.b58PubkeyAddressPrefix, - this.b58ScriptAddressPrefix, - }); - - factory ConsensusParams.fromJson(Map json) { - return ConsensusParams( - overwinterActivationHeight: json['overwinter_activation_height'] as num?, - saplingActivationHeight: json['sapling_activation_height'] as num?, - blossomActivationHeight: json['blossom_activation_height'] as num?, - heartwoodActivationHeight: json['heartwood_activation_height'] as num?, - canopyActivationHeight: json['canopy_activation_height'] as num?, - coinType: json['coin_type'] as num?, - hrpSaplingExtendedSpendingKey: - json['hrp_sapling_extended_spending_key'] as String?, - hrpSaplingExtendedFullViewingKey: - json['hrp_sapling_extended_full_viewing_key'] as String?, - hrpSaplingPaymentAddress: json['hrp_sapling_payment_address'] as String?, - b58PubkeyAddressPrefix: json['b58_pubkey_address_prefix'] != null - ? List.from(json['b58_pubkey_address_prefix'] as List) - : null, - b58ScriptAddressPrefix: json['b58_script_address_prefix'] != null - ? List.from(json['b58_script_address_prefix'] as List) - : null, - ); - } - - final num? overwinterActivationHeight; - final num? saplingActivationHeight; - final num? blossomActivationHeight; - final num? heartwoodActivationHeight; - final num? canopyActivationHeight; - final num? coinType; - final String? hrpSaplingExtendedSpendingKey; - final String? hrpSaplingExtendedFullViewingKey; - final String? hrpSaplingPaymentAddress; - final List? b58PubkeyAddressPrefix; - final List? b58ScriptAddressPrefix; - - Map toJson() { - return { - 'overwinter_activation_height': overwinterActivationHeight, - 'sapling_activation_height': saplingActivationHeight, - 'blossom_activation_height': blossomActivationHeight, - 'heartwood_activation_height': heartwoodActivationHeight, - 'canopy_activation_height': canopyActivationHeight, - 'coin_type': coinType, - 'hrp_sapling_extended_spending_key': hrpSaplingExtendedSpendingKey, - 'hrp_sapling_extended_full_viewing_key': hrpSaplingExtendedFullViewingKey, - 'hrp_sapling_payment_address': hrpSaplingPaymentAddress, - 'b58_pubkey_address_prefix': b58PubkeyAddressPrefix, - 'b58_script_address_prefix': b58ScriptAddressPrefix, - }; - } - - @override - List get props => [ - overwinterActivationHeight, - saplingActivationHeight, - blossomActivationHeight, - heartwoodActivationHeight, - canopyActivationHeight, - coinType, - hrpSaplingExtendedSpendingKey, - hrpSaplingExtendedFullViewingKey, - hrpSaplingPaymentAddress, - b58PubkeyAddressPrefix, - b58ScriptAddressPrefix, - ]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/contact.dart b/packages/komodo_coin_updates/lib/src/models/contact.dart deleted file mode 100644 index 309638d8e2..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/contact.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/contact_adapter.dart'; - -class Contact extends Equatable { - const Contact({this.email, this.github}); - - factory Contact.fromJson(Map json) { - return Contact( - email: json['email'] as String?, - github: json['github'] as String?, - ); - } - - final String? email; - final String? github; - - Map toJson() { - return { - 'email': email, - 'github': github, - }; - } - - @override - List get props => [email, github]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/electrum.dart b/packages/komodo_coin_updates/lib/src/models/electrum.dart deleted file mode 100644 index 978ab5d289..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/electrum.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; -import 'contact.dart'; - -part 'adapters/electrum_adapter.dart'; - -// ignore: must_be_immutable -class Electrum extends Equatable { - Electrum({ - this.url, - this.wsUrl, - this.protocol, - this.contact, - }); - - factory Electrum.fromJson(Map json) { - return Electrum( - url: json['url'] as String?, - wsUrl: json['ws_url'] as String?, - protocol: json['protocol'] as String?, - contact: (json['contact'] as List?) - ?.map((dynamic e) => Contact.fromJson(e as Map)) - .toList(), - ); - } - - final String? url; - String? wsUrl; - final String? protocol; - final List? contact; - - Map toJson() { - return { - 'url': url, - 'ws_url': wsUrl, - 'protocol': protocol, - 'contact': contact?.map((Contact e) => e.toJson()).toList(), - }; - } - - @override - List get props => [url, wsUrl, protocol, contact]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/links.dart b/packages/komodo_coin_updates/lib/src/models/links.dart deleted file mode 100644 index d23675c44c..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/links.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/links_adapter.dart'; - -class Links extends Equatable { - const Links({ - this.github, - this.homepage, - }); - - factory Links.fromJson(Map json) { - return Links( - github: json['github'] as String?, - homepage: json['homepage'] as String?, - ); - } - - final String? github; - final String? homepage; - - Map toJson() { - return { - 'github': github, - 'homepage': homepage, - }; - } - - @override - List get props => [github, homepage]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/models.dart b/packages/komodo_coin_updates/lib/src/models/models.dart deleted file mode 100644 index 691addb211..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/models.dart +++ /dev/null @@ -1,13 +0,0 @@ -export 'address_format.dart'; -export 'checkpoint_block.dart'; -export 'coin.dart'; -export 'coin_config.dart'; -export 'consensus_params.dart'; -export 'contact.dart'; -export 'electrum.dart'; -export 'links.dart'; -export 'node.dart'; -export 'protocol.dart'; -export 'protocol_data.dart'; -export 'rpc_url.dart'; -export 'runtime_update_config.dart'; diff --git a/packages/komodo_coin_updates/lib/src/models/node.dart b/packages/komodo_coin_updates/lib/src/models/node.dart deleted file mode 100644 index 2c854378b5..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/node.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -import '../../komodo_coin_updates.dart'; - -part 'adapters/node_adapter.dart'; - -class Node extends Equatable { - const Node({this.url, this.wsUrl, this.guiAuth, this.contact}); - - factory Node.fromJson(Map json) { - return Node( - url: json['url'] as String?, - wsUrl: json['ws_url'] as String?, - guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) as bool?, - contact: json['contact'] != null - ? Contact.fromJson(json['contact'] as Map) - : null, - ); - } - - final String? url; - final String? wsUrl; - final bool? guiAuth; - final Contact? contact; - - Map toJson() { - return { - 'url': url, - 'ws_url': wsUrl, - 'gui_auth': guiAuth, - 'komodo_proxy': guiAuth, - 'contact': contact?.toJson(), - }; - } - - @override - List get props => [url, wsUrl, guiAuth, contact]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/protocol.dart b/packages/komodo_coin_updates/lib/src/models/protocol.dart deleted file mode 100644 index c09a84a5c7..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/protocol.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -import 'protocol_data.dart'; - -part 'adapters/protocol_adapter.dart'; - -class Protocol extends Equatable { - const Protocol({ - this.type, - this.protocolData, - this.bip44, - }); - - factory Protocol.fromJson(Map json) { - return Protocol( - type: json['type'] as String?, - protocolData: (json['protocol_data'] != null) - ? ProtocolData.fromJson(json['protocol_data'] as Map) - : null, - bip44: json['bip44'] as String?, - ); - } - - final String? type; - final ProtocolData? protocolData; - final String? bip44; - - Map toJson() { - return { - 'type': type, - 'protocol_data': protocolData?.toJson(), - 'bip44': bip44, - }; - } - - @override - List get props => [type, protocolData, bip44]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/protocol_data.dart b/packages/komodo_coin_updates/lib/src/models/protocol_data.dart deleted file mode 100644 index 7d014d3cef..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/protocol_data.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -import 'checkpoint_block.dart'; -import 'consensus_params.dart'; - -part 'adapters/protocol_data_adapter.dart'; - -class ProtocolData extends Equatable { - const ProtocolData({ - this.platform, - this.contractAddress, - this.consensusParams, - this.checkPointBlock, - this.slpPrefix, - this.decimals, - this.tokenId, - this.requiredConfirmations, - this.denom, - this.accountPrefix, - this.chainId, - this.gasPrice, - }); - - factory ProtocolData.fromJson(Map json) { - return ProtocolData( - platform: json['platform'] as String?, - contractAddress: json['contract_address'] as String?, - consensusParams: json['consensus_params'] != null - ? ConsensusParams.fromJson( - json['consensus_params'] as Map, - ) - : null, - checkPointBlock: json['check_point_block'] != null - ? CheckPointBlock.fromJson( - json['check_point_block'] as Map, - ) - : null, - slpPrefix: json['slp_prefix'] as String?, - decimals: json['decimals'] as num?, - tokenId: json['token_id'] as String?, - requiredConfirmations: json['required_confirmations'] as num?, - denom: json['denom'] as String?, - accountPrefix: json['account_prefix'] as String?, - chainId: json['chain_id'] as String?, - gasPrice: json['gas_price'] as num?, - ); - } - - final String? platform; - final String? contractAddress; - final ConsensusParams? consensusParams; - final CheckPointBlock? checkPointBlock; - final String? slpPrefix; - final num? decimals; - final String? tokenId; - final num? requiredConfirmations; - final String? denom; - final String? accountPrefix; - final String? chainId; - final num? gasPrice; - - Map toJson() { - return { - 'platform': platform, - 'contract_address': contractAddress, - 'consensus_params': consensusParams?.toJson(), - 'check_point_block': checkPointBlock?.toJson(), - 'slp_prefix': slpPrefix, - 'decimals': decimals, - 'token_id': tokenId, - 'required_confirmations': requiredConfirmations, - 'denom': denom, - 'account_prefix': accountPrefix, - 'chain_id': chainId, - 'gas_price': gasPrice, - }; - } - - @override - List get props => [ - platform, - contractAddress, - consensusParams, - checkPointBlock, - slpPrefix, - decimals, - tokenId, - requiredConfirmations, - denom, - accountPrefix, - chainId, - gasPrice, - ]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/rpc_url.dart b/packages/komodo_coin_updates/lib/src/models/rpc_url.dart deleted file mode 100644 index 71c2639d7c..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/rpc_url.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:hive/hive.dart'; - -part 'adapters/rpc_url_adapter.dart'; - -class RpcUrl extends Equatable { - const RpcUrl({this.url}); - - factory RpcUrl.fromJson(Map json) { - return RpcUrl( - url: json['url'] as String?, - ); - } - - final String? url; - - Map toJson() { - return { - 'url': url, - }; - } - - @override - List get props => [url]; -} diff --git a/packages/komodo_coin_updates/lib/src/models/runtime_update_config.dart b/packages/komodo_coin_updates/lib/src/models/runtime_update_config.dart deleted file mode 100644 index 7d048e9153..0000000000 --- a/packages/komodo_coin_updates/lib/src/models/runtime_update_config.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class RuntimeUpdateConfig extends Equatable { - const RuntimeUpdateConfig({ - required this.bundledCoinsRepoCommit, - required this.coinsRepoApiUrl, - required this.coinsRepoContentUrl, - required this.coinsRepoBranch, - required this.runtimeUpdatesEnabled, - }); - - factory RuntimeUpdateConfig.fromJson(Map json) { - return RuntimeUpdateConfig( - bundledCoinsRepoCommit: json['bundled_coins_repo_commit'] as String, - coinsRepoApiUrl: json['coins_repo_api_url'] as String, - coinsRepoContentUrl: json['coins_repo_content_url'] as String, - coinsRepoBranch: json['coins_repo_branch'] as String, - runtimeUpdatesEnabled: json['runtime_updates_enabled'] as bool, - ); - } - final String bundledCoinsRepoCommit; - final String coinsRepoApiUrl; - final String coinsRepoContentUrl; - final String coinsRepoBranch; - final bool runtimeUpdatesEnabled; - - Map toJson() { - return { - 'bundled_coins_repo_commit': bundledCoinsRepoCommit, - 'coins_repo_api_url': coinsRepoApiUrl, - 'coins_repo_content_url': coinsRepoContentUrl, - 'coins_repo_branch': coinsRepoBranch, - 'runtime_updates_enabled': runtimeUpdatesEnabled, - }; - } - - @override - List get props => [ - bundledCoinsRepoCommit, - coinsRepoApiUrl, - coinsRepoContentUrl, - coinsRepoBranch, - runtimeUpdatesEnabled, - ]; -} diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml deleted file mode 100644 index c8e1319832..0000000000 --- a/packages/komodo_coin_updates/pubspec.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: komodo_coin_updates -description: Runtime coin config update coin updates. -version: 1.0.0 -publish_to: none # publishable packages can't have git dependencies - -environment: - sdk: ">=3.0.0 <4.0.0" - -# Add regular dependencies here. -dependencies: - http: 1.2.2 # dart.dev - path_provider: 2.1.4 # flutter.dev - path: 1.9.0 # dart.dev - transitive to direct - - komodo_persistence_layer: - path: ../komodo_persistence_layer/ - - flutter_bloc: 8.1.6 - equatable: 2.0.5 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - hive: - git: - url: https://github.com/KomodoPlatform/hive.git - path: hive/ - ref: 470473ffc1ba39f6c90f31ababe0ee63b76b69fe #2.2.3 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - hive_flutter: - git: - url: https://github.com/KomodoPlatform/hive.git - path: hive_flutter/ - ref: 0cbaab793be77b19b4740bc85d7ea6461b9762b4 #1.1.0 - -dev_dependencies: - lints: ^2.1.0 - test: ^1.24.0 diff --git a/packages/komodo_coin_updates/test/komodo_coin_updates_test.dart b/packages/komodo_coin_updates/test/komodo_coin_updates_test.dart deleted file mode 100644 index 9e1306911c..0000000000 --- a/packages/komodo_coin_updates/test/komodo_coin_updates_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - // TODO(Francois): Implement test - throw UnimplementedError(); - }); - }); -} diff --git a/packages/komodo_ui_kit/lib/src/buttons/language_switcher/language_line.dart b/packages/komodo_ui_kit/lib/src/buttons/language_switcher/language_line.dart index 3e425d33ea..e4a7937de5 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/language_switcher/language_line.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/language_switcher/language_line.dart @@ -33,8 +33,11 @@ class LanguageLine extends StatelessWidget { Icon( Icons.keyboard_arrow_down_rounded, size: 20, - color: - Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(.5), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: .5), ), ], ); diff --git a/packages/komodo_ui_kit/lib/src/buttons/theme_switcher/theme_switcher.dart b/packages/komodo_ui_kit/lib/src/buttons/theme_switcher/theme_switcher.dart index d22624a6ff..13fe1d0a6d 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/theme_switcher/theme_switcher.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/theme_switcher/theme_switcher.dart @@ -165,7 +165,7 @@ class _Thumb extends StatelessWidget { curve: style.curve, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: style.thumbBgColor.withOpacity(isHovered ? 0.6 : 1), + color: style.thumbBgColor.withValues(alpha: isHovered ? 0.6 : 1), borderRadius: BorderRadius.circular(15), ), child: AnimatedScale( diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart index 08338e04e0..baf85dc34e 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart @@ -63,10 +63,10 @@ class UiBorderButton extends StatelessWidget { child: InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(15), - hoverColor: secondaryColor.withOpacity(0.05), - highlightColor: secondaryColor.withOpacity(0.1), - focusColor: secondaryColor.withOpacity(0.2), - splashColor: secondaryColor.withOpacity(0.4), + hoverColor: secondaryColor.withValues(alpha: 0.05), + highlightColor: secondaryColor.withValues(alpha: 0.1), + focusColor: secondaryColor.withValues(alpha: 0.2), + splashColor: secondaryColor.withValues(alpha: 0.4), child: Padding( padding: const EdgeInsets.fromLTRB(12, 6, 12, 6), child: Builder( diff --git a/packages/komodo_ui_kit/lib/src/buttons/upload_button.dart b/packages/komodo_ui_kit/lib/src/buttons/upload_button.dart index dd92ebccda..8b9d614f0e 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/upload_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/upload_button.dart @@ -20,7 +20,7 @@ class UploadButton extends StatelessWidget { text: buttonText, width: double.infinity, textColor: themeData.colorScheme.primary, - borderColor: themeData.colorScheme.primary.withOpacity(0.3), + borderColor: themeData.colorScheme.primary.withValues(alpha: 0.3), backgroundColor: Theme.of(context).colorScheme.surface, ); } diff --git a/packages/komodo_ui_kit/lib/src/display/statistic_card.dart b/packages/komodo_ui_kit/lib/src/display/statistic_card.dart index ef02741ab7..8146b205a2 100644 --- a/packages/komodo_ui_kit/lib/src/display/statistic_card.dart +++ b/packages/komodo_ui_kit/lib/src/display/statistic_card.dart @@ -49,7 +49,7 @@ class StatisticCard extends StatelessWidget { Theme.of(context) .colorScheme .primaryContainer - .withOpacity(0.25), + .withValues(alpha: 0.25), Colors.transparent, ], center: const Alignment(0.2, 0.1), diff --git a/packages/komodo_ui_kit/lib/src/painter/focus_decorator.dart b/packages/komodo_ui_kit/lib/src/painter/focus_decorator.dart index 03b6d52ee7..2e29eb2d08 100644 --- a/packages/komodo_ui_kit/lib/src/painter/focus_decorator.dart +++ b/packages/komodo_ui_kit/lib/src/painter/focus_decorator.dart @@ -35,7 +35,7 @@ class _FocusDecoratorState extends State { child: CustomPaint( painter: DashRectPainter( color: _hasFocus - ? theme.custom.buttonColorDefaultHover.withOpacity(.8) + ? theme.custom.buttonColorDefaultHover.withValues(alpha: .8) : Colors.transparent, strokeWidth: 1, gap: 2, diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index 4423bfaba0..c80c47b15a 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -65,18 +65,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" path: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 758717de1d..96b47f1054 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,10 +134,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -231,10 +231,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -494,10 +494,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -640,19 +640,12 @@ packages: relative: true source: path version: "0.0.1" - komodo_coin_updates: - dependency: "direct main" - description: - path: "packages/komodo_coin_updates" - relative: true - source: path - version: "1.0.0" komodo_coins: dependency: transitive description: path: "packages/komodo_coins" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -661,7 +654,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0" @@ -670,7 +663,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -679,7 +672,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -688,7 +681,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.1.0+1" @@ -697,7 +690,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: e51e7587b389daa0875b5cb1a37efb1536507add url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -720,7 +713,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "94b050bbfc24260c30426363ae130d82cecfbc4e" + resolved-ref: "2707fc8050207400a46121bc2e47d4da4bc3882a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -728,18 +721,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1162,7 +1155,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_map_stack_trace: dependency: transitive description: @@ -1199,10 +1192,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1223,10 +1216,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" sync_http: dependency: transitive description: @@ -1247,26 +1240,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.7" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" typed_data: dependency: transitive description: @@ -1440,10 +1433,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" watcher: dependency: transitive description: @@ -1472,10 +1465,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a82de0cd2b..de28c6ea8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,9 +40,6 @@ dependencies: komodo_persistence_layer: path: packages/komodo_persistence_layer - komodo_coin_updates: - path: packages/komodo_coin_updates - komodo_cex_market_data: path: packages/komodo_cex_market_data @@ -81,7 +78,7 @@ dependencies: ## ---- Fluttercommunity.dev # does not require review, since hosted and git versions are the same - equatable: 2.0.5 # sdk depends on hosted version, and not from git + equatable: 2.0.7 # sdk depends on hosted version, and not from git # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 package_info_plus: @@ -155,7 +152,7 @@ dependencies: ref: 69958a3a2d6d5dd108393832acde6bda06bd10bc flutter_slidable: # last reviewed 27bbe0dfa9866ae01e8001267e873221ef5fbd67 - ^3.1.0 + ^3.1.2 # git: # url: https://github.com/KomodoPlatform/flutter_slidable.git # ref: 175b0735f5577dd7d378e60cfe2fe1ca607df9fa #1.1.0 diff --git a/run_integration_tests.dart b/run_integration_tests.dart index 9b9d01a2b9..66235a0c64 100644 --- a/run_integration_tests.dart +++ b/run_integration_tests.dart @@ -12,8 +12,9 @@ Future main(List args) async { final ArgParser parser = _configureArgParser(); IntegrationTestArguments testArgs = IntegrationTestArguments.fromArgs(parser.parse(args)); + // TODO!: re-enable suspended assets test when issues are figured out final Set testsList = - getTestsList(testArgs.isWeb && testArgs.isChrome); + getTestsList(false); //testArgs.isWeb && testArgs.isChrome); bool didTestFail = false; WebBrowserDriver? driver; const testsWithUrlBlocking = [ diff --git a/test_integration/common/goto.dart b/test_integration/common/goto.dart index 01586dc955..ad371477c7 100644 --- a/test_integration/common/goto.dart +++ b/test_integration/common/goto.dart @@ -37,5 +37,5 @@ Future _go(String key, WidgetTester tester, {int nFrames = 60}) async { await tester.tapAndPump(finder); await tester.pumpNFrames(nFrames); // ignore: avoid_print - print('🔍 GOTO: finished navigating to to $key'); + print('🔍 GOTO: finished navigating to $key'); } diff --git a/test_integration/helpers/restore_wallet.dart b/test_integration/helpers/restore_wallet.dart index c7852f208f..0f5ed5625e 100644 --- a/test_integration/helpers/restore_wallet.dart +++ b/test_integration/helpers/restore_wallet.dart @@ -86,7 +86,7 @@ Future restoreWalletToTest(WidgetTester tester) async { const Offset(0, -15), ); await tester.pumpNFrames(10); - await tester.tapAndPump(importConfirmButton); + await tester.tap(importConfirmButton); print('🔍 RESTORE WALLET: Waiting for completion'); await tester.pumpUntilDisappear(walletsManagerWrapper); diff --git a/test_integration/runners/drivers/chrome_driver.dart b/test_integration/runners/drivers/chrome_driver.dart index f2af690159..ab87725ba0 100644 --- a/test_integration/runners/drivers/chrome_driver.dart +++ b/test_integration/runners/drivers/chrome_driver.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'chrome_config_manager.dart'; import 'find.dart'; import 'web_browser_driver.dart'; @@ -36,7 +38,11 @@ class ChromeDriver extends WebBrowserDriver with WebDriverProcessMixin { @override Future stop() async { - chromeConfigManager.restoreChromeConfiguration(); + try { + chromeConfigManager.restoreChromeConfiguration(); + } catch (e) { + print('Failed to restore Chrome configuration: $e'); + } await stopDriver(); } diff --git a/test_integration/tests/dex_tests/maker_orders_test.dart b/test_integration/tests/dex_tests/maker_orders_test.dart index 901c322ef2..c2c99490d3 100644 --- a/test_integration/tests/dex_tests/maker_orders_test.dart +++ b/test_integration/tests/dex_tests/maker_orders_test.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; +import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; import 'package:web_dex/shared/widgets/focusable_widget.dart'; import 'package:web_dex/views/dex/entities_list/orders/order_item.dart'; import '../../common/pause.dart'; import '../../common/widget_tester_action_extensions.dart'; +import '../../common/widget_tester_find_extension.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; import '../wallets_tests/wallet_tools.dart'; @@ -49,6 +51,8 @@ Future testMakerOrder(WidgetTester tester) async { final Finder orderListItem = find.byType(OrderItem); final Finder orderUuidWidget = find.byKey(const Key('maker-order-uuid')); + await useFaucetIfBalanceInsufficient(tester); + // Open maker order form await tester.tapAndPump(dexSectionButton); print('🔍 MAKER ORDER: Tapped DEX section button'); @@ -138,6 +142,77 @@ Future testMakerOrder(WidgetTester tester) async { expect(truncatedUuid?.isNotEmpty, isTrue); } +Future useFaucetIfBalanceInsufficient(WidgetTester tester) async { + final walletTab = find.byKeyName('main-menu-wallet'); + final coinsList = find.byKey(const Key('wallet-page-coins-list')); + final docItem = find.byKeyName('coins-manager-list-item-doc'); + final docCoinActive = find.byKeyName('active-coin-item-doc'); + final docCoinBalance = find.byKeyName('coin-balance-asset-doc'); + final martyItem = find.byKeyName('coins-manager-list-item-marty'); + final martyCoinActive = find.byKeyName('active-coin-item-marty'); + final martyCoinBalance = find.byKeyName('coin-balance-asset-marty'); + final walletPageScrollView = find.byKeyName('wallet-page-scroll-view'); + final faucetButton = find.byKeyName('coin-details-faucet-button'); + + await tester.tap(walletTab); + await tester.pumpAndSettle(); + + await addAsset(tester, asset: docItem, search: 'DOC'); + print('🔍 Added doc asset'); + await addAsset(tester, asset: martyItem, search: 'MARTY'); + print('🔍 Added marty asset'); + + await tester.dragUntilVisible( + docCoinActive, + walletPageScrollView, + const Offset(0, -50), + ); + await tester.pumpAndSettle(); + print('🔍 dragged until doc coin item visible'); + final docText = docCoinBalance.evaluate().single.widget as AutoScrollText; + final String? docBalanceStr = docText.text.split(' ').firstOrNull; + print('🔍 doc balance str: $docBalanceStr'); + final double? docBalance = double.tryParse(docBalanceStr ?? ''); + print('🔍 doc balance: $docBalance'); + if (docBalance != null && docBalance <= 0.2) { + await tester.tapAndPump(docCoinActive); + await tester.pumpAndSettle(); // wait for page and tx history + print('🔍 navigated to doc coin details page'); + await tester.tap(faucetButton); + await tester.pumpAndSettle(); // wait for page & loader + print('🔍 pressed faucet button for doc'); + await pause(sec: 60); + } + + await tester.tap(walletTab); + await tester.pumpAndSettle(); + + await tester.dragUntilVisible( + coinsList, + walletPageScrollView, + const Offset(0, -50), + ); + await tester.dragUntilVisible( + martyCoinActive, + walletPageScrollView, + const Offset(0, -50), + ); + final martyText = martyCoinBalance.evaluate().single.widget as AutoScrollText; + final String? martyBalanceStr = martyText.text.split(' ').firstOrNull; + print('🔍 marty balance str: $martyBalanceStr'); + final double? martyBalance = double.tryParse(martyBalanceStr ?? ''); + print('🔍 marty balance: $martyBalance'); + if (martyBalance != null && martyBalance <= 0.2) { + await tester.tapAndPump(martyCoinActive); + await tester.pumpAndSettle(); // wait for page and tx history + print('🔍 navigated to marty coin details page'); + await tester.tap(faucetButton); + await tester.pumpAndSettle(); // wait for page & loader + print('🔍 pressed faucet button for marty'); + await pause(sec: 60); + } +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets( diff --git a/test_integration/tests/suspended_assets_test/suspended_assets_test.dart b/test_integration/tests/suspended_assets_test/suspended_assets_test.dart index 824891d05a..917b8e8979 100644 --- a/test_integration/tests/suspended_assets_test/suspended_assets_test.dart +++ b/test_integration/tests/suspended_assets_test/suspended_assets_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: avoid_print +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -16,31 +18,35 @@ void main() { testWidgets( 'Run suspended asset tests:', (WidgetTester tester) async { - const String suspendedAsset = 'KMD'; - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); - - await acceptAlphaWarning(tester); - - print('RESTORE WALLET TO TEST'); - await restoreWalletToTest(tester); - await tester.pumpAndSettle(); - - await goto.walletPage(tester); - final Finder searchCoinsField = - find.byKey(const Key('wallet-page-search-field')); - await tester.enterText(searchCoinsField, suspendedAsset); - await tester.pumpAndSettle(); - final Finder suspendedCoinLabel = isMobile - ? find.byKey(const Key('retry-suspended-asset-$suspendedAsset')) - : find.byKey(const Key('suspended-asset-message-$suspendedAsset')); - expect( - suspendedCoinLabel, - findsOneWidget, - reason: 'Test error: $suspendedAsset should be suspended,' - ' but corresponding label was not found.', - ); + await runZonedGuarded(() async { + FlutterError.onError = (FlutterErrorDetails details) {/** */}; + + const String suspendedAsset = 'KMD'; + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + + await acceptAlphaWarning(tester); + + print('RESTORE WALLET TO TEST'); + await restoreWalletToTest(tester); + await tester.pumpAndSettle(); + + await goto.walletPage(tester); + final Finder searchCoinsField = + find.byKey(const Key('wallet-page-search-field')); + await tester.enterText(searchCoinsField, suspendedAsset); + await tester.pumpAndSettle(); + final Finder suspendedCoinLabel = isMobile + ? find.byKey(const Key('retry-suspended-asset-$suspendedAsset')) + : find.byKey(const Key('suspended-asset-message-$suspendedAsset')); + expect( + suspendedCoinLabel, + findsOneWidget, + reason: 'Test error: $suspendedAsset should be suspended,' + ' but corresponding label was not found.', + ); + }, (_, __) {/** */}); }, semanticsEnabled: false, ); diff --git a/test_integration/tests/wallets_tests/wallet_tools.dart b/test_integration/tests/wallets_tests/wallet_tools.dart index 894026e0e8..fb22693fb8 100644 --- a/test_integration/tests/wallets_tests/wallet_tools.dart +++ b/test_integration/tests/wallets_tests/wallet_tools.dart @@ -98,7 +98,8 @@ Future addAsset( return; } - await tester.tapAndPump(addAssetsButton); + await tester.tap(addAssetsButton); + await tester.pumpAndSettle(); // wait for page switch and list loading print('🔍 ADD ASSET: Tapped add assets button'); try { diff --git a/test_integration/tests/wallets_tests/wallets_tests.dart b/test_integration/tests/wallets_tests/wallets_tests.dart index 701bfccf7c..ad8ed75491 100644 --- a/test_integration/tests/wallets_tests/wallets_tests.dart +++ b/test_integration/tests/wallets_tests/wallets_tests.dart @@ -20,8 +20,6 @@ void main() { void walletsWidgetTests({ bool skip = false, - int retryLimit = 0, - Duration timeout = const Duration(minutes: 10), }) { return testWidgets( 'Run wallet tests:', @@ -43,8 +41,6 @@ void walletsWidgetTests({ // await testBitrefillIntegration(tester); }, semanticsEnabled: false, - timeout: Timeout(timeout), - retry: retryLimit, skip: skip, ); } diff --git a/test_units/tests/cex_market_data/profit_loss_repository_test.dart b/test_units/tests/cex_market_data/profit_loss_repository_test.dart index 0a4097bea8..43dc0b5a13 100644 --- a/test_units/tests/cex_market_data/profit_loss_repository_test.dart +++ b/test_units/tests/cex_market_data/profit_loss_repository_test.dart @@ -250,4 +250,4 @@ void testRealisedProfitLossRepository() { expect(result[1].profitLoss, 0.0); }); }); -} +} \ No newline at end of file diff --git a/test_units/tests/cex_market_data/transaction_generation.dart b/test_units/tests/cex_market_data/transaction_generation.dart index 6fb675c212..6af0b2f51f 100644 --- a/test_units/tests/cex_market_data/transaction_generation.dart +++ b/test_units/tests/cex_market_data/transaction_generation.dart @@ -1,5 +1,5 @@ -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; -import 'package:web_dex/model/withdraw_details/fee_details.dart'; +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; Transaction createBuyTransaction( double balanceChange, { @@ -7,20 +7,28 @@ Transaction createBuyTransaction( }) { final String value = balanceChange.toString(); return Transaction( + id: '0', blockHeight: 10000, - coin: 'BTC', + assetId: AssetId( + id: 'BTC', + name: 'Bitcoin', + symbol: AssetSymbol(assetConfigId: 'BTC'), + chainId: AssetChainId(chainId: 9), + derivationPath: '', + subClass: CoinSubClass.utxo, + ), confirmations: 6, - feeDetails: FeeDetails(type: 'utxo', coin: 'BTC'), - from: ['1ABC...'], + balanceChanges: BalanceChanges( + netChange: Decimal.parse(value), + receivedByMe: Decimal.parse(value), + spentByMe: Decimal.zero, + totalAmount: Decimal.parse(value), + ), + from: const ['1ABC...'], internalId: 'internal1', - myBalanceChange: value, - receivedByMe: value, - spentByMe: '0.0', - timestamp: timeStamp, - to: ['1XYZ...'], - totalAmount: value, + timestamp: DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000), + to: const ['1XYZ...'], txHash: 'hash1', - txHex: 'hex1', memo: 'Buy 1 BTC', ); } @@ -34,21 +42,30 @@ Transaction createSellTransaction( adjustedBalanceChange = -adjustedBalanceChange; } final String value = adjustedBalanceChange.toString(); + return Transaction( + id: '0', blockHeight: 100200, - coin: 'BTC', + assetId: AssetId( + id: 'BTC', + name: 'Bitcoin', + symbol: AssetSymbol(assetConfigId: 'BTC'), + chainId: AssetChainId(chainId: 9), + derivationPath: '', + subClass: CoinSubClass.utxo, + ), confirmations: 6, - feeDetails: FeeDetails(type: 'utxo', coin: 'BTC'), - from: ['1XYZ...'], + balanceChanges: BalanceChanges( + netChange: Decimal.parse(value), + receivedByMe: Decimal.zero, + spentByMe: Decimal.parse(adjustedBalanceChange.abs().toString()), + totalAmount: Decimal.parse(value), + ), + from: const ['1ABC...'], internalId: 'internal3', - myBalanceChange: value, - receivedByMe: '0.0', - spentByMe: adjustedBalanceChange.abs().toString(), - timestamp: timeStamp, - to: ['1GHI...'], - totalAmount: value, + timestamp: DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000), + to: const ['1GHI...'], txHash: 'hash3', - txHex: 'hex3', memo: 'Sell 0.5 BTC', ); } diff --git a/test_units/tests/utils/test_util.dart b/test_units/tests/utils/test_util.dart index 4977e49596..f5d8a7f130 100644 --- a/test_units/tests/utils/test_util.dart +++ b/test_units/tests/utils/test_util.dart @@ -4,15 +4,13 @@ import 'package:web_dex/model/coin_type.dart'; Coin setCoin( {double? usdPrice, double? change24h, String? coinAbbr, double? balance}) { - final coin = Coin( + return Coin( abbr: coinAbbr ?? 'KMD', accounts: null, activeByDefault: true, - bchdUrls: [], coingeckoId: "komodo", coinpaprikaId: "kmd-komodo", derivationPath: "m/44'/141'/0'", - electrum: [], explorerUrl: "https://kmdexplorer.io/address/", explorerAddressUrl: "address/", explorerTxUrl: "tx/", @@ -20,16 +18,15 @@ Coin setCoin( isTestCoin: false, mode: CoinMode.standard, name: 'Komodo', - nodes: [], priority: 30, protocolData: null, protocolType: 'UTXO', parentCoin: null, - rpcUrls: [], state: CoinState.inactive, swapContractAddress: null, type: CoinType.smartChain, walletOnly: false, + balance: balance, usdPrice: usdPrice != null ? CexPrice( price: usdPrice, @@ -39,8 +36,4 @@ Coin setCoin( ) : null, ); - if (balance != null) { - coin.balance = balance; - } - return coin; } From eda3b2a98efbd3816a25e8872e49bea9c594898c Mon Sep 17 00:00:00 2001 From: naezith Date: Wed, 8 Jan 2025 17:36:10 +0300 Subject: [PATCH 28/34] hide trading bot setting --- .../widgets/general_settings/general_settings.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/views/settings/widgets/general_settings/general_settings.dart b/lib/views/settings/widgets/general_settings/general_settings.dart index 4fc19b40f2..6928baa4e8 100644 --- a/lib/views/settings/widgets/general_settings/general_settings.dart +++ b/lib/views/settings/widgets/general_settings/general_settings.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/shared/widgets/hidden_with_wallet.dart'; import 'package:web_dex/shared/widgets/hidden_without_wallet.dart'; import 'package:web_dex/views/settings/widgets/general_settings/import_swaps.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_download_logs.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_analytics.dart'; -import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_test_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_trading_bot.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_reset_activated_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_theme_switcher.dart'; @@ -25,11 +25,10 @@ class GeneralSettings extends StatelessWidget { const SizedBox(height: 25), const SettingsManageAnalytics(), const SizedBox(height: 25), - const SettingsManageTestCoins(), - const SizedBox(height: 25), - const HiddenWithoutWallet( - child: SettingsManageTradingBot(), - ), + if (!kIsWalletOnly) + const HiddenWithoutWallet( + child: SettingsManageTradingBot(), + ), const SizedBox(height: 25), const SettingsDownloadLogs(), const SizedBox(height: 25), From 9b6661423690603e26997ec9976b0aecb04f78b8 Mon Sep 17 00:00:00 2001 From: naezith Date: Wed, 8 Jan 2025 18:28:09 +0300 Subject: [PATCH 29/34] default to wallet route parser if wallet only --- lib/router/parsers/root_route_parser.dart | 35 +++++++++++------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/router/parsers/root_route_parser.dart b/lib/router/parsers/root_route_parser.dart index 78eb65c0d4..ed5ca51a8f 100644 --- a/lib/router/parsers/root_route_parser.dart +++ b/lib/router/parsers/root_route_parser.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/model/first_uri_segment.dart'; import 'package:web_dex/router/parsers/base_route_parser.dart'; import 'package:web_dex/router/parsers/bridge_route_parser.dart'; @@ -11,26 +11,22 @@ import 'package:web_dex/router/parsers/wallet_route_parser.dart'; import 'package:web_dex/router/routes.dart'; class RootRouteInformationParser extends RouteInformationParser { - RootRouteInformationParser(this.coinsBloc); - - final CoinsBloc coinsBloc; - - Map get _parsers => { - firstUriSegment.wallet: WalletRouteParser(coinsBloc), - firstUriSegment.fiat: fiatRouteParser, - firstUriSegment.dex: dexRouteParser, - firstUriSegment.bridge: bridgeRouteParser, - firstUriSegment.nfts: nftRouteParser, - firstUriSegment.settings: settingsRouteParser, - }; + final Map _parsers = { + firstUriSegment.wallet: walletRouteParser, + firstUriSegment.fiat: fiatRouteParser, + firstUriSegment.dex: dexRouteParser, + firstUriSegment.bridge: bridgeRouteParser, + firstUriSegment.nfts: nftRouteParser, + firstUriSegment.settings: settingsRouteParser, + }; @override Future parseRouteInformation( RouteInformation routeInformation) async { - final BaseRouteParser parser = - _getRoutParser(Uri.parse(routeInformation.uri.path)); + final uri = Uri.parse(routeInformation.uri.path); + final BaseRouteParser parser = _getRoutParser(uri); - return parser.getRoutePath(routeInformation.uri); + return parser.getRoutePath(uri); } @override @@ -39,7 +35,10 @@ class RootRouteInformationParser extends RouteInformationParser { } BaseRouteParser _getRoutParser(Uri uri) { - if (uri.pathSegments.isEmpty) return dexRouteParser; - return _parsers[uri.pathSegments.first] ?? dexRouteParser; + const defaultRouteParser = + kIsWalletOnly ? walletRouteParser : dexRouteParser; + + if (uri.pathSegments.isEmpty) return defaultRouteParser; + return _parsers[uri.pathSegments.first] ?? defaultRouteParser; } } From c6f04b8ae4104ac003e8ae370c8c6eb2ec53886c Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 9 Jan 2025 10:05:47 +0200 Subject: [PATCH 30/34] Merge branch 'main' into merge/dev-hotfixes-v0_8_2 --- lib/mm2/mm2_api/mm2_api.dart | 337 ++++++++++++++++--------- lib/views/dex/dex_page.dart | 4 + lib/views/main_layout/main_layout.dart | 52 ++++ 3 files changed, 269 insertions(+), 124 deletions(-) diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 43ef4552b1..770a1592bd 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -11,6 +11,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/cancel_order/cancel_order_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/import_swaps/import_swaps_response.dart'; @@ -41,6 +43,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_request.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/stop/stop_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart'; @@ -74,24 +78,143 @@ class Mm2Api { path: 'api=> disableCoin => _call', trace: s, isError: true, - ); + ).ignore(); return; } } + Future getBalance(String abbr) async { + dynamic response; + try { + response = await _call(MyBalanceReq(coin: abbr)); + } catch (e, s) { + log( + 'Error getting balance $abbr: ${e.toString()}', + path: 'api => getBalance => _call', + trace: s, + isError: true, + ); + return null; + } + + Map json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of get balance $abbr response: ${e.toString()}', + path: 'api => getBalance => jsonDecode', + trace: s, + isError: true, + ); + return null; + } + + return json['balance']; + } + + Future getMaxMakerVol(String abbr) async { + dynamic response; + try { + response = await _call(MaxMakerVolRequest(coin: abbr)); + } catch (e, s) { + log( + 'Error getting max maker vol $abbr: ${e.toString()}', + path: 'api => getMaxMakerVol => _call', + trace: s, + isError: true, + ); + return _fallbackToBalance(abbr); + } + + Map json; + try { + json = jsonDecode(response); + } catch (e, s) { + log( + 'Error parsing of max maker vol $abbr response: ${e.toString()}', + path: 'api => getMaxMakerVol => jsonDecode', + trace: s, + isError: true, + ); + return _fallbackToBalance(abbr); + } + + final error = json['error']; + if (error != null) { + log( + 'Error parsing of max maker vol $abbr response: ${error.toString()}', + path: 'api => getMaxMakerVol => error', + isError: true, + ); + return _fallbackToBalance(abbr); + } + + try { + return MaxMakerVolResponse.fromJson(json['result']); + } catch (e, s) { + log( + 'Error constructing MaxMakerVolResponse for $abbr: ${e.toString()}', + path: 'api => getMaxMakerVol => fromJson', + trace: s, + isError: true, + ); + return _fallbackToBalance(abbr); + } + } + + Future _fallbackToBalance(String abbr) async { + final balance = await getBalance(abbr); + if (balance == null) { + log( + 'Failed to retrieve balance for fallback construction of MaxMakerVolResponse for $abbr', + path: 'api => _fallbackToBalance', + isError: true, + ); + return null; + } + + final balanceValue = MaxMakerVolResponseValue(decimal: balance); + return MaxMakerVolResponse( + volume: balanceValue, + balance: balanceValue, + ); + } + + Future _fallbackToBalanceTaker(String abbr) async { + final balance = await getBalance(abbr); + if (balance == null) { + log( + 'Failed to retrieve balance for fallback construction of MaxTakerVolResponse for $abbr', + path: 'api => _fallbackToBalanceTaker', + isError: true, + ); + return null; + } + final rational = Rational.parse(balance); + final result = MaxTakerVolumeResponseResult( + numer: rational.numerator.toString(), + denom: rational.denominator.toString(), + ); + + return MaxTakerVolResponse( + coin: abbr, + result: result, + ); + } + Future?> getActiveSwaps( ActiveSwapsRequest request, ) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting active swaps: ${e.toString()}', + 'Error getting active swaps: $e', path: 'api => getActiveSwaps', trace: s, isError: true, - ); + ).ignore(); return {'error': 'something went wrong'}; } } @@ -101,36 +224,30 @@ class Mm2Api { String address, ) async { try { - final dynamic response = await _call( + return await _mm2.call( ValidateAddressRequest(coin: coinAbbr, address: address), ); - final Map json = jsonDecode(response); - - return json; } catch (e, s) { log( - 'Error validating address $coinAbbr: ${e.toString()}', + 'Error validating address $coinAbbr: $e', path: 'api => validateAddress', trace: s, isError: true, - ); + ).ignore(); return null; } } Future?> withdraw(WithdrawRequest request) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error withdrawing ${request.params.coin}: ${e.toString()}', + 'Error withdrawing ${request.params.coin}: $e', path: 'api => withdraw', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -149,7 +266,7 @@ class Mm2Api { return SendRawTransactionResponse.fromJson(response); } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.coin}: $e', path: 'api => sendRawTransaction', trace: s, isError: true, @@ -165,17 +282,14 @@ class Mm2Api { MyTxHistoryRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error sending raw transaction ${request.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.coin}: $e', path: 'api => getTransactions', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -184,17 +298,14 @@ class Mm2Api { MyTxHistoryV2Request request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error sending raw transaction ${request.params.coin}: ${e.toString()}', + 'Error sending raw transaction ${request.params.coin}: $e', path: 'api => getTransactions', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -203,92 +314,84 @@ class Mm2Api { KmdRewardsInfoRequest request, ) async { try { - final dynamic response = await _call(request); - final Map json = jsonDecode(response); - - return json; + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting rewards info: ${e.toString()}', + 'Error getting rewards info: $e', path: 'api => getRewardsInfo', trace: s, isError: true, - ); + ).ignore(); return null; } } Future?> getBestOrders(BestOrdersRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error getting best orders ${request.coin}: ${e.toString()}', + 'Error getting best orders ${request.coin}: $e', path: 'api => getBestOrders', trace: s, isError: true, - ); - return {'error': e}; + ).ignore(); + return {'error': e.toString()}; } } Future> sell(SellRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error sell ${request.base}/${request.rel}: ${e.toString()}', + 'Error sell ${request.base}/${request.rel}: $e', path: 'api => sell', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future?> setprice(SetPriceRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request) as Map?; } catch (e, s) { log( - 'Error setprice ${request.base}/${request.rel}: ${e.toString()}', + 'Error setprice ${request.base}/${request.rel}: $e', path: 'api => setprice', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future> cancelOrder(CancelOrderRequest request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error cancelOrder ${request.uuid}: ${e.toString()}', + 'Error cancelOrder ${request.uuid}: $e', path: 'api => cancelOrder', trace: s, isError: true, - ); + ).ignore(); return {'error': e}; } } Future> getSwapStatus(MySwapStatusReq request) async { try { - final String response = await _call(request); - return jsonDecode(response); + return await _mm2.call(request); } catch (e, s) { log( - 'Error sell getting swap status ${request.uuid}: ${e.toString()}', + 'Error sell getting swap status ${request.uuid}: $e', path: 'api => getSwapStatus', trace: s, isError: true, - ); + ).ignore(); return {'error': 'something went wrong'}; } } @@ -300,44 +403,42 @@ class Mm2Api { } final MyOrdersRequest request = MyOrdersRequest(); - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return MyOrdersResponse.fromJson(json); + return MyOrdersResponse.fromJson(response); } catch (e, s) { log( - 'Error getting my orders: ${e.toString()}', + 'Error getting my orders: $e', path: 'api => getMyOrders', trace: s, isError: true, - ); + ).ignore(); return null; } } Future getRawSwapData(MyRecentSwapsRequest request) async { - return await _call(request); + return jsonEncode(await _mm2.call(request)); } Future getMyRecentSwaps( MyRecentSwapsRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return MyRecentSwapsResponse.fromJson(json); + return MyRecentSwapsResponse.fromJson(response); } catch (e, s) { log( - 'Error getting my recent swaps: ${e.toString()}', + 'Error getting my recent swaps: $e', path: 'api => getMyRecentSwaps', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -345,38 +446,36 @@ class Mm2Api { Future getOrderStatus(String uuid) async { try { final OrderStatusRequest request = OrderStatusRequest(uuid: uuid); - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return OrderStatusResponse.fromJson(json); + return OrderStatusResponse.fromJson(response); } catch (e, s) { log( - 'Error getting order status $uuid: ${e.toString()}', + 'Error getting order status $uuid: $e', path: 'api => getOrderStatus', trace: s, isError: true, - ); + ).ignore(); return null; } } Future importSwaps(ImportSwapsRequest request) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); - if (json['error'] != null) { + final JsonMap response = await _mm2.call(request); + if (response['error'] != null) { return null; } - return ImportSwapsResponse.fromJson(json); + return ImportSwapsResponse.fromJson(response); } catch (e, s) { log( - 'Error import swaps : ${e.toString()}', + 'Error import swaps : $e', path: 'api => importSwaps', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -385,24 +484,23 @@ class Mm2Api { RecoverFundsOfSwapRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { log( 'Error recovering funds of swap ${request.uuid}: ${json['error']}', path: 'api => recoverFundsOfSwap', isError: true, - ); + ).ignore(); return null; } return RecoverFundsOfSwapResponse.fromJson(json); } catch (e, s) { log( - 'Error recovering funds of swap ${request.uuid}: ${e.toString()}', + 'Error recovering funds of swap ${request.uuid}: $e', path: 'api => recoverFundsOfSwap', trace: s, isError: true, - ); + ).ignore(); return null; } } @@ -411,19 +509,18 @@ class Mm2Api { MaxTakerVolRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return await _fallbackToBalanceTaker(request.coin); } return MaxTakerVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting max taker volume ${request.coin}: ${e.toString()}', + 'Error getting max taker volume ${request.coin}: $e', path: 'api => getMaxTakerVolume', trace: s, isError: true, - ); + ).ignore(); return await _fallbackToBalanceTaker(request.coin); } } @@ -432,32 +529,30 @@ class Mm2Api { MinTradingVolRequest request, ) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } return MinTradingVolResponse.fromJson(json); } catch (e, s) { log( - 'Error getting min trading volume ${request.coin}: ${e.toString()}', + 'Error getting min trading volume ${request.coin}: $e', path: 'api => getMinTradingVol', trace: s, isError: true, - ); + ).ignore(); return null; } } Future getOrderbook(OrderbookRequest request) async { try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return OrderbookResponse( request: request, - error: json['error'], + error: json['error'] as String?, ); } @@ -467,11 +562,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting orderbook ${request.base}/${request.rel}: ${e.toString()}', + 'Error getting orderbook ${request.base}/${request.rel}: $e', path: 'api => getOrderbook', trace: s, isError: true, - ); + ).ignore(); return OrderbookResponse( request: request, @@ -486,18 +581,17 @@ class Mm2Api { ) async { final request = OrderBookDepthReq(pairs: pairs); try { - final String response = await _call(request); - final Map json = jsonDecode(response); + final JsonMap json = await _mm2.call(request); if (json['error'] != null) { return null; } return OrderBookDepthResponse.fromJson(json, coinsRepository); } catch (e, s) { log( - 'Error getting orderbook depth $request: ${e.toString()}', + 'Error getting orderbook depth $request: $e', path: 'api => getOrderBookDepth', trace: s, - ); + ).ignore(); } return null; } @@ -508,8 +602,7 @@ class Mm2Api { TradePreimageRequest request, ) async { try { - final String response = await _call(request); - final Map responseJson = await jsonDecode(response); + final JsonMap responseJson = await _mm2.call(request); if (responseJson['error'] != null) { return ApiResponse(request: request, error: responseJson); } @@ -519,11 +612,11 @@ class Mm2Api { ); } catch (e, s) { log( - 'Error getting trade preimage ${request.base}/${request.rel}: ${e.toString()}', + 'Error getting trade preimage ${request.base}/${request.rel}: $e', path: 'api => getTradePreimage', trace: s, isError: true, - ); + ).ignore(); return ApiResponse( request: request, ); @@ -543,25 +636,22 @@ class Mm2Api { MarketMakerBotRequest marketMakerBotRequest, ) async { try { - final dynamic response = await _call(marketMakerBotRequest.toJson()); + final JsonMap response = await _mm2.call(marketMakerBotRequest.toJson()); log( - response, + response.toString(), path: 'api => ${marketMakerBotRequest.method} => _call', - ); + ).ignore(); - if (response is String) { - final Map responseJson = jsonDecode(response); - if (responseJson['error'] != null) { - throw RpcException(RpcError.fromJson(responseJson)); - } + if (response['error'] != null) { + throw RpcException(RpcError.fromJson(response)); } } catch (e, s) { log( - 'Error starting or stopping simple market maker bot: ${e.toString()}', + 'Error starting or stopping simple market maker bot: $e', path: 'api => start_simple_market_maker_bot => _call', trace: s, isError: true, - ); + ).ignore(); rethrow; } } @@ -583,22 +673,21 @@ class Mm2Api { Future convertLegacyAddress(ConvertAddressRequest request) async { try { - final String response = await _call(request); - final Map responseJson = jsonDecode(response); - return responseJson['result']?['address']; + final JsonMap responseJson = await _mm2.call(request); + return responseJson['result']?['address'] as String?; } catch (e, s) { log( - 'Convert address error: ${e.toString()}', + 'Convert address error: $e', path: 'api => convertLegacyAddress', trace: s, isError: true, - ); + ).ignore(); return null; } } Future stop() async { - await _call(StopReq()); + await _mm2.call(StopReq()); } Future showPrivKey( diff --git a/lib/views/dex/dex_page.dart b/lib/views/dex/dex_page.dart index 816ed4d156..8d969a2ee2 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -1,6 +1,7 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; @@ -42,6 +43,9 @@ class _DexPageState extends State { @override Widget build(BuildContext context) { + if (kIsWalletOnly) { + return const Placeholder(child: Text('You should not see this page')); + } final tradingEntitiesBloc = RepositoryProvider.of(context); final coinsRepository = RepositoryProvider.of(context); diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 353e7c4d18..38a19d3138 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -66,4 +66,56 @@ class _MainLayoutState extends State { ), ); } + + Widget _buildAppBody() { + return StreamBuilder( + initialData: startUpBloc.running, + stream: startUpBloc.outRunning, + builder: (context, snapshot) { + log('_LayoutWrapperState.build([context]) StreamBuilder: $snapshot'); + if (!snapshot.hasData) { + return const Center(child: UiSpinner()); + } + + return MainLayoutRouter(); + }); + } + + // Method to show an alert dialog with an option to agree if the app is in + // debug mode stating that trading features may not be used for actual trading + // and that only test assets/networks may be used. + Future _showDebugModeDialog() async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('Debug mode'), + content: const Text( + 'This app is in debug mode. Trading features may not be used for ' + 'actual trading. Only test assets/networks may be used.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _saveAgreedState().ignore(); + }, + child: const Text('I agree'), + ), + ], + ); + }, + ); + } + + Future _saveAgreedState() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool('wallet_only_agreed', true); + } + + Future _hasAgreedNoTrading() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('wallet_only_agreed') ?? false; + } } From eb912aecdbd5820e930da92b38a0b3443521f393 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 9 Jan 2025 10:23:52 +0200 Subject: [PATCH 31/34] add removed max maker vol models --- lib/mm2/mm2_api/mm2_api.dart | 71 +------------------ .../rpc/max_maker_vol/max_maker_vol_req.dart | 20 ++++++ .../max_maker_vol/max_maker_vol_response.dart | 30 ++++++++ 3 files changed, 52 insertions(+), 69 deletions(-) create mode 100644 lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart create mode 100644 lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index 770a1592bd..f0be1b7316 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:rational/rational.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart'; @@ -86,7 +87,7 @@ class Mm2Api { Future getBalance(String abbr) async { dynamic response; try { - response = await _call(MyBalanceReq(coin: abbr)); + response = await _mm2.call(MyBalanceReq(coin: abbr)); } catch (e, s) { log( 'Error getting balance $abbr: ${e.toString()}', @@ -113,74 +114,6 @@ class Mm2Api { return json['balance']; } - Future getMaxMakerVol(String abbr) async { - dynamic response; - try { - response = await _call(MaxMakerVolRequest(coin: abbr)); - } catch (e, s) { - log( - 'Error getting max maker vol $abbr: ${e.toString()}', - path: 'api => getMaxMakerVol => _call', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - - Map json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of max maker vol $abbr response: ${e.toString()}', - path: 'api => getMaxMakerVol => jsonDecode', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - - final error = json['error']; - if (error != null) { - log( - 'Error parsing of max maker vol $abbr response: ${error.toString()}', - path: 'api => getMaxMakerVol => error', - isError: true, - ); - return _fallbackToBalance(abbr); - } - - try { - return MaxMakerVolResponse.fromJson(json['result']); - } catch (e, s) { - log( - 'Error constructing MaxMakerVolResponse for $abbr: ${e.toString()}', - path: 'api => getMaxMakerVol => fromJson', - trace: s, - isError: true, - ); - return _fallbackToBalance(abbr); - } - } - - Future _fallbackToBalance(String abbr) async { - final balance = await getBalance(abbr); - if (balance == null) { - log( - 'Failed to retrieve balance for fallback construction of MaxMakerVolResponse for $abbr', - path: 'api => _fallbackToBalance', - isError: true, - ); - return null; - } - - final balanceValue = MaxMakerVolResponseValue(decimal: balance); - return MaxMakerVolResponse( - volume: balanceValue, - balance: balanceValue, - ); - } - Future _fallbackToBalanceTaker(String abbr) async { final balance = await getBalance(abbr); if (balance == null) { diff --git a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart new file mode 100644 index 0000000000..5b1423db50 --- /dev/null +++ b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_req.dart @@ -0,0 +1,20 @@ +class MaxMakerVolRequest { + MaxMakerVolRequest({ + required this.coin, + }); + + static const String method = 'max_maker_vol'; + final String coin; + late String userpass; + + Map toJson() { + return { + 'method': method, + 'mmrpc': '2.0', + 'userpass': userpass, + 'params': { + 'coin': coin, + }, + }; + } +} diff --git a/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart new file mode 100644 index 0000000000..463e8db80e --- /dev/null +++ b/lib/mm2/mm2_api/rpc/max_maker_vol/max_maker_vol_response.dart @@ -0,0 +1,30 @@ +class MaxMakerVolResponse { + MaxMakerVolResponse({ + required this.volume, + required this.balance, + }); + + final MaxMakerVolResponseValue volume; + final MaxMakerVolResponseValue balance; + + factory MaxMakerVolResponse.fromJson(Map json) => + MaxMakerVolResponse( + volume: MaxMakerVolResponseValue.fromJson(json['volume']), + balance: MaxMakerVolResponseValue.fromJson(json['balance']), + ); +} + +class MaxMakerVolResponseValue { + MaxMakerVolResponseValue({ + required this.decimal, + }); + + final String decimal; + + factory MaxMakerVolResponseValue.fromJson(Map json) => + MaxMakerVolResponseValue(decimal: json['decimal']); + + Map toJson() => { + 'decimal': decimal, + }; +} From 853517b08ea5b1ced38c04a006a4d3f7e851568b Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 9 Jan 2025 10:27:55 +0200 Subject: [PATCH 32/34] fix merge conflicts with moved/modified code --- lib/views/main_layout/main_layout.dart | 14 ------------ .../coin_details_common_buttons.dart | 22 +++++++++---------- .../coin_details_info/coin_details_info.dart | 6 ++--- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 38a19d3138..66ce61801d 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -67,20 +67,6 @@ class _MainLayoutState extends State { ); } - Widget _buildAppBody() { - return StreamBuilder( - initialData: startUpBloc.running, - stream: startUpBloc.outRunning, - builder: (context, snapshot) { - log('_LayoutWrapperState.build([context]) StreamBuilder: $snapshot'); - if (!snapshot.hasData) { - return const Center(child: UiSpinner()); - } - - return MainLayoutRouter(); - }); - } - // Method to show an alert dialog with an option to agree if the app is in // debug mode stating that trading features may not be used for actual trading // and that only test assets/networks may be used. diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index e18c169bd6..3f50fddd01 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -17,7 +17,7 @@ class CoinDetailsCommonButtons extends StatelessWidget { const CoinDetailsCommonButtons({ required this.isMobile, required this.selectWidget, - required this.clickSwapButton, + required this.onClickSwapButton, required this.coin, super.key, }); @@ -25,7 +25,7 @@ class CoinDetailsCommonButtons extends StatelessWidget { final bool isMobile; final Coin coin; final void Function(CoinPageType) selectWidget; - final VoidCallback? clickSwapButton; + final VoidCallback? onClickSwapButton; @override Widget build(BuildContext context) { @@ -34,14 +34,14 @@ class CoinDetailsCommonButtons extends StatelessWidget { coin: coin, isMobile: isMobile, selectWidget: selectWidget, - clickSwapButton: clickSwapButton, + clickSwapButton: onClickSwapButton, context: context, ) : CoinDetailsCommonButtonsDesktopLayout( isMobile: isMobile, coin: coin, selectWidget: selectWidget, - clickSwapButton: clickSwapButton, + clickSwapButton: onClickSwapButton, context: context, ); } @@ -60,7 +60,7 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { final Coin coin; final bool isMobile; final void Function(CoinPageType p1) selectWidget; - final VoidCallback clickSwapButton; + final VoidCallback? clickSwapButton; final BuildContext context; @override @@ -114,7 +114,7 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { child: CoinDetailsSwapButton( isMobile: isMobile, coin: coin, - clickSwapButton: clickSwapButton, + onClickSwapButton: clickSwapButton, context: context, ), ), @@ -138,7 +138,7 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { final bool isMobile; final Coin coin; final void Function(CoinPageType p1) selectWidget; - final VoidCallback clickSwapButton; + final VoidCallback? clickSwapButton; final BuildContext context; @override @@ -171,7 +171,7 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { child: CoinDetailsSwapButton( isMobile: isMobile, coin: coin, - clickSwapButton: clickSwapButton, + onClickSwapButton: clickSwapButton, context: context, ), ), @@ -291,14 +291,14 @@ class CoinDetailsSwapButton extends StatelessWidget { const CoinDetailsSwapButton({ required this.isMobile, required this.coin, - required this.clickSwapButton, + required this.onClickSwapButton, required this.context, super.key, }); final bool isMobile; final Coin coin; - final VoidCallback clickSwapButton; + final VoidCallback? onClickSwapButton; final BuildContext context; @override @@ -322,7 +322,7 @@ class CoinDetailsSwapButton extends StatelessWidget { '$assetsPath/others/swap.svg', ), ), - onPressed: coin.isSuspended ? null : clickSwapButton, + onPressed: coin.isSuspended ? null : onClickSwapButton, ); } } diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index 3d75910997..235e5e0f5f 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -250,7 +250,7 @@ class _DesktopCoinDetails extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: false, selectWidget: setPageType, - clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() ? null : () => _goToSwap(context, coin), coin: coin, @@ -347,7 +347,7 @@ class _CoinDetailsInfoHeader extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: true, selectWidget: setPageType, - clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() ? null : () => _goToSwap(context, coin), coin: coin, @@ -379,7 +379,7 @@ class _CoinDetailsMarketMetricsTabBar extends StatelessWidget { profitLossState is! PortfolioProfitLossChartUnsupported; final areChartsSupported = isPortfolioGrowthSupported || isProfitLossSupported; - final numChartsSupported = 0 + + final numChartsSupported = 0 + (isPortfolioGrowthSupported ? 1 : 0) + (isProfitLossSupported ? 1 : 0); From 4e7f329723ab17a6b55686e96f0e2c8fd0204acc Mon Sep 17 00:00:00 2001 From: naezith Date: Thu, 9 Jan 2025 19:37:04 +0300 Subject: [PATCH 33/34] wallet only logout confirmation description --- assets/translations/en.json | 1 + lib/generated/codegen_loader.g.dart | 1 + lib/shared/widgets/logout_popup.dart | 101 +++++++++++++-------------- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index c6f028d426..665dec1a10 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -194,6 +194,7 @@ "swapFeeDetailsNone": "None", "swapFeeDetailsPaidFromReceivedVolume": "Paid from received volume", "logoutPopupTitle": "Confirm log out?", + "logoutPopupDescriptionWalletOnly": "Are you sure you want to logout?", "logoutPopupDescription": "Are you sure you want to logout? Your opened orders will no longer be available to match with other users and any trades in progress may not be completed", "transactionDetailsTitle": "Transaction completed", "customSeedWarningText": "Custom seed phrases are generally less secure and easier to crack than a generated BIP39 compliant seed phrase. To confirm you understand and are aware of the risk, type \"I\u00A0Understand\" in the box below.", diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 1b0948b52a..96acec1a76 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -190,6 +190,7 @@ abstract class LocaleKeys { static const swapFeeDetailsPaidFromReceivedVolume = 'swapFeeDetailsPaidFromReceivedVolume'; static const logoutPopupTitle = 'logoutPopupTitle'; static const logoutPopupDescription = 'logoutPopupDescription'; + static const logoutPopupDescriptionWalletOnly = 'logoutPopupDescriptionWalletOnly'; static const transactionDetailsTitle = 'transactionDetailsTitle'; static const customSeedWarningText = 'customSeedWarningText'; static const customSeedIUnderstand = 'customSeedIUnderstand'; diff --git a/lib/shared/widgets/logout_popup.dart b/lib/shared/widgets/logout_popup.dart index 50eab6c528..6b209b3f71 100644 --- a/lib/shared/widgets/logout_popup.dart +++ b/lib/shared/widgets/logout_popup.dart @@ -1,11 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc_event.dart'; +import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class LogOutPopup extends StatelessWidget { const LogOutPopup({ @@ -18,63 +20,56 @@ class LogOutPopup extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final currentWallet = state.currentUser?.wallet; - return Container( - constraints: const BoxConstraints(maxWidth: 300), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + return Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SelectableText( + LocaleKeys.logoutPopupTitle.tr(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 12), + if (currentWalletBloc.wallet?.config.type == WalletType.iguana) + SelectableText( + kIsWalletOnly + ? LocaleKeys.logoutPopupDescriptionWalletOnly.tr() + : LocaleKeys.logoutPopupDescription.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 25), + Row( mainAxisSize: MainAxisSize.min, children: [ - SelectableText( - LocaleKeys.logoutPopupTitle.tr(), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - ), + UiUnderlineTextButton( + key: const Key('popup-cancel-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.cancel.tr(), + onPressed: onCancel, ), - const SizedBox(height: 12), - if (currentWallet?.config.type == WalletType.iguana) - SelectableText( - LocaleKeys.logoutPopupDescription.tr(), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 25), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - UiUnderlineTextButton( - key: const Key('popup-cancel-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.cancel.tr(), - onPressed: onCancel, - ), - const SizedBox(width: 12), - UiPrimaryButton( - key: const Key('popup-confirm-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.logOut.tr(), - onPressed: () => _onConfirmLogout(context), - ), - ], + const SizedBox(width: 12), + UiPrimaryButton( + key: const Key('popup-confirm-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.logOut.tr(), + onPressed: () { + context.read().add(const AuthLogOutEvent()); + onConfirm(); + }, ), ], ), - ); - }, + ], + ), ); } - - void _onConfirmLogout(BuildContext context) { - // stop listening to balance updates before logging out - context.read().add(CoinsSessionEnded()); - context.read().add(const AuthSignOutRequested()); - onConfirm(); - } } From 6de0759ad6b08d4c91b2be510d1b09664deae075 Mon Sep 17 00:00:00 2001 From: naezith Date: Thu, 9 Jan 2025 22:54:08 +0300 Subject: [PATCH 34/34] Merge branch 'dev' into disable-trading-bot-setting --- .github/workflows/unit-tests-on-pr.yml | 2 +- app_build/build_config.json | 3 +- lib/router/parsers/root_route_parser.dart | 31 +++--- lib/shared/widgets/logout_popup.dart | 102 ++++++++++-------- .../general_settings/general_settings.dart | 3 + .../lib/src/images/coin_icon.dart | 3 +- 6 files changed, 81 insertions(+), 63 deletions(-) diff --git a/.github/workflows/unit-tests-on-pr.yml b/.github/workflows/unit-tests-on-pr.yml index 159034ca51..820dcb9b3e 100644 --- a/.github/workflows/unit-tests-on-pr.yml +++ b/.github/workflows/unit-tests-on-pr.yml @@ -8,7 +8,7 @@ on: jobs: unit_tests_: - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] timeout-minutes: 15 steps: diff --git a/app_build/build_config.json b/app_build/build_config.json index 2f102fa952..dd99aa334c 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -18,10 +18,11 @@ } }, "coins": { + "fetch_at_build_enabled": true, "update_commit_on_build": true, "bundled_coins_repo_commit": "b27db8e6e1c6a9264219fef8292811122538088a", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", - "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", + "coins_repo_content_url": "https://komodoplatform.github.io/coins", "coins_repo_branch": "master", "runtime_updates_enabled": true, "mapped_files": { diff --git a/lib/router/parsers/root_route_parser.dart b/lib/router/parsers/root_route_parser.dart index ed5ca51a8f..f46246480e 100644 --- a/lib/router/parsers/root_route_parser.dart +++ b/lib/router/parsers/root_route_parser.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/model/first_uri_segment.dart'; import 'package:web_dex/router/parsers/base_route_parser.dart'; import 'package:web_dex/router/parsers/bridge_route_parser.dart'; @@ -11,22 +12,26 @@ import 'package:web_dex/router/parsers/wallet_route_parser.dart'; import 'package:web_dex/router/routes.dart'; class RootRouteInformationParser extends RouteInformationParser { - final Map _parsers = { - firstUriSegment.wallet: walletRouteParser, - firstUriSegment.fiat: fiatRouteParser, - firstUriSegment.dex: dexRouteParser, - firstUriSegment.bridge: bridgeRouteParser, - firstUriSegment.nfts: nftRouteParser, - firstUriSegment.settings: settingsRouteParser, - }; + RootRouteInformationParser(this.coinsBloc); + + final CoinsBloc coinsBloc; + + Map get _parsers => { + firstUriSegment.wallet: WalletRouteParser(coinsBloc), + firstUriSegment.fiat: fiatRouteParser, + firstUriSegment.dex: dexRouteParser, + firstUriSegment.bridge: bridgeRouteParser, + firstUriSegment.nfts: nftRouteParser, + firstUriSegment.settings: settingsRouteParser, + }; @override Future parseRouteInformation( RouteInformation routeInformation) async { - final uri = Uri.parse(routeInformation.uri.path); - final BaseRouteParser parser = _getRoutParser(uri); + final BaseRouteParser parser = + _getRoutParser(Uri.parse(routeInformation.uri.path)); - return parser.getRoutePath(uri); + return parser.getRoutePath(routeInformation.uri); } @override @@ -35,8 +40,8 @@ class RootRouteInformationParser extends RouteInformationParser { } BaseRouteParser _getRoutParser(Uri uri) { - const defaultRouteParser = - kIsWalletOnly ? walletRouteParser : dexRouteParser; + final defaultRouteParser = + kIsWalletOnly ? _parsers[firstUriSegment.wallet]! : dexRouteParser; if (uri.pathSegments.isEmpty) return defaultRouteParser; return _parsers[uri.pathSegments.first] ?? defaultRouteParser; diff --git a/lib/shared/widgets/logout_popup.dart b/lib/shared/widgets/logout_popup.dart index 6b209b3f71..d457f46681 100644 --- a/lib/shared/widgets/logout_popup.dart +++ b/lib/shared/widgets/logout_popup.dart @@ -2,12 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/auth_bloc/auth_bloc_event.dart'; -import 'package:web_dex/blocs/blocs.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class LogOutPopup extends StatelessWidget { const LogOutPopup({ @@ -20,56 +19,65 @@ class LogOutPopup extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxWidth: 300), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SelectableText( - LocaleKeys.logoutPopupTitle.tr(), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 12), - if (currentWalletBloc.wallet?.config.type == WalletType.iguana) - SelectableText( - kIsWalletOnly - ? LocaleKeys.logoutPopupDescriptionWalletOnly.tr() - : LocaleKeys.logoutPopupDescription.tr(), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 25), - Row( + return BlocBuilder( + builder: (context, state) { + final currentWallet = state.currentUser?.wallet; + return Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - UiUnderlineTextButton( - key: const Key('popup-cancel-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.cancel.tr(), - onPressed: onCancel, + SelectableText( + LocaleKeys.logoutPopupTitle.tr(), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), ), - const SizedBox(width: 12), - UiPrimaryButton( - key: const Key('popup-confirm-logout-button'), - width: 120, - height: 36, - text: LocaleKeys.logOut.tr(), - onPressed: () { - context.read().add(const AuthLogOutEvent()); - onConfirm(); - }, + const SizedBox(height: 12), + if (currentWallet?.config.type == WalletType.iguana) + SelectableText( + kIsWalletOnly + ? LocaleKeys.logoutPopupDescriptionWalletOnly.tr() + : LocaleKeys.logoutPopupDescription.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 25), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + UiUnderlineTextButton( + key: const Key('popup-cancel-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.cancel.tr(), + onPressed: onCancel, + ), + const SizedBox(width: 12), + UiPrimaryButton( + key: const Key('popup-confirm-logout-button'), + width: 120, + height: 36, + text: LocaleKeys.logOut.tr(), + onPressed: () => _onConfirmLogout(context), + ), + ], ), ], ), - ], - ), + ); + }, ); } + + void _onConfirmLogout(BuildContext context) { + // stop listening to balance updates before logging out + context.read().add(CoinsSessionEnded()); + context.read().add(const AuthSignOutRequested()); + onConfirm(); + } } diff --git a/lib/views/settings/widgets/general_settings/general_settings.dart b/lib/views/settings/widgets/general_settings/general_settings.dart index 6928baa4e8..b9f9e2880a 100644 --- a/lib/views/settings/widgets/general_settings/general_settings.dart +++ b/lib/views/settings/widgets/general_settings/general_settings.dart @@ -6,6 +6,7 @@ import 'package:web_dex/shared/widgets/hidden_without_wallet.dart'; import 'package:web_dex/views/settings/widgets/general_settings/import_swaps.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_download_logs.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_analytics.dart'; +import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_test_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_manage_trading_bot.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_reset_activated_coins.dart'; import 'package:web_dex/views/settings/widgets/general_settings/settings_theme_switcher.dart'; @@ -25,6 +26,8 @@ class GeneralSettings extends StatelessWidget { const SizedBox(height: 25), const SettingsManageAnalytics(), const SizedBox(height: 25), + const SettingsManageTestCoins(), + const SizedBox(height: 25), if (!kIsWalletOnly) const HiddenWithoutWallet( child: SettingsManageTradingBot(), diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 57bcb448e3..4d729332ad 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,7 +2,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -const coinImagesFolder = 'coin_icons/png/'; +const coinImagesFolder = 'packages/komodo_defi_framework/assets/coin_icons/png/'; +// NB: ENSURE IT STAYS IN SYNC WITH MAIN PROJECT in `lib/src/utils/utils.dart`. const mediaCdnUrl = 'https://komodoplatform.github.io/coins/icons/'; final Map _assetExistenceCache = {};