diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..0875d99d27 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,117 @@ +FROM docker.io/ubuntu:22.04 + +ENV FLUTTER_VERSION="3.29.0" +ENV USER="komodo" +ENV USER_ID=1000 +ENV PATH=$PATH:/opt/flutter/bin +ENV PATH=$PATH:/android-ndk/bin +ENV ANDROID_HOME=/opt/android-sdk-linux \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANGUAGE=en_US:en +ENV TMPDIR=/tmp/ \ + ANDROID_DATA=/ \ + ANDROID_DNS_MODE=local \ + ANDROID_ROOT=/system + +ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ + PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator + +# comes from https://developer.android.com/studio/#command-tools +ENV ANDROID_SDK_TOOLS_VERSION=11076708 + +# https://developer.android.com/studio/releases/build-tools +ENV ANDROID_PLATFORM_VERSION=35 +ENV ANDROID_BUILD_TOOLS_VERSION=35.0.1 + +# https://developer.android.com/ndk/downloads +ENV ANDROID_NDK_VERSION=27.2.12479018 + +RUN apt update && apt install -y sudo && \ + useradd -u $USER_ID -m $USER && \ + usermod -aG sudo $USER && \ + echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ + mkdir -p /workspaces && \ + chown -R $USER:$USER /workspaces && \ + chown -R $USER:$USER /opt + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + build-essential \ + libssl-dev \ + cmake \ + llvm-dev \ + libclang-dev \ + lld \ + gcc \ + libc6-dev \ + jq \ + make \ + pkg-config \ + git \ + automake \ + libtool \ + m4 \ + autoconf \ + make \ + file \ + curl \ + wget \ + gnupg \ + software-properties-common \ + lsb-release \ + libudev-dev \ + zip unzip \ + nodejs npm \ + binutils && \ + apt-get clean + +USER $USER + +RUN set -e -o xtrace \ + && cd /opt \ + && sudo chown -R $USER:$USER /opt \ + && sudo apt-get update \ + && sudo apt-get install -y jq \ + openjdk-17-jdk \ + # For Linux build + clang cmake git \ + ninja-build pkg-config \ + libgtk-3-dev liblzma-dev \ + libstdc++-12-dev \ + xz-utils \ + wget zip unzip git openssh-client curl bc software-properties-common build-essential \ + ruby-full ruby-bundler libstdc++6 libpulse0 libglu1-mesa locales lcov \ + libsqlite3-dev --no-install-recommends \ + # for x86 emulators + libxtst6 libnss3-dev libnspr4 libxss1 libatk-bridge2.0-0 libgtk-3-0 libgdk-pixbuf2.0-0 \ + && sudo rm -rf /var/lib/apt/lists/* \ + && sudo sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ + && sudo locale-gen \ + && sudo update-locale LANG=en_US.UTF-8 \ + && wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip -O android-sdk-tools.zip \ + && mkdir -p ${ANDROID_HOME}/cmdline-tools/ \ + && unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \ + && mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ + && sudo chown -R $USER:$USER $ANDROID_HOME \ + && rm android-sdk-tools.zip \ + && yes | sdkmanager --licenses \ + && sdkmanager platform-tools \ + && git config --global user.email "hello@komodoplatform.com" \ + && git config --global user.name "Komodo Platform" \ + && yes | sdkmanager \ + "platforms;android-$ANDROID_PLATFORM_VERSION" \ + "build-tools;$ANDROID_BUILD_TOOLS_VERSION" + +RUN cd /opt && \ + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz && \ + tar -xvf flutter_linux_${FLUTTER_VERSION}-stable.tar.xz -C /opt && \ + rm flutter_linux_${FLUTTER_VERSION}-stable.tar.xz && \ + flutter config --no-analytics && \ + flutter precache && \ + yes "y" | flutter doctor --android-licenses && \ + flutter doctor && \ + flutter update-packages && \ + mkdir -p /workspaces/komodo-wallet && \ + chown -R $USER_ID:$USER_ID /workspaces/komodo-wallet \ No newline at end of file diff --git a/.devcontainer/dev-setup.sh b/.devcontainer/dev-setup.sh deleted file mode 100644 index 333fb1edb0..0000000000 --- a/.devcontainer/dev-setup.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -# Uncomment this to fail on the first error. This is useful to debug the script. -# However, it is not recommended for production. -# set -e - -sudo git config core.fileMode false -git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework -sudo chmod -R u+rwx /home/komodo/workspace -sudo chown -R komodo:komodo /home/komodo/workspace - -mkdir -p android/app/src/main/cpp/libs/armeabi-v7a -mkdir -p android/app/src/main/cpp/libs/arm64-v8a -mkdir -p web/src/mm2 - -rustup default stable -cargo install wasm-pack -rustup default nightly-2023-06-01 - -cd /kdf -export PATH="$HOME/.cargo/bin:$PATH" -export PATH=$PATH:/android-ndk/bin -CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib -CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib -wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release - -mv /kdf/target/aarch64-linux-android/release/libkdflib.a /home/komodo/workspace/android/app/src/main/cpp/libs/arm64-v8a/libmm2.a -mv /kdf/target/armv7-linux-androideabi/release/libkdflib.a /home/komodo/workspace/android/app/src/main/cpp/libs/armeabi-v7a/libmm2.a -rm -rf /home/komodo/workspace/web/src/mm2/ -cp -R /kdf/target/target-wasm-release/ /home/komodo/workspace/web/src/mm2/ - -cd /home/komodo/workspace -flutter pub get -npm i && npm run build \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 34288dfca7..e92a2584dd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,33 +1,27 @@ { "name": "flutter_docker", "context": "..", - "dockerFile": "komodo-wallet-android-dev.dockerfile", + "dockerFile": "Dockerfile", "remoteUser": "komodo", - "postAttachCommand": "sh .devcontainer/dev-setup.sh", + "workspaceFolder": "/workspaces/komodo-wallet", + "postCreateCommand": "sudo chown -R komodo:komodo /workspaces/komodo-wallet", + "postAttachCommand": "flutter pub get", "runArgs": [ - "--privileged" + "--privileged", + "-v", + "/dev/bus/usb:/dev/bus/usb" + ], + "forwardPorts": [ + 8081, + 5037 ], - "workspaceMount": "source=${localWorkspaceFolder},target=/home/komodo/workspace,type=bind,consistency=delegated", - "workspaceFolder": "/home/komodo/workspace", - "hostRequirements": { - "cpus": 4, - "memory": "16gb", - "storage": "32gb" - }, "customizations": { "vscode": { "extensions": [ - "FelixAngelov.bloc", "Dart-Code.dart-code", - "Dart-Code.flutter", - "DavidAnson.vscode-markdownlint", - "pflannery.vscode-versionlens", - "GitHub.copilot", - "GitHub.copilot-chat" + "Dart-Code.flutter" ], "settings": { - "terminal.integrated.shell.linux": null, - "extensions.verifySignature": false, // https://github.com/microsoft/vscode/issues/174632 "dart.showTodos": true, "dart.debugExternalPackageLibraries": true, "dart.promptToGetPackages": false, diff --git a/.devcontainer/komodo-wallet-android-dev.dockerfile b/.devcontainer/komodo-wallet-android-dev.dockerfile deleted file mode 100644 index 6182c4a8f1..0000000000 --- a/.devcontainer/komodo-wallet-android-dev.dockerfile +++ /dev/null @@ -1,177 +0,0 @@ -FROM docker.io/ubuntu:22.04 - -ARG KDF_BRANCH=main -ENV KDF_DIR=/kdf -ENV FLUTTER_VERSION="3.22.3" -ENV FLUTTER_HOME="/home/komodo/.flutter-sdk" -ENV USER="komodo" -ENV USER_ID=1000 -ENV PATH=$PATH:$FLUTTER_HOME/bin -ENV AR=/usr/bin/llvm-ar-16 -ENV CC=/usr/bin/clang-16 -ENV PATH="$HOME/.cargo/bin:$PATH" -ENV PATH=$PATH:/android-ndk/bin -ENV ANDROID_HOME=/opt/android-sdk-linux \ - LANG=en_US.UTF-8 \ - LC_ALL=en_US.UTF-8 \ - LANGUAGE=en_US:en - -# Libz is distributed in the android ndk, but for some unknown reason it is not -# found in the build process of some crates, so we explicit set the DEP_Z_ROOT -ENV CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android-clang \ - CARGO_TARGET_X86_64_LINUX_ANDROID_RUNNER="qemu-x86_64 -cpu qemu64,+mmx,+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+popcnt" \ - CC_x86_64_linux_android=x86_64-linux-android-clang \ - CXX_x86_64_linux_android=x86_64-linux-android-clang++ \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_RUNNER=qemu-arm \ - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang \ - CXX_armv7_linux_androideabi=armv7a-linux-androideabi21-clang++ \ - CC_aarch64_linux_android=aarch64-linux-android21-clang \ - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang \ - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang \ - DEP_Z_INCLUDE=/android-ndk/sysroot/usr/include/ \ - OPENSSL_STATIC=1 \ - OPENSSL_DIR=/openssl \ - OPENSSL_INCLUDE_DIR=/openssl/include \ - OPENSSL_LIB_DIR=/openssl/lib \ - RUST_TEST_THREADS=1 \ - HOME=/home/komodo/ \ - TMPDIR=/tmp/ \ - ANDROID_DATA=/ \ - ANDROID_DNS_MODE=local \ - ANDROID_ROOT=/system - -ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ - PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator - -# comes from https://developer.android.com/studio/#command-tools -ENV ANDROID_SDK_TOOLS_VERSION=11076708 - -# https://developer.android.com/studio/releases/build-tools -ENV ANDROID_PLATFORM_VERSION=34 -ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 - -# https://developer.android.com/ndk/downloads -ENV ANDROID_NDK_VERSION=26.3.11579264 - -RUN apt update && apt install -y sudo && \ - useradd -u $USER_ID -m $USER && \ - usermod -aG sudo $USER && \ - echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers - -USER $USER - -RUN sudo apt-get update -y && \ - sudo apt-get install -y --no-install-recommends \ - ca-certificates \ - build-essential \ - libssl-dev \ - cmake \ - llvm-dev \ - libclang-dev \ - lld \ - gcc \ - libc6-dev \ - jq \ - make \ - pkg-config \ - git \ - automake \ - libtool \ - m4 \ - autoconf \ - make \ - file \ - curl \ - wget \ - gnupg \ - software-properties-common \ - lsb-release \ - libudev-dev \ - zip unzip \ - nodejs npm \ - binutils && \ - sudo apt-get clean - -RUN sudo ln -s /usr/bin/python3 /bin/python &&\ - sudo curl --output llvm.sh https://apt.llvm.org/llvm.sh && \ - sudo chmod +x llvm.sh && \ - sudo ./llvm.sh 16 && \ - sudo rm ./llvm.sh && \ - sudo ln -s /usr/bin/clang-16 /usr/bin/clang && \ - PROTOC_ZIP=protoc-25.3-linux-x86_64.zip && \ - sudo curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP && \ - sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc && \ - sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*' && \ - sudo rm -f $PROTOC_ZIP && \ - sudo mkdir $KDF_DIR && \ - sudo chown -R $USER:$USER $KDF_DIR - -RUN PATH="$HOME/.cargo/bin:$PATH" && \ - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - export PATH="$HOME/.cargo/bin:$PATH" && \ - sudo chown -R $USER:$USER $HOME/.cargo && \ - rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal && \ - rustup default nightly-2023-06-01 && \ - rustup target add aarch64-linux-android && \ - rustup target add armv7-linux-androideabi && \ - rustup target add wasm32-unknown-unknown && \ - sudo apt install -y python3 python3-pip git curl nodejs python3-venv sudo && \ - git clone https://github.com/KomodoPlatform/komodo-defi-framework.git $KDF_DIR && \ - cd $KDF_DIR && \ - git fetch --all && \ - git checkout origin/$KDF_BRANCH && \ - if [ "$(uname -m)" = "x86_64" ]; then \ - bash ./scripts/ci/android-ndk.sh x86 23; \ - elif [ "$(uname -m)" = "aarch64" ]; then \ - bash ./scripts/ci/android-ndk.sh arm64 23; \ - else \ - echo "Unsupported architecture: $(uname -m)"; \ - exit 1; \ - fi - -RUN set -e -o xtrace \ - && cd /opt \ - && sudo chown -R $USER:$USER /opt \ - && sudo apt-get update \ - && sudo apt-get install -y jq \ - openjdk-17-jdk \ - # For Linux build - clang cmake git \ - ninja-build pkg-config \ - libgtk-3-dev liblzma-dev \ - libstdc++-12-dev \ - xz-utils \ - wget zip unzip git openssh-client curl bc software-properties-common build-essential \ - ruby-full ruby-bundler libstdc++6 libpulse0 libglu1-mesa locales lcov \ - libsqlite3-dev --no-install-recommends \ - # for x86 emulators - libxtst6 libnss3-dev libnspr4 libxss1 libatk-bridge2.0-0 libgtk-3-0 libgdk-pixbuf2.0-0 \ - && sudo rm -rf /var/lib/apt/lists/* \ - && sudo sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ - && sudo locale-gen \ - && sudo update-locale LANG=en_US.UTF-8 \ - && wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip -O android-sdk-tools.zip \ - && mkdir -p ${ANDROID_HOME}/cmdline-tools/ \ - && unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \ - && mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ - && sudo chown -R $USER:$USER $ANDROID_HOME \ - && rm android-sdk-tools.zip \ - && yes | sdkmanager --licenses \ - && sdkmanager platform-tools \ - && git config --global user.email "hello@komodoplatform.com" \ - && git config --global user.name "Komodo Platform" \ - && yes | sdkmanager \ - "platforms;android-$ANDROID_PLATFORM_VERSION" \ - "build-tools;$ANDROID_BUILD_TOOLS_VERSION" - -RUN git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME} \ - && cd ${FLUTTER_HOME} \ - && git fetch \ - && git checkout tags/$FLUTTER_VERSION \ - && flutter config --no-analytics \ - && flutter precache \ - && yes "y" | flutter doctor --android-licenses \ - && flutter doctor \ - && flutter update-packages \ No newline at end of file diff --git a/.docker/android-sdk.dockerfile b/.docker/android-sdk.dockerfile index 4735d8996a..23fe7b8d70 100644 --- a/.docker/android-sdk.dockerfile +++ b/.docker/android-sdk.dockerfile @@ -25,11 +25,11 @@ ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ ENV ANDROID_SDK_TOOLS_VERSION=11076708 # https://developer.android.com/studio/releases/build-tools -ENV ANDROID_PLATFORM_VERSION=34 -ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 +ENV ANDROID_PLATFORM_VERSION=35 +ENV ANDROID_BUILD_TOOLS_VERSION=35.0.1 # https://developer.android.com/ndk/downloads -ENV ANDROID_NDK_VERSION=26.3.11579264 +ENV ANDROID_NDK_VERSION=27.2.12479018 RUN set -o xtrace \ && sudo chown -R $USER:$USER /opt \ diff --git a/.docker/build.sh b/.docker/build.sh index 8ea6253ead..de24d12bcf 100644 --- a/.docker/build.sh +++ b/.docker/build.sh @@ -16,17 +16,18 @@ fi echo "Building with target: $BUILD_TARGET, mode: $BUILD_MODE" -if [ "$(uname)" == "Darwin" ]; then +if [ "$(uname)" = "Darwin" ]; then PLATFORM_FLAG="--platform linux/amd64" else PLATFORM_FLAG="" fi -docker build $PLATFORM_FLAG -f .docker/kdf-android.dockerfile . -t komodo/kdf-android --build-arg KDF_BRANCH=main -docker build $PLATFORM_FLAG -f .docker/android-sdk.dockerfile . -t komodo/android-sdk:34 +docker build $PLATFORM_FLAG -f .docker/android-sdk.dockerfile . -t komodo/android-sdk:35 docker build $PLATFORM_FLAG -f .docker/komodo-wallet-android.dockerfile . -t komodo/komodo-wallet # Use the provided arguments for flutter build +# Build a second time if needed, as asset downloads will require a rebuild on the first attempt docker run $PLATFORM_FLAG --rm -v ./build:/app/build \ - -u $(id -u):$(id -g) \ - komodo/komodo-wallet:latest bash -c "flutter pub get && flutter analyze && flutter build $BUILD_TARGET --$BUILD_MODE || flutter build $BUILD_TARGET --$BUILD_MODE" + -u "$(id -u):$(id -g)" \ + komodo/komodo-wallet:latest sh -c \ + "flutter build $BUILD_TARGET --$BUILD_MODE || flutter build $BUILD_TARGET --$BUILD_MODE" diff --git a/.docker/kdf-android.dockerfile b/.docker/kdf-android.dockerfile deleted file mode 100644 index be59a4fe36..0000000000 --- a/.docker/kdf-android.dockerfile +++ /dev/null @@ -1,102 +0,0 @@ -FROM docker.io/ubuntu:22.04 - -LABEL Author="Onur Γ–zkan " -ARG KDF_BRANCH=main - -RUN apt-get update -y && \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - build-essential \ - libssl-dev \ - cmake \ - llvm-dev \ - libclang-dev \ - lld \ - gcc \ - libc6-dev \ - jq \ - make \ - pkg-config \ - git \ - automake \ - libtool \ - m4 \ - autoconf \ - make \ - file \ - curl \ - wget \ - gnupg \ - software-properties-common \ - lsb-release \ - libudev-dev \ - zip unzip \ - binutils && \ - apt-get clean - -RUN ln -s /usr/bin/python3 /bin/python &&\ - curl --output llvm.sh https://apt.llvm.org/llvm.sh && \ - chmod +x llvm.sh && \ - ./llvm.sh 16 && \ - rm ./llvm.sh && \ - ln -s /usr/bin/clang-16 /usr/bin/clang && \ - PROTOC_ZIP=protoc-25.3-linux-x86_64.zip && \ - curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP && \ - unzip -o $PROTOC_ZIP -d /usr/local bin/protoc && \ - unzip -o $PROTOC_ZIP -d /usr/local 'include/*' && \ - rm -f $PROTOC_ZIP - -ENV AR=/usr/bin/llvm-ar-16 -ENV CC=/usr/bin/clang-16 - -RUN mkdir -m 0755 -p /etc/apt/keyrings - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ - export PATH="/root/.cargo/bin:$PATH" && \ - rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal && \ - rustup default nightly-2023-06-01 && \ - rustup target add aarch64-linux-android && \ - rustup target add armv7-linux-androideabi && \ - apt install -y python3 python3-pip git curl nodejs python3-venv sudo && \ - git clone https://github.com/KomodoPlatform/komodo-defi-framework.git /app && \ - cd /app && \ - git fetch --all && \ - git checkout origin/$KDF_BRANCH && \ - if [ "$(uname -m)" = "x86_64" ]; then \ - bash ./scripts/ci/android-ndk.sh x86 23; \ - elif [ "$(uname -m)" = "aarch64" ]; then \ - bash ./scripts/ci/android-ndk.sh arm64 23; \ - else \ - echo "Unsupported architecture"; \ - exit 1; \ - fi - -ENV PATH="/root/.cargo/bin:$PATH" - -ENV PATH=$PATH:/android-ndk/bin - -# Libz is distributed in the android ndk, but for some unknown reason it is not -# found in the build process of some crates, so we explicit set the DEP_Z_ROOT -ENV CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android-clang \ - CARGO_TARGET_X86_64_LINUX_ANDROID_RUNNER="qemu-x86_64 -cpu qemu64,+mmx,+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+popcnt" \ - CC_x86_64_linux_android=x86_64-linux-android-clang \ - CXX_x86_64_linux_android=x86_64-linux-android-clang++ \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_RUNNER=qemu-arm \ - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang \ - CXX_armv7_linux_androideabi=armv7a-linux-androideabi21-clang++ \ - CC_aarch64_linux_android=aarch64-linux-android21-clang \ - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang \ - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang \ - DEP_Z_INCLUDE=/android-ndk/sysroot/usr/include/ \ - OPENSSL_STATIC=1 \ - OPENSSL_DIR=/openssl \ - OPENSSL_INCLUDE_DIR=/openssl/include \ - OPENSSL_LIB_DIR=/openssl/lib \ - RUST_TEST_THREADS=1 \ - HOME=/tmp/ \ - TMPDIR=/tmp/ \ - ANDROID_DATA=/ \ - ANDROID_DNS_MODE=local \ - ANDROID_ROOT=/system \ No newline at end of file diff --git a/.docker/komodo-wallet-android.dockerfile b/.docker/komodo-wallet-android.dockerfile index 6fef3b9146..136d1be6a3 100644 --- a/.docker/komodo-wallet-android.dockerfile +++ b/.docker/komodo-wallet-android.dockerfile @@ -1,41 +1,18 @@ -FROM komodo/kdf-android:latest AS build +FROM komodo/android-sdk:35 AS final -RUN cd /app && \ - rustup default nightly-2023-06-01 && \ - rustup target add aarch64-linux-android && \ - rustup target add armv7-linux-androideabi && \ - export PATH=$PATH:/android-ndk/bin && \ - CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib && \ - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib && \ - mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a && \ - mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a - -FROM komodo/android-sdk:34 AS final - -ENV FLUTTER_VERSION="3.22.3" -ENV FLUTTER_HOME="/home/komodo/.flutter-sdk" +ENV FLUTTER_VERSION="3.29.0" +ENV HOME="/home/komodo" ENV USER="komodo" -ENV PATH=$PATH:$FLUTTER_HOME/bin -ENV ANDROID_AARCH64_LIB=android/app/src/main/cpp/libs/arm64-v8a -ENV ANDROID_AARCH64_LIB_SRC=/app/target/aarch64-linux-android/release/libmm2.a -ENV ANDROID_ARMV7_LIB=android/app/src/main/cpp/libs/armeabi-v7a -ENV ANDROID_ARMV7_LIB_SRC=/app/target/armv7-linux-androideabi/release/libmm2.a +ENV PATH=$PATH:$HOME/flutter/bin USER $USER WORKDIR /app COPY --chown=$USER:$USER . . -RUN mkdir -p android/app/src/main/cpp/libs/armeabi-v7a && \ - mkdir -p android/app/src/main/cpp/libs/arm64-v8a && \ - git clone https://github.com/flutter/flutter.git ${FLUTTER_HOME} && \ - cd ${FLUTTER_HOME} && \ - git fetch && \ - git checkout tags/$FLUTTER_VERSION - -COPY --from=build --chown=$USER:$USER ${ANDROID_AARCH64_LIB_SRC} ${ANDROID_AARCH64_LIB} -COPY --from=build --chown=$USER:$USER ${ANDROID_ARMV7_LIB_SRC} ${ANDROID_ARMV7_LIB} - -RUN flutter config --no-analytics \ - && yes "y" | flutter doctor --android-licenses \ - && flutter doctor \ No newline at end of file +RUN curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz && \ + tar -xvf flutter_linux_${FLUTTER_VERSION}-stable.tar.xz -C ${HOME} && \ + rm flutter_linux_${FLUTTER_VERSION}-stable.tar.xz && \ + flutter config --no-analytics && \ + yes "y" | flutter doctor --android-licenses && \ + flutter doctor \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..32bf6e62c8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +**/.git +**/build +**/.fvm \ No newline at end of file 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..88316e838d --- /dev/null +++ b/.github/actions/flutter-deps/action.yml @@ -0,0 +1,19 @@ +name: "Flutter Dependencies" +description: "Installs Flutter and any other dependencies required for the build" +runs: + using: "composite" + steps: + - name: Get stable flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.29.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..f87772da9b --- /dev/null +++ b/.github/actions/generate-assets/action.yml @@ -0,0 +1,49 @@ +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 + BUILD_COMMAND: + description: "The flutter build command to run to generate assets for the deployment build" + required: false + default: "flutter build web --release" +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 + + # 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 || true + ${{ inputs.BUILD_COMMAND }} > /dev/null 2>&1 || true + rm -rf build/* + + # Run flutter build and capture its output and exit status + # Allow error messages from `flutter pub get` to be printed for debugging + # in case there are legitimate, unexpected errors. + flutter pub get > /dev/null + output=$(${{ inputs.BUILD_COMMAND }} 2>&1) + 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/releases/setup-android/action.yml b/.github/actions/releases/setup-android/action.yml new file mode 100644 index 0000000000..6beb851e64 --- /dev/null +++ b/.github/actions/releases/setup-android/action.yml @@ -0,0 +1,45 @@ +--- +name: "Setup Android Build Environment" +description: "Configures Java, Android SDK and signing for Android builds" + +inputs: + java-version: + description: "Java version to use" + required: false + default: "21" + keystore-base64: + description: "Base64 encoded Android keystore file" + required: false + key-alias: + description: "Android keystore key alias" + required: false + store-password: + description: "Android keystore password" + required: false + key-password: + description: "Android keystore key password" + required: false + +runs: + using: "composite" + steps: + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: "temurin" + cache: "gradle" + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Setup Android keystore + if: inputs.keystore-base64 != '' && 'KomodoPlatform/komodo-wallet' == github.repository + shell: bash + run: | + echo "${{ inputs.keystore-base64 }}" | base64 --decode > android/app/upload-keystore.jks + rm -f android/key.properties # Remove existing file to avoid duplication on successive runs + echo "storeFile=upload-keystore.jks" >> android/key.properties + echo "keyAlias=${{ inputs.key-alias }}" >> android/key.properties + echo "storePassword=${{ inputs.store-password }}" >> android/key.properties + echo "keyPassword=${{ inputs.key-password }}" >> android/key.properties diff --git a/.github/actions/releases/setup-ios/action.yml b/.github/actions/releases/setup-ios/action.yml new file mode 100644 index 0000000000..af521530ba --- /dev/null +++ b/.github/actions/releases/setup-ios/action.yml @@ -0,0 +1,68 @@ +--- +name: "Setup iOS Build Environment" +description: "Configures Xcode and iOS signing requirements" + +inputs: + xcode-version: + description: "Xcode version to use" + required: false + default: "latest-stable" + p12-file-base64: + description: "Base64-encoded P12 certificate file" + required: false + p12-password: + description: "P12 certificate password" + required: false + bundle-id: + description: "App bundle identifier" + required: false + default: "com.komodo.wallet" + profile-type: + description: "Provisioning profile type" + required: false + default: "IOS_APP_STORE" + issuer-id: + description: "App Store Connect issuer ID" + required: false + api-key-id: + description: "App Store Connect API key ID" + required: false + api-private-key: + description: "App Store Connect API private key" + required: false + +runs: + using: "composite" + steps: + - name: Setup Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ inputs.xcode-version }} + + # Download iOS platform to avoid the missing platform error from xcode 15 + # https://github.com/flutter/flutter/issues/129558 + # https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#Install-and-manage-Simulator-runtimes-from-the-command-line + - name: Install iOS dependencies + shell: bash + run: | + xcodebuild -downloadPlatform iOS + flutter pub get + cd ios + pod install + + - name: Import iOS certificates + if: inputs.p12-file-base64 != '' && 'KomodoPlatform/komodo-wallet' == github.repository + uses: apple-actions/import-codesign-certs@v3 + with: + p12-file-base64: ${{ inputs.p12-file-base64 }} + p12-password: ${{ inputs.p12-password }} + + - name: Download iOS provisioning profile + if: inputs.issuer-id != '' && 'KomodoPlatform/komodo-wallet' == github.repository + uses: apple-actions/download-provisioning-profiles@v1 + with: + bundle-id: ${{ inputs.bundle-id }} + profile-type: ${{ inputs.profile-type }} + issuer-id: ${{ inputs.issuer-id }} + api-key-id: ${{ inputs.api-key-id }} + api-private-key: ${{ inputs.api-private-key }} diff --git a/.github/actions/releases/setup-linux/action.yml b/.github/actions/releases/setup-linux/action.yml new file mode 100644 index 0000000000..7cb5216820 --- /dev/null +++ b/.github/actions/releases/setup-linux/action.yml @@ -0,0 +1,39 @@ +--- +name: "Setup Linux Build Environment" +description: "Configures Flutter and dependencies for Linux builds" + +inputs: + gpg-key: + description: "Base64-encoded GPG private key" + required: false + gpg-key-id: + description: "GPG key ID for signing" + required: false + +runs: + using: "composite" + steps: + - name: Install system dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libgtk-3-dev libblkid-dev \ + liblzma-dev libsecret-1-dev libjsoncpp-dev libsqlite3-dev \ + libxdg-basedir-dev cmake pkg-config clang git xz-utils zip \ + libglu1-mesa curl libstdc++-12-dev + + - name: Install Linux build dependencies + shell: bash + run: | + flutter pub get + flutter doctor + + - name: Setup Linux code signing + if: inputs.gpg-key != '' && 'KomodoPlatform/komodo-wallet' == github.repository + shell: bash + run: | + echo "${{ inputs.gpg-key }}" | base64 --decode | gpg --import --batch --yes + mkdir -p ~/.gnupg + echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf + gpg-connect-agent reloadagent /bye + echo "DEBSIGN_KEYID=${{ inputs.gpg-key-id }}" > ~/.devscripts diff --git a/.github/actions/releases/setup-macos/action.yml b/.github/actions/releases/setup-macos/action.yml new file mode 100644 index 0000000000..ae5b2b15d4 --- /dev/null +++ b/.github/actions/releases/setup-macos/action.yml @@ -0,0 +1,73 @@ +--- +name: "Setup macOS Build Environment" +description: "Configures Flutter and dependencies for macOS builds" + +inputs: + xcode-version: + description: "Xcode version to use" + required: false + default: "latest-stable" + p12-file-base64: + description: "Base64-encoded P12 certificate file" + required: false + p12-password: + description: "P12 certificate password" + required: false + bundle-id: + description: "App bundle identifier" + required: false + default: "com.komodo.wallet" + profile-type: + description: "Provisioning profile type" + required: false + default: "MAC_APP_STORE" + issuer-id: + description: "App Store Connect issuer ID" + required: false + api-key-id: + description: "App Store Connect API key ID" + required: false + api-private-key: + description: "App Store Connect API private key" + required: false + +runs: + using: "composite" + steps: + - name: Setup Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ inputs.xcode-version }} + + - name: Setup Xcode command line tools + shell: bash + run: | + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + sudo xcodebuild -license accept + + - name: Install macOS build dependencies + shell: bash + run: | + flutter pub get + cd macos + pod install + + - name: Import macOS certificates + if: inputs.p12-file-base64 != '' && 'KomodoPlatform/komodo-wallet' == github.repository + uses: apple-actions/import-codesign-certs@v3 + with: + keychain: ${{ github.run_id }} + keychain-password: ${{ github.run_id }} + create-keychain: false + p12-file-base64: ${{ inputs.p12-file-base64 }} + p12-password: ${{ inputs.p12-password }} + + - name: Download macOS provisioning profile + if: inputs.issuer-id != '' && 'KomodoPlatform/komodo-wallet' == github.repository + uses: apple-actions/download-provisioning-profiles@v1 + with: + bundle-id: ${{ inputs.bundle-id }} + profile-type: ${{ inputs.profile-type }} + issuer-id: ${{ inputs.issuer-id }} + api-key-id: ${{ inputs.api-key-id }} + api-private-key: ${{ inputs.api-private-key }} diff --git a/.github/actions/releases/setup-windows/action.yml b/.github/actions/releases/setup-windows/action.yml new file mode 100644 index 0000000000..633a7bfaa6 --- /dev/null +++ b/.github/actions/releases/setup-windows/action.yml @@ -0,0 +1,38 @@ +--- +name: "Setup Windows Build Environment" +description: "Configures Flutter and dependencies for Windows builds" + +inputs: + pfx-base64: + description: "Base64-encoded PFX certificate file" + required: false + pfx-password: + description: "PFX certificate password" + required: false + +runs: + using: "composite" + steps: + - name: Setup Visual Studio + uses: microsoft/setup-msbuild@v1.3 + + - name: Install Windows SDK + uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 + with: + sdk-version: 26100 + + - name: Install Windows build dependencies + shell: pwsh + run: | + flutter pub get + flutter doctor + + - name: Setup Windows code signing + if: inputs.pfx-base64 != '' && 'KomodoPlatform/komodo-wallet' == github.repository + shell: pwsh + run: | + echo "${{ inputs.pfx-base64 }}" | base64 --decode > signing_certificate.pfx + $pfxPassword = '${{ inputs.pfx-password }}' + $certPassword = ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText + Import-PfxCertificate -FilePath signing_certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password $certPassword + Remove-Item signing_certificate.pfx diff --git a/.github/actions/validate-build/action.yml b/.github/actions/validate-build/action.yml new file mode 100644 index 0000000000..213729b34a --- /dev/null +++ b/.github/actions/validate-build/action.yml @@ -0,0 +1,79 @@ +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: | + 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 + + 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 + + # 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: KMD 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 build/web/assets | grep kmd.png + if ! strings build/web/assets/AssetManifest.bin | grep -qi "assets/coin_icons/png/kmd.png"; then + echo "Error: KMD coin icon not found in AssetManifest.bin" + exit 1 + fi + fi + + # 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 app_build/build_config.json ]; then + echo "Error: build_config.json file not found." + exit 1 + fi + if ! jq . app_build/build_config.json > /dev/null; then + echo "Error: build_config.json is not valid json" + exit 1 + fi + + # 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 $COIN_ASSETS_DIR/config/coins.json ]; then + echo "Error: coins.json file not found." + exit 1 + fi + if ! jq . $COIN_ASSETS_DIR/config/coins.json > /dev/null; then + echo "Error: coins.json is not valid json" + exit 1 + fi + + # 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 $COIN_ASSETS_DIR/config/coins_config.json ]; then + echo "Error: coins_config.json file not found." + exit 1 + fi + 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/desktop-builds.yml b/.github/workflows/desktop-builds.yml new file mode 100644 index 0000000000..95aae41e02 --- /dev/null +++ b/.github/workflows/desktop-builds.yml @@ -0,0 +1,87 @@ +name: Desktop Builds +run-name: Building desktop apps πŸ–₯️ + +on: + pull_request: + branches: [dev, main, release/*, hotfix/*, feature/*] + workflow_dispatch: + release: + types: [created] + +jobs: + build_desktop: + name: Build Desktop (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - platform: macos + runner: macos-latest + build_command: flutter build macos --release + artifact_path: build/macos/Build/Products/Release/*.app + artifact_name: komodo-wallet-macos + - platform: windows + runner: windows-latest + build_command: flutter build windows --release + artifact_path: build/windows/x64/runner/Release/* + artifact_name: komodo-wallet-windows + - platform: linux + runner: ubuntu-latest + build_command: flutter build linux --release + artifact_path: build/linux/x64/release/bundle/* + artifact_name: komodo-wallet-linux + + steps: + - uses: actions/checkout@v4 + + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps + + # macOS setup + - name: Setup macOS environment + if: ${{ matrix.platform == 'macos' }} + uses: ./.github/actions/releases/setup-macos + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + bundle-id: "com.komodo.komodowallet" + profile-type: "MAC_APP_DEVELOPMENT" + issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} + api-key-id: ${{ secrets.APPSTORE_KEY_ID }} + api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + + # Linux setup + - name: Setup Linux environment + if: ${{ matrix.platform == 'linux' }} + uses: ./.github/actions/releases/setup-linux + with: + gpg-key: ${{ secrets.LINUX_GPG_KEY }} + gpg-key-id: ${{ secrets.LINUX_GPG_KEY_ID }} + + # Windows setup + - name: Setup Windows environment + if: ${{ matrix.platform == 'windows' }} + uses: ./.github/actions/releases/setup-windows + with: + pfx-base64: ${{ secrets.WINDOWS_PFX_BASE64 }} + pfx-password: ${{ secrets.WINDOWS_PFX_PASSWORD }} + + - name: Fetch packages and generate assets + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_COMMAND: ${{ matrix.build_command }} + + - name: Build for ${{ matrix.platform }} + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ${{ matrix.build_command }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + path: ${{ matrix.artifact_path }} + name: ${{ matrix.artifact_name }}.zip + retention-days: 5 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 b3f807e101..6e5e08b33c 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: @@ -34,72 +34,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/mobile-builds.yml b/.github/workflows/mobile-builds.yml new file mode 100644 index 0000000000..4ead0ad26d --- /dev/null +++ b/.github/workflows/mobile-builds.yml @@ -0,0 +1,76 @@ +name: Mobile Builds +run-name: Building mobile apps πŸ“± + +on: + pull_request: + branches: [dev, main, release/*, hotfix/*, feature/*] + workflow_dispatch: + release: + types: [created] + +jobs: + build_mobile: + name: Build Mobile (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - platform: iOS + runner: macos-latest + build_command: flutter build ios --release --no-codesign + artifact_path: build/ios/iphoneos/Runner.app + artifact_name: komodo-wallet-ios-unsigned.app + - platform: Android + runner: ubuntu-latest + build_command: flutter build apk --release + artifact_path: build/app/outputs/flutter-apk/app-release.apk + artifact_name: komodo-wallet-android-unsigned.apk + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps + + - name: Setup iOS environment + if: ${{ matrix.platform == 'iOS' }} + uses: ./.github/actions/releases/setup-ios + with: + p12-file-base64: ${{ secrets.IOS_P12_BASE64 }} + p12-password: ${{ secrets.IOS_P12_PASSWORD }} + bundle-id: "com.komodo.wallet" + profile-type: "IOS_APP_STORE" + issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} + api-key-id: ${{ secrets.APPSTORE_KEY_ID }} + api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + + - name: Setup Android environment + if: ${{ matrix.platform == 'Android' }} + uses: ./.github/actions/releases/setup-android + with: + keystore-base64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + key-alias: ${{ secrets.ANDROID_KEY_ALIAS }} + store-password: ${{ secrets.ANDROID_STORE_PASSWORD }} + key-password: ${{ secrets.ANDROID_KEY_PASSWORD }} + + - name: Fetch packages and generate assets + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Use default value for BUILD_COMMAND, as iOS does not appear to run + # the build transformer required to generate the required assets. + + - name: Build for ${{ matrix.platform }} + env: + GITHUB_API_PUBLIC_READONLY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ${{ matrix.build_command }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_path }} + retention-days: 5 diff --git a/.github/workflows/ui-tests-on-pr.yml b/.github/workflows/ui-tests-on-pr.yml index f5f4536da4..61d7bf918a 100644 --- a/.github/workflows/ui-tests-on-pr.yml +++ b/.github/workflows/ui-tests-on-pr.yml @@ -1,141 +1,108 @@ -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 strategy: fail-fast: false matrix: - name: [ - web-app-linux, - web-app-macos, - ] - include: - - name: web-app-linux - os: [self-hosted, Linux] - - - name: web-app-macos - os: [self-hosted, macos] + name: [web-app-linux-profile, web-app-macos] + include: + - name: web-app-linux-profile + os: ubuntu-latest + 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-latest + 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 - - 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 + # Flutter integration test setup + - name: Install Chrome and chromedriver + if: ${{ matrix.browser == 'chrome' }} + uses: browser-actions/setup-chrome@v1 + id: setup-chrome 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: 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" - - - 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 - - - name: Test air_dex chrome (unix) (Linux) - if: runner.name == 'ci-builder-radon' - id: tests-chrome - continue-on-error: true - timeout-minutes: 35 - 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 + chrome-version: 116.0.5845.96 + install-chromedriver: true + install-dependencies: true - name: Enable safaridriver (sudo) (MacOS) - if: runner.name == 'ci-builder-astatine' + if: ${{ matrix.browser == 'safari' }} 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' - continue-on-error: true - run: | - safaridriver -p 4444 & + - name: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps + + - name: Fetch packages and generate assets + uses: ./.github/actions/generate-assets + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Test air_dex safari (MacOS) - if: runner.name == 'ci-builder-astatine' - id: tests-safari + - name: Validate build + uses: ./.github/actions/validate-build + + # 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: | - flutter pub get - dart run_integration_tests.dart --browser-name=safari - - - name: Upload logs (Linux) - if: 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: Fail workflow if tests failed (Linux) - if: 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' + name: ${{ runner.os }}-${{ matrix.browser }}-logs + path: ${{ matrix.driver_logs }} + if-no-files-found: warn + + # 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' }} run: exit 1 diff --git a/.github/workflows/unit-tests-on-pr.yml b/.github/workflows/unit-tests-on-pr.yml index b31ae0a9ac..687ca2bcd1 100644 --- a/.github/workflows/unit-tests-on-pr.yml +++ b/.github/workflows/unit-tests-on-pr.yml @@ -1,86 +1,43 @@ +# 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: - runs-on: [self-hosted, Linux] + unit_tests: + runs-on: ubuntu-latest timeout-minutes: 15 steps: - 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: Install Flutter and dependencies + uses: ./.github/actions/flutter-deps - - name: Get stable flutter - uses: subosito/flutter-action@v2 + - name: Fetch packages and generate assets + uses: ./.github/actions/generate-assets 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/* + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - 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" - - 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..d51548ca39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# First-party +sdk/ + # Miscellaneous *.class *.log @@ -35,8 +38,11 @@ .pub/ /build/ contrib/coins_config.json +devtools_options.yaml # Web related +web/index.html +web/src/* web/dist/*.js web/dist/*.wasm web/dist/*LICENSE.txt @@ -63,30 +69,17 @@ airdex-build.tar.gz # js node_modules - -assets/config/test_wallet.json assets/**/debug_data.json contrib/coins_config.json -# api native library -libmm2.a -windows/**/mm2.exe -linux/mm2/mm2 -macos/mm2 -libkdf.a -windows/**/kdf.exe -linux/mm2/kdf -macos/kdf **/.api_last_updated* # 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/ \ No newline at end of file +venv/ + +# FVM Version Cache +.fvm/ +/macos/build 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/analysis_options.yaml b/analysis_options.yaml index 111e0b9647..61d8cbb187 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,7 +26,7 @@ linter: # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Turned off to avoid redundant dependecies (already included to other dependencies), - # and reduce amount of work on secure code review + # and reduce amount of work on secure code review depend_on_referenced_packages: false # Converting all (97) widgets to const will require significant amount of testing, # debugging and refactoring, so disabled for now. @@ -36,6 +36,5 @@ linter: # to all code created/updated in your PRs. This will help to keep the code clean # and consistent by making the formatting deterministic and consistent. # require_trailing_commas: true - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/android/app/build.gradle b/android/app/build.gradle index babbe598c0..8c89822176 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,13 +25,13 @@ if (flutterVersionName == null) { android { namespace 'com.komodoplatform.atomicdex' - compileSdk 34 + compileSdk 35 compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '21' } sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,22 +40,16 @@ android { defaultConfig { applicationId "com.komodoplatform.atomicdex" minSdkVersion 28 - targetSdkVersion 34 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - externalNativeBuild { - cmake { - // mm2_lib.a requires libc++ - cppFlags "-std=c++17 -stdlib=libc++" - } - } ndk { //noinspection ChromeOsAbiSupport abiFilters 'armeabi-v7a', 'arm64-v8a' } } - ndkVersion '25.1.8937393' + ndkVersion '27.2.12479018' buildTypes { release { @@ -64,12 +58,6 @@ android { signingConfig signingConfigs.debug } } - - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } } flutter { diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt deleted file mode 100644 index b368a53295..0000000000 --- a/android/app/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) - -find_library(log-lib log) -find_package(ZLIB) - -set(IMPORT_DIR ${CMAKE_SOURCE_DIR}/libs) - -add_library(mm2-lib - SHARED - mm2_native.cpp -) - -add_library(mm2-api - STATIC - IMPORTED -) - -set_target_properties(mm2-api - PROPERTIES IMPORTED_LOCATION - ${IMPORT_DIR}/${ANDROID_ABI}/libmm2.a -) - -target_link_libraries(mm2-lib - mm2-api - ${log-lib} - ZLIB::ZLIB -) diff --git a/android/app/src/main/cpp/libs/arm64-v8a/.gitkeep b/android/app/src/main/cpp/libs/arm64-v8a/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/android/app/src/main/cpp/libs/armeabi-v7a/.gitkeep b/android/app/src/main/cpp/libs/armeabi-v7a/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/android/app/src/main/cpp/mm2_native.cpp b/android/app/src/main/cpp/mm2_native.cpp deleted file mode 100644 index 7bfc682aa6..0000000000 --- a/android/app/src/main/cpp/mm2_native.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include -#include -#include -#include - -extern "C" { - -/// Defined in "mm2_lib.rs". -int8_t mm2_main(const char *conf, void (*log_cb)(const char *line)); - -/// 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(); - -/// Defined in "mm2_lib.rs". -/// 0 .. MM2 has been stopped successfully. -/// 1 .. not running. -/// 2 .. error stopping an MM2 instance. -int8_t mm2_stop(); - -} - -#define STRINGIFY(x) #x -#define TOSTRING(x) STRINGIFY(x) -#define LTAG "mm2_native:" TOSTRING(__LINE__) "] " - -#define LOG_D(format, ...) __android_log_print(ANDROID_LOG_DEBUG, LTAG, format "\n", ##__VA_ARGS__) -#define LOG_E(format, ...) __android_log_print(ANDROID_LOG_ERROR, LTAG, format "\n", ##__VA_ARGS__) - -class LogHandler { -public: - static std::optional create(JNIEnv *env, jobject log_listener) { - JavaVM *jvm = nullptr; - // Returns β€œ0” on success; returns a negative value on failure. - if (env->GetJavaVM(&jvm)) { - LOG_E("Couldn't get JavaVM"); - return std::nullopt; - } - - // Returns a global reference, or NULL if the system runs out of memory. - jobject listener = env->NewGlobalRef(log_listener); - if (!listener) { - LOG_E("Couldn't create a listener global reference"); - return std::nullopt; - } - - jclass obj = env->GetObjectClass(listener); - // Returns a method ID, or NULL if the specified method cannot be found. - jmethodID log_callback = env->GetMethodID(obj, "onLog", "(Ljava/lang/String;)V"); - if (!log_callback) { - LOG_E("Couldn't get method ID"); - // GetMethodID could threw an exception. - exception_check(env); - return std::nullopt; - } - - return LogHandler(jvm, listener, log_callback); - } - - explicit LogHandler(JavaVM *jvm, jobject listener, jmethodID log_callback) - : m_jvm(jvm), - m_listener(listener), - m_log_callback(log_callback) { - } - - // Note: LogHandler::release() must be called before the destructor. - ~LogHandler() = default; - - void release(JNIEnv *env) { - env->DeleteGlobalRef(m_listener); - // Do not release m_jvm and m_log_callback. - } - - void replaceInvalidUtf8Bytes(const char *src, char *dst) { - unsigned int len = strlen(src); - unsigned int j = 0; - - for (unsigned int i = 0; i < len;) { - unsigned char byte = static_cast(src[i]); - - if (byte <= 0x7F) { - dst[j++] = byte; - i += 1; - } else if ((byte & 0xE0) == 0xC0 && i + 1 < len && (src[i + 1] & 0xC0) == 0x80) { - dst[j++] = byte; - dst[j++] = src[i + 1]; - i += 2; - } else if ((byte & 0xF0) == 0xE0 && i + 2 < len && (src[i + 1] & 0xC0) == 0x80 && (src[i + 2] & 0xC0) == 0x80) { - dst[j++] = byte; - dst[j++] = src[i + 1]; - dst[j++] = src[i + 2]; - i += 3; - } else if ((byte & 0xF8) == 0xF0 && i + 3 < len && (src[i + 1] & 0xC0) == 0x80 && (src[i + 2] & 0xC0) == 0x80 && (src[i + 3] & 0xC0) == 0x80) { - dst[j++] = byte; - dst[j++] = src[i + 1]; - dst[j++] = src[i + 2]; - dst[j++] = src[i + 3]; - i += 4; - } else { - // Replace invalid byte sequence with '?' (0x3F) - dst[j++] = '?'; - i += 1; - } - } - - dst[j] = '\0'; - } - - - void process_log_line(const char *line) { - JNIEnv *env = nullptr; - int env_stat = m_jvm->GetEnv((void **)&env, JNI_VERSION_1_6); - if (env_stat == JNI_EDETACHED) { - // Should another thread need to access the Java VM, it must first call AttachCurrentThread() - // to attach itself to the VM and obtain a JNI interface pointer. - // https://docs.oracle.com/javase/9/docs/specs/jni/invocation.html#attaching-to-the-vm - - if (m_jvm->AttachCurrentThread(&env, nullptr) != 0) { - LOG_E("Failed to attach"); - return; - } - } else if (env_stat == JNI_EVERSION) { - LOG_E("Version not supported"); - return; - } else if (env_stat != JNI_OK) { - LOG_E("Unexpected error"); - return; - } - - char rplc_line[strlen(line) * 3 + 1]; // Space for the worst case scenario (all bytes replaced by the 3-byte replacement character sequence) - replaceInvalidUtf8Bytes(line, rplc_line); - - jstring jline = env->NewStringUTF(rplc_line); - // Call a Java callback. - env->CallVoidMethod(m_listener, m_log_callback, jline); - // CallVoidMethod could threw an exception. - exception_check(env); - - if (env_stat == JNI_EDETACHED) { - // Detach itself before exiting. - m_jvm->DetachCurrentThread(); - } - } - -private: - static void exception_check(JNIEnv *env) { - if (env->ExceptionCheck()) { - LOG_E("An exception is being thrown"); - // Prints an exception and a backtrace of the stack to a system error-reporting channel, such as stderr - env->ExceptionDescribe(); - // Clears any exception that is currently being thrown - env->ExceptionClear(); - } - } - - JavaVM *m_jvm = nullptr; - jobject m_listener = nullptr; - jmethodID m_log_callback = nullptr; -}; - -static std::mutex LOG_MUTEX; -static std::optional LOG_HANDLER; - -extern "C" JNIEXPORT jbyte JNICALL -Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2Main( - JNIEnv *env, - jobject, /* this */ - jstring conf, - jobject log_listener) { - { - const auto lock = std::lock_guard(LOG_MUTEX); - if (LOG_HANDLER) { - LOG_D("LOG_HANDLER is initialized already, release it"); - LOG_HANDLER->release(env); - } - LOG_HANDLER = LogHandler::create(env, log_listener); - } - - const char *c_conf = env->GetStringUTFChars(conf, nullptr); - - const auto result = mm2_main(c_conf, [](const char *line) { - const auto lock = std::lock_guard(LOG_MUTEX); - if (!LOG_HANDLER) { - LOG_E("LOG_HANDLER is not initialized"); - return; - } - LOG_HANDLER->process_log_line(line); - }); - - env->ReleaseStringUTFChars(conf, c_conf); - return static_cast(result); -} - -extern "C" JNIEXPORT jbyte JNICALL -Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2MainStatus( - JNIEnv *, - jobject /* this */) { - return static_cast(mm2_main_status()); -} - -extern "C" JNIEXPORT jbyte JNICALL -Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2Stop( - JNIEnv *, - jobject /* this */) { - return static_cast(mm2_stop()); -} diff --git a/android/app/src/main/java/com/komodoplatform/atomicdex/MainActivity.java b/android/app/src/main/java/com/komodoplatform/atomicdex/MainActivity.java index 23a56f8da4..5156c1cdfb 100644 --- a/android/app/src/main/java/com/komodoplatform/atomicdex/MainActivity.java +++ b/android/app/src/main/java/com/komodoplatform/atomicdex/MainActivity.java @@ -25,9 +25,6 @@ public class MainActivity extends FlutterActivity { - private EventChannel.EventSink logsSink; - private final Handler logHandler = new Handler(Looper.getMainLooper()); - private boolean isSafBytes = false; private MethodChannel.Result safResult; private String safData; @@ -35,51 +32,6 @@ public class MainActivity extends FlutterActivity { private static final int CREATE_SAF_FILE = 21; private static final String TAG_CREATE_SAF_FILE = "CREATE_SAF_FILE"; - static { - System.loadLibrary("mm2-lib"); - } - - - private void nativeC(FlutterEngine flutterEngine) { - BinaryMessenger bm = flutterEngine.getDartExecutor().getBinaryMessenger(); - EventChannel logsChannel = new EventChannel(bm, "komodo-web-dex/event"); - logsChannel.setStreamHandler(new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - logsSink = events; - } - - @Override - public void onCancel(Object arguments) { - logsSink = null; - } - }); - - // https://flutter.dev/docs/development/platform-integration/platform-channels?tab=android-channel-kotlin-tab#step-3-add-an-android-platform-specific-implementation - new MethodChannel(bm, "komodo-web-dex") - .setMethodCallHandler((call, result) -> { - switch (call.method) { - case "start": { - Map arguments = call.arguments(); - onStart(arguments, result); - break; - } - case "status": { - onStatus(result); - break; - } - case "stop": { - onStop(result); - break; - } - default: { - result.notImplemented(); - } - } - }); - - } - private void setupSaf(FlutterEngine flutterEngine) { BinaryMessenger bm = flutterEngine.getDartExecutor().getBinaryMessenger(); new MethodChannel(bm, "komodo-web-dex/AndroidSAF") @@ -167,64 +119,9 @@ public void onActivityResult(int requestCode, int resultCode, safDataBytes = null; } - - private void onStart(Map arguments, MethodChannel.Result result) { - - if (arguments == null) { - result.success(0); - return; - } - String arg = arguments.get("params"); - if (arg == null) { - result.success(0); - return; - } - - int ret = startMm2(arg); - result.success(ret); - } - - private void onStatus(MethodChannel.Result result) { - int status = nativeMm2MainStatus(); - result.success(status); - } - - private void onStop(MethodChannel.Result result) { - int ret = nativeMm2Stop(); - result.success(ret); - } - - private int startMm2(String conf) { - - sendLog("START MM2 --------------------------------"); - - return nativeMm2Main(conf, this::sendLog); - } - - private void sendLog(String line) { - if (logsSink != null) { - logHandler.post(() -> logsSink.success(line)); - - } - } - - /// Corresponds to Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2MainStatus in main.cpp - private native byte nativeMm2MainStatus(); - - /// Corresponds to Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2Main in main.cpp - private native byte nativeMm2Main(String conf, JNILogListener listener); - - /// Corresponds to Java_com_komodoplatform_atomicdex_MainActivity_nativeMm2Stop in main.cpp - private native byte nativeMm2Stop(); - @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine); - nativeC(flutterEngine); setupSaf(flutterEngine); } } - -interface JNILogListener { - void onLog(String line); -} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 381baa9cef..d71047787f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/android/settings.gradle b/android/settings.gradle index 69473a136c..4d51df0db5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,10 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + // Gradle plugin version https://developer.android.com/build/releases/gradle-plugin + id "com.android.application" version "8.8.0" apply false + // Kotlin release with JVM 21 support: https://kotlinlang.org/docs/releases.html#release-details + id "org.jetbrains.kotlin.android" version "2.1.10" apply false } include ":app" \ No newline at end of file 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 2f102fa952..bc7afa8bf9 100644 --- a/app_build/build_config.json +++ b/app_build/build_config.json @@ -1,27 +1,10 @@ { - "api": { - "api_commit_hash": "8206c6ef61ee5c0f697fab5fcfd7256e3ebe13ac", - "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": [ - "f82e330a8d9bc1c2011604648233922cf216a732858c5a6a935ad77a286b1993" - ], - "path": "web/src/mm2" - } - } - }, "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/app_theme/lib/src/dark/theme_global_dark.dart b/app_theme/lib/src/dark/theme_global_dark.dart index 3526a8e8fc..c4a814551d 100644 --- a/app_theme/lib/src/dark/theme_global_dark.dart +++ b/app_theme/lib/src/dark/theme_global_dark.dart @@ -7,7 +7,8 @@ ThemeData get themeGlobalDark { SnackBarThemeData snackBarThemeLight() => const SnackBarThemeData( elevation: 12.0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))), + borderRadius: BorderRadius.all(Radius.circular(4)), + ), actionTextColor: Colors.green, behavior: SnackBarBehavior.floating, ); @@ -32,19 +33,32 @@ ThemeData get themeGlobalDark { final TextTheme textTheme = TextTheme( headlineMedium: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w700, color: textColor), + fontSize: 16, + fontWeight: FontWeight.w700, + color: textColor, + ), headlineSmall: const TextStyle( - fontSize: 40, fontWeight: FontWeight.w700, color: textColor), + fontSize: 40, + fontWeight: FontWeight.w700, + color: textColor, + ), titleLarge: const TextStyle( - fontSize: 26.0, color: textColor, fontWeight: FontWeight.w700), + fontSize: 26.0, + color: textColor, + fontWeight: FontWeight.w700, + ), titleSmall: const TextStyle(fontSize: 18.0, color: textColor), bodyMedium: const TextStyle( - fontSize: 16.0, color: textColor, fontWeight: FontWeight.w300), + 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, ), ); @@ -67,8 +81,8 @@ ThemeData get themeGlobalDark { iconTheme: IconThemeData(color: colorScheme.primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: colorScheme.primary), - dialogBackgroundColor: const Color.fromRGBO(14, 16, 27, 1), dialogTheme: const DialogTheme( + backgroundColor: Color.fromRGBO(14, 16, 27, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(16), @@ -80,7 +94,8 @@ 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 +111,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 +133,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( @@ -157,8 +172,9 @@ ThemeData get themeGlobalDark { ), 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 diff --git a/app_theme/lib/src/light/theme_global_light.dart b/app_theme/lib/src/light/theme_global_light.dart index 8e985dacff..9e5f0113a6 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, ), ); @@ -64,8 +65,8 @@ ThemeData get themeGlobalLight { iconTheme: IconThemeData(color: colorScheme.primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: colorScheme.primary), - dialogBackgroundColor: const Color.fromRGBO(255, 255, 255, 1), dialogTheme: const DialogTheme( + backgroundColor: Color.fromRGBO(255, 255, 255, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(16), @@ -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..14e13e3615 100644 --- a/app_theme/pubspec.lock +++ b/app_theme/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" flutter: dependency: "direct main" description: flutter @@ -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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" plugin_platform_interface: dependency: "direct main" description: @@ -50,7 +50,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" vector_math: dependency: transitive description: @@ -60,5 +60,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=3.3.0-0 <4.0.0" - flutter: ">=2.5.0" + dart: ">=3.7.0-0 <4.0.0" + flutter: ">=3.29.0" diff --git a/app_theme/pubspec.yaml b/app_theme/pubspec.yaml index e84ef9bfb0..4ffef7466b 100644 --- a/app_theme/pubspec.yaml +++ b/app_theme/pubspec.yaml @@ -1,11 +1,11 @@ name: app_theme description: App theme. version: 0.0.1 -homepage: +# homepage: environment: - sdk: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + sdk: ">=3.6.0 <4.0.0" + flutter: ^3.29.0 dependencies: flutter: diff --git a/assets/config/.gitkeep b/assets/config/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/assets/logo/not_found.png b/assets/logo/not_found.png new file mode 100644 index 0000000000..90c3ea4d8f Binary files /dev/null and b/assets/logo/not_found.png differ 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 866f05ab0d..4cfd98b685 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -3,10 +3,12 @@ "rewardClaiming": "Rewards claim in progress", "noKmdAddress": "No KMD address found", "dex": "DEX", - "asset": "Assets", + "asset": "Asset", + "assets": "Assets", "price": "Price", "volume": "Volume", "history": "History", + "lastTransactions": "Last transactions", "active": "Active", "change24h": "Change 24h", "change24hRevert": "24h %", @@ -113,7 +115,7 @@ "walletImportTitle": "Import wallet", "walletImportByFileTitle": "Seed file has been imported", "walletImportCreatePasswordTitle": "Create a password for \"{}\" wallet", - "walletImportByFileDescription": "Enter the password for your seed file to decrypt it. This password will be used to log in to your wallet", + "walletImportByFileDescription": "Create a password of your seed file to decrypt it. This password will be used to log in to your wallet", "walletLogInTitle": "Log in", "walletCreationNameHint": "Wallet name", "walletCreationPasswordHint": "Wallet password", @@ -175,8 +177,8 @@ "swapProgressStatusFailed": "Failed swap", "swapDetailsStepStatusFailed": "Failed", "disclaimerAcceptEulaCheckbox": "EULA", - "disclaimerAcceptTermsAndConditionsCheckbox": "TERMS and CONDITIONS", - "disclaimerAcceptDescription": "By clicking the button below, you confirm that you have read and accept the \"EULA\" and \"TOC\"", + "disclaimerAcceptTermsAndConditionsCheckbox": "Terms & Conditions", + "disclaimerAcceptDescription": "By clicking the button below, you confirm that you have read and accept:", "swapDetailsStepStatusInProcess": "In progress", "swapDetailsStepStatusTimeSpent": "Time spent {}", "milliseconds": "ms", @@ -196,7 +198,7 @@ "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.", + "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 understand\" in the box below.", "customSeedIUnderstand": "i understand", "walletCreationBip39SeedError": "BIP39 seed validation failed, try again or select 'Allow custom seed'", "walletPageNoSuchAsset": "No assets match search criteria", @@ -310,7 +312,7 @@ "seedPhraseGotIt": "I got it", "viewSeedPhrase": "View seed phrase", "backupSeedPhrase": "Backup seed phrase", - "seedOr": "Or", + "seedOr": "OR", "seedDownload": "Download seed file", "seedSaveAndRemember": "Save and remember", "seedIntroWarning": "This phrase is the main access to your\nassets, save and never share this phrase", @@ -397,6 +399,15 @@ "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", + "hdWalletModeSwitchTitle": "Multi-address Wallet?", + "hdWalletModeSwitchSubtitle": "Enabling HD wallet allows you to create multiple addresses for each coin. However, your addresses and balances will change. You can easily switch between the modes when logging in.", + "hdWalletModeSwitchTooltip": "HD wallets require a valid BIP39 seed phrase.", + "noActiveWallet": "No active wallet - please sign in first", "memo": "Memo", "gasPriceGwei": "Gas price [Gwei]", "gasLimit": "Gas limit", @@ -556,6 +567,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", @@ -563,7 +584,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", @@ -584,6 +607,20 @@ "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.", + "importCustomToken": "Import Custom Token", + "importTokenWarning": "Ensure the token is trustworthy before you import it.", + "importToken": "Import Token", + "selectNetwork": "Select Network", + "tokenContractAddress": "Token Contract Address", + "tokenNotFound": "Token is not found.", + "decimals": "Decimals", + "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/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_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/BUILD_RELEASE.md b/docs/BUILD_RELEASE.md index b5fc3401ff..4dae5778d4 100644 --- a/docs/BUILD_RELEASE.md +++ b/docs/BUILD_RELEASE.md @@ -1,6 +1,6 @@ # Build Release version of the App -### Environment setup +## Environment setup Before building the app, make sure you have all the necessary tools installed. Follow the instructions in the [Environment Setup](./PROJECT_SETUP.md) document. Alternatively, you can use the Docker image as described here: (TODO!). @@ -35,7 +35,7 @@ flutter build apk ## Docker builds -### Build for Web +### Build for web ```bash sh .docker/build.sh web release diff --git a/docs/BUILD_RUN_APP.md b/docs/BUILD_RUN_APP.md index 7a3cbdb468..bd309b9ce1 100644 --- a/docs/BUILD_RUN_APP.md +++ b/docs/BUILD_RUN_APP.md @@ -107,6 +107,24 @@ Run `flutter config --enable-windows-desktop` to enable Windows desktop support. If you are using Windows 10, please ensure that [Microsoft WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2?form=MA13LH) is installed for Webview support. Windows 11 ships with it, but Windows 10 users might need to install it. +Please ensure the following prerequisites are installed: + +- [Visual Studio](https://visualstudio.microsoft.com/vs/community/) | Community 17.13.0 (Windows only), with the `Desktop development with C++` workload installed. +- [Nuget CLI](https://www.nuget.org/downloads) is required for Windows desktop builds. Install with winget + + ```PowerShell + winget install -e --id Microsoft.NuGet + # Add a primary package source + . $profile + nuget sources add -name "NuGet.org" -source https://api.nuget.org/v3/index.json + ``` + +- Enable long paths in Windows registry. Open CMD or PowerShell as Administrator, run the following, and restart: + + ```PowerShell + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 + ``` + Before building for Windows, run `flutter doctor` to check if all the dependencies are installed. If not, follow the instructions in the error message. ```bash @@ -161,10 +179,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/FIREBASE_SETUP.md b/docs/FIREBASE_SETUP.md index c7ce656342..8b8459acd3 100644 --- a/docs/FIREBASE_SETUP.md +++ b/docs/FIREBASE_SETUP.md @@ -1,6 +1,7 @@ # Firebase setup (local builds) To generate the configuration files for Firebase, follow the steps below: + - Create a Firebase account and add a new project. - Add a new web app to the project. @@ -8,8 +9,9 @@ To generate the configuration files for Firebase, follow the steps below: - Install flutterfire CLI: `dart pub global activate flutterfire_cli` - Login to Firebase: `firebase login` - Generate config files: `flutterfire configure` -- Disable github tracking of config files: -``` +- Disable github tracking of config files: + +```bash git update-index --assume-unchanged android/app/google-services.json git update-index --assume-unchanged ios/firebase_app_id_file.json git update-index --assume-unchanged macos/firebase_app_id_file.json diff --git a/docs/FLUTTER_VERSION.md b/docs/FLUTTER_VERSION.md index dd84b43b4a..ec9a330896 100644 --- a/docs/FLUTTER_VERSION.md +++ b/docs/FLUTTER_VERSION.md @@ -1,17 +1,28 @@ -# Flutter version +# Flutter Version Management -This project aims to keep the Flutter version up-to-date with the latest stable release. See the section below for the latest version officially supported by this project. +## Supported Flutter Version -## Current version +This project supports Flutter `3.29.0` (latest stable release). We aim to keep the project up-to-date with the most recent stable Flutter versions. -3.22.3 +## Recommended Approach: Multiple Flutter Versions -## Pin Flutter version +For the best development experience, we recommend using a Flutter version manager rather than pinning your global Flutter installation. This allows for better isolation when working with multiple projects that may require different Flutter versions. -``` +See our guide on [Multiple Flutter Versions](MULTIPLE_FLUTTER_VERSIONS.md) for detailed instructions on setting up a version management solution. + +## Alternative: Pinning Flutter Version (Not Recommended) + +While it's possible to pin your global Flutter installation to a specific version, **this approach is not recommended** due to: +- Lack of isolation between projects +- Known issues with `flutter pub get` when using Flutter 3.29.0 +- Difficulty switching between versions for different projects + +If you still choose to use this method, you can run: + +```bash cd ~/flutter -git checkout 3.22.3 +git checkout 3.29.0 flutter doctor ``` -See also: [Multiple flutter versions](MULTIPLE_FLUTTER_VERSIONS.md) +However, we strongly encourage using the multiple Flutter versions approach instead for a better development experience. \ No newline at end of file diff --git a/docs/INSTALL_FLUTTER.md b/docs/INSTALL_FLUTTER.md index cc308acc73..ef89c19632 100644 --- a/docs/INSTALL_FLUTTER.md +++ b/docs/INSTALL_FLUTTER.md @@ -6,17 +6,13 @@ on [FLUTTER_VERSION.md](FLUTTER_VERSION.md). While it should be possible to go a few bugfixes versions over that version without issues, it's generally intended to use that exact version. -There are two main ways to get an older copy of Flutter. +To install the Flutter SDK, you can use the [Flutter extension for VS Code](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter) (recommended), or download and install the Flutter bundle yourself from the [SDK Archive](https://docs.flutter.dev/release/archive), or use the [Flutter Version Manager (FVM)](https://fvm.app/documentation/getting-started/installation). The [official guide](https://docs.flutter.dev/get-started/install/linux/web) goes into further detail. -The first way is by cloning the official repository and then pinning to an older version. +## Use VS Code to install -1. Clone Flutter with - ``` - cd ~ - git clone https://github.com/flutter/flutter.git - ``` -2. [Pin Flutter version](FLUTTER_VERSION.md#pin-flutter-version) +The recommended approach is to install the [Flutter extension for VSCode](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter) (or the [Flutter extension for Android Studio/IntelliJ](https://plugins.jetbrains.com/plugin/9212-flutter)) and installing the Flutter SDK via the extension to simplify the process. +## Download and install 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) @@ -25,27 +21,33 @@ Remember to extract the file into a convenient place, such as `~/flutter`. Choose the option that is more convenient for you at the time. -If you opt for the SDK Archive, you easily change to use the [Pin Flutter version](FLUTTER_VERSION.md#pin-flutter-version) later if you prefer. - 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. +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. 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`. + +## Use Flutter Version Manager (FVM) + +Should you need to install and manage multiple versions of the Flutter SDK, it is recommended to use [FVM](https://fvm.app/documentation/getting-started/installation). See [MULTIPLE_FLUTTER_VERSIONS.md](MULTIPLE_FLUTTER_VERSIONS.md) for more details. diff --git a/docs/INTEGRATION_TESTING.md b/docs/INTEGRATION_TESTING.md index 8760e8dfd0..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 + chromedriver --port=4444 --silent --enable-chrome-logs --log-path=console.log ``` - - or Firefox + +- or Firefox + ```bash - ./geckodriver --port=4444 + geckodriver --port=4444 ``` - - or Safari + +- 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: + +#### 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/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/docs/MULTIPLE_FLUTTER_VERSIONS.md b/docs/MULTIPLE_FLUTTER_VERSIONS.md index 16fcca984e..d697870690 100644 --- a/docs/MULTIPLE_FLUTTER_VERSIONS.md +++ b/docs/MULTIPLE_FLUTTER_VERSIONS.md @@ -1,47 +1,170 @@ -# Handle multiple Flutter versions +# Managing Multiple Flutter Versions -## macOS +For the best development experience with Komodo DeFi SDK, we recommend using a Flutter version manager to easily switch between different Flutter versions. This document outlines two recommended approaches: -### 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 -``` +1. **Flutter Sidekick** - A user-friendly GUI application (recommended for beginners) +2. **FVM (Flutter Version Manager)** - A command-line tool (recommended for advanced users) -### pen (or create) `.zshrc` file in your home directory: -``` -nano ~/.zshrc -``` -Add line: -``` -alias flutter_web="$HOME/flutter_web/bin/flutter" +## Before You Begin: Remove Existing Flutter Installations + +Before installing a version manager, you should remove any existing Flutter installations to avoid conflicts: + +### macOS +```bash +# If installed via git +rm -rf ~/flutter + +# If installed via Homebrew +brew uninstall flutter + +# Remove from PATH in ~/.zshrc or ~/.bash_profile +# Find and remove any lines containing flutter/bin ``` -Save and close. -### 3. Check if newly installed Flutter version is accessible from terminal: +### Windows +```powershell +# If installed manually, delete the Flutter folder +# Remove from PATH in Environment Variables +# Start β†’ Edit environment variables for your account β†’ Path β†’ Remove Flutter entry + +# If installed via winget +winget uninstall flutter ``` -cd ~ -flutter_web doctor + +### Linux +```bash +# If installed via git +rm -rf ~/flutter + +# If installed via package manager +sudo apt remove flutter # for Ubuntu/Debian +sudo pacman -R flutter # for Arch + +# Remove from PATH in ~/.bashrc +# Find and remove any lines containing flutter/bin ``` +## Option 1: Flutter Sidekick (Recommended for Beginners) + +[Flutter Sidekick](https://github.com/leoafarias/sidekick) is a GUI application for managing Flutter versions across Windows, macOS, and Linux. + +### Installation Steps + +1. Download and install Flutter Sidekick from the [GitHub releases page](https://github.com/leoafarias/sidekick/releases) + +2. Launch Flutter Sidekick + +3. Click on "Versions" in the sidebar and download Flutter version `3.29.0` + +4. Set this version as the global default by clicking the "Set as Global" button + +5. Add Flutter to your PATH: + - Click on "Settings" in the sidebar + - Find the path to the FVM installation directory (typically `~/.fvm/default/bin` on macOS/Linux or `%LOCALAPPDATA%\fvm\default\bin` on Windows) + - Add this path to your system's PATH environment variable + +6. Restart your terminal/IDE for the changes to take effect + +7. Verify the installation: + ```bash + flutter --version + ``` + +## Option 2: FVM Command Line + +[FVM](https://fvm.app) is a command-line tool for managing Flutter versions. While most developers will be well-served by Flutter Sidekick's graphical interface, the command-line version might be preferable in specific scenarios: + +- If you need to integrate Flutter version management into CI/CD pipelines or scripts +- When working in environments without a graphical interface (e.g., remote servers) +- If you prefer terminal-based workflows and want to avoid graphical applications +- For automation in team environments where consistent versions need to be enforced programmatically + +### macOS and Linux + +1. Install FVM using the installation script: + ```bash + curl -fsSL https://fvm.app/install.sh | bash + ``` + +2. Install and use Flutter 3.29.0: + ```bash + fvm install 3.29.0 + fvm global 3.29.0 + ``` + +3. Add FVM's default Flutter version to your PATH by adding the following to your `~/.bashrc`, `~/.zshrc`, or equivalent: + ```bash + export PATH="$PATH:$HOME/.fvm/default/bin" + ``` + +4. Reload your shell configuration: + ```bash + source ~/.bashrc # or ~/.zshrc + ``` + +5. Verify the installation: + ```bash + flutter --version + ``` + +### Windows + +1. Install Chocolatey (if not already installed): + ```powershell + Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + ``` + +2. Install FVM: + ```powershell + choco install fvm + ``` + +3. Install and use Flutter 3.29.0: + ```powershell + fvm install 3.29.0 + fvm global 3.29.0 + ``` + +4. Add FVM's Flutter version to your PATH: + - Open "Edit environment variables for your account" + - Edit the Path variable + - Add `%LOCALAPPDATA%\fvm\default\bin` + +5. Restart your terminal/PowerShell and verify the installation: + ```powershell + flutter --version + ``` + +## Project-Specific Flutter Version + +To use a specific Flutter version for a project: -### 4. Add new Flutter version to VSCode: +1. Navigate to your project directory +2. Run: + ```bash + fvm use 3.29.0 + ``` - - Settings (⌘,) -> Extensions -> Dart -> SDK -> Flutter Sdk Paths -> Add Item -> `~/flutter_web` - - βŒ˜β‡§P -> Developer: Reload window - - βŒ˜β‡§P -> Flutter: Change SDK +This will create a `.fvmrc` file in your project, which specifies the Flutter version to use for this project. +## Using with VS Code -### 5. Add to Android Studio +For optimal integration with VS Code: - - Settings (⌘,) -> Languages & Frameworks -> Flutter -> SDK -> Flutter SDK Path -> `~/flutter_web` +1. Install the [Flutter FVM](https://marketplace.visualstudio.com/items?itemName=leoafarias.fvm) extension to automatically use the project-specific Flutter version. ----- +2. If you're using a project-specific Flutter version, you need to specify the Flutter SDK path in your VS Code settings: -## Windows TBD + - Open the project in VS Code + - Create or edit the `.vscode/settings.json` file in your project root and add: + ```json + { + "dart.flutterSdkPath": ".fvm/flutter_sdk", + // Or if you're using a global FVM version: + // "dart.flutterSdkPath": "${userHome}/.fvm/default/bin/flutter" + } + ``` ----- +3. Restart VS Code after making these changes. -## Linux TBD \ No newline at end of file +This ensures VS Code uses the correct Flutter SDK version for your project, including for features like code completion, diagnostics, and debugging. \ No newline at end of file diff --git a/docs/PROJECT_SETUP.md b/docs/PROJECT_SETUP.md index 79e2a2295b..56aa5a84bd 100644 --- a/docs/PROJECT_SETUP.md +++ b/docs/PROJECT_SETUP.md @@ -15,31 +15,29 @@ Komodo Wallet is a cross-platform application, meaning it can be built for multi - [VS Code](https://code.visualstudio.com/) - install and enable `Dart` and `Flutter` extensions - enable `Dart: Use recommended settings` via the Command Pallette - - [Android Studio](https://developer.android.com/studio) - Flamingo | 2024.1.2 + - [Android Studio](https://developer.android.com/studio) - Ladybug | 2024.2.2 - install and enable `Dart` and `Flutter` plugins - SDK Manager -> SDK Tools: - - [x] Android SDK Build-Tools 35 - - [x] NDK (Side by side) 27.1 - - [x] Android command line tools (latest) - - [x] CMake 3.30.3 (latest) - - [xCode](https://developer.apple.com/xcode/) - 15.4 (macOS only) - - [Visual Studio](https://visualstudio.microsoft.com/vs/community/) - Community 17.11.3 (Windows only) + - [x] Android SDK Build-Tools - (latest) 35.0.1 + - [x] NDK (Side by side) - (latest) 28.0 + - [x] Android command line tools - (latest) 19.0.0 + - [x] CMake - (latest) 3.31.5 + - [xCode](https://developer.apple.com/xcode/) | 16.2 (macOS only) + - [Visual Studio](https://visualstudio.microsoft.com/vs/community/) | Community 17.13.0 (Windows only) - `Desktop development with C++` workload required + - [Nuget CLI](https://www.nuget.org/downloads) required for Windows desktop builds + - [Enable long paths in Windows registry](BUILD_RUN_APP.md#windows-desktop) 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) @@ -51,3 +49,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/docs/SIGN_RELEASE.md b/docs/SIGN_RELEASE.md new file mode 100644 index 0000000000..55be137fda --- /dev/null +++ b/docs/SIGN_RELEASE.md @@ -0,0 +1,178 @@ +# Signing builds + +## Android + +1. Generate keystore file: + + ```bash + keytool -genkey -v -keystore komodo-wallet.jks -keyalg RSA -keysize 2048 -validity 10000 -alias komodo + ``` + +2. Convert keystore to base64: + + ```bash + base64 -i komodo-wallet.jks -o keystore-base64.txt + ``` + +3. Validate + + ```bash + keytool -list -v -keystore komodo-wallet.jks + ``` + +Example secrets: + +```yaml +ANDROID_KEYSTORE_BASE64: "/u3+7QAAAAIAAAABAAAAAQAHa29tb2RvAAABjK6LSU8AAAUBMIIE..." +ANDROID_KEY_ALIAS: "komodo" +ANDROID_STORE_PASSWORD: "your-keystore-password" +ANDROID_KEY_PASSWORD: "your-key-password" +``` + +Documentation: + +- [Android Signing Guide](https://developer.android.com/studio/publish/app-signing) + +## iOS/macOS + +1. Create Apple Developer Account +2. Generate certificates in Apple Developer Portal: + iOS: App Store and Ad Hoc distribution certificate + macOS: Mac App Store certificate +3. Export P12: + + ```bash + # Export from Keychain and convert to base64 + base64 -i certificate.p12 -o cert-base64.txt + ``` + +4. Create App Store Connect API Key +5. Validate + + ```bash + security find-identity -v -p codesigning + ``` + +Example secrets: + +```yaml +IOS_P12_BASE64: "MIIKsQIBAzCCCnsGCSqGSIb3DQEHAaCCCmwEggpo..." +IOS_P12_PASSWORD: "your-p12-password" +MACOS_P12_BASE64: "MIIKsQIBAzCCCnsGCSqGSIb3DQEHAaCCCmwEggpo..." +MACOS_P12_PASSWORD: "your-p12-password" +APPSTORE_ISSUER_ID: "57246542-96fe-1a63-e053-0824d011072a" +APPSTORE_KEY_ID: "2X9R4HXF34" +APPSTORE_PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMG..." +``` + +Documentation: + +- [iOS Code Signing Guide](https://medium.com/@bingkuo/a-beginners-guide-to-code-signing-in-ios-development-d3d5285f0960) +- [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi) +- [Provisioning Profiles](https://developer.apple.com/documentation/xcode/distributing-your-app-for-beta-testing-and-releases) + +## Windows + +1. Purchase a code signing certificate from a trusted CA (like DigiCert) +2. Export as PFX with private key +3. Convert to base64: + + ```Powershell + certutil -encode certificate.pfx cert-base64.txt + ``` + +4. Validate + + ```Powershell + signtool verify /pa your-app.exe + ``` + +Example secrets: + +```yaml +WINDOWS_PFX_BASE64: "MIIKkgIBAzCCClYGCSqGSIb3DQEHAaCCCkcEggpD..." +WINDOWS_PFX_PASSWORD: "your-pfx-password" +``` + +Documentation: + +- [Windows Code Signing Guide](https://learn.microsoft.com/en-us/windows/win32/appxpkg/how-to-sign-a-package-using-signtool) +- [Microsoft Authenticode](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/authenticode) + +## Linux + +1. Generate GPG key: + + ```bash + gpg --full-generate-key + ``` + +2. Export private key: + + ```bash + gpg --export-secret-keys --armor YOUR_KEY_ID | base64 > gpg-key-base64.txt + ``` + +3. Validate + + ```bash + gpg --verify your-package.deb.asc your-package.deb + ``` + +Example secrets: + +```yaml + +``` + +Documentation: + +- [GPG Guide](https://gnupg.org/documentation/guides.html) +- [Debian Package Signing](https://wiki.debian.org/SecureApt) + +## Summary of Github Workflow Secrets + +### Shared between iOS and macOS + +| **Name** | **Description** | +| ---------------------- | ----------------------------------------------------------------------------------------------------- | +| `APPSTORE_ISSUER_ID` | The Issuer ID from your Apple Developer account, used to authenticate with App Store APIs. | +| `APPSTORE_KEY_ID` | The Key ID associated with your App Store Connect API key. | +| `APPSTORE_PRIVATE_KEY` | The private key (Base64 encoded) for your App Store Connect API key, used for signing releases. | + +### iOS (mobile builds) + +| **Name** | **Description** | +| -------------------- | ----------------------------------------------------------------------------------------------------- | +| `IOS_P12_BASE64` | The iOS distribution certificate encoded in Base64 for signing iOS builds. | +| `IOS_P12_PASSWORD` | The password for the iOS distribution certificate (`.p12` file). | + +### macOS (desktop builds) + +| **Name** | **Description** | +| ---------------------- | ----------------------------------------------------------------------------------------------------- | +| `MACOS_P12_BASE64` | The macOS distribution certificate encoded in Base64 for signing macOS builds. | +| `MACOS_P12_PASSWORD` | The password for the macOS distribution certificate (`.p12` file). | + +### Android (mobile builds) + +| **Name** | **Description** | +| -------------------------- | --------------------------------------------------------------------------------------------------- | +| `ANDROID_KEYSTORE_BASE64` | The Android Keystore file encoded in Base64 for signing Android applications. | +| `ANDROID_KEY_ALIAS` | The alias name of the key within the Android Keystore. | +| `ANDROID_STORE_PASSWORD` | The password for the Android Keystore. | +| `ANDROID_KEY_PASSWORD` | The password for the specific key within the Android Keystore. | + +### Windows (desktop builds) + +| **Name** | **Description** | +| ---------------------- | ----------------------------------------------------------------------------------------------------- | +| `WINDOWS_PFX_BASE64` | The Windows code signing certificate in PFX format, encoded in Base64. | +| `WINDOWS_PFX_PASSWORD` | The password for the Windows PFX certificate (`.pfx` file). | + +### Linux (desktop builds) + +| **Name** | **Description** | +| -------------------- | ----------------------------------------------------------------------------------------------------- | +| `LINUX_GPG_KEY` | The GPG private key for signing Linux packages, encoded in Base64. | +| `LINUX_GPG_KEY_ID` | The ID of the GPG key used to sign Linux packages. | 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/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/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 88359b225f..e30b311cb6 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, '15.5.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..77ee2a75d9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,200 +1,206 @@ 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 (11.8.0): - Firebase/Core - - Firebase/Core (10.10.0): + - Firebase/Core (11.8.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 (~> 11.8.0) + - Firebase/CoreOnly (11.8.0): + - FirebaseCore (~> 11.8.0) + - firebase_analytics (11.4.4): + - Firebase/Analytics (= 11.8.0) - firebase_core - Flutter - - firebase_core (2.14.0): - - Firebase/CoreOnly (= 10.10.0) + - firebase_core (3.12.1): + - Firebase/CoreOnly (= 11.8.0) - Flutter - - FirebaseAnalytics (10.10.0): - - FirebaseAnalytics/AdIdSupport (= 10.10.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): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.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) - - FirebaseCore (10.10.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.11.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.11.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) + - FirebaseAnalytics (11.8.0): + - FirebaseAnalytics/AdIdSupport (= 11.8.0) + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/AdIdSupport (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseCore (11.8.1): + - FirebaseCoreInternal (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.8.0): + - FirebaseCore (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - Flutter (1.0.0) - 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) - - 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) - - 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): - - 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): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (4.0.0): + - OrderedSet (~> 6.0.3) + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - GoogleAppMeasurement (11.8.0): + - GoogleAppMeasurement/AdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/AdIdSupport (11.8.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/WithoutAdIdSupport (11.8.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.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 (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.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 (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.0.2): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - - GoogleUtilitiesComponents (1.1.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (2.3.0) + - GoogleUtilities/Privacy + - 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) - - 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): + - komodo_defi_framework (0.0.1): + - Flutter + - local_auth_darwin (0.0.1): - 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) + - FlutterMacOS + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (8.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-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.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.21.0): + - SDWebImage/Core (= 5.21.0) + - SDWebImage/Core (5.21.0) - 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`) @@ -202,14 +208,17 @@ 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_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - 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`) - 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: @@ -225,7 +234,6 @@ SPEC REPOS: - GoogleMLKit - GoogleToolboxForMac - GoogleUtilities - - GoogleUtilitiesComponents - GTMSessionFetcher - MLImage - MLKitBarcodeScanning @@ -248,8 +256,14 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" 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: @@ -263,46 +277,48 @@ 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 - GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - integration_test: 13825b8a9334a850581300559b8839134b124670 - MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b - MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 - MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 - MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 47056db0c04027ea5f41a716385542da28574662 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef - SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 - share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 - video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 + Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf + firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa + firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874 + FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b + FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d + FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 + FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_secure_storage_darwin: 12d2375c690785d97a4e586f15f11be5ae35d5b0 + GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + komodo_defi_framework: 4d6349ef69fcfabc1f8465a92b72e3639325d55a + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: fd0054c52ede661e80bf5a4dea477a2467356bee + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 +PODFILE CHECKSUM: f2a1ebd07796ee082cb4d8e8fa742449f698c4f1 -COCOAPODS: 1.12.1 +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 52fdfe96da..34967357bd 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 */, @@ -173,6 +166,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FC78CCE93902D5D826DCE20C /* [CP] Embed Pods Frameworks */, + 629DFBD49650E4794D77944D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -189,7 +183,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -270,6 +264,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; @@ -283,7 +294,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; @@ -309,7 +320,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D6C50BDA2702024E0095EE3C /* mm2.m in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); @@ -397,11 +407,13 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -412,6 +424,7 @@ "$(PROJECT_DIR)", ); PRODUCT_BUNDLE_IDENTIFIER = com.komodoplatform.atomicdex; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -537,11 +550,13 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -552,6 +567,7 @@ "$(PROJECT_DIR)", ); PRODUCT_BUNDLE_IDENTIFIER = com.komodoplatform.atomicdex; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,11 +585,13 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = G3VBBBMD8T; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -584,6 +602,7 @@ "$(PROJECT_DIR)", ); PRODUCT_BUNDLE_IDENTIFIER = com.komodoplatform.atomicdex; + MACOSX_DEPLOYMENT_TARGET = 15.0; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a335..c53e2b314e 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ 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 348907d6f5..a5878f54a3 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -12,11 +12,19 @@ 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 const bool isBitrefillIntegrationEnabled = false; +/// Const to define if trading is enabled in the app. Trading is only permitted +/// with test coins for development purposes while the regulatory compliance +/// framework is being developed. +/// +///! You are solely responsible for any losses/damage that may occur. Komodo +///! Platform does not condone the use of this app for trading purposes and +///! unequivocally forbids it. const bool kIsWalletOnly = !kDebugMode; const Duration kPerformanceLogInterval = Duration(minutes: 1); @@ -24,14 +32,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"; - -// 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'; +String get appTitle => 'Komodo Wallet | Non-Custodial Multi-Coin Wallet & DEX'; +String get appShortTitle => 'Komodo Wallet'; Map priorityCoinsAbbrMap = { 'KMD': 30, @@ -116,8 +118,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/app_config/coins_config_parser.dart b/lib/app_config/coins_config_parser.dart deleted file mode 100644 index eea3ec0c73..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('$assetsPath/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 = '$assetsPath/config/coins.json', - String coinsConfigPath = '$assetsPath/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('$assetsPath/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/analytics/analytics_repo.dart b/lib/bloc/analytics/analytics_repo.dart index eb6cc83511..696e35971a 100644 --- a/lib/bloc/analytics/analytics_repo.dart +++ b/lib/bloc/analytics/analytics_repo.dart @@ -6,7 +6,7 @@ import 'package:web_dex/firebase_options.dart'; abstract class AnalyticsEventData { late String name; - Map get parameters; + Map get parameters; } abstract class AnalyticsRepo { @@ -50,8 +50,15 @@ class FirebaseAnalyticsRepo implements AnalyticsRepo { return; } + final sanitizedParameters = event.parameters.map((key, value) { + return MapEntry(key, value is Object ? value : value.toString()); + }); + try { - await _instance.logEvent(name: event.name, parameters: event.parameters); + await _instance.logEvent( + name: event.name, + parameters: sanitizedParameters, + ); } catch (e, s) { log( e.toString(), diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index d69a478fe2..be87f65c89 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,23 +26,31 @@ 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/coins_manager/coins_manager_bloc.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'; 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_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'; 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'; 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/kmd_rewards_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'; @@ -63,13 +67,13 @@ import 'package:web_dex/shared/widgets/coin_icon.dart'; class AppBlocRoot extends StatelessWidget { const AppBlocRoot({ - Key? key, required this.storedPrefs, - required this.runtimeUpdateConfig, + required this.komodoDefiSdk, + super.key, }); final StoredSettings storedPrefs; - final RuntimeUpdateConfig runtimeUpdateConfig; + final KomodoDefiSdk komodoDefiSdk; // TODO: Refactor to clean up the bloat in this main file void _clearCachesIfPerformanceModeChanged( @@ -99,14 +103,28 @@ 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 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()); + : */ + SdkTransactionHistoryRepository(sdk: komodoDefiSdk); final profitLossRepo = ProfitLossRepository.withDefaults( transactionHistoryRepo: transactionsRepo, @@ -114,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( @@ -128,13 +150,49 @@ 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), + RepositoryProvider( + create: (_) => KmdRewardsBloc(coinsRepository, mm2Api), + ), + ], child: MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => CoinsBloc( + komodoDefiSdk, + coinsRepository, + trezorBloc, + mm2Api, + )..add(CoinsStarted()), + ), BlocProvider( - create: (context) => PriceChartBloc(binanceRepository) - ..add( + create: (context) => PriceChartBloc( + binanceRepository, + komodoDefiSdk, + )..add( const PriceChartStarted( symbols: ['KMD'], period: Duration(days: 30), @@ -157,11 +215,12 @@ class AppBlocRoot extends StatelessWidget { BlocProvider( create: (BuildContext ctx) => PortfolioGrowthBloc( portfolioGrowthRepository: portfolioGrowthRepo, + coinsRepository: coinsRepository, ), ), BlocProvider( create: (BuildContext ctx) => TransactionHistoryBloc( - repo: transactionsRepo, + sdk: komodoDefiSdk, ), ), BlocProvider( @@ -178,24 +237,27 @@ 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, ), lazy: false, ), @@ -203,7 +265,7 @@ class AppBlocRoot extends StatelessWidget { lazy: false, create: (context) => NftMainBloc( repo: context.read(), - authRepo: authRepo, + kdfSdk: komodoDefiSdk, isLoggedIn: context.read().state.mode == AuthorizeMode.logIn, ), @@ -222,21 +284,25 @@ class AppBlocRoot extends StatelessWidget { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), + coinsRepository, ), ), ), BlocProvider( - create: (_) => SystemHealthBloc(), + 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, + ), + ), + BlocProvider( + create: (context) => CoinsManagerBloc( + coinsRepo: coinsRepository, + sdk: komodoDefiSdk, + ), ), ], child: _MyAppView(), @@ -252,19 +318,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(); } @@ -289,7 +360,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. @@ -316,17 +388,19 @@ class _MyAppViewState extends State<_MyAppView> { Completer? _currentPrecacheOperation; - Future _precacheCoinIcons() async { + Future _precacheCoinIcons(CoinsRepo coinsRepo) 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(); 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/assets_overview/bloc/asset_overview_bloc.dart b/lib/bloc/assets_overview/bloc/asset_overview_bloc.dart index 8b96403366..b47cc1c1e3 100644 --- a/lib/bloc/assets_overview/bloc/asset_overview_bloc.dart +++ b/lib/bloc/assets_overview/bloc/asset_overview_bloc.dart @@ -39,7 +39,7 @@ class AssetOverviewBloc extends Bloc { try { final profitLosses = await profitLossRepository.getProfitLoss( - event.coin.abbr, + event.coin.id, 'USDT', event.walletId, ); @@ -96,7 +96,7 @@ class AssetOverviewBloc extends Bloc { // affect the total investment calculation. try { return await profitLossRepository.getProfitLoss( - coin.abbr, + coin.id, 'USDT', event.walletId, ); diff --git a/lib/bloc/assets_overview/investment_repository.dart b/lib/bloc/assets_overview/investment_repository.dart index 825c4fa1f0..94ddd014a7 100644 --- a/lib/bloc/assets_overview/investment_repository.dart +++ b/lib/bloc/assets_overview/investment_repository.dart @@ -1,7 +1,6 @@ import 'package:web_dex/bloc/cex_market_data/profit_loss/models/fiat_value.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_repository.dart'; import 'package:web_dex/model/coin.dart'; - import 'package:web_dex/shared/utils/utils.dart' as logger; class InvestmentRepository { @@ -29,7 +28,7 @@ class InvestmentRepository { // affect the total investment calculation. try { final profitLoss = await _profitLossRepository.getProfitLoss( - coin.abbr, + coin.id, 'USDT', walletId, ); diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 8ccd4a7f00..5ca71be1f4 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -1,106 +1,264 @@ 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/kdf_auth_metadata_extension.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); + on(_onSeedBackupConfirmed); + on(_onWalletDownloadRequested); } - 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', - ); + try { + if (event.wallet.isLegacyWallet) { + return add( + AuthRestoreRequested( + wallet: event.wallet, + password: event.password, + seed: await event.wallet.getLegacySeed(event.password), + ), + ); + } - await _logOut(); - await _logIn(event.seed, event.wallet); - - 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: AuthOptions( + derivationMethod: event.wallet.config.type == WalletType.hdwallet + ? DerivationMethod.hdWallet + : 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: AuthOptions( + derivationMethod: event.wallet.config.type == WalletType.hdwallet + ? DerivationMethod.hdWallet + : 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 _logIn( - String seed, - Wallet wallet, + Future _onRestore( + AuthRestoreRequested event, + Emitter emit, ) async { - await _authRepo.logIn(AuthorizeMode.logIn, seed); - currentWalletBloc.wallet = wallet; - if (wallet.config.type == WalletType.iguana) { - _authChecker.addSession(wallet.config.seedPhrase); + 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: AuthOptions( + derivationMethod: event.wallet.config.type == WalletType.hdwallet + ? DerivationMethod.hdWallet + : 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)); } } + + Future _onSeedBackupConfirmed( + AuthSeedBackupConfirmed event, + Emitter emit, + ) async { + // emit the current user again to pull in the updated seed backup status + // and make the backup notification banner disappear + await _kdfSdk.confirmSeedBackup(); + emit( + AuthBlocState( + mode: AuthorizeMode.logIn, + currentUser: await _kdfSdk.auth.currentUser, + ), + ); + } + + Future _onWalletDownloadRequested( + AuthWalletDownloadRequested event, + Emitter emit, + ) async { + final Wallet? wallet = (await _kdfSdk.auth.currentUser)?.wallet; + if (wallet == null) return; + + await _walletsRepository.downloadEncryptedWallet(wallet, event.password); + + await _kdfSdk.confirmSeedBackup(); + emit( + AuthBlocState( + mode: AuthorizeMode.logIn, + currentUser: await _kdfSdk.auth.currentUser, + ), + ); + } + + 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 b1b8c64f39..df403bbe81 100644 --- a/lib/bloc/auth_bloc/auth_bloc_event.dart +++ b/lib/bloc/auth_bloc/auth_bloc_event.dart @@ -1,22 +1,51 @@ -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.wallet}); +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 String seed; final Wallet wallet; + final String password; +} + +class AuthRestoreRequested extends AuthBlocEvent { + const AuthRestoreRequested({ + required this.wallet, + required this.password, + required this.seed, + }); + + final Wallet wallet; + final String password; + final String seed; +} + +class AuthSeedBackupConfirmed extends AuthBlocEvent { + const AuthSeedBackupConfirmed(); +} + +class AuthWalletDownloadRequested extends AuthBlocEvent { + const AuthWalletDownloadRequested({required this.password}); + final String password; } 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 cb62dfa571..0000000000 --- a/lib/bloc/auth_bloc/auth_repository.dart +++ /dev/null @@ -1,38 +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]) async { - await _startMM2(seed); - await waitMM2StatusChange(MM2Status.rpcIsUp, mm2, waitingTime: 60000); - setAuthMode(mode); - } - - void setAuthMode(AuthorizeMode mode) { - _authController.sink.add(mode); - } - - 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/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/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..5b728a7a69 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,10 +10,11 @@ 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 { if (tickers == null) return null; @@ -24,9 +26,11 @@ class BridgeRepository { tickers.entries.fold({}, (previousValue, entry) { final List coins = previousValue[entry.key] ?? []; final List tickerDepths = depths - .where((depth) => - (abbr2Ticker(depth.source.abbr) == entry.key) && - (abbr2Ticker(depth.target.abbr) == entry.key)) + .where( + (depth) => + (abbr2Ticker(depth.source.abbr) == entry.key) && + (abbr2Ticker(depth.target.abbr) == entry.key), + ) .toList(); if (tickerDepths.isEmpty) return previousValue; @@ -48,11 +52,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 = @@ -65,7 +68,9 @@ class BridgeRepository { return multiProtocolCoins; } else { return removeTokensWithEmptyOrderbook( - multiProtocolCoins, orderBookDepths); + multiProtocolCoins, + orderBookDepths, + ); } } @@ -82,7 +87,8 @@ class BridgeRepository { } Future?> _frequentRequestDepth( - List> depthsPairs) async { + List> depthsPairs, + ) async { int attempts = 5; List? orderBookDepthsLocal; @@ -102,9 +108,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 c57db4e245..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))); @@ -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/cex_market_data/charts.dart b/lib/bloc/cex_market_data/charts.dart index 08b3105a5f..7463ca3c1b 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/komodo_defi_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/generate_demo_data.dart b/lib/bloc/cex_market_data/mockup/generate_demo_data.dart index a50c7015fa..c6d07d1a24 100644 --- a/lib/bloc/cex_market_data/mockup/generate_demo_data.dart +++ b/lib/bloc/cex_market_data/mockup/generate_demo_data.dart @@ -1,10 +1,10 @@ import 'dart:math'; +import 'package:decimal/decimal.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:uuid/uuid.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; -import 'package:web_dex/model/withdraw_details/fee_details.dart'; // similar to generator implementation to allow for const constructor final _ohlcvCache = >{}; @@ -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; @@ -110,7 +110,7 @@ class DemoDataGenerator { transactions.add(transaction); } - totalBalance += double.parse(transaction.myBalanceChange); + totalBalance += transaction.balanceChanges.netChange.toDouble(); if (totalBalance <= 0) { totalBalance = targetFinalBalance; break; @@ -131,22 +131,23 @@ class DemoDataGenerator { double totalBalance, List transactions, ) { - double adjustmentFactor = targetFinalBalance / totalBalance; + final Decimal adjustmentFactor = + Decimal.parse((targetFinalBalance / totalBalance).toString()); final adjustedTransactions = []; for (var transaction in transactions) { + final netChange = transaction.balanceChanges.netChange; + final received = transaction.balanceChanges.receivedByMe; + final spent = transaction.balanceChanges.spentByMe; + final totalAmount = transaction.balanceChanges.totalAmount; + adjustedTransactions.add( transaction.copyWith( - myBalanceChange: - (double.parse(transaction.myBalanceChange) * adjustmentFactor) - .toString(), - receivedByMe: - (double.parse(transaction.receivedByMe) * adjustmentFactor) - .toString(), - spentByMe: (double.parse(transaction.spentByMe) * adjustmentFactor) - .toString(), - totalAmount: - (double.parse(transaction.totalAmount) * adjustmentFactor) - .toString(), + balanceChanges: BalanceChanges( + netChange: netChange * adjustmentFactor, + receivedByMe: received * adjustmentFactor, + spentByMe: spent * adjustmentFactor, + totalAmount: totalAmount * adjustmentFactor, + ), ), ); } @@ -155,7 +156,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)); @@ -185,25 +194,30 @@ Transaction fromTradeAmount( final random = Random(42); return Transaction( + id: uuid.v4(), blockHeight: random.nextInt(100000) + 100000, - coin: coinId, - confirmations: random.nextInt(3) + 1, - feeDetails: FeeDetails( - type: "fixed", - coin: "USDT", - amount: "1.0", - totalFee: "1.0", + assetId: AssetId( + chainId: AssetChainId(chainId: 0), + derivationPath: '', + id: coinId, + name: coinId, + subClass: CoinSubClass.smartChain, + symbol: AssetSymbol(assetConfigId: coinId), ), - from: ["address1"], + confirmations: random.nextInt(3) + 1, + from: const ["address1"], internalId: uuid.v4(), - myBalanceChange: isBuy ? tradeAmount.toString() : (-tradeAmount).toString(), - receivedByMe: !isBuy ? tradeAmount.toString() : '0', - spentByMe: isBuy ? tradeAmount.toString() : '0', - timestamp: closeTimestamp ~/ 1000, - to: ["address2"], - totalAmount: tradeAmount.toString(), + balanceChanges: BalanceChanges( + netChange: Decimal.parse( + isBuy ? tradeAmount.toString() : (-tradeAmount).toString(), + ), + receivedByMe: Decimal.parse(!isBuy ? tradeAmount.toString() : '0'), + spentByMe: Decimal.parse(isBuy ? tradeAmount.toString() : '0'), + totalAmount: Decimal.parse(tradeAmount.toString()), + ), + timestamp: DateTime.fromMillisecondsSinceEpoch(closeTimestamp), + to: const ["address2"], txHash: uuid.v4(), - txHex: "hexstring", memo: "memo", ); } diff --git a/lib/bloc/cex_market_data/mockup/generator.dart b/lib/bloc/cex_market_data/mockup/generator.dart index a1e8c50884..fdd9cfe1a5 100644 --- a/lib/bloc/cex_market_data/mockup/generator.dart +++ b/lib/bloc/cex_market_data/mockup/generator.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_types/komodo_defi_types.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 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; final _supportedCoinsCache = >{}; final _transactionsCache = >>{}; 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..0763a0678f 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 @@ -1,12 +1,11 @@ import 'package:http/http.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/generator.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; -import 'package:web_dex/model/coin.dart'; -class MockTransactionHistoryRepo extends TransactionHistoryRepo { +class MockTransactionHistoryRepo implements TransactionHistoryRepo { final PerformanceMode performanceMode; final DemoDataCache demoDataGenerator; @@ -15,13 +14,17 @@ class MockTransactionHistoryRepo extends TransactionHistoryRepo { required Client client, required this.performanceMode, required this.demoDataGenerator, - }) : super(api: api, client: client); - + }); @override - Future> fetchTransactions(Coin coin) async { + Future> fetch(AssetId assetId) { return demoDataGenerator.loadTransactionsDemoData( performanceMode, - coin.abbr, + assetId.id, ); } + + @override + Future> fetchCompletedTransactions(AssetId assetId) { + return fetch(assetId); + } } 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..6b05f2a775 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.id, 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, @@ -188,7 +204,7 @@ class PortfolioGrowthBloc isError: true, trace: s, path: 'PortfolioGrowthBloc', - ); + ).ignore(); return ChartData.empty(); } } 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..fd6d4c8c38 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 @@ -3,15 +3,16 @@ import 'dart:math'; import 'package:hive/hive.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' as cex; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/charts.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart'; 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: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()) @@ -83,7 +93,7 @@ class PortfolioGrowthRepository { /// /// Returns the growth [ChartData] for the coin ([List] of [Point]). Future getCoinGrowthChart( - String coinId, { + AssetId coinId, { // avoid the possibility of accidentally swapping the order of these // required parameters by using named parameters required String fiatCoinId, @@ -95,7 +105,7 @@ class PortfolioGrowthRepository { }) async { if (useCache) { final String compoundKey = GraphCache.getPrimaryKey( - coinId, + coinId.id, fiatCoinId, GraphType.balanceGrowth, walletId, @@ -111,9 +121,9 @@ 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.getCoinFromId(coinId)!; final List transactions = await _transactionHistoryRepository - .fetchCompletedTransactions(coin) + .fetchCompletedTransactions(coin.id) .then((value) => value.toList()) .catchError((Object e) { if (ignoreTransactionFetchErrors) { @@ -129,7 +139,7 @@ class PortfolioGrowthRepository { // called later with useCache set to false to fetch the transactions again await _graphCache.insert( GraphCache( - coinId: coinId, + coinId: coinId.id, fiatCoinId: fiatCoinId, lastUpdated: DateTime.now(), graph: List.empty(), @@ -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; @@ -222,7 +232,7 @@ class PortfolioGrowthRepository { final chartDataFutures = coins.map((coin) async { try { return await getCoinGrowthChart( - coin.abbr, + coin.id, fiatCoinId: fiatCoinId, useCache: useCache, walletId: walletId, @@ -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()); } }); @@ -297,11 +310,11 @@ class PortfolioGrowthRepository { /// Returns `true` if the coin is supported by the CEX API for charting. /// Returns `false` if the coin is not supported by the CEX API for charting. Future isCoinChartSupported( - String coinId, + AssetId coinId, String fiatCoinId, { bool allowFiatAsBase = true, }) async { - final Coin coin = coinsBlocRepository.getCoin(coinId)!; + final Coin coin = _coinsRepository.getCoinFromId(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..f2ebc0b077 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,8 +20,6 @@ enum PriceChartPeriod { return '1M'; case PriceChartPeriod.oneYear: return '1Y'; - default: - throw Exception('Unknown interval'); } } @@ -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..1dc84f0d64 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,8 +20,6 @@ enum TimePeriod { return '1M'; case TimePeriod.oneYear: return '1Y'; - default: - throw Exception('Unknown interval'); } } @@ -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/price_chart/price_chart_bloc.dart b/lib/bloc/cex_market_data/price_chart/price_chart_bloc.dart index b20a7f89d3..ce46224f73 100644 --- a/lib/bloc/cex_market_data/price_chart/price_chart_bloc.dart +++ b/lib/bloc/cex_market_data/price_chart/price_chart_bloc.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'models/price_chart_data.dart'; @@ -7,13 +8,15 @@ import 'price_chart_event.dart'; import 'price_chart_state.dart'; class PriceChartBloc extends Bloc { - PriceChartBloc(this.cexPriceRepository) : super(const PriceChartState()) { + PriceChartBloc(this.cexPriceRepository, this.sdk) + : super(const PriceChartState()) { on(_onStarted); on(_onIntervalChanged); on(_onSymbolChanged); } final BinanceRepository cexPriceRepository; + final KomodoDefiSdk sdk; final KomodoPriceRepository _komodoPriceRepository = KomodoPriceRepository( cexPriceProvider: KomodoPriceProvider(), ); @@ -30,6 +33,11 @@ class PriceChartBloc extends Bloc { if (state.availableCoins.isEmpty) { final coins = (await cexPriceRepository.getCoinList()) .where((coin) => coin.currencies.contains('USDT')) + // `cexPriceRepository.getCoinList()` returns coins from a CEX + // (e.g. Binance), some of which are not in our known/available + // assets/coins list. This filter ensures that we only attempt to + // fetch and display data for supported coins + .where((coin) => sdk.assets.assetsFromTicker(coin.id).isNotEmpty) .map((coin) async { double? dayChangePercent = coinPrices[coin.symbol]?.change24h; 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..1ebfbdd7ed 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/komodo_defi_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..0e6079fcee 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/komodo_defi_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..e54c8e6b99 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/komodo_defi_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 4bc82319ed..f6b51274cc 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, @@ -143,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.id, + event.fiatCoinId, + allowInactiveCoins: allowInactiveCoins, + ); + if (coin.isTestCoin || !isCoinSupported) { + coins.remove(coin); + } + } return coins; } @@ -197,19 +202,20 @@ class ProfitLossBloc extends Bloc { // from breaking the entire portfolio chart. try { final profitLosses = await _profitLossRepository.getProfitLoss( - coin.abbr, + coin.id, event.fiatCoinId, event.walletId, useCache: useCache, ); - profitLosses.removeRange( - 0, - profitLosses.indexOf( - profitLosses.firstWhere((element) => element.profitLoss != 0), - ), + final startIndex = profitLosses.indexOf( + profitLosses.firstWhere((element) => element.profitLoss != 0), ); + if (startIndex == -1) { + profitLosses.removeRange(0, startIndex); + } + return profitLosses.toChartData(); } catch (e) { logger.log( 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..231b38b49a 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,12 +2,11 @@ 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/komodo_defi_types.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( @@ -31,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); @@ -49,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) { @@ -69,7 +65,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,10 +75,10 @@ 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) { + if (transaction.amount.toDouble() > 0) { state = _processBuyTransaction(state, transaction); } else { state = _processSellTransaction(state, transaction); @@ -106,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(), ); } @@ -119,15 +115,15 @@ 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; - var newHoldings = + final newHoldings = List<({double holdings, double price})>.from(state.holdings); while (remainingToSell > 0) { @@ -153,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( @@ -175,8 +171,7 @@ class ProfitLossCalculator { } class RealisedProfitLossCalculator extends ProfitLossCalculator { - RealisedProfitLossCalculator(CexRepository cexRepository) - : super(cexRepository); + RealisedProfitLossCalculator(super.cexRepository); @override double _calculateProfitLoss( @@ -188,8 +183,7 @@ class RealisedProfitLossCalculator extends ProfitLossCalculator { } class UnRealisedProfitLossCalculator extends ProfitLossCalculator { - UnRealisedProfitLossCalculator(CexRepository cexRepository) - : super(cexRepository); + UnRealisedProfitLossCalculator(super.cexRepository); @override double _calculateProfitLoss( @@ -203,15 +197,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/bloc/cex_market_data/profit_loss/profit_loss_repository.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart index ac11e20391..a574583f63 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 @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:hive/hive.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' as cex; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/charts.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; @@ -12,8 +13,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 +25,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 +56,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 +75,7 @@ class ProfitLossRepository { HiveLazyBoxProvider(name: cacheTableName), cexRepository: cexRepository, profitLossCalculator: RealisedProfitLossCalculator(cexRepository), + coinsRepository: coinsRepository, ); } @@ -79,23 +90,23 @@ class ProfitLossRepository { /// Returns `true` if the coin is supported by the CEX API for charting. /// Returns `false` if the coin is not supported by the CEX API for charting. Future isCoinChartSupported( - String coinId, + AssetId coinId, String fiatCoinId, { bool allowFiatAsBase = false, bool allowInactiveCoins = false, }) async { if (!allowInactiveCoins) { - final coin = coinsBlocRepository.getCoin(coinId)!; - if (coin.isActivating || !coin.isActive) { + final coin = await _coinsRepository.getEnabledCoin(coinId.id); + if (coin == null || coin.isActivating || !coin.isActive) { return false; } } final supportedCoins = await _cexRepository.getCoinList(); - final coinTicker = abbr2Ticker(coinId).toUpperCase(); + final coinTicker = abbr2Ticker(coinId.id).toUpperCase(); // Allow fiat coins through, as they are represented by a constant value, // 1, in the repository layer and are not supported by the CEX API - if (allowFiatAsBase && coinId == fiatCoinId.toUpperCase()) { + if (allowFiatAsBase && coinId.id == fiatCoinId.toUpperCase()) { return true; } @@ -119,14 +130,14 @@ class ProfitLossRepository { /// /// Returns the list of [ProfitLoss] for the coin. Future> getProfitLoss( - String coinId, + AssetId coinId, String fiatCoinId, String walletId, { bool useCache = true, }) async { if (useCache) { final String compoundKey = ProfitLossCache.getPrimaryKey( - coinId, + coinId.id, fiatCoinId, walletId, ); @@ -151,13 +162,13 @@ 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)!, + coinId, ); if (transactions.isEmpty) { await _profitLossCacheProvider.insert( ProfitLossCache( - coinId: coinId, + coinId: coinId.id, profitLosses: List.empty(), fiatCoinId: fiatCoinId, lastUpdated: DateTime.now(), @@ -170,13 +181,13 @@ class ProfitLossRepository { final List profitLosses = await _profitLossCalculator.getProfitFromTransactions( transactions, - coinId: coinId, + coinId: coinId.id, fiatCoinId: fiatCoinId, ); await _profitLossCacheProvider.insert( ProfitLossCache( - coinId: coinId, + coinId: coinId.id, profitLosses: profitLosses, fiatCoinId: fiatCoinId, lastUpdated: DateTime.now(), 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..f9435f0a37 --- /dev/null +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -0,0 +1,79 @@ +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 { + 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(sdk)).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..b59376c4a1 --- /dev/null +++ b/lib/bloc/coins_bloc/asset_coin_extension.dart @@ -0,0 +1,171 @@ +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +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 = protocol.subClass.toCoinType(); + // temporary measure to get metadata, like `wallet_only`, that isn't exposed + // by the SDK (and might be phased out completely later on) + // TODO: Remove this once the SDK exposes all the necessary metadata + final config = protocol.config; + final logoImageUrl = config.valueOrNull('logo_image_url'); + final isCustomToken = + (config.valueOrNull('is_custom_token') ?? false) || + logoImageUrl != null; + // TODO: Remove this once the SDK exposes all the necessary metadata + // This is the logic from the previous _getCoinMode function + final isSegwit = id.id.toLowerCase().contains('-segwit'); + + return Coin( + type: type, + abbr: id.id, + id: id, + name: id.name, + logoImageUrl: logoImageUrl ?? '', + isCustomCoin: isCustomToken, + 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, + state: CoinState.inactive, + walletOnly: config.valueOrNull('wallet_only') ?? false, + mode: isSegwit ? CoinMode.segwit : CoinMode.standard, + derivationPath: id.derivationPath, + ); + } + + String? get contractAddress => protocol.config + .valueOrNull('protocol', 'protocol_data', 'contract_address'); +} + +extension CoinTypeExtension on CoinSubClass { + CoinType toCoinType() { + switch (this) { + case CoinSubClass.ftm20: + return CoinType.ftm20; + case CoinSubClass.arbitrum: + return CoinType.arb20; + 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; + } + } + + bool isEvmProtocol() { + switch (this) { + case CoinSubClass.avx20: + case CoinSubClass.bep20: + case CoinSubClass.ftm20: + case CoinSubClass.matic: + case CoinSubClass.hrc20: + case CoinSubClass.arbitrum: + case CoinSubClass.moonriver: + case CoinSubClass.moonbeam: + case CoinSubClass.ethereumClassic: + case CoinSubClass.ubiq: + case CoinSubClass.krc20: + case CoinSubClass.ewt: + case CoinSubClass.hecoChain: + case CoinSubClass.rskSmartBitcoin: + case CoinSubClass.erc20: + return true; + default: + return false; + } + } +} + +extension CoinSubClassExtension on CoinType { + CoinSubClass toCoinSubClass() { + switch (this) { + case CoinType.ftm20: + return CoinSubClass.ftm20; + case CoinType.arb20: + return CoinSubClass.arbitrum; + case CoinType.slp: + return CoinSubClass.slp; + case CoinType.qrc20: + return CoinSubClass.qrc20; + case CoinType.avx20: + return CoinSubClass.avx20; + case CoinType.smartChain: + return CoinSubClass.smartChain; + case CoinType.mvr20: + return CoinSubClass.moonriver; + case CoinType.etc: + return CoinSubClass.ethereumClassic; + case CoinType.hco20: + return CoinSubClass.hecoChain; + case CoinType.hrc20: + return CoinSubClass.hrc20; + case CoinType.iris: + return CoinSubClass.tendermintToken; + case CoinType.cosmos: + return CoinSubClass.tendermint; + case CoinType.ubiq: + return CoinSubClass.ubiq; + case CoinType.bep20: + return CoinSubClass.bep20; + case CoinType.plg20: + return CoinSubClass.matic; + case CoinType.utxo: + return CoinSubClass.utxo; + case CoinType.sbch: + return CoinSubClass.smartBch; + case CoinType.erc20: + return CoinSubClass.erc20; + case CoinType.krc20: + return CoinSubClass.krc20; + } + } +} diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart new file mode 100644 index 0000000000..2560aaf702 --- /dev/null +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -0,0 +1,581 @@ +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/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/kdf_auth_metadata_extension.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._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: droppable()); + on(_onCoinsActivated, transformer: concurrent()); + on(_onCoinsDeactivated, transformer: concurrent()); + on(_onPricesUpdated, transformer: droppable()); + on(_onLogin, transformer: droppable()); + on(_onLogout, transformer: droppable()); + on( + _onReactivateSuspended, + transformer: droppable(), + ); + on(_onWalletCoinUpdated, transformer: sequential()); + on( + _onCoinsPubkeysRequested, + transformer: concurrent(), + ); + } + + final KomodoDefiSdk _kdfSdk; + 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 _onCoinsPubkeysRequested( + CoinsPubkeysRequested event, + Emitter emit, + ) async { + try { + // Get current coin + final coin = state.coins[event.coinId]; + if (coin == null) return; + + // Get pubkeys from the SDK through the repo + final asset = _kdfSdk.assets.assetsFromTicker(event.coinId).single; + final pubkeys = await _kdfSdk.pubkeys.getPubkeys(asset); + + // Update state with new pubkeys + emit( + state.copyWith( + pubkeys: { + ...state.pubkeys, + event.coinId: pubkeys, + }, + ), + ); + } catch (e, s) { + log( + 'Failed to get pubkeys for ${event.coinId}: $e', + isError: true, + path: 'coins_bloc => _onCoinsPubkeysRequested', + trace: s, + ).ignore(); + } + } + + 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 WalletType.hdwallet: + 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.of(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 _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(minutes: 1), + (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 || + _currentUserCache?.wallet.config.type == WalletType.hdwallet) { + 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 _mm2Api.disableCoin(coin.abbr); + + final newWalletCoins = Map.of(state.walletCoins) + ..remove(coin.abbr); + final newCoins = Map.of(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.of(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); + } + } + + if (changed) { + final newWalletCoins = state.walletCoins.map( + (String coinId, Coin coin) => MapEntry( + coinId, + coin.copyWith(usdPrice: coins[coinId]!.usdPrice), + ), + ); + emit( + state.copyWith( + coins: coins, + walletCoins: {...state.walletCoins, ...newWalletCoins}, + ), + ); + } + + 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: + case WalletType.hdwallet: + coin.reset(); + final newWalletCoins = Map.of(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); + + try { + await _kdfSdk.addActivatedCoins(coins); + } catch (e, s) { + log( + 'Failed to activate coins in SDK: $e', + isError: true, + path: 'coins_bloc => _activateCoins', + trace: s, + ).ignore(); + // Update state to reflect failure + return []; + } + + final enabledAssets = await _kdfSdk.assets.getEnabledCoins(); + final coinsToActivate = + coins.where((coin) => !enabledAssets.contains(coin)); + + final enableFutures = + coinsToActivate.map((coin) => _activateCoin(coin)).toList(); + final results = []; + await for (final coin + in Stream.fromFutures(enableFutures).asBroadcastStream()) { + results.add(coin); + emit( + state.copyWith( + walletCoins: {...state.walletCoins, coin.abbr: coin}, + coins: {...state.coins, coin.abbr: coin}, + ), + ); + } + + return results; + } + + void _prePopulateListWithActivatingCoins( + Iterable coins, + Emitter emit, + ) { + final activatingCoins = Map.fromIterable( + coins + .map( + (coin) { + final sdkCoin = state.coins[coin] ?? _coinsRepo.getCoin(coin); + return sdkCoin?.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] ?? _coinsRepo.getCoin(coinId); + if (coin == null) { + throw ArgumentError.value(coinId, 'coinId', 'Coin not found'); + } + + try { + final isLoggedIn = _currentUserCache != null; + if (!isLoggedIn || coin.isActive) { + return coin; + } + + switch (_currentUserCache?.wallet.config.type) { + case WalletType.iguana: + case WalletType.hdwallet: + coin = await _activateIguanaCoin(coin); + case WalletType.trezor: + coin = await _activateTrezorCoin(coin, coinId); + case WalletType.metamask: + case WalletType.keplr: + case null: + break; + } + } catch (e, s) { + log( + 'Error activating coin ${coin!.id.toString()}', + isError: true, + trace: s, + ); + } + + return coin; + } + + Future _activateTrezorCoin(Coin coin, String coinId) async { + final asset = _kdfSdk.assets.available[coin.id]; + if (asset == null) { + log('Failed to find asset for coin: ${coin.id}', isError: true); + return coin.copyWith(state: CoinState.suspended); + } + final accounts = await _trezorBloc.activateCoin(asset); + final state = accounts.isNotEmpty ? CoinState.active : CoinState.suspended; + return coin.copyWith(state: state, accounts: accounts); + } + + 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(); + } + + return _activateCoins(currentWallet.config.activatedCoins, emit); + } + + Stream> _reActivateSuspended( + Emitter emit, { + int attempts = 1, + }) async* { + final List coinsToBeActivated = []; + + for (int i = 0; i < attempts; i++) { + final List suspended = state.walletCoins.values + .where((coin) => coin.isSuspended) + .map((coin) => coin.abbr) + .toList(); + + coinsToBeActivated.addAll(suspended); + coinsToBeActivated.addAll(_getUnactivatedWalletCoins()); + + if (coinsToBeActivated.isEmpty) return; + yield await _activateCoins(coinsToBeActivated, emit); + } + } + + List _getUnactivatedWalletCoins() { + final Wallet? currentWallet = _currentUserCache?.wallet; + if (currentWallet == null) { + return List.empty(); + } + + return currentWallet.config.activatedCoins + .where((coinId) => !state.walletCoins.containsKey(coinId)) + .toList(); + } + + /// 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 String apiCoinId in await _kdfSdk.assets.getEnabledCoins()) { + if (!walletCoins.containsKey(apiCoinId)) { + // enabled on api side, but not on gui side - enable on gui side + final apiCoin = await _coinsRepo.getEnabledCoin(apiCoinId); + if (apiCoin != null) { + 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..a7af64ab8e --- /dev/null +++ b/lib/bloc/coins_bloc/coins_event.dart @@ -0,0 +1,89 @@ +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]; +} + +// TODO! Refactor to remove this so that the pubkeys are loaded with the coins +class CoinsPubkeysRequested extends CoinsEvent { + const CoinsPubkeysRequested(this.coinId); + + final String coinId; + + @override + List get props => [coinId]; +} diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 6733723417..cb895ccbbc 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -1,332 +1,571 @@ 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:flutter/material.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:komodo_ui_kit/komodo_ui_kit.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/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() { + // Intentionally avoid flushing the prices cache - prices are independent + // of the user's session and should be updated on a regular basis. + _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 Map assets = _kdfSdk.assets.available; + return assets.values.map(_assetToCoinWithoutAddress).toList(); + } + + Map getKnownCoinsMap() { + final Map 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? getCoinFromId(AssetId id) { + final asset = _kdfSdk.assets.available[id]; + if (asset == null) return null; + return _assetToCoinWithoutAddress(asset); + } - 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(); + @Deprecated('Use KomodoDefiSdk assets or getCoinFromId instead. ' + 'This uses the deprecated assetsFromTicker method that uses a separate ' + 'cache that does not update with custom token activation.') + Coin? getCoin(String coinId) { + if (coinId.isEmpty) return null; + + try { + final assets = _kdfSdk.assets.assetsFromTicker(coinId); + if (assets.isEmpty || assets.length > 1) { + log( + 'Coin "$coinId" not found. ${assets.length} results returned', + isError: true, + ).ignore(); + return null; } + return _assetToCoinWithoutAddress(assets.single); + } catch (_) { + return null; + } + } + + 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 = await _kdfSdk.assets.getActivatedAssets(); + return activatedCoins + .map((Asset asset) => _assetToCoinWithoutAddress(asset)) .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); + Future getEnabledCoin(String coinId) async { + final enabledAssets = _kdfSdk.assets.assetsFromTicker(coinId); + if (enabledAssets.length != 1) { + return null; + } + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + return null; } - _removeOrphans(coins); - - final List unmodifiableCoins = List.unmodifiable(coins); - _cachedKnownCoins = unmodifiableCoins; - return unmodifiableCoins; + final coin = _assetToCoinWithoutAddress(enabledAssets.single); + final coinAddress = await getFirstPubkey(coin.abbr); + return coin.copyWith( + address: coinAddress, + state: CoinState.active, + enabledType: currentUser.wallet.config.type, + ); } - /// 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']); - } - final newCoin = Coin.fromJson(coinJson, coinJson); - return newCoin; + Future> getEnabledCoins() async { + final enabledCoinsMap = await getEnabledCoinsMap(); + return enabledCoinsMap.values.toList(); } - /// 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; + Future> getEnabledCoinsMap() async { + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + return {}; } - if (getCoinType(coin.type, coin.coin) == null) { - return true; + 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 getFirstPubkey(coin.abbr); + coinsMap[coinId] = coin.copyWith( + address: coinAddress, + state: CoinState.active, + enabledType: currentUser.wallet.config.type, + ); } + return coinsMap; + } - if (coin.electrum != null && coin.electrum?.isNotEmpty == true) { - return coin.electrum! - .every((e) => !_isConnectionTypeSupported(e.protocol ?? '')); + 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!; + final parentAsset = _kdfSdk.assets.available[parentCoinId]; + if (parentAsset == null) { + log('Parent coin $parentCoinId not found.', isError: true).ignore(); + parentCoin = null; + } else { + parentCoin = _assetToCoinWithoutAddress(parentAsset); + } } - return false; + return coin.copyWith( + balance: balance, + sendableBalance: sendableBalance, + usdPrice: price, + parentCoin: parentCoin, + ); } - /// 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(); + /// Attempts to get the balance of a coin. If the coin is not found, it will + /// return a zero balance. + Future tryGetBalanceInfo(AssetId coinId) async { + try { + final asset = _kdfSdk.assets.available[coinId]; + if (asset == null) { + throw ArgumentError.value(coinId, 'coinId', 'Coin $coinId not found'); + } - if (kIsWeb) { - return uppercaseProtocol == 'WSS'; + final pubkeys = await _kdfSdk.pubkeys.getPubkeys(asset); + return pubkeys.balance; + } catch (e, s) { + log( + 'Failed to get coin $coinId balance: $e', + isError: true, + path: 'coins_repo => tryGetBalanceInfo', + trace: s, + ).ignore(); + return kdf_rpc.BalanceInfo.zero(); } - - return uppercaseProtocol == 'SSL' || uppercaseProtocol == 'TCP'; } - Future> _getKnownCoinsFromConfig() async { - final List globalCoinsJson = - await coinConfigParser.getGlobalCoinsJson(); - final Map appCoinsJson = - await coinConfigParser.getUnifiedCoinsJson(); - - final List appItems = appCoinsJson.values.toList(); + Future activateAssetsSync(List assets) async { + final isSignedIn = await _kdfSdk.auth.isSignedIn(); + if (!isSignedIn) { + final coinIdList = assets.map((e) => e.id.id).join(', '); + log('No wallet signed in. Skipping activation of [$coinIdList}]'); + return; + } - _removeUnknown(appItems, globalCoinsJson); + for (final asset in assets) { + final coin = asset.toCoin(); + try { + await _broadcastAsset(coin.copyWith(state: CoinState.activating)); - final List coins = appItems.map((dynamic appItem) { - final dynamic globalItem = - _getGlobalItemByAbbr(appItem['coin'], globalCoinsJson); + // ignore: deprecated_member_use + final progress = await _kdfSdk.assets.activateAsset(assets.single).last; + if (!progress.isSuccess) { + throw StateError('Failed to activate coin ${asset.id.id}'); + } - return Coin.fromJson(appItem, globalItem); - }).toList(); + await _broadcastAsset(coin.copyWith(state: CoinState.active)); + } catch (e, s) { + log( + 'Error activating coin: ${asset.id.id} \n$e', + isError: true, + trace: s, + ).ignore(); + await _broadcastAsset( + asset.toCoin().copyWith(state: CoinState.suspended), + ); + } finally { + // Register outside of the try-catch to ensure icon is available even + // in a suspended or failing activation status. + if (coin.logoImageUrl?.isNotEmpty == true) { + CoinIcon.registerCustomIcon( + coin.id.id, + NetworkImage(coin.logoImageUrl!), + ); + } + } + } + } - for (Coin coin in coins) { - coin.parentCoin = _getParentCoin(coin, coins); + Future activateCoinsSync(List coins) async { + final isSignedIn = await _kdfSdk.auth.isSignedIn(); + if (!isSignedIn) { + final coinIdList = coins.map((e) => e.abbr).join(', '); + log('No wallet signed in. Skipping activation of [$coinIdList}]'); + return; } - _removeOrphans(coins); + for (final coin in coins) { + try { + final asset = _kdfSdk.assets.available[coin.id]; + if (asset == null) { + log( + 'Coin ${coin.id} not found. Skipping activation.', + isError: true, + ).ignore(); + continue; + } - final List unmodifiableCoins = List.unmodifiable(coins); - _cachedKnownCoins = unmodifiableCoins; - return unmodifiableCoins; - } + await _broadcastAsset(coin.copyWith(state: CoinState.activating)); - // '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); + // ignore: deprecated_member_use + final progress = await _kdfSdk.assets.activateAsset(asset).last; + if (!progress.isSuccess) { + throw StateError('Failed to activate coin ${coin.abbr}'); + } - coins.removeWhere((coin) { - final String? platform = coin.protocolData?.platform; - if (platform == null) return false; + await _broadcastAsset(coin.copyWith(state: CoinState.active)); + } catch (e, s) { + log( + 'Error activating coin: ${coin.abbr} \n$e', + isError: true, + trace: s, + path: 'coins_repo->activateCoinsSync', + ).ignore(); + await _broadcastAsset(coin.copyWith(state: CoinState.suspended)); + } finally { + // Register outside of the try-catch to ensure icon is available even + // in a suspended or failing activation status. + if (coin.logoImageUrl?.isNotEmpty == true) { + CoinIcon.registerCustomIcon( + coin.id.id, + NetworkImage(coin.logoImageUrl!), + ); + } + } + } + } - final parentCoin = - original.firstWhereOrNull((coin) => coin.abbr == platform); + Future deactivateCoinsSync(List coins) async { + if (!await _kdfSdk.auth.isSignedIn()) return; - return parentCoin == null; - }); + for (final coin in coins) { + await _disableCoin(coin.abbr); + await _broadcastAsset(coin.copyWith(state: CoinState.inactive)); + } } - void _removeUnknown( - List appItems, - List globalItems, - ) { - appItems.removeWhere((dynamic appItem) { - return _getGlobalItemByAbbr(appItem['coin'], globalItems) == null; - }); + 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; + } } - dynamic _getGlobalItemByAbbr(String abbr, List globalItems) { - return globalItems.firstWhereOrNull((dynamic item) => abbr == item['coin']); + @Deprecated('Use SDK pubkeys.getPubkeys instead and let the user ' + 'select from the available options.') + Future getFirstPubkey(String coinId) async { + final asset = _kdfSdk.assets.findAssetsByTicker(coinId).single; + final pubkeys = await _kdfSdk.pubkeys.getPubkeys(asset); + if (pubkeys.keys.isEmpty) { + return null; + } + return pubkeys.keys.first.address; } - Coin? _getParentCoin(Coin? coin, List coins) { - final String? parentCoinAbbr = coin?.protocolData?.platform; - if (parentCoinAbbr == null) return null; + double? getUsdPriceByAmount(String amount, String coinAbbr) { + final Coin? coin = getCoin(coinAbbr); + final double? parsedAmount = double.tryParse(amount); + final double? usdPrice = coin?.usdPrice?.price; - return coins.firstWhereOrNull( - (item) => item.abbr.toUpperCase() == parentCoinAbbr.toUpperCase()); + if (coin == null || usdPrice == null || parsedAmount == null) { + return null; + } + return parsedAmount * usdPrice; } - Future> getEnabledCoins(List knownCoins) async { - final enabledCoins = await _api.getEnabledCoins(knownCoins); - return enabledCoins ?? []; + Future?> fetchCurrentPrices() async { + final Map? prices = + await _updateFromMain() ?? await _updateFromFallback(); + + if (prices != null) { + _pricesCache = prices; + } + + return _pricesCache; } - Future getBalanceInfo(String abbr) async { - return await _api.getMaxMakerVol(abbr); + Future fetchPrice(String ticker) async { + final Map? prices = await fetchCurrentPrices(); + if (prices == null || !prices.containsKey(ticker)) return null; + + return prices[ticker]!; } - Future deactivateCoin(Coin coin) async { - await _api.disableCoin(coin.abbr); + 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; + } + + 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(); + } + + 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; } - Future?> validateCoinAddress( - Coin coin, String address) async { - return await _api.validateAddress(coin.abbr, address); + 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', + ); + + 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; + } + + 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(); + } + + 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()), + ); + } + } + + return prices; } - Future?> withdraw(WithdrawRequest request) async { - return await _api.withdraw(request); + 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 sendRawTransaction( - SendRawTransactionRequest request) async { - final response = await _api.sendRawTransaction(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.id))); + + for (int i = 0; i < coins.length; i++) { + final newBalance = newBalances[i].total.toDouble(); + final newSendableBalance = newBalances[i].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> 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); - } - - 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)); - } - } + if (response['error'] != null) { + log('Withdraw error: ${response['error']}', isError: true).ignore(); + return BlocResponse( + error: withdrawErrorFactory.getError(response, request.params.coin), + ); } - await _api.enableCoins( - ethWithTokensRequests: ethWithTokensRequests, - erc20Requests: erc20Requests, - electrumCoinRequests: electrumCoinRequests, - tendermintRequests: tendermintRequests, - tendermintTokenRequests: tendermintTokenRequests, - bchWithTokens: bchWithTokens, - slpTokens: slpTokens, + + final WithdrawDetails withdrawDetails = WithdrawDetails.fromJson( + response['result'] as Map? ?? {}, ); - } - Future convertLegacyAddress(Coin coin, String address) async { - final request = ConvertAddressRequest( - coin: coin.abbr, - from: address, - isErc: coin.isErcType, + return BlocResponse( + result: withdrawDetails, ); - return await _api.convertLegacyAddress(request); } } diff --git a/lib/bloc/coins_bloc/coins_state.dart b/lib/bloc/coins_bloc/coins_state.dart new file mode 100644 index 0000000000..ae1e9d196e --- /dev/null +++ b/lib/bloc/coins_bloc/coins_state.dart @@ -0,0 +1,53 @@ +part of 'coins_bloc.dart'; + +class CoinsState extends Equatable { + const CoinsState({ + required this.coins, + required this.walletCoins, + required this.loginActivationFinished, + required this.pubkeys, + }); + + factory CoinsState.initial() => const CoinsState( + coins: {}, + walletCoins: {}, + loginActivationFinished: false, + pubkeys: {}, + ); + + final Map coins; + final Map walletCoins; + final bool loginActivationFinished; + final Map pubkeys; + + @override + List get props => + [coins, walletCoins, loginActivationFinished, pubkeys]; + + CoinsState copyWith({ + Map? coins, + Map? walletCoins, + bool? loginActivationFinished, + Map? pubkeys, + }) { + return CoinsState( + coins: coins ?? this.coins, + walletCoins: walletCoins ?? this.walletCoins, + loginActivationFinished: + loginActivationFinished ?? this.loginActivationFinished, + pubkeys: pubkeys ?? this.pubkeys, + ); + } + + // TODO! Migrate to SDK + 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; + } +} diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index 9ce11b82bb..e1d38d412b 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -1,47 +1,38 @@ 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:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.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/kdf_auth_metadata_extension.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 CoinsManagerAction action, + required CoinsRepo coinsRepo, + required KomodoDefiSdk sdk, }) : _coinsRepo = coinsRepo, - super( - CoinsManagerState.initial( - action: action, - coins: _getOriginalCoinList(coinsRepo, action), - ), - ) { + _sdk = sdk, + super(CoinsManagerState.initial(coins: [])) { on(_onCoinsUpdate); + on(_onCoinsListReset); on(_onCoinTypeSelect); on(_onCoinsSwitch); on(_onCoinSelect); 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 KomodoDefiSdk _sdk; List mergeCoinLists(List originalList, List newList) { Map coinMap = {}; @@ -60,14 +51,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, event.action, _sdk), + state.coins, + ); if (state.searchPhrase.isNotEmpty) { filters.add(_filterByPhrase); @@ -80,7 +73,21 @@ class CoinsManagerBloc extends Bloc { list = filter(list); } - emit(state.copyWith(coins: list)); + emit(state.copyWith(coins: list, action: event.action)); + } + + Future _onCoinsListReset( + CoinsManagerCoinsListReset event, + Emitter emit, + ) async { + emit(CoinsManagerState.initial(coins: [], action: event.action)); + final List coins = await _getOriginalCoinList( + _coinsRepo, + event.action, + _sdk, + ) + ..sort((a, b) => a.abbr.compareTo(b.abbr)); + emit(state.copyWith(coins: coins, action: event.action)); } void _onCoinTypeSelect( @@ -93,7 +100,7 @@ class CoinsManagerBloc extends Bloc { emit(state.copyWith(selectedCoinTypes: newTypes)); - add(const CoinsManagerCoinsUpdate()); + add(CoinsManagerCoinsUpdate(state.action)); } Future _onCoinsSwitch( @@ -104,8 +111,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 +128,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)); @@ -151,7 +158,7 @@ class CoinsManagerBloc extends Bloc { Emitter emit, ) { emit(state.copyWith(selectedCoinTypes: [])); - add(const CoinsManagerCoinsUpdate()); + add(CoinsManagerCoinsUpdate(state.action)); } FutureOr _onSearchUpdate( @@ -159,7 +166,7 @@ class CoinsManagerBloc extends Bloc { Emitter emit, ) { emit(state.copyWith(searchPhrase: event.text)); - add(const CoinsManagerCoinsUpdate()); + add(CoinsManagerCoinsUpdate(state.action)); } List _filterByPhrase(List coins) { @@ -196,42 +203,41 @@ class CoinsManagerBloc extends Bloc { } } -List _getOriginalCoinList( - CoinsBloc coinsRepo, +Future> _getOriginalCoinList( + CoinsRepo coinsRepo, CoinsManagerAction action, -) { - final WalletType? walletType = currentWalletBloc.wallet?.config.type; + KomodoDefiSdk sdk, +) async { + final WalletType? walletType = (await sdk.currentWallet())?.config.type; if (walletType == null) return []; switch (action) { case CoinsManagerAction.add: - return _getDeactivatedCoins(coinsRepo, walletType); + return await _getDeactivatedCoins(coinsRepo, sdk, walletType); case CoinsManagerAction.remove: - return _getActivatedCoins(coinsRepo); + return await coinsRepo.getWalletCoins(); case CoinsManagerAction.none: return []; } } -List _getActivatedCoins(CoinsBloc coinsRepo) { - return coinsRepo.walletCoins.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, + KomodoDefiSdk sdk, + WalletType walletType, +) async { + final Iterable enabledCoins = await sdk.assets.getEnabledCoins(); + final Map disabledCoins = coinsRepo.getKnownCoinsMap() + ..removeWhere((key, coin) => enabledCoins.contains(key)); switch (walletType) { case WalletType.iguana: - return disabledCoinsMap.values.toList(); + case WalletType.hdwallet: + 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..06275ebab6 100644 --- a/lib/bloc/coins_manager/coins_manager_event.dart +++ b/lib/bloc/coins_manager/coins_manager_event.dart @@ -1,12 +1,17 @@ -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(); } +class CoinsManagerCoinsListReset extends CoinsManagerEvent { + const CoinsManagerCoinsListReset(this.action); + final CoinsManagerAction action; +} + class CoinsManagerCoinsUpdate extends CoinsManagerEvent { - const CoinsManagerCoinsUpdate(); + const CoinsManagerCoinsUpdate(this.action); + final CoinsManagerAction action; } class CoinsManagerCoinTypeSelect extends CoinsManagerEvent { diff --git a/lib/bloc/coins_manager/coins_manager_state.dart b/lib/bloc/coins_manager/coins_manager_state.dart index 104fcbe3ea..41202711f9 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({ @@ -20,8 +17,8 @@ class CoinsManagerState extends Equatable { final bool isSwitching; static CoinsManagerState initial({ - required CoinsManagerAction action, required List coins, + CoinsManagerAction action = CoinsManagerAction.add, }) { return CoinsManagerState( action: action, diff --git a/lib/bloc/custom_token_import/bloc/custom_token_import_bloc.dart b/lib/bloc/custom_token_import/bloc/custom_token_import_bloc.dart new file mode 100644 index 0000000000..2063f53ed5 --- /dev/null +++ b/lib/bloc/custom_token_import/bloc/custom_token_import_bloc.dart @@ -0,0 +1,149 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_bloc/flutter_bloc.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/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_event.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_state.dart'; +import 'package:web_dex/bloc/custom_token_import/data/custom_token_import_repository.dart'; +import 'package:web_dex/model/coin_type.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class CustomTokenImportBloc + extends Bloc { + final ICustomTokenImportRepository repository; + final CoinsRepo _coinsRepo; + + CustomTokenImportBloc(this.repository, this._coinsRepo) + : super(CustomTokenImportState.defaults()) { + on(_onUpdateAsset); + on(_onUpdateAddress); + on(_onSubmitImportCustomToken); + on(_onSubmitFetchCustomToken); + on(_onResetFormStatus); + } + + void _onResetFormStatus( + ResetFormStatusEvent event, + Emitter emit, + ) { + final availableCoinTypes = + CoinType.values.map((CoinType type) => type.toCoinSubClass()); + final items = CoinSubClass.values + .where( + (CoinSubClass type) => + type.isEvmProtocol() && availableCoinTypes.contains(type), + ) + .toList() + ..sort((a, b) => a.name.compareTo(b.name)); + + emit( + state.copyWith( + formStatus: FormStatus.initial, + formErrorMessage: '', + importStatus: FormStatus.initial, + importErrorMessage: '', + evmNetworks: items, + ), + ); + } + + void _onUpdateAsset( + UpdateNetworkEvent event, + Emitter emit, + ) { + if (event.network == null) { + return; + } + emit(state.copyWith(network: event.network)); + } + + void _onUpdateAddress( + UpdateAddressEvent event, + Emitter emit, + ) { + emit(state.copyWith(address: event.address)); + } + + Future _onSubmitFetchCustomToken( + SubmitFetchCustomTokenEvent event, + Emitter emit, + ) async { + emit(state.copyWith(formStatus: FormStatus.submitting)); + + try { + final networkAsset = _coinsRepo.getCoin(state.network.ticker); + if (networkAsset == null) { + throw Exception('Network asset ${state.network.formatted} not found'); + } + + await _coinsRepo.activateCoinsSync([networkAsset]); + final tokenData = + await repository.fetchCustomToken(state.network, state.address); + await _coinsRepo.activateAssetsSync([tokenData]); + + final balanceInfo = await _coinsRepo.tryGetBalanceInfo(tokenData.id); + final balance = balanceInfo.spendable; + final usdBalance = + _coinsRepo.getUsdPriceByAmount(balance.toString(), tokenData.id.id); + + emit( + state.copyWith( + formStatus: FormStatus.success, + tokenData: () => tokenData, + tokenBalance: balance, + tokenBalanceUsd: + Decimal.tryParse(usdBalance?.toString() ?? '0.0') ?? Decimal.zero, + formErrorMessage: '', + ), + ); + + await _coinsRepo.deactivateCoinsSync([tokenData.toCoin()]); + } catch (e, s) { + log( + 'Error fetching custom token: ${e.toString()}', + path: 'CustomTokenImportBloc._onSubmitFetchCustomToken', + isError: true, + trace: s, + ); + emit( + state.copyWith( + formStatus: FormStatus.failure, + tokenData: () => null, + formErrorMessage: e.toString(), + ), + ); + } + } + + Future _onSubmitImportCustomToken( + SubmitImportCustomTokenEvent event, + Emitter emit, + ) async { + emit(state.copyWith(importStatus: FormStatus.submitting)); + + try { + await repository.importCustomToken(state.coin!); + + emit( + state.copyWith( + importStatus: FormStatus.success, + importErrorMessage: '', + ), + ); + } catch (e, s) { + log( + 'Error importing custom token: ${e.toString()}', + path: 'CustomTokenImportBloc._onSubmitImportCustomToken', + isError: true, + trace: s, + ); + emit( + state.copyWith( + importStatus: FormStatus.failure, + importErrorMessage: e.toString(), + ), + ); + } + } +} diff --git a/lib/bloc/custom_token_import/bloc/custom_token_import_event.dart b/lib/bloc/custom_token_import/bloc/custom_token_import_event.dart new file mode 100644 index 0000000000..ef129aca08 --- /dev/null +++ b/lib/bloc/custom_token_import/bloc/custom_token_import_event.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +abstract class CustomTokenImportEvent extends Equatable { + const CustomTokenImportEvent(); + + @override + List get props => []; +} + +class UpdateNetworkEvent extends CustomTokenImportEvent { + final CoinSubClass? network; + + const UpdateNetworkEvent(this.network); + + @override + List get props => [network]; +} + +class UpdateAddressEvent extends CustomTokenImportEvent { + final String address; + + const UpdateAddressEvent(this.address); + + @override + List get props => [address]; +} + +class SubmitImportCustomTokenEvent extends CustomTokenImportEvent { + const SubmitImportCustomTokenEvent(); +} + +class SubmitFetchCustomTokenEvent extends CustomTokenImportEvent { + const SubmitFetchCustomTokenEvent(); +} + +class ResetFormStatusEvent extends CustomTokenImportEvent { + const ResetFormStatusEvent(); +} diff --git a/lib/bloc/custom_token_import/bloc/custom_token_import_state.dart b/lib/bloc/custom_token_import/bloc/custom_token_import_state.dart new file mode 100644 index 0000000000..3c523749aa --- /dev/null +++ b/lib/bloc/custom_token_import/bloc/custom_token_import_state.dart @@ -0,0 +1,82 @@ +import 'package:decimal/decimal.dart'; +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +enum FormStatus { initial, submitting, success, failure } + +class CustomTokenImportState extends Equatable { + const CustomTokenImportState({ + required this.formStatus, + required this.importStatus, + required this.network, + required this.address, + required this.formErrorMessage, + required this.importErrorMessage, + required this.coin, + required this.coinBalance, + required this.coinBalanceUsd, + required this.evmNetworks, + }); + + CustomTokenImportState.defaults({ + this.network = CoinSubClass.erc20, + this.address = '', + this.formStatus = FormStatus.initial, + this.importStatus = FormStatus.initial, + this.formErrorMessage = '', + this.importErrorMessage = '', + this.coin, + this.evmNetworks = const [], + }) : coinBalance = Decimal.zero, + coinBalanceUsd = Decimal.zero; + + final FormStatus formStatus; + final FormStatus importStatus; + final CoinSubClass network; + final String address; + final String formErrorMessage; + final String importErrorMessage; + final Asset? coin; + final Decimal coinBalance; + final Decimal coinBalanceUsd; + final Iterable evmNetworks; + + CustomTokenImportState copyWith({ + FormStatus? formStatus, + FormStatus? importStatus, + CoinSubClass? network, + String? address, + String? formErrorMessage, + String? importErrorMessage, + Asset? Function()? tokenData, + Decimal? tokenBalance, + Decimal? tokenBalanceUsd, + Iterable? evmNetworks, + }) { + return CustomTokenImportState( + formStatus: formStatus ?? this.formStatus, + importStatus: importStatus ?? this.importStatus, + network: network ?? this.network, + address: address ?? this.address, + formErrorMessage: formErrorMessage ?? this.formErrorMessage, + importErrorMessage: importErrorMessage ?? this.importErrorMessage, + coin: tokenData?.call() ?? coin, + evmNetworks: evmNetworks ?? this.evmNetworks, + coinBalance: tokenBalance ?? coinBalance, + coinBalanceUsd: tokenBalanceUsd ?? coinBalanceUsd, + ); + } + + @override + List get props => [ + formStatus, + importStatus, + network, + address, + formErrorMessage, + importErrorMessage, + coin, + coinBalance, + evmNetworks, + ]; +} diff --git a/lib/bloc/custom_token_import/data/custom_token_import_repository.dart b/lib/bloc/custom_token_import/data/custom_token_import_repository.dart new file mode 100644 index 0000000000..5888044056 --- /dev/null +++ b/lib/bloc/custom_token_import/data/custom_token_import_repository.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.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/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_icon.dart'; + +abstract class ICustomTokenImportRepository { + Future fetchCustomToken(CoinSubClass network, String address); + Future importCustomToken(Asset asset); +} + +class KdfCustomTokenImportRepository implements ICustomTokenImportRepository { + KdfCustomTokenImportRepository(this._kdfSdk, this._coinsRepo); + + final CoinsRepo _coinsRepo; + final KomodoDefiSdk _kdfSdk; + + @override + Future fetchCustomToken(CoinSubClass network, String address) async { + final convertAddressResponse = + await _kdfSdk.client.rpc.address.convertAddress( + from: address, + coin: network.ticker, + toFormat: AddressFormat.fromCoinSubClass(CoinSubClass.erc20), + ); + final contractAddress = convertAddressResponse.address; + final knownCoin = _kdfSdk.assets.available.values.firstWhereOrNull( + (asset) => asset.contractAddress == contractAddress, + ); + if (knownCoin == null) { + return await _createNewCoin( + contractAddress, + network, + address, + ); + } + + return knownCoin; + } + + Future _createNewCoin( + String contractAddress, + CoinSubClass network, + String address, + ) async { + final response = await _kdfSdk.client.rpc.utility.getTokenInfo( + contractAddress: contractAddress, + platform: network.ticker, + protocolType: CoinSubClass.erc20.formatted, + ); + + final platformAssets = _kdfSdk.assets.findAssetsByTicker(network.ticker); + if (platformAssets.length != 1) { + throw Exception('Platform asset not found. ${platformAssets.length} ' + 'results returned.'); + } + final platformAsset = platformAssets.single; + final platformConfig = platformAsset.protocol.config; + final String ticker = response.info.symbol; + final tokenApi = await fetchTokenInfoFromApi(network, contractAddress); + + final coinId = '$ticker-${network.ticker}'; + final logoImageUrl = tokenApi?['image']?['large'] ?? + tokenApi?['image']?['small'] ?? + tokenApi?['image']?['thumb']; + final newCoin = Asset( + id: AssetId( + id: coinId, + name: tokenApi?['name'] ?? ticker, + symbol: AssetSymbol( + assetConfigId: '$ticker-${network.ticker}', + coinGeckoId: tokenApi?['id'], + coinPaprikaId: tokenApi?['id'], + ), + chainId: AssetChainId(chainId: 0), + subClass: network, + derivationPath: '', + ), + protocol: Erc20Protocol.fromJson({ + 'type': network.formatted, + 'chain_id': 0, + 'nodes': [], + 'swap_contract_address': + platformConfig.valueOrNull('swap_contract_address'), + 'fallback_swap_contract': + platformConfig.valueOrNull('fallback_swap_contract'), + 'protocol': { + 'protocol_data': { + 'platform': network.ticker, + 'contract_address': address, + }, + }, + 'logo_image_url': logoImageUrl, + 'explorer_url': platformConfig.valueOrNull('explorer_url'), + 'explorer_url_tx': + platformConfig.valueOrNull('explorer_url_tx'), + 'explorer_url_address': + platformConfig.valueOrNull('explorer_url_address'), + }).copyWith(isCustomToken: true), + ); + + CoinIcon.registerCustomIcon( + newCoin.id.id, + NetworkImage( + tokenApi?['image']?['large'] ?? + 'assets/coin_icons/png/${ticker.toLowerCase()}.png', + ), + ); + + return newCoin; + } + + @override + Future importCustomToken(Asset asset) async { + await _coinsRepo.activateAssetsSync([asset]); + } + + Future?> fetchTokenInfoFromApi( + CoinSubClass coinType, + String contractAddress, + ) async { + final platform = getNetworkApiName(coinType); + if (platform == null) { + log('Unsupported Image URL Network: $coinType'); + return null; + } + + final url = Uri.parse( + 'https://api.coingecko.com/api/v3/coins/$platform/' + 'contract/$contractAddress', + ); + + try { + final response = await http.get(url); + final data = jsonDecode(response.body); + return data; + } catch (e) { + log('Error fetching token image URL: $e'); + return null; + } + } + + // this does not appear to match the coingecko id field in the coins config. + // notable differences are bep20, matic, and hrc20 + // these could possibly be mapped with another field, or it should be changed + // to the subclass formatted/ticker fields + String? getNetworkApiName(CoinSubClass coinType) { + switch (coinType) { + case CoinSubClass.erc20: + return 'ethereum'; + case CoinSubClass.bep20: + return 'binance-smart-chain'; + case CoinSubClass.qrc20: + return 'qtum'; + case CoinSubClass.ftm20: + return 'fantom'; + case CoinSubClass.arbitrum: + return 'arbitrum-one'; + case CoinSubClass.avx20: + return 'avalanche'; + case CoinSubClass.moonriver: + return 'moonriver'; + case CoinSubClass.hecoChain: + return 'huobi-token'; + case CoinSubClass.matic: + return 'polygon-pos'; + case CoinSubClass.hrc20: + return 'harmony-shard-0'; + case CoinSubClass.krc20: + return 'kcc'; + default: + return null; + } + } +} diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index c1833f6aa5..096fff1c14 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())); @@ -35,8 +37,13 @@ class DexRepository { } Future> getTradePreimage( - String base, String rel, Rational price, String swapMethod, - [Rational? volume, bool max = false]) async { + String base, + String rel, + Rational price, + String swapMethod, [ + Rational? volume, + bool max = false, + ]) async { final request = TradePreimageRequest( base: base, rel: rel, @@ -46,7 +53,8 @@ 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; @@ -60,8 +68,11 @@ class DexRepository { } try { return DataFromService( - data: mapTradePreimageResponseResultToTradePreimage( - result, response.request)); + data: mapTradePreimageResponseResultToTradePreimage( + result, + response.request, + ), + ); } catch (e, s) { log( e.toString(), @@ -76,7 +87,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 +97,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 +112,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())); } @@ -125,14 +136,16 @@ class DexRepository { log('Error parsing best_orders response: $e', trace: s, isError: true); return BestOrders( - error: TextError( - error: 'Something went wrong! Unexpected response format.')); + error: TextError( + error: 'Something went wrong! Unexpected response format.', + ), + ); } } 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 ac228d5239..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,9 +2,13 @@ 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/model/authorize_mode.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/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,33 +16,78 @@ part 'dex_tab_bar_event.dart'; part 'dex_tab_bar_state.dart'; class DexTabBarBloc extends Bloc { - DexTabBarBloc(super.initialState, AuthRepository authRepo) { + DexTabBarBloc( + this._kdfSdk, + this._tradingEntitiesBloc, + this._tradingBotRepository, + ) : super(const DexTabBarState.initial()) { on(_onTabChanged); on(_onFilterChanged); - - _authorizationSubscription = authRepo.authMode.listen((event) { - if (event == AuthorizeMode.noLogin) { - add(const TabChanged(0)); - } - }); + on(_onStartListening); + on(_onStopListening); + on(_onMyOrdersUpdated); + on(_onSwapsUpdated); + on(_onTradeBotOrdersUpdated); } + final TradingEntitiesBloc _tradingEntitiesBloc; + final MarketMakerBotOrderListRepository _tradingBotRepository; + final KomodoDefiSdk _kdfSdk; + + 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; - int get ordersCount => tradingEntitiesBloc.myOrders.length; + void _onStartListening( + ListenToOrdersRequested event, + Emitter emit, + ) { + _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) { + add(SwapsUpdated(swaps)); + }); + + _tradeBotOrdersSubscription = Stream.periodic(const Duration(seconds: 3)) + .asyncMap((_) => _tradingBotRepository.getTradePairs()) + .listen((orders) { + add(TradeBotOrdersUpdated(orders)); + }); + } + + Future _onStopListening( + StopListeningToOrdersRequested 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 +98,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..7e3680922d 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 ListenToOrdersRequested extends DexTabBarEvent { + const ListenToOrdersRequested(); +} + +class StopListeningToOrdersRequested extends DexTabBarEvent { + const StopListeningToOrdersRequested(); +} + +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/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..827bfe32b0 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; } @@ -244,36 +211,72 @@ abstract class BaseFiatProvider { // ZILLIQA } + // TODO: migrate to SDK [CoinSubClass] ticker/formatted getters 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..2126b24457 --- /dev/null +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart @@ -0,0 +1,465 @@ +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/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/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({ + 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( + _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 CoinsRepo _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 accountReference = await _coinsRepository.getFirstPubkey('KMD'); + final address = await _coinsRepository + .getFirstPubkey(state.selectedCoin.value!.getAbbr()); + + emit( + state.copyWith( + accountReference: accountReference, + coinReceiveAddress: address, + ), + ); + } + + void _onClearAccountInformation( + ClearAccountInformationRequested event, + Emitter emit, + ) { + emit( + state.copyWith( + accountReference: '', + coinReceiveAddress: '', + ), + ); + } + + Future _fetchAccountInfo(Emitter emit) async { + final address = + await _coinsRepository.getFirstPubkey(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 await 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 (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..3cc916900d --- /dev/null +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart @@ -0,0 +1,165 @@ +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..522ae1e7ac 100644 --- a/lib/bloc/fiat/fiat_repository.dart +++ b/lib/bloc/fiat/fiat_repository.dart @@ -1,25 +1,23 @@ 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/ramp/ramp_fiat_provider.dart'; +import 'package:web_dex/bloc/fiat/models/models.dart'; import 'package:web_dex/shared/utils/utils.dart'; -final fiatRepository = - FiatRepository([BanxaFiatProvider(), RampFiatProvider()]); - class FiatRepository { + FiatRepository(this.fiatProviders, this._coinsRepo); + final List fiatProviders; - FiatRepository(this.fiatProviders); + final CoinsRepo _coinsRepo; 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 +32,7 @@ class FiatRepository { } Stream watchOrderStatus( - Map paymentMethod, + FiatPaymentMethod paymentMethod, String orderId, ) async* { final provider = _getPaymentMethodProvider(paymentMethod); @@ -43,18 +41,19 @@ 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; if (isCoin) { - final knownCoins = await coinsRepo.getKnownCoins(); + final knownCoins = _coinsRepo.getKnownCoins(); knownCoinAbbreviations = knownCoins.map((coin) => coin.abbr).toSet(); } @@ -62,7 +61,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 +75,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 +99,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 +124,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 +141,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 +158,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 +167,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 +195,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 +211,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 +229,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 +246,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..aad2288ab1 --- /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..8b13789179 --- /dev/null +++ b/lib/bloc/fiat/models/fiat_order.dart @@ -0,0 +1 @@ + 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..983d1c19b1 --- /dev/null +++ b/lib/bloc/fiat/models/i_currency.dart @@ -0,0 +1,69 @@ +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_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 45e7fee7ca..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,3 +1,5 @@ +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/mm2/mm2_api/rpc/market_maker_bot/trade_coin_pair_config.dart'; @@ -5,14 +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; - const MarketMakerBotOrderListRepository( this._ordersService, this._settingsRepository, + this._coinsRepository, ); + final CoinsRepo _coinsRepository; + final MyOrdersService _ordersService; + final SettingsRepository _settingsRepository; + Future cancelOrders(List tradePairs) async { final orders = await _ordersService.getOrders(); final ordersToCancel = orders @@ -27,7 +31,7 @@ class MarketMakerBotOrderListRepository { ) .toList(); - if (ordersToCancel?.isEmpty == true) { + if (ordersToCancel?.isEmpty ?? false) { return; } @@ -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/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_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/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..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,11 +62,11 @@ 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, - Emitter emit, + Emitter emit, ) async { final identicalBuyAndSellCoins = state.buyCoin.value == event.sellCoin; final sellCoinBalance = event.sellCoin?.balance ?? 0; @@ -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/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/nft_receive/bloc/nft_receive_bloc.dart b/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart index eba799889f..90fa0ea53d 100644 --- a/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart +++ b/lib/bloc/nft_receive/bloc/nft_receive_bloc.dart @@ -1,8 +1,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:web_dex/blocs/coins_bloc.dart'; -import 'package:web_dex/blocs/current_wallet_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/model/coin.dart'; +import 'package:web_dex/model/kdf_auth_metadata_extension.dart'; import 'package:web_dex/model/nft.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; @@ -11,18 +12,18 @@ part 'nft_receive_state.dart'; class NftReceiveBloc extends Bloc { NftReceiveBloc({ - required CoinsBloc coinsRepo, - required CurrentWalletBloc currentWalletBloc, + required CoinsRepo coinsRepo, + required KomodoDefiSdk sdk, }) : _coinsRepo = coinsRepo, - _currentWalletBloc = currentWalletBloc, + _sdk = sdk, super(NftReceiveInitial()) { on(_onInitial); on(_onRefresh); on(_onChangeAddress); } - final CoinsBloc _coinsRepo; - final CurrentWalletBloc _currentWalletBloc; + final CoinsRepo _coinsRepo; + final KomodoDefiSdk _sdk; NftBlockchains? chain; Future _onInitial(NftReceiveEventInitial event, Emitter emit) async { @@ -32,7 +33,7 @@ class NftReceiveBloc extends Bloc { var coin = _coinsRepo.getCoin(abbr); if (coin != null) { - final walletConfig = _currentWalletBloc.wallet?.config; + final walletConfig = (await _sdk.currentWallet())?.config; if (walletConfig?.hasBackup == false && !coin.isTestCoin) { return emit( NftReceiveHasBackup(), @@ -40,7 +41,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..3b179e26c6 100644 --- a/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart +++ b/lib/bloc/nft_withdraw/nft_withdraw_bloc.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; 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:web_dex/bloc/coins_bloc/asset_coin_extension.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/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; @@ -23,9 +25,11 @@ class NftWithdrawBloc extends Bloc { NftWithdrawBloc({ required NftWithdrawRepo repo, required NftToken nft, - required CoinsBloc coinsBloc, + required KomodoDefiSdk kdfSdk, + required CoinsRepo coinsRepository, }) : _repo = repo, - _coinsBloc = coinsBloc, + _coinsRepository = coinsRepository, + _kdfSdk = kdfSdk, super(NftWithdrawFillState.initial(nft)) { on(_onAddressChanged); on(_onAmountChanged); @@ -37,7 +41,8 @@ class NftWithdrawBloc extends Bloc { } final NftWithdrawRepo _repo; - final CoinsBloc _coinsBloc; + final KomodoDefiSdk _kdfSdk; + final CoinsRepo _coinsRepository; Future _onSend( NftWithdrawSendEvent event, @@ -47,12 +52,14 @@ class NftWithdrawBloc extends Bloc { if (state is! NftWithdrawFillState) return; if (state.isSending) return; - emit(state.copyWith( - isSending: () => true, - addressError: () => null, - amountError: () => null, - sendError: () => null, - )); + emit( + state.copyWith( + isSending: () => true, + addressError: () => null, + amountError: () => null, + sendError: () => null, + ), + ); final NftToken nft = state.nft; final String address = state.address; final int? amount = state.amount; @@ -64,11 +71,13 @@ class NftWithdrawBloc extends Bloc { final BaseError? amountError = _validateAmount(amount, int.parse(nft.amount), nft.contractType); if (addressError != null || amountError != null) { - emit(state.copyWith( - isSending: () => false, - addressError: () => addressError, - amountError: () => amountError, - )); + emit( + state.copyWith( + isSending: () => false, + addressError: () => addressError, + amountError: () => amountError, + ), + ); return; } @@ -78,12 +87,14 @@ class NftWithdrawBloc extends Bloc { final NftTransactionDetails result = response.result; - emit(NftWithdrawConfirmState( - nft: state.nft, - isSending: false, - txDetails: result, - sendError: null, - )); + emit( + NftWithdrawConfirmState( + nft: state.nft, + isSending: false, + txDetails: result, + sendError: null, + ), + ); } on ApiError catch (e) { emit(state.copyWith(sendError: () => e, isSending: () => false)); } on TransportError catch (e) { @@ -97,14 +108,18 @@ class NftWithdrawBloc extends Bloc { } Future _onConfirmSend( - NftWithdrawConfirmSendEvent event, Emitter emit) async { + NftWithdrawConfirmSendEvent event, + Emitter emit, + ) async { final state = this.state; if (state is! NftWithdrawConfirmState) return; - emit(state.copyWith( - isSending: () => true, - sendError: () => null, - )); + emit( + state.copyWith( + isSending: () => true, + sendError: () => null, + ), + ); final txDetails = state.txDetails; final SendRawTransactionResponse response = @@ -112,30 +127,38 @@ class NftWithdrawBloc extends Bloc { final BaseError? responseError = response.error; final String? txHash = response.txHash; if (txHash == null) { - emit(state.copyWith( - isSending: () => false, - sendError: () => - responseError ?? TextError(error: LocaleKeys.somethingWrong), - )); + emit( + state.copyWith( + isSending: () => false, + sendError: () => + responseError ?? TextError(error: LocaleKeys.somethingWrong), + ), + ); } else { - emit(NftWithdrawSuccessState( - txHash: txHash, - nft: state.nft, - timestamp: txDetails.timestamp, - to: txDetails.to.first, - )); + emit( + NftWithdrawSuccessState( + txHash: txHash, + nft: state.nft, + timestamp: txDetails.timestamp, + to: txDetails.to.first, + ), + ); } } void _onAddressChanged( - NftWithdrawAddressChanged event, Emitter emit) { + NftWithdrawAddressChanged event, + Emitter emit, + ) { final state = this.state; if (state is! NftWithdrawFillState) return; - emit(state.copyWith( - address: () => event.address, - addressError: () => null, - sendError: () => null, - )); + emit( + state.copyWith( + address: () => event.address, + addressError: () => null, + sendError: () => null, + ), + ); } void _onAmountChanged( @@ -145,11 +168,13 @@ class NftWithdrawBloc extends Bloc { final state = this.state; if (state is! NftWithdrawFillState) return; - emit(state.copyWith( - amount: () => event.amount, - amountError: () => null, - sendError: () => null, - )); + emit( + state.copyWith( + amount: () => event.amount, + amountError: () => null, + sendError: () => null, + ), + ); } Future _validateAddress( @@ -188,22 +213,27 @@ class NftWithdrawBloc extends Bloc { } if (amount > totalAmount) { return TextError( - error: LocaleKeys.maxCount.tr(args: [totalAmount.toString()])); + error: LocaleKeys.maxCount.tr(args: [totalAmount.toString()]), + ); } return null; } FutureOr _onShowFillForm( - NftWithdrawShowFillStep event, Emitter emit) { + NftWithdrawShowFillStep event, + Emitter emit, + ) { final state = this.state; if (state is NftWithdrawConfirmState) { - emit(NftWithdrawFillState( - address: state.txDetails.to.first, - amount: int.tryParse(state.txDetails.amount), - isSending: false, - nft: state.nft, - )); + emit( + NftWithdrawFillState( + address: state.txDetails.to.first, + amount: int.tryParse(state.txDetails.amount), + isSending: false, + nft: state.nft, + ), + ); } else { emit(NftWithdrawFillState.initial(state.nft)); } @@ -219,24 +249,35 @@ class NftWithdrawBloc extends Bloc { } Future _onConvertAddress( - NftWithdrawConvertAddress event, Emitter emit) async { + NftWithdrawConvertAddress event, + Emitter emit, + ) async { final state = this.state; if (state is! NftWithdrawFillState) return; - final result = await coinsRepo.convertLegacyAddress( - state.nft.parentCoin, - state.address, - ); - if (result == null) return; - - add(NftWithdrawAddressChanged(result)); + try { + final subclass = state.nft.parentCoin.type.toCoinSubClass(); + final result = await _kdfSdk.client.rpc.address.convertAddress( + from: state.address, + coin: subclass.ticker, + toFormat: AddressFormat.fromCoinSubClass(subclass), + ); + add(NftWithdrawAddressChanged(result.address)); + } catch (e) { + emit( + state.copyWith( + address: () => '', + addressError: () => TextError(error: e.toString()), + ), + ); + } } Future _activateParentCoinIfNeeded(NftToken nft) async { 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..b0d15b145d 100644 --- a/lib/bloc/security_settings/security_settings_bloc.dart +++ b/lib/bloc/security_settings/security_settings_bloc.dart @@ -3,11 +3,10 @@ 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'; class SecuritySettingsBloc extends Bloc { - SecuritySettingsBloc(SecuritySettingsState state) : super(state) { + SecuritySettingsBloc(super.state) { on(_onReset); on(_onShowSeed); on(_onSeedConfirm); @@ -17,6 +16,7 @@ class SecuritySettingsBloc on(_onSeedCopied); } + void _onReset( ResetEvent event, Emitter emit, @@ -35,10 +35,10 @@ class SecuritySettingsBloc emit(newState); } - void _onShowSeedWords( + Future _onShowSeedWords( ShowSeedWordsEvent event, Emitter emit, - ) { + ) async { final newState = state.copyWith( step: SecuritySettingsStep.seedShow, showSeedWords: event.isShow, @@ -51,11 +51,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( @@ -73,7 +75,6 @@ class SecuritySettingsBloc SeedConfirmedEvent event, Emitter emit, ) async { - await currentWalletBloc.confirmBackup(); final newState = state.copyWith( step: SecuritySettingsStep.seedSuccess, showSeedWords: false, @@ -81,8 +82,10 @@ class SecuritySettingsBloc emit(newState); } - FutureOr _onSeedCopied( - ShowSeedCopiedEvent event, Emitter emit) { + Future _onSeedCopied( + ShowSeedCopiedEvent event, + Emitter emit, + ) async { 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..9dd9a12f3f 100644 --- a/lib/bloc/security_settings/security_settings_state.dart +++ b/lib/bloc/security_settings/security_settings_state.dart @@ -1,11 +1,20 @@ import 'package:equatable/equatable.dart'; enum SecuritySettingsStep { + /// The main security settings screen. securityMain, + + /// The screen showing the seed words. seedShow, + + /// The screen confirming that the seed words have been written down. seedConfirm, + + /// The screen showing that the seed words have been successfully confirmed. seedSuccess, - passwordUpdate, + + /// The screen for updating the password. + // passwordUpdate, } class SecuritySettingsState extends Equatable { @@ -23,8 +32,14 @@ class SecuritySettingsState extends Equatable { ); } + /// The current step of the security settings flow. final SecuritySettingsStep step; + + /// Whether the seed words are currently being shown. final bool showSeedWords; + + /// Whether the seed words have been written down or saved somewhere by the + /// user. final bool isSeedSaved; @override 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/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..7635960f57 100644 --- a/lib/bloc/system_health/system_health_bloc.dart +++ b/lib/bloc/system_health/system_health_bloc.dart @@ -1,21 +1,25 @@ 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()); - - _timer = Timer.periodic(const Duration(seconds: 30), (timer) { + _timer = Timer.periodic(const Duration(seconds: 60), (timer) { add(CheckSystemClock()); }); } @@ -26,12 +30,30 @@ 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 (_) { + // do not prevent usage if no peers are connected + // mm2 api is responsible for logging, so only return result here + return false; + } + } + @override Future close() { _timer?.cancel(); diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index 97f8159d89..918b08ba22 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 { @@ -394,7 +394,7 @@ class TakerBloc extends Bloc { availableBalanceState: () => AvailableBalanceState.loading)); } - if (!_isLoggedIn) { + if (!_isLoggedIn || !state.sellCoin!.isActive) { emitter(state.copyWith( availableBalanceState: () => AvailableBalanceState.unavailable)); } else { @@ -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 a69c41bb6c..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,17 +124,17 @@ 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 == ownAddress) { + if (selectedOrderAddress.addressData == ownAddress) { add(TakerAddError(_tradingWithSelfError())); return true; } @@ -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..2fe75731de 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -1,140 +1,299 @@ import 'dart:async'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:easy_localization/easy_localization.dart'; 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: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: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'; class TransactionHistoryBloc extends Bloc { TransactionHistoryBloc({ - required TransactionHistoryRepo repo, - }) : _repo = repo, - super(TransactionHistoryInitialState()) { - on(_onSubscribe); - on(_onUnsubscribe); + required KomodoDefiSdk sdk, + }) : _sdk = sdk, + super(const TransactionHistoryState.initial()) { + on(_onSubscribe, transformer: restartable()); + on(_onStartedLoading); on(_onUpdated); on(_onFailure); } - final TransactionHistoryRepo _repo; - Timer? _updateTransactionsTimer; - final _updateTime = const Duration(seconds: 10); + final KomodoDefiSdk _sdk; + StreamSubscription>? _historySubscription; + StreamSubscription? _newTransactionsSubscription; + + // TODO: Remove or move to SDK + final Set _processedTxIds = {}; + + @override + Future close() async { + await _historySubscription?.cancel(); + await _newTransactionsSubscription?.cancel(); + return super.close(); + } Future _onSubscribe( TransactionHistorySubscribe event, Emitter emit, ) async { + emit(const TransactionHistoryState.initial()); + if (!hasTxHistorySupport(event.coin)) { + emit( + state.copyWith( + loading: false, + error: TextError( + error: 'Transaction history is not supported for this coin.', + ), + transactions: const [], + ), + ); return; } - emit(TransactionHistoryInitialState()); - await _update(event.coin); - _stopTimers(); - _updateTransactionsTimer = Timer.periodic(_updateTime, (_) async { - await _update(event.coin); - }); - } - void _onUnsubscribe( - TransactionHistoryUnsubscribe event, - Emitter emit, - ) { - _stopTimers(); - } + try { + await _historySubscription?.cancel(); + await _newTransactionsSubscription?.cancel(); + _processedTxIds.clear(); - void _onUpdated( - TransactionHistoryUpdated event, - Emitter emit, - ) { - if (event.isInProgress) { - emit(TransactionHistoryInProgressState(transactions: event.transactions)); - return; - } - emit(TransactionHistoryLoadedState(transactions: event.transactions)); - } + add(const TransactionHistoryStartedLoading()); + final asset = _sdk.assets.available[event.coin.id]; + if (asset == null) { + throw Exception('Asset ${event.coin.id} not found in known coins list'); + } - void _onFailure( - TransactionHistoryFailure event, - Emitter emit, - ) { - emit(TransactionHistoryFailureState(error: event.error)); - } + // Subscribe to historical transactions + _historySubscription = + _sdk.transactions.getTransactionsStreamed(asset).listen( + (newTransactions) { + // Filter out any transactions we've already processed + final uniqueTransactions = newTransactions.where((tx) { + final isNew = !_processedTxIds.contains(tx.internalId); + if (isNew) { + _processedTxIds.add(tx.internalId); + } + return isNew; + }).toList(); - Future _update(Coin coin) async { - final DataFromService - transactionsResponse = await _repo.fetch(coin); - if (isClosed) { - return; - } - final TransactionHistoryResponseResult? result = transactionsResponse.data; + if (uniqueTransactions.isEmpty) return; - final BaseError? responseError = transactionsResponse.error; - if (responseError != null) { - add(TransactionHistoryFailure(error: responseError)); - return; - } else if (result == null) { + final updatedTransactions = List.of(state.transactions) + ..addAll(uniqueTransactions) + ..sort(_sortTransactions); + + if (event.coin.isErcType) { + _flagTransactions(updatedTransactions, event.coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, + onDone: () { + if (state.error == null && state.loading) { + add(TransactionHistoryUpdated(transactions: state.transactions)); + } + // Once historical load is complete, start watching for new transactions + _subscribeToNewTransactions(asset, event.coin); + }, + ); + } catch (e, s) { + log( + 'Error loading transaction history: $e', + isError: true, + path: 'transaction_history_bloc->_onSubscribe', + trace: s, + ); add( TransactionHistoryFailure( error: TextError(error: LocaleKeys.somethingWrong.tr()), ), ); - return; } + } - final List transactions = List.from(result.transactions); - transactions.sort(_sortTransactions); - _flagTransactions(transactions, coin); + void _subscribeToNewTransactions(Asset asset, Coin coin) { + _newTransactionsSubscription = + _sdk.transactions.watchTransactions(asset).listen( + (newTransaction) { + if (_processedTxIds.contains(newTransaction.internalId)) return; - add( - TransactionHistoryUpdated( - transactions: transactions, - isInProgress: result.syncStatus.state == SyncStatusState.inProgress, - ), + _processedTxIds.add(newTransaction.internalId); + + final updatedTransactions = List.of(state.transactions) + ..add(newTransaction) + ..sort(_sortTransactions); + + if (coin.isErcType) { + _flagTransactions(updatedTransactions, coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, ); } - @override - Future close() { - _stopTimers(); + void _onUpdated( + TransactionHistoryUpdated event, + Emitter emit, + ) { + emit( + state.copyWith( + transactions: event.transactions, + loading: false, + ), + ); + } - return super.close(); + void _onStartedLoading( + TransactionHistoryStartedLoading event, + Emitter emit, + ) { + emit(state.copyWith(loading: true)); } - void _stopTimers() { - _updateTransactionsTimer?.cancel(); - _updateTransactionsTimer = null; + void _onFailure( + TransactionHistoryFailure event, + Emitter emit, + ) { + emit( + state.copyWith( + loading: false, + error: event.error, + ), + ); } } int _sortTransactions(Transaction tx1, Transaction tx2) { - if (tx2.timestamp == 0) { + if (tx2.timestamp == DateTime.now()) { return 1; - } else if (tx1.timestamp == 0) { + } else if (tx1.timestamp == DateTime.now()) { return -1; } return tx2.timestamp.compareTo(tx1.timestamp); } void _flagTransactions(List transactions, Coin coin) { - // First response to https://trezor.io/support/a/address-poisoning-attacks, - // need to be refactored. - // ref: https://github.com/KomodoPlatform/komodowallet/issues/1091 - if (!coin.isErcType) return; + transactions + .removeWhere((tx) => tx.balanceChanges.totalAmount.toDouble() == 0.0); +} - for (final Transaction tx in List.from(transactions)) { - if (double.tryParse(tx.totalAmount) == 0.0) { - transactions.remove(tx); - } - } +class Pagination { + Pagination({ + this.fromId, + this.pageNumber, + }); + final String? fromId; + final int? pageNumber; + + Map toJson() => { + if (fromId != null) 'FromId': fromId, + if (pageNumber != null) 'PageNumber': pageNumber, + }; +} + +/// Represents different ways to paginate transaction history +sealed class TransactionPagination { + const TransactionPagination(); + + /// Get the limit of transactions to return, if applicable + int? get limit; +} + +/// Standard page-based pagination +class PagePagination extends TransactionPagination { + const PagePagination({ + required this.pageNumber, + required this.itemsPerPage, + }); + + final int pageNumber; + final int itemsPerPage; + + @override + int get limit => itemsPerPage; +} + +/// Pagination from a specific transaction ID +class TransactionBasedPagination extends TransactionPagination { + const TransactionBasedPagination({ + required this.fromId, + required this.itemCount, + }); + + final String fromId; + final int itemCount; + + @override + int get limit => itemCount; +} + +/// Pagination by block range +class BlockRangePagination extends TransactionPagination { + const BlockRangePagination({ + required this.fromBlock, + required this.toBlock, + this.maxItems, + }); + + final int fromBlock; + final int toBlock; + final int? maxItems; + + @override + int? get limit => maxItems; +} + +/// Pagination by timestamp range +class TimestampRangePagination extends TransactionPagination { + const TimestampRangePagination({ + required this.fromTimestamp, + required this.toTimestamp, + this.maxItems, + }); + + final DateTime fromTimestamp; + final DateTime toTimestamp; + final int? maxItems; + + @override + int? get limit => maxItems; +} + +/// Contract-specific pagination (e.g., for ERC20 token transfers) +class ContractEventPagination extends TransactionPagination { + const ContractEventPagination({ + required this.contractAddress, + required this.fromBlock, + this.toBlock, + this.maxItems, + }); + + final String contractAddress; + final int fromBlock; + final int? toBlock; + final int? maxItems; + + @override + int? get limit => maxItems; } diff --git a/lib/bloc/transaction_history/transaction_history_event.dart b/lib/bloc/transaction_history/transaction_history_event.dart index 921c1d2a02..55343ed847 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/komodo_defi_types.dart'; abstract class TransactionHistoryEvent { const TransactionHistoryEvent(); @@ -11,18 +11,13 @@ class TransactionHistorySubscribe extends TransactionHistoryEvent { final Coin coin; } -class TransactionHistoryUnsubscribe extends TransactionHistoryEvent { - const TransactionHistoryUnsubscribe({required this.coin}); - final Coin coin; +class TransactionHistoryUpdated extends TransactionHistoryEvent { + const TransactionHistoryUpdated({required this.transactions}); + final List? transactions; } -class TransactionHistoryUpdated extends TransactionHistoryEvent { - const TransactionHistoryUpdated({ - required this.transactions, - required this.isInProgress, - }); - final List transactions; - final bool isInProgress; +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..19208222c5 100644 --- a/lib/bloc/transaction_history/transaction_history_repo.dart +++ b/lib/bloc/transaction_history/transaction_history_repo.dart @@ -1,193 +1,61 @@ 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: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'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; -class TransactionHistoryRepo { - TransactionHistoryRepo({required Mm2Api api, required Client client}) - : _api = api, - _client = client; - final Mm2Api _api; - final Client _client; +/// Throws [TransactionFetchException] if the transaction history could not be +/// fetched. +abstract class TransactionHistoryRepo { + Future?> fetch(AssetId assetId); + Future> fetchCompletedTransactions(AssetId assetId); +} - Future> fetch( - Coin coin) async { - if (_checkV2RequestSupport(coin)) { - return await fetchTransactionHistoryV2(MyTxHistoryV2Request( - coin: coin.abbr, - type: coin.enabledType ?? WalletType.iguana, - )); +class SdkTransactionHistoryRepository implements TransactionHistoryRepo { + SdkTransactionHistoryRepository({ + required KomodoDefiSdk sdk, + }) : _sdk = sdk; + final KomodoDefiSdk _sdk; + + @override + Future?> fetch(AssetId assetId, {String? fromId}) async { + final asset = _sdk.assets.available[assetId]; + if (asset == null) { + throw TransactionFetchException('Asset $assetId not found'); } - 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; - 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: fromId == null + ? const PagePagination( + pageNumber: 1, + // TODO: Handle cases with more than 2000 transactions and/or + // adopt a pagination strategy. Migrate form + itemsPerPage: 2000, + ) + : TransactionBasedPagination( + fromId: fromId, + itemCount: 2000, + ), + ); + 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) - ..sort( - (a, b) => a.timestamp.compareTo(b.timestamp), + @override + Future> fetchCompletedTransactions(AssetId assetId) async { + final List transactions = (await fetch(assetId) ?? []) + ..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..3a55e999bb 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/komodo_defi_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, error]; + + 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..e0e0c8b6bf 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,23 @@ 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/kdf_auth_metadata_extension.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()); @@ -49,17 +58,21 @@ class TrezorRepo { } Future sendPin(String pin, TrezorTask trezorTask) async { - await _api.pin(TrezorPinRequest( - pin: pin, - task: trezorTask, - )); + await _api.pin( + TrezorPinRequest( + pin: pin, + task: trezorTask, + ), + ); } Future sendPassphrase(String passphrase, TrezorTask trezorTask) async { - await _api.passphrase(TrezorPassphraseRequest( - passphrase: passphrase, - task: trezorTask, - )); + await _api.passphrase( + TrezorPassphraseRequest( + passphrase: passphrase, + task: trezorTask, + ), + ); } Future initCancel(int taskId) async { @@ -74,8 +87,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 +100,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 +135,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 9a135ad81d..80b90c0664 100644 --- a/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart +++ b/lib/bloc/trezor_connection_bloc/trezor_connection_bloc.dart @@ -1,13 +1,11 @@ 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 +13,13 @@ class TrezorConnectionBloc extends Bloc { TrezorConnectionBloc({ required TrezorRepo trezorRepo, - required AuthRepository authRepo, - required CurrentWalletBloc walletRepo, - }) : _authRepo = authRepo, - _walletRepo = walletRepo, + required KomodoDefiSdk kdfSdk, + }) : _kdfSdk = kdfSdk, + _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 +28,24 @@ class TrezorConnectionBloc add(TrezorConnectionStatusChange(status: status)); } - void _onAuthModeChanged(AuthorizeMode mode) { - if (mode == AuthorizeMode.logIn) { - final Wallet? currentWallet = _walletRepo.wallet; - if (currentWallet == null) return; - if (currentWallet.config.type != WalletType.trezor) return; + void _onAuthModeChanged(KdfUser? user) { + if (user == null) { + return _trezorRepo.unsubscribeFromConnectionStatus(); + } - final String? pubKey = currentWallet.config.pubKey; - if (pubKey == null) return; + if (user.wallet.config.type != WalletType.trezor) return; - trezorRepo.subscribeOnConnectionStatus(pubKey); - } else { - trezorRepo.unsubscribeFromConnectionStatus(); - } + final String? pubKey = user.walletId.pubkeyHash; + if (pubKey == null) return; + + _trezorRepo.subscribeOnConnectionStatus(pubKey); } - final AuthRepository _authRepo; - final CurrentWalletBloc _walletRepo; + final KomodoDefiSdk _kdfSdk; + final TrezorRepo _trezorRepo; late StreamSubscription _trezorConnectionStatusListener; - late StreamSubscription _authModeListener; + late StreamSubscription _authModeListener; Future _onConnectionStatusChange(TrezorConnectionStatusChange event, Emitter emit) async { @@ -59,9 +54,7 @@ class TrezorConnectionBloc switch (status) { case TrezorConnectionStatus.unreachable: - final MM2Status mm2Status = await mm2.status(); - if (mm2Status == MM2Status.rpcIsUp) 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 1dfbb7430f..8eb1614615 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart @@ -1,34 +1,35 @@ 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/kdf_auth_metadata_extension.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 +40,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,58 +56,91 @@ 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; final InitTrezorResult? responseResult = response.result; if (responseError != null) { - emit(state.copyWith( - error: () => TextError(error: responseError), - inProgress: () => false, - )); - await _logoutFromHiddenMode(); + emit( + state.copyWith( + error: () => TextError(error: responseError), + inProgress: () => false, + ), + ); + await _logout(); return; } if (responseResult == null) { - emit(state.copyWith( - error: () => TextError(error: LocaleKeys.somethingWrong.tr()), - inProgress: () => false, - )); - await _logoutFromHiddenMode(); + emit( + state.copyWith( + error: () => TextError(error: LocaleKeys.somethingWrong.tr()), + inProgress: () => false, + ), + ); + await _logout(); return; } add(const TrezorInitSubscribeStatus()); - emit(state.copyWith( - taskId: () => responseResult.taskId, - inProgress: () => false, - )); + emit( + state.copyWith( + taskId: () => responseResult.taskId, + inProgress: () => false, + ), + ); } 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 +149,7 @@ class TrezorInitBloc extends Bloc { if (response.errorType == 'NoSuchTask') { _unsubscribeStatus(); emit(state.copyWith(taskId: () => null)); - await _logoutFromHiddenMode(); + await _logout(); return; } @@ -122,59 +157,62 @@ class TrezorInitBloc extends Bloc { if (responseError != null) { emit(state.copyWith(error: () => TextError(error: responseError))); - await _logoutFromHiddenMode(); + await _logout(); return; } final InitTrezorStatusData? initTrezorStatus = response.result; if (initTrezorStatus == null) { - emit(state.copyWith( + emit( + state.copyWith( error: () => - TextError(error: 'Something went wrong. Empty init status.'))); + 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, - ); - - if (wallet == null) { - emit(state.copyWith( - error: () => TextError( - error: LocaleKeys.trezorImportFailed.tr(args: [name])))); + // final String name = deviceDetails.name ?? 'My Trezor'; - 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 +226,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,60 +243,91 @@ 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(); - emit(state.copyWith( - taskId: () => null, - status: () => null, - error: () => null, - )); + _logout(); + emit( + state.copyWith( + taskId: () => null, + status: () => null, + error: () => null, + ), + ); } 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 MM2Status mm2Status = await mm2.status(); + /// 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 (state.authMode == AuthorizeMode.hiddenLogin && - mm2Status == MM2Status.rpcIsUp) return; + // 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; + } - if (mm2Status == MM2Status.rpcIsUp) await _authRepo.logOut(); - await _authRepo.logIn(AuthorizeMode.hiddenLogin, seedForHiddenLogin); + await _kdfSdk.auth.register( + walletName: walletName, + password: password, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + await _kdfSdk.setWalletType(WalletType.trezor); + await _kdfSdk.confirmSeedBackup(); } - Future _logoutFromHiddenMode() async { - final MM2Status mm2Status = await mm2.status(); - - if (state.authMode != AuthorizeMode.hiddenLogin && - mm2Status == MM2Status.rpcIsUp) return; + Future _logout() async { + final bool isSignedIn = await _kdfSdk.auth.isSignedIn(); + if (!isSignedIn && state.kdfUser == null) { + return; + } - if (mm2Status == MM2Status.rpcIsUp) 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..cee77ca059 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -1,609 +1,493 @@ 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/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:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/withdraw_form/withdraw_form_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/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'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/fee/fee_request.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/coin_type.dart'; -import 'package:web_dex/model/fee_type.dart'; -import 'package:web_dex/model/hw_wallet/trezor_progress_status.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'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_event.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_state.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; +import 'package:decimal/decimal.dart'; + class WithdrawFormBloc extends Bloc { - WithdrawFormBloc( - {required Coin coin, required CoinsBloc coinsBloc, required this.goBack}) - : _coinsRepo = coinsBloc, - super(WithdrawFormState.initial(coin, coinsBloc)) { - on(_onAddressChanged); + final KomodoDefiSdk _sdk; + + WithdrawFormBloc({ + required Asset asset, + required KomodoDefiSdk sdk, + }) : _sdk = sdk, + super( + WithdrawFormState( + asset: asset, + step: WithdrawFormStep.fill, + recipientAddress: '', + amount: '0', + ), + ) { + on(_onRecipientChanged); on(_onAmountChanged); - on(_onCustomFeeChanged); - on(_onCustomEvmFeeChanged); - on(_onSenderAddressChanged); - on(_onMaxTapped); - on(_onSubmitted); - on(_onWithdrawSuccess); - on(_onWithdrawFailed); - on(_onSendRawTransaction); + on(_onSourceChanged); + on(_onMaxAmountEnabled); on(_onCustomFeeEnabled); - on(_onCustomFeeDisabled); - on(_onConvertMixedCaseAddress); - on(_onStepReverted); - on(_onWithdrawFormReset); - on(_onTrezorProgressUpdated); - on(_onMemoUpdated); + on(_onFeeChanged); + on(_onMemoChanged); + on(_onIbcTransferEnabled); + on(_onIbcChannelChanged); + on(_onPreviewSubmitted); + on(_onSubmitted); + on(_onCancelled); + on(_onReset); + on(_onSourcesLoadRequested); + on(_onConvertAddress); + + add(const WithdrawFormSourcesLoadRequested()); } - // will use actual CoinsRepo when implemented - final CoinsBloc _coinsRepo; - final VoidCallback goBack; + Future _onSourcesLoadRequested( + WithdrawFormSourcesLoadRequested event, + Emitter emit, + ) async { + try { + final pubkeys = await state.asset.getPubkeys(_sdk); + if (pubkeys.keys.isNotEmpty) { + emit( + state.copyWith( + pubkeys: () => pubkeys, + networkError: () => null, + selectedSourceAddress: state.selectedSourceAddress == null + ? null + : () => pubkeys.keys.firstOrNull, + ), + ); + } else { + emit( + state.copyWith( + networkError: () => TextError( + error: 'No addresses found for ${state.asset.id.name}', + ), + ), + ); + } + } catch (e) { + emit( + state.copyWith( + networkError: () => TextError(error: 'Failed to load addresses: $e'), + ), + ); + } + } - // Event handlers - void _onAddressChanged( - WithdrawFormAddressChanged event, - Emitter emitter, - ) { - emitter(state.copyWith(address: event.address)); + FeeInfo? _getDefaultFee() { + final protocol = state.asset.protocol; + if (protocol is Erc20Protocol) { + return FeeInfo.ethGas( + coin: state.asset.id.id, + gasPrice: Decimal.one, + gas: 21000, + ); + } else if (protocol is UtxoProtocol) { + return FeeInfo.utxoFixed( + coin: state.asset.id.id, + amount: Decimal.fromInt(protocol.txFee ?? 10000), + ); + } + return null; + } + + Future _onRecipientChanged( + WithdrawFormRecipientChanged event, + Emitter emit, + ) async { + try { + final validation = await _sdk.addresses.validateAddress( + asset: state.asset, + address: event.address, + ); + + if (!validation.isValid) { + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: validation.invalidReason ?? 'Invalid address'), + isMixedCaseAddress: false, + ), + ); + return; + } + + // Check for mixed case if EVM address + if (state.asset.protocol is Erc20Protocol) { + try { + // Try to convert to checksum to detect mixed case + final result = await _sdk.addresses.convertFormat( + asset: state.asset, + address: event.address, + format: const AddressFormat(format: 'checksummed', network: ''), + ); + + final isMixedCase = result.convertedAddress != event.address; + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => null, + isMixedCaseAddress: isMixedCase, + ), + ); + return; + } catch (e) { + // If conversion fails, treat as normal address validation error + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: 'Invalid EVM address: $e'), + isMixedCaseAddress: false, + ), + ); + return; + } + } + + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => null, + isMixedCaseAddress: false, + ), + ); + } catch (e) { + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: 'Address validation failed: $e'), + isMixedCaseAddress: false, + ), + ); + } } void _onAmountChanged( WithdrawFormAmountChanged event, - Emitter emitter, + Emitter emit, ) { - emitter(state.copyWith(amount: event.amount, isMaxAmount: false)); - } + if (state.isMaxAmount) return; + + try { + final amount = Decimal.parse(event.amount); + final balance = state.selectedSourceAddress?.balance.spendable ?? + state.pubkeys?.balance.spendable; + + if (balance != null && amount > balance) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => TextError(error: 'Insufficient funds'), + ), + ); + return; + } - void _onCustomFeeEnabled( - WithdrawFormCustomFeeEnabled event, - Emitter emitter, - ) { - emitter(state.copyWith( - isCustomFeeEnabled: true, customFee: FeeRequest(type: _customFeeType))); - } + if (amount <= Decimal.zero) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => + TextError(error: 'Amount must be greater than 0'), + ), + ); + return; + } - void _onCustomFeeDisabled( - WithdrawFormCustomFeeDisabled event, - Emitter emitter, - ) { - emitter(state.copyWith( - isCustomFeeEnabled: false, - customFee: FeeRequest(type: _customFeeType), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - )); + emit( + state.copyWith( + amount: event.amount, + amountError: () => null, + ), + ); + } catch (e) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => TextError(error: 'Invalid amount'), + ), + ); + } } - void _onCustomFeeChanged( - WithdrawFormCustomFeeChanged event, - Emitter emitter, + void _onSourceChanged( + WithdrawFormSourceChanged event, + Emitter emit, ) { - emitter(state.copyWith( - customFee: FeeRequest( - type: feeType.utxoFixed, - amount: event.amount, + emit( + state.copyWith( + selectedSourceAddress: () => event.address, + networkError: () => null, ), - )); + ); } - void _onCustomEvmFeeChanged( - WithdrawFormCustomEvmFeeChanged event, - Emitter emitter, + void _onMaxAmountEnabled( + WithdrawFormMaxAmountEnabled event, + Emitter emit, ) { - emitter(state.copyWith( - customFee: FeeRequest( - type: feeType.ethGas, - gas: event.gas, - gasPrice: event.gasPrice, + final balance = + state.selectedSourceAddress?.balance ?? state.pubkeys?.balance; + emit( + state.copyWith( + isMaxAmount: event.isEnabled, + amount: event.isEnabled ? balance?.spendable.toString() : '0', + amountError: () => null, ), - )); + ); } - void _onSenderAddressChanged( - WithdrawFormSenderAddressChanged event, - Emitter emitter, + void _onCustomFeeEnabled( + WithdrawFormCustomFeeEnabled event, + Emitter emit, ) { - emitter( + // If enabling custom fees, set a default fee or reuse from `_getDefaultFee()` + emit( state.copyWith( - selectedSenderAddress: event.address, - amount: state.isMaxAmount - ? doubleToString( - state.coin.getHdAddress(event.address)?.balance.spendable ?? - 0.0) - : state.amount, + isCustomFee: event.isEnabled, + customFee: event.isEnabled ? () => _getDefaultFee() : () => null, + customFeeError: () => null, ), ); } - void _onMaxTapped( - WithdrawFormMaxTapped event, - Emitter emitter, + void _onFeeChanged( + WithdrawFormCustomFeeChanged event, + Emitter emit, ) { - emitter(state.copyWith( - amount: event.isEnabled ? doubleToString(state.senderAddressBalance) : '', - isMaxAmount: event.isEnabled, - )); + try { + // Validate the new fee, e.g. if it's EthGas => check gasPrice, gas > 0, etc. + if (event.fee is FeeInfoEthGas) { + _validateEvmFee(event.fee as FeeInfoEthGas); + } else if (event.fee is FeeInfoUtxoFixed) { + _validateUtxoFee(event.fee as FeeInfoUtxoFixed); + } + emit( + state.copyWith( + customFee: () => event.fee, + customFeeError: () => null, + ), + ); + } catch (e) { + emit( + state.copyWith( + customFeeError: () => TextError(error: e.toString()), + ), + ); + } } - Future _onSubmitted( - WithdrawFormSubmitted event, - Emitter emitter, - ) async { - if (state.isSending) return; - emitter(state.copyWith( - isSending: true, - trezorProgressStatus: null, - sendError: TextError.empty(), - amountError: TextError.empty(), - addressError: TextError.empty(), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - )); - - bool isValid = await _validateEnterForm(emitter); - if (!isValid) { - return; + void _validateEvmFee(FeeInfoEthGas fee) { + if (fee.gasPrice <= Decimal.zero) { + throw Exception('Gas price must be greater than 0'); } - - isValid = await _additionalValidate(emitter); - if (!isValid) { - emitter(state.copyWith(isSending: false)); - return; + if (fee.gas <= 0) { + throw Exception('Gas limit must be greater than 0'); } - - final withdrawResponse = state.coin.enabledType == WalletType.trezor - ? await _coinsRepo.trezor.withdraw( - TrezorWithdrawRequest( - coin: state.coin, - from: state.selectedSenderAddress, - to: state.address, - amount: state.isMaxAmount - ? state.senderAddressBalance - : double.parse(state.amount), - max: state.isMaxAmount, - fee: state.isCustomFeeEnabled ? state.customFee : null, - ), - onProgressUpdated: (TrezorProgressStatus? status) { - add(WithdrawFormTrezorStatusUpdated(status: status)); - }, - ) - : await _coinsRepo.withdraw(WithdrawRequest( - to: state.address, - coin: state.coin.abbr, - max: state.isMaxAmount, - amount: state.isMaxAmount ? null : state.amount, - memo: state.memo, - fee: state.isCustomFeeEnabled - ? state.customFee - : state.coin.type == CoinType.cosmos || - state.coin.type == CoinType.iris - ? FeeRequest( - type: feeType.cosmosGas, - gasLimit: 150000, - gasPrice: 0.05, - ) - : null, - )); - - final BaseError? error = withdrawResponse.error; - final WithdrawDetails? result = withdrawResponse.result; - - if (error != null) { - add(WithdrawFormWithdrawFailed(error: error)); - log('WithdrawFormBloc: withdraw error: ${error.message}', isError: true); - return; - } - - if (result == null) { - emitter(state.copyWith( - sendError: TextError(error: LocaleKeys.somethingWrong.tr()), - isSending: false, - )); - return; - } - - add(WithdrawFormWithdrawSuccessful(details: result)); } - void _onWithdrawSuccess( - WithdrawFormWithdrawSuccessful event, - Emitter emitter, - ) { - emitter(state.copyWith( - isSending: false, - withdrawDetails: event.details, - step: WithdrawFormStep.confirm, - )); + void _validateUtxoFee(FeeInfoUtxoFixed fee) { + if (fee.amount <= Decimal.zero) { + throw Exception('Fee amount must be greater than 0'); + } } - void _onWithdrawFailed( - WithdrawFormWithdrawFailed event, - Emitter emitter, + void _onMemoChanged( + WithdrawFormMemoChanged event, + Emitter emit, ) { - final error = event.error; - - emitter(state.copyWith( - sendError: error, - isSending: false, - step: WithdrawFormStep.failed, - )); + emit(state.copyWith(memo: () => event.memo)); } - void _onTrezorProgressUpdated( - WithdrawFormTrezorStatusUpdated event, - Emitter emitter, + void _onIbcTransferEnabled( + WithdrawFormIbcTransferEnabled event, + Emitter emit, ) { - String? message; - - switch (event.status) { - case TrezorProgressStatus.waitingForUserToConfirmSigning: - message = LocaleKeys.confirmOnTrezor.tr(); - break; - default: - } - - if (state.trezorProgressStatus != message) { - emitter(state.copyWith(trezorProgressStatus: message)); - } - } - - Future _onConvertMixedCaseAddress( - WithdrawFormConvertAddress event, - Emitter emitter, - ) async { - final result = await coinsRepo.convertLegacyAddress( - state.coin, - state.address, - ); - - add(WithdrawFormAddressChanged(address: result ?? '')); - } - - Future _onSendRawTransaction( - WithdrawFormSendRawTx event, - Emitter emitter, - ) async { - if (state.isSending) return; - emitter(state.copyWith(isSending: true, sendError: TextError.empty())); - final BaseError? parentCoinError = _checkParentCoinErrors( - coin: state.coin, - fee: state.withdrawDetails.feeValue, - ); - if (parentCoinError != null) { - emitter(state.copyWith( - isSending: false, - sendError: parentCoinError, - )); - return; - } - - final response = await _coinsRepo.sendRawTransaction( - SendRawTransactionRequest( - coin: state.withdrawDetails.coin, - txHex: state.withdrawDetails.txHex, + emit( + state.copyWith( + isIbcTransfer: event.isEnabled, + ibcChannel: event.isEnabled ? () => state.ibcChannel : () => null, + ibcChannelError: () => null, ), ); - - final BaseError? responseError = response.error; - final String? txHash = response.txHash; - - if (responseError != null) { - log( - 'WithdrawFormBloc: sendRawTransaction error: ${responseError.message}', - isError: true, - ); - emitter(state.copyWith( - isSending: false, - sendError: responseError, - step: WithdrawFormStep.failed, - )); - return; - } - - if (txHash == null) { - emitter(state.copyWith( - isSending: false, - sendError: TextError(error: LocaleKeys.somethingWrong.tr()), - step: WithdrawFormStep.failed, - )); - return; - } - await _coinsRepo.updateBalances(); - emitter(state.copyWith(step: WithdrawFormStep.success)); } - void _onStepReverted( - WithdrawFormStepReverted event, - Emitter emitter, + void _onIbcChannelChanged( + WithdrawFormIbcChannelChanged event, + Emitter emit, ) { - if (event.step == WithdrawFormStep.confirm) { - emitter( + if (event.channel.isEmpty) { + emit( state.copyWith( - step: WithdrawFormStep.fill, - withdrawDetails: WithdrawDetails.empty(), + ibcChannel: () => event.channel, + ibcChannelError: () => TextError(error: 'Channel ID is required'), ), ); - } - } - - void _onMemoUpdated( - WithdrawFormMemoUpdated event, - Emitter emitter, - ) { - emitter(state.copyWith(memo: event.text)); - } - - void _onWithdrawFormReset( - WithdrawFormReset event, - Emitter emitter, - ) { - emitter(WithdrawFormState.initial(state.coin, _coinsRepo)); - } - - String get _customFeeType => - state.coin.type == CoinType.smartChain || state.coin.type == CoinType.utxo - ? feeType.utxoFixed - : feeType.ethGas; - - // Validators - Future _additionalValidate(Emitter emitter) async { - final BaseError? parentCoinError = _checkParentCoinErrors(coin: state.coin); - if (parentCoinError != null) { - emitter(state.copyWith(sendError: parentCoinError)); - return false; - } - return true; - } - - Future _validateEnterForm(Emitter emitter) async { - final bool isAddressValid = await _validateAddress(emitter); - final bool isAmountValid = _validateAmount(emitter); - final bool isCustomFeeValid = _validateCustomFee(emitter); - - return isAddressValid && isAmountValid && isCustomFeeValid; - } - - Future _validateAddress(Emitter emitter) async { - final String address = state.address; - if (address.isEmpty) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - if (state.coin.enabledType == WalletType.trezor && - state.selectedSenderAddress.isEmpty) { - emitter(state.copyWith( - isSending: false, - addressError: TextError(error: LocaleKeys.noSenderAddress.tr()), - )); - return false; + return; } - final Map? validateRawResponse = - await coinsRepo.validateCoinAddress( - state.coin, - state.address, + emit( + state.copyWith( + ibcChannel: () => event.channel, + ibcChannelError: () => null, + ), ); - if (validateRawResponse == null) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - - final ValidateAddressResponse validateResponse = - ValidateAddressResponse.fromJson(validateRawResponse); - - final reason = validateResponse.reason ?? ''; - final isNonMixed = _isErcNonMixedCase(reason); - final isValid = validateResponse.isValid; - - if (isNonMixed) { - emitter(state.copyWith( - isSending: false, - addressError: MixedCaseAddressError(), - )); - return false; - } else if (!isValid) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - - emitter(state.copyWith( - addressError: TextError.empty(), - amountError: state.amountError, - )); - return true; } - bool _isErcNonMixedCase(String error) { - if (!state.coin.isErcType) return false; - if (!error.contains(LocaleKeys.invalidAddressChecksum.tr())) return false; - return true; - } + Future _onPreviewSubmitted( + WithdrawFormPreviewSubmitted event, + Emitter emit, + ) async { + if (state.hasValidationErrors) return; - bool _validateAmount(Emitter emitter) { - if (state.amount.isEmpty) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.enterAmountToSend.tr(args: [state.coin.abbr]), - ))); - return false; - } - final double? parsedValue = double.tryParse(state.amount); + try { + emit( + state.copyWith( + isSending: true, + previewError: () => null, + ), + ); - if (parsedValue == null) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.enterAmountToSend.tr(args: [state.coin.abbr]), - ))); - return false; - } + final preview = await _sdk.withdrawals.previewWithdrawal( + state.toWithdrawParameters(), + ); - if (parsedValue == 0) { - emitter(state.copyWith( + emit( + state.copyWith( + preview: () => preview, + step: WithdrawFormStep.confirm, isSending: false, - amountError: TextError( - error: LocaleKeys.inferiorSendAmount.tr(args: [state.coin.abbr]), - ))); - return false; - } - - final double formattedBalance = - double.parse(doubleToString(state.senderAddressBalance)); - - if (parsedValue > formattedBalance) { - emitter(state.copyWith( + ), + ); + } catch (e) { + emit( + state.copyWith( + previewError: () => + TextError(error: 'Failed to generate preview: $e'), isSending: false, - amountError: TextError( - error: LocaleKeys.notEnoughBalance.tr(), - ))); - return false; + ), + ); } + } - if (state.isCustomFeeEnabled && - !state.isMaxAmount && - state.customFee.type == feeType.utxoFixed) { - final double feeValue = - double.tryParse(state.customFee.amount ?? '0.0') ?? 0.0; - if ((parsedValue + feeValue) > formattedBalance) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.notEnoughBalance.tr(), - ))); - return false; - } - } + Future _onSubmitted( + WithdrawFormSubmitted event, + Emitter emit, + ) async { + if (state.hasValidationErrors) return; - return true; - } + try { + emit( + state.copyWith( + isSending: true, + transactionError: () => null, + ), + ); - bool _validateCustomFee(Emitter emitter) { - final customFee = state.customFee; - if (!state.isCustomFeeEnabled) { - return true; - } - if (customFee.type == feeType.utxoFixed) { - return _validateUtxoCustomFee(emitter); - } - if (customFee.type == feeType.ethGas) { - return _validateEvmCustomFee(emitter); - } - return true; - } + await for (final progress in _sdk.withdrawals.withdraw( + state.toWithdrawParameters(), + )) { + if (progress.status == WithdrawalStatus.complete) { + emit( + state.copyWith( + step: WithdrawFormStep.success, + result: () => progress.withdrawalResult, + isSending: false, + ), + ); + return; + } - bool _validateUtxoCustomFee(Emitter emitter) { - final value = state.customFee.amount; - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + if (progress.status == WithdrawalStatus.error) { + throw Exception(progress.errorMessage); + } + } + } catch (e) { + emit( + state.copyWith( + transactionError: () => TextError(error: 'Transaction failed: $e'), + step: WithdrawFormStep.failed, isSending: false, - utxoCustomFeeError: - TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; + ), + ); } - final double amountToSend = state.amountToSendDouble; + } - if (feeAmount > amountToSend) { - emitter(state.copyWith( - isSending: false, - utxoCustomFeeError: - TextError(error: LocaleKeys.customFeeHigherAmount.tr()))); - return false; - } + void _onCancelled( + WithdrawFormCancelled event, + Emitter emit, + ) { + // TODO: Cancel withdrawal if in progress - return true; + add(const WithdrawFormReset()); } - bool _validateEvmCustomFee(Emitter emitter) { - final bool isGasLimitValid = _gasLimitValidator(emitter); - final bool isGasPriceValid = _gasPriceValidator(emitter); - return isGasLimitValid && isGasPriceValid; + void _onReset( + WithdrawFormReset event, + Emitter emit, + ) { + emit( + WithdrawFormState( + asset: state.asset, + step: WithdrawFormStep.fill, + recipientAddress: '', + amount: '0', + pubkeys: state.pubkeys, + selectedSourceAddress: state.pubkeys?.keys.first, + ), + ); } - BaseError? _checkParentCoinErrors({required Coin? coin, String? fee}) { - final Coin? parentCoin = coin?.parentCoin; - if (parentCoin == null) return null; + bool _hasEthAddressMixedCase(String address) { + if (!address.startsWith('0x')) return false; + final chars = address.substring(2).split(''); + return chars.any((c) => c.toLowerCase() != c) && + chars.any((c) => c.toUpperCase() != c); + } - if (!parentCoin.isActive) { - return TextError( - error: - LocaleKeys.withdrawNoParentCoinError.tr(args: [parentCoin.abbr])); - } + Future _onConvertAddress( + WithdrawFormConvertAddressRequested event, + Emitter emit, + ) async { + if (!state.isMixedCaseAddress) return; - final double balance = parentCoin.balance; + try { + emit(state.copyWith(isSending: true)); - if (balance == 0) { - return TextError( - error: LocaleKeys.withdrawTopUpBalanceError.tr(args: [parentCoin.abbr]), - ); - } else if (fee != null && parentCoin.balance < double.parse(fee)) { - return TextError( - error: LocaleKeys.withdrawNotEnoughBalanceForGasError - .tr(args: [parentCoin.abbr]), + // For EVM addresses, we want to convert to checksum format + final result = await _sdk.addresses.convertFormat( + asset: state.asset, + address: state.recipientAddress, + format: const AddressFormat(format: 'checksummed', network: ''), ); - } - - return null; - } - bool _gasLimitValidator(Emitter emitter) { - final value = state.customFee.gas.toString(); - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + emit( + state.copyWith( + recipientAddress: result.convertedAddress, + isMixedCaseAddress: false, + recipientAddressError: () => null, isSending: false, - gasLimitError: TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; - } - return true; - } - - bool _gasPriceValidator(Emitter emitter) { - final value = state.customFee.gasPrice; - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + ), + ); + } catch (e) { + emit( + state.copyWith( + recipientAddressError: () => + TextError(error: 'Failed to convert address: $e'), isSending: false, - gasPriceError: TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; + ), + ); } - return true; - } - - double? _valueToAmount(String? value) { - if (value == null) return null; - value = value.replaceAll(',', '.'); - return double.tryParse(value); } } diff --git a/lib/bloc/withdraw_form/withdraw_form_event.dart b/lib/bloc/withdraw_form/withdraw_form_event.dart index 4a8982ae8b..54ed172596 100644 --- a/lib/bloc/withdraw_form/withdraw_form_event.dart +++ b/lib/bloc/withdraw_form/withdraw_form_event.dart @@ -1,131 +1,78 @@ -import 'package:equatable/equatable.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/model/hw_wallet/trezor_progress_status.dart'; -import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; -abstract class WithdrawFormEvent extends Equatable { +sealed class WithdrawFormEvent { const WithdrawFormEvent(); - - @override - List get props => []; } -class WithdrawFormAddressChanged extends WithdrawFormEvent { - const WithdrawFormAddressChanged({required this.address}); +class WithdrawFormRecipientChanged extends WithdrawFormEvent { final String address; - - @override - List get props => [address]; + const WithdrawFormRecipientChanged(this.address); } class WithdrawFormAmountChanged extends WithdrawFormEvent { - const WithdrawFormAmountChanged({required this.amount}); - final String amount; - - @override - List get props => [amount]; -} - -class WithdrawFormCustomFeeChanged extends WithdrawFormEvent { - const WithdrawFormCustomFeeChanged({required this.amount}); final String amount; - - @override - List get props => [amount]; + const WithdrawFormAmountChanged(this.amount); } -class WithdrawFormCustomEvmFeeChanged extends WithdrawFormEvent { - const WithdrawFormCustomEvmFeeChanged({this.gasPrice, this.gas}); - final String? gasPrice; - final int? gas; - - @override - List get props => [gasPrice, gas]; +class WithdrawFormSourceChanged extends WithdrawFormEvent { + final PubkeyInfo address; + const WithdrawFormSourceChanged(this.address); } -class WithdrawFormSenderAddressChanged extends WithdrawFormEvent { - const WithdrawFormSenderAddressChanged({required this.address}); - final String address; - - @override - List get props => [address]; +class WithdrawFormMaxAmountEnabled extends WithdrawFormEvent { + final bool isEnabled; + const WithdrawFormMaxAmountEnabled(this.isEnabled); } -class WithdrawFormMaxTapped extends WithdrawFormEvent { - const WithdrawFormMaxTapped({required this.isEnabled}); +class WithdrawFormCustomFeeEnabled extends WithdrawFormEvent { final bool isEnabled; - - @override - List get props => [isEnabled]; + const WithdrawFormCustomFeeEnabled(this.isEnabled); } -class WithdrawFormWithdrawSuccessful extends WithdrawFormEvent { - const WithdrawFormWithdrawSuccessful({required this.details}); - final WithdrawDetails details; - - @override - List get props => [details]; +class WithdrawFormCustomFeeChanged extends WithdrawFormEvent { + final FeeInfo fee; + const WithdrawFormCustomFeeChanged(this.fee); } -class WithdrawFormWithdrawFailed extends WithdrawFormEvent { - const WithdrawFormWithdrawFailed({required this.error}); - final BaseError error; - - @override - List get props => [error]; +class WithdrawFormMemoChanged extends WithdrawFormEvent { + final String? memo; + const WithdrawFormMemoChanged(this.memo); } -class WithdrawFormTrezorStatusUpdated extends WithdrawFormEvent { - const WithdrawFormTrezorStatusUpdated({required this.status}); - final TrezorProgressStatus? status; - - @override - List get props => [status]; +class WithdrawFormPreviewSubmitted extends WithdrawFormEvent { + const WithdrawFormPreviewSubmitted(); } -class WithdrawFormSendRawTx extends WithdrawFormEvent { - const WithdrawFormSendRawTx(); - - @override - List get props => []; +class WithdrawFormSubmitted extends WithdrawFormEvent { + const WithdrawFormSubmitted(); } -class WithdrawFormCustomFeeDisabled extends WithdrawFormEvent { - const WithdrawFormCustomFeeDisabled(); - - @override - List get props => []; +class WithdrawFormCancelled extends WithdrawFormEvent { + const WithdrawFormCancelled(); } -class WithdrawFormCustomFeeEnabled extends WithdrawFormEvent { - const WithdrawFormCustomFeeEnabled(); - - @override - List get props => []; +class WithdrawFormReset extends WithdrawFormEvent { + const WithdrawFormReset(); } -class WithdrawFormConvertAddress extends WithdrawFormEvent { - const WithdrawFormConvertAddress(); - - @override - List get props => []; +class WithdrawFormIbcTransferEnabled extends WithdrawFormEvent { + final bool isEnabled; + WithdrawFormIbcTransferEnabled(this.isEnabled); } -class WithdrawFormSubmitted extends WithdrawFormEvent { - const WithdrawFormSubmitted(); +class WithdrawFormIbcChannelChanged extends WithdrawFormEvent { + final String channel; + WithdrawFormIbcChannelChanged(this.channel); } -class WithdrawFormReset extends WithdrawFormEvent { - const WithdrawFormReset(); +class WithdrawFormSourcesLoadRequested extends WithdrawFormEvent { + const WithdrawFormSourcesLoadRequested(); } class WithdrawFormStepReverted extends WithdrawFormEvent { - const WithdrawFormStepReverted({required this.step}); - final WithdrawFormStep step; + const WithdrawFormStepReverted(); } -class WithdrawFormMemoUpdated extends WithdrawFormEvent { - const WithdrawFormMemoUpdated({required this.text}); - final String? text; +class WithdrawFormConvertAddressRequested extends WithdrawFormEvent { + const WithdrawFormConvertAddressRequested(); } diff --git a/lib/bloc/withdraw_form/withdraw_form_state.dart b/lib/bloc/withdraw_form/withdraw_form_state.dart index 4c8ee32be4..82ea4279d2 100644 --- a/lib/bloc/withdraw_form/withdraw_form_state.dart +++ b/lib/bloc/withdraw_form/withdraw_form_state.dart @@ -1,207 +1,193 @@ +import 'package:decimal/decimal.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:komodo_defi_types/komodo_defi_types.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'; -import 'package:web_dex/model/hd_account/hd_account.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'; class WithdrawFormState extends Equatable { + final Asset asset; + final AssetPubkeys? pubkeys; + final WithdrawFormStep step; + + // Form fields + final String recipientAddress; + final String amount; + final PubkeyInfo? selectedSourceAddress; + final bool isMaxAmount; + final bool isCustomFee; + final FeeInfo? customFee; + final String? memo; + final bool isIbcTransfer; + final String? ibcChannel; + + // Transaction state + final WithdrawalPreview? preview; + final bool isSending; + final WithdrawalResult? result; + + // Validation errors + final TextError? recipientAddressError; // Basic address validation + final bool isMixedCaseAddress; // EVM mixed case specific error + final TextError? amountError; // Amount validation (insufficient funds etc) + final TextError? customFeeError; // Fee validation for custom fees + final TextError? ibcChannelError; // IBC channel validation + + // Network/Transaction errors + final TextError? previewError; // Errors during preview generation + final TextError? transactionError; // Errors during transaction submission + final TextError? networkError; // Network connectivity errors + + bool get isCustomFeeSupported => + asset.protocol is UtxoProtocol || asset.protocol is Erc20Protocol; + + bool get hasPreviewError => previewError != null; + bool get hasTransactionError => transactionError != null; + bool get hasAddressError => + recipientAddressError != null || isMixedCaseAddress; + bool get hasValidationErrors => + hasAddressError || + amountError != null || + customFeeError != null || + ibcChannelError != null; + const WithdrawFormState({ - required this.coin, + required this.asset, + this.pubkeys, required this.step, - required this.address, + required this.recipientAddress, required this.amount, - required this.senderAddresses, - required this.selectedSenderAddress, - required bool isMaxAmount, - required this.customFee, - required this.withdrawDetails, - required this.isSending, - required this.trezorProgressStatus, - required this.sendError, - required this.addressError, - required this.amountError, - required this.utxoCustomFeeError, - required this.gasLimitError, - required this.gasPriceError, - required this.isCustomFeeEnabled, - required this.memo, - required CoinsBloc coinsBloc, - }) : _isMaxAmount = isMaxAmount, - _coinsRepo = coinsBloc; - - static WithdrawFormState initial(Coin coin, CoinsBloc coinsBloc) { - final List initSenderAddresses = coin.nonEmptyHdAddresses(); - final String selectedSenderAddress = - initSenderAddresses.isNotEmpty ? initSenderAddresses.first.address : ''; - - return WithdrawFormState( - coin: coin, - step: WithdrawFormStep.fill, - address: '', - amount: '', - senderAddresses: initSenderAddresses, - selectedSenderAddress: selectedSenderAddress, - isMaxAmount: false, - customFee: FeeRequest(type: ''), - withdrawDetails: WithdrawDetails.empty(), - isSending: false, - isCustomFeeEnabled: false, - trezorProgressStatus: null, - sendError: TextError.empty(), - addressError: TextError.empty(), - amountError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - memo: null, - coinsBloc: coinsBloc, - ); - } + this.selectedSourceAddress, + this.isMaxAmount = false, + this.isCustomFee = false, + this.customFee, + this.memo, + this.isIbcTransfer = false, + this.ibcChannel, + this.preview, + this.isSending = false, + this.result, + // Error states + this.recipientAddressError, + this.isMixedCaseAddress = false, + this.amountError, + this.customFeeError, + this.ibcChannelError, + this.previewError, + this.transactionError, + this.networkError, + }); WithdrawFormState copyWith({ - Coin? coin, - String? address, - String? amount, + Asset? asset, + ValueGetter? pubkeys, WithdrawFormStep? step, - FeeRequest? customFee, - List? senderAddresses, - String? selectedSenderAddress, + String? recipientAddress, + String? amount, + ValueGetter? selectedSourceAddress, bool? isMaxAmount, - BaseError? sendError, - BaseError? addressError, - BaseError? amountError, - BaseError? utxoCustomFeeError, - BaseError? gasLimitError, - BaseError? gasPriceError, - WithdrawDetails? withdrawDetails, + bool? isCustomFee, + ValueGetter? customFee, + ValueGetter? memo, + bool? isIbcTransfer, + ValueGetter? ibcChannel, + ValueGetter? preview, bool? isSending, - bool? isCustomFeeEnabled, - String? trezorProgressStatus, - String? memo, - CoinsBloc? coinsBloc, + ValueGetter? result, + // Error states + ValueGetter? recipientAddressError, + bool? isMixedCaseAddress, + ValueGetter? amountError, + ValueGetter? customFeeError, + ValueGetter? ibcChannelError, + ValueGetter? previewError, + ValueGetter? transactionError, + ValueGetter? networkError, }) { return WithdrawFormState( - coin: coin ?? this.coin, - address: address ?? this.address, - amount: amount ?? this.amount, + asset: asset ?? this.asset, + pubkeys: pubkeys != null ? pubkeys() : this.pubkeys, step: step ?? this.step, - customFee: customFee ?? this.customFee, + recipientAddress: recipientAddress ?? this.recipientAddress, + amount: amount ?? this.amount, + selectedSourceAddress: selectedSourceAddress != null + ? selectedSourceAddress() + : this.selectedSourceAddress, isMaxAmount: isMaxAmount ?? this.isMaxAmount, - senderAddresses: senderAddresses ?? this.senderAddresses, - selectedSenderAddress: - selectedSenderAddress ?? this.selectedSenderAddress, - sendError: sendError ?? this.sendError, - withdrawDetails: withdrawDetails ?? this.withdrawDetails, + isCustomFee: isCustomFee ?? this.isCustomFee, + customFee: customFee != null ? customFee() : this.customFee, + memo: memo != null ? memo() : this.memo, + isIbcTransfer: isIbcTransfer ?? this.isIbcTransfer, + ibcChannel: ibcChannel != null ? ibcChannel() : this.ibcChannel, + preview: preview != null ? preview() : this.preview, isSending: isSending ?? this.isSending, - addressError: addressError ?? this.addressError, - amountError: amountError ?? this.amountError, - gasLimitError: gasLimitError ?? this.gasLimitError, - gasPriceError: gasPriceError ?? this.gasPriceError, - utxoCustomFeeError: utxoCustomFeeError ?? this.utxoCustomFeeError, - isCustomFeeEnabled: isCustomFeeEnabled ?? this.isCustomFeeEnabled, - trezorProgressStatus: trezorProgressStatus, - memo: memo ?? this.memo, - coinsBloc: coinsBloc ?? _coinsRepo, + result: result != null ? result() : this.result, + recipientAddressError: recipientAddressError != null + ? recipientAddressError() + : this.recipientAddressError, + isMixedCaseAddress: isMixedCaseAddress ?? this.isMixedCaseAddress, + amountError: amountError != null ? amountError() : this.amountError, + customFeeError: + customFeeError != null ? customFeeError() : this.customFeeError, + ibcChannelError: + ibcChannelError != null ? ibcChannelError() : this.ibcChannelError, + previewError: previewError != null ? previewError() : this.previewError, + transactionError: + transactionError != null ? transactionError() : this.transactionError, + networkError: networkError != null ? networkError() : this.networkError, ); } - final Coin coin; - final String address; - final String amount; - final WithdrawFormStep step; - final List senderAddresses; - final String selectedSenderAddress; - final FeeRequest customFee; - final WithdrawDetails withdrawDetails; - final bool isSending; - final bool isCustomFeeEnabled; - final String? trezorProgressStatus; - final BaseError sendError; - final BaseError addressError; - final BaseError amountError; - final BaseError utxoCustomFeeError; - final BaseError gasLimitError; - final BaseError gasPriceError; - final bool _isMaxAmount; - final String? memo; - final CoinsBloc _coinsRepo; + WithdrawParameters toWithdrawParameters() { + return WithdrawParameters( + asset: asset.id.id, + toAddress: recipientAddress, + amount: isMaxAmount ? null : Decimal.parse(amount), + fee: isCustomFee ? customFee : null, + from: selectedSourceAddress?.derivationPath != null + ? WithdrawalSource.hdDerivationPath( + selectedSourceAddress!.derivationPath!, + ) + : null, + memo: memo, + ibcTransfer: isIbcTransfer ? true : null, + isMax: isMaxAmount, + ); + } + + //TODO! + double? get usdFeePrice => 0.0; + + //TODO! + double? get usdAmountPrice => 0.0; + + //TODO! + bool get isFeePriceExpensive => false; @override List get props => [ - coin, - address, - amount, + asset, + pubkeys, step, - senderAddresses, - selectedSenderAddress, + recipientAddress, + amount, + selectedSourceAddress, isMaxAmount, + isCustomFee, customFee, - withdrawDetails, + memo, + isIbcTransfer, + ibcChannel, + preview, isSending, - isCustomFeeEnabled, - trezorProgressStatus, - sendError, - addressError, + result, + recipientAddressError, + isMixedCaseAddress, amountError, - utxoCustomFeeError, - gasLimitError, - gasPriceError, - memo, + customFeeError, + ibcChannelError, + previewError, + transactionError, + networkError, ]; - - bool get isMaxAmount => - _isMaxAmount || amount == doubleToString(senderAddressBalance); - double get amountToSendDouble => double.tryParse(amount) ?? 0; - String get amountToSendString { - if (isMaxAmount && coin.abbr == withdrawDetails.feeCoin) { - return doubleToString( - amountToSendDouble - double.parse(withdrawDetails.feeValue), - ); - } - return amount; - } - - double get senderAddressBalance { - switch (coin.enabledType) { - case WalletType.trezor: - return coin.getHdAddress(selectedSenderAddress)?.balance.spendable ?? - 0.0; - default: - return coin.sendableBalance; - } - } - - bool get hasAddressError => addressError.message.isNotEmpty; - bool get hasAmountError => amountError.message.isNotEmpty; - bool get hasSendError => sendError.message.isNotEmpty; - bool get hasGasLimitError => gasLimitError.message.isNotEmpty; - bool get hasGasPriceError => gasPriceError.message.isNotEmpty; - bool get hasUtxoFeeError => utxoCustomFeeError.message.isNotEmpty; - - double? get usdAmountPrice => _coinsRepo.getUsdPriceByAmount( - amount, - coin.abbr, - ); - - double? get usdFeePrice => _coinsRepo.getUsdPriceByAmount( - withdrawDetails.feeValue, - withdrawDetails.feeCoin, - ); - - bool get isFeePriceExpensive { - final usdFeePrice = this.usdFeePrice; - final usdAmountPrice = this.usdAmountPrice; - - if (usdFeePrice == null || usdAmountPrice == null || usdAmountPrice == 0) { - return false; - } - - return usdFeePrice / usdAmountPrice >= 0.05; - } } diff --git a/lib/bloc/withdraw_form/withdraw_form_step.dart b/lib/bloc/withdraw_form/withdraw_form_step.dart index 24d0f50dca..54aa822ca8 100644 --- a/lib/bloc/withdraw_form/withdraw_form_step.dart +++ b/lib/bloc/withdraw_form/withdraw_form_step.dart @@ -2,10 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; enum WithdrawFormStep { - failed, fill, confirm, - success; + success, + failed; + + static WithdrawFormStep initial() => WithdrawFormStep.fill; String get title { switch (this) { diff --git a/lib/blocs/blocs.dart b/lib/blocs/blocs.dart deleted file mode 100644 index 6fe3a54411..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/get_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, - 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 2ed9bf8fbd..0000000000 --- a/lib/blocs/coins_bloc.dart +++ /dev/null @@ -1,467 +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, - ); - - _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']); - - 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; - } - - @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 deleted file mode 100644 index 6e1de765ac..0000000000 --- a/lib/blocs/current_wallet_bloc.dart +++ /dev/null @@ -1,118 +0,0 @@ -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: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'; - -class CurrentWalletBloc implements BlocBase { - CurrentWalletBloc({ - required EncryptionTool encryptionTool, - required FileLoader fileLoader, - required WalletsRepo walletsRepo, - required AuthRepository authRepo, - }) : _encryptionTool = encryptionTool, - _fileLoader = fileLoader, - _walletsRepo = walletsRepo; - - 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; - Wallet? get wallet => _wallet; - set wallet(Wallet? wallet) { - _wallet = wallet; - _inWallet.add(_wallet); - } - - @override - void dispose() { - _walletController.close(); - _authModeListener.cancel(); - } - - 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; - } - } - - Future addCoin(Coin coin) async { - final String coinAbbr = coin.abbr; - final Wallet? wallet = this.wallet; - if (wallet == null) { - return false; - } - if (wallet.config.activatedCoins.contains(coinAbbr)) { - return false; - } - wallet.config.activatedCoins.add(coinAbbr); - - final bool isSuccess = await _walletsRepo.save(wallet); - return isSuccess; - } - - Future removeCoin(String coinAbbr) async { - final Wallet? wallet = this.wallet; - if (wallet == null) { - return false; - } - - wallet.config.activatedCoins.remove(coinAbbr); - final bool isSuccess = await _walletsRepo.save(wallet); - this.wallet = wallet; - return isSuccess; - } - - Future downloadCurrentWallet(String password) async { - final Wallet? wallet = this.wallet; - if (wallet == null) return; - - final String data = jsonEncode(wallet.config); - final String encryptedData = - await _encryptionTool.encryptData(password, data); - - _fileLoader.save( - fileName: wallet.name, - data: encryptedData, - type: LoadFileType.text, - ); - - await confirmBackup(); - this.wallet = wallet; - } - - Future confirmBackup() async { - final Wallet? wallet = this.wallet; - if (wallet == null || wallet.config.hasBackup) return; - - wallet.config.hasBackup = true; - await _walletsRepo.save(wallet); - 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 59f44dcbb6..0000000000 --- a/lib/blocs/startup_bloc.dart +++ /dev/null @@ -1,51 +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 { - if (mm2 is MM2WithInit) await (mm2 as MM2WithInit).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..2b621f69ef --- /dev/null +++ b/lib/blocs/wallets_repository.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:convert'; + +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/file_loader/file_loader.dart'; +import 'package:web_dex/services/storage/base_storage.dart'; +import 'package:web_dex/shared/utils/encryption_tool.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class WalletsRepository { + WalletsRepository( + this._kdfSdk, + this._mm2Api, + this._legacyWalletStorage, { + EncryptionTool? encryptionTool, + FileLoader? fileLoader, + }) : _encryptionTool = encryptionTool ?? EncryptionTool(), + _fileLoader = fileLoader ?? FileLoader.fromPlatform(); + + final KomodoDefiSdk _kdfSdk; + final Mm2Api _mm2Api; + final BaseStorage _legacyWalletStorage; + final EncryptionTool _encryptionTool; + final FileLoader _fileLoader; + + 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); + } + } + + @Deprecated('Use the KomodoDefiSdk.auth.getMnemonicEncrypted method instead.') + Future downloadEncryptedWallet(Wallet wallet, String password) async { + try { + if (wallet.config.seedPhrase.isEmpty) { + final mnemonic = await _kdfSdk.auth.getMnemonicPlainText(password); + wallet.config.seedPhrase = await _encryptionTool.encryptData( + password, + mnemonic.plaintextMnemonic ?? '', + ); + } + final String data = jsonEncode(wallet.config); + final String encryptedData = + await _encryptionTool.encryptData(password, data); + final String sanitizedFileName = _sanitizeFileName(wallet.name); + await _fileLoader.save( + fileName: sanitizedFileName, + data: encryptedData, + type: LoadFileType.text, + ); + } catch (e) { + throw Exception('Failed to download encrypted wallet: ${e.toString()}'); + } + } + + String _sanitizeFileName(String fileName) { + return fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); + } +} diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index edb284e24f..af25874caf 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -6,9 +6,11 @@ abstract class LocaleKeys { static const noKmdAddress = 'noKmdAddress'; static const dex = 'dex'; static const asset = 'asset'; + static const assets = 'assets'; 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'; @@ -392,6 +394,15 @@ 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 hdWalletModeSwitchTitle = 'hdWalletModeSwitchTitle'; + static const hdWalletModeSwitchSubtitle = 'hdWalletModeSwitchSubtitle'; + static const hdWalletModeSwitchTooltip = 'hdWalletModeSwitchTooltip'; + static const noActiveWallet = 'noActiveWallet'; static const memo = 'memo'; static const gasPriceGwei = 'gasPriceGwei'; static const gasLimit = 'gasLimit'; @@ -551,6 +562,16 @@ 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'; @@ -558,7 +579,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'; @@ -579,6 +602,20 @@ abstract class LocaleKeys { static const mmBotFirstTradePreview = 'mmBotFirstTradePreview'; static const mmBotFirstTradeEstimate = 'mmBotFirstTradeEstimate'; static const mmBotFirstOrderVolume = 'mmBotFirstOrderVolume'; + static const importCustomToken = 'importCustomToken'; + static const importTokenWarning = 'importTokenWarning'; + static const importToken = 'importToken'; + static const selectNetwork = 'selectNetwork'; + static const tokenNotFound = 'tokenNotFound'; + static const tokenContractAddress = 'tokenContractAddress'; + static const decimals = 'decimals'; + 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 52410cfbe1..5a096fcbe7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,23 +5,32 @@ 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/startup_bloc.dart'; +import 'package:web_dex/bloc/trezor_bloc/trezor_repo.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/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/platform_tuner.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -33,28 +42,75 @@ 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(); + 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, sdk: komodoDefiSdk); + 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: (_) => 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; @@ -82,17 +138,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 bcf35ce63e..43085502f0 100644 --- a/lib/mm2/mm2.dart +++ b/lib/mm2/mm2.dart @@ -1,119 +1,94 @@ -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:web_dex/mm2/mm2_api/rpc/get_my_peer_id/get_my_peer_id_request.dart'; +import 'dart:async'; + +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.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(); - -abstract class MM2 { - const MM2(); - static late String _rpcPassword; - - Future start(String? passphrase); +final MM2 mm2 = MM2(); + +final class MM2 { + MM2() { + _kdfSdk = KomodoDefiSdk( + config: const KomodoDefiSdkConfig( + // Syncing pre-activation coin states is not yet implemented, + // so we disable it for now. + // TODO: sync pre-activation of coins (show activating coins in list) + preActivateHistoricalAssets: false, + preActivateDefaultAssets: false, + preActivateCustomTokenAssets: true, + ), + ); + } - Future stop(); + late final KomodoDefiSdk _kdfSdk; + bool _isInitializing = false; + final Completer _initCompleter = Completer(); - Future version() async { - final dynamic responseStr = await call(VersionRequest()); - final Map responseJson = jsonDecode(responseStr); - final VersionResponse response = VersionResponse.fromJson(responseJson); + Future isSignedIn() => _kdfSdk.auth.isSignedIn(); - return response.result; - } + Future initialize() async { + if (_initCompleter.isCompleted) return _kdfSdk; + if (_isInitializing) return _initCompleter.future; - Future isLive() async { try { - final String response = await call(GetMyPeerIdRequest()); - final Map responseJson = jsonDecode(response); - - return responseJson['result']?.isNotEmpty ?? false; - } catch (e, s) { - log( - 'Get my peer id error: ${e.toString()}', - path: 'mm2 => isLive', - trace: s, - isError: true, - ); - return false; + _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; } } - Future status(); - - Future call(dynamic reqStr); + Future version() async { + final JsonMap responseJson = await call(VersionRequest()); + final VersionResponse response = VersionResponse.fromJson(responseJson); - static String prepareRequest(dynamic req) { - final String reqStr = jsonEncode(_assertPass(req)); - return reqStr; + return response.result; } - 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'); + Future call(dynamic request) async { + try { + 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; } - _rpcPassword = newRpcPassword; - - // 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(), - }; } - 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) { - element.userpass = _rpcPassword; + for (final dynamic element in req) { + // ignore: avoid_dynamic_calls + element.userpass = ''; } } else { if (req is Map) { - req['userpass'] = _rpcPassword; + req['userpass'] = ''; } else { - req.userpass = _rpcPassword; + // ignore: avoid_dynamic_calls + req.userpass = ''; } } @@ -121,24 +96,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 +121,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 2239c25723..f9365014bb 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; -import 'package:collection/collection.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.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'; import 'package:web_dex/mm2/mm2_api/mm2_api_trezor.dart'; @@ -10,24 +12,17 @@ import 'package:web_dex/mm2/mm2_api/rpc/active_swaps/active_swaps_request.dart'; 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'; -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'; 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'; @@ -46,6 +41,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/show_priv_key/show_priv_key_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/show_priv_key/show_priv_key_response.dart'; @@ -55,408 +51,52 @@ 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, - }) : _mm2 = mm2 { - trezor = Mm2ApiTrezor(_call); - nft = Mm2ApiNft(_call); + required CoinsRepo coinsRepo, + required KomodoDefiSdk sdk, + }) : _sdk = sdk, + _mm2 = mm2 { + trezor = Mm2ApiTrezor(_mm2.call); + nft = Mm2ApiNft(_mm2.call, coinsRepo); } final MM2 _mm2; + + // Ideally we will transition cleanly over to the SDK, but for methods + // which are deeply intertwined with the app and are broken by HD wallet + // changes, we will tie into the SDK here. + final KomodoDefiSdk _sdk; + late Mm2ApiTrezor trezor; late Mm2ApiNft nft; VersionResponse? _versionResponse; - Future?> getEnabledCoins(List knownCoins) async { - dynamic response; + Future disableCoin(String coinId) async { 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']; + await _mm2.call(DisableCoinReq(coin: coinId)); } 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 { - try { - await _call(DisableCoinReq(coin: coin)); - } catch (e, s) { - log( - 'Error disabling $coin: ${e.toString()}', + 'Error disabling $coinId: $e', path: 'api=> disableCoin => _call', trace: s, isError: true, - ); + ).ignore(); return; } } + @Deprecated('Use balance from KomoDefiSdk instead') 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); - } + final sdkAsset = _sdk.assets.assetsFromTicker(abbr).single; + final addresses = await sdkAsset.getPubkeys(_sdk); - 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, - ); + return addresses.balance.total.toString(); } Future _fallbackToBalanceTaker(String abbr) async { @@ -485,15 +125,14 @@ class Mm2Api { 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'}; } } @@ -503,56 +142,57 @@ 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; } } - 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()}', + 'Error sending raw transaction ${request.coin}: $e', path: 'api => sendRawTransaction', trace: s, isError: true, + ).ignore(); + return SendRawTransactionResponse( + txHash: null, + error: TextError(error: 'null response'), ); - return null; } } @@ -560,17 +200,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; } } @@ -579,17 +216,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; } } @@ -598,137 +232,131 @@ 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'}; } } 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); - 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; } } @@ -736,38 +364,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; } } @@ -776,24 +402,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; } } @@ -802,19 +427,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); } } @@ -823,32 +447,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?, ); } @@ -858,11 +480,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, @@ -873,21 +495,21 @@ class Mm2Api { Future getOrderBookDepth( List> pairs, + CoinsRepo coinsRepository, ) 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); + 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; } @@ -898,8 +520,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); } @@ -909,11 +530,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, ); @@ -933,25 +554,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; } } @@ -971,43 +589,15 @@ class Mm2Api { } } - Future convertLegacyAddress(ConvertAddressRequest request) async { - try { - final String response = await _call(request); - final Map responseJson = jsonDecode(response); - return responseJson['result']?['address']; - } catch (e, s) { - log( - 'Convert address error: ${e.toString()}', - path: 'api => convertLegacyAddress', - trace: s, - isError: true, - ); - return null; - } - } - Future stop() async { - await _call(StopReq()); - } - - Future _call(dynamic req) async { - final MM2Status mm2Status = await _mm2.status(); - if (mm2Status != MM2Status.rpcIsUp) { - return '{"error": "Error, mm2 status: $mm2Status"}'; - } - - final dynamic response = await _mm2.call(req); - - return response; + await _mm2.call(StopReq()); } 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; } @@ -1018,8 +608,34 @@ class Mm2Api { path: 'api => showPrivKey', trace: s, isError: true, - ); + ).ignore(); return null; } } + + 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 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 4bf166ab77..460bdec0f1 100644 --- a/lib/mm2/mm2_api/mm2_api_nft.dart +++ b/lib/mm2/mm2_api/mm2_api_nft.dart @@ -1,45 +1,48 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.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 Future Function(dynamic) call; + final CoinsRepo _coinsRepo; + final Future Function(dynamic) call; Future> updateNftList( - List chains) async { + List chains, + ) async { try { 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,26 +50,29 @@ class Mm2ApiNft { path: 'UpdateNftResponse', trace: s, isError: true, - ); + ).ignore(); throw TransportError(message: e.toString()); } } - Future> refreshNftMetadata( - {required String chain, - required String tokenAddress, - required String tokenId}) async { + Future> refreshNftMetadata({ + required String chain, + required String tokenAddress, + required String tokenId, + }) async { try { final RefreshNftMetadataRequest request = RefreshNftMetadataRequest( - chain: chain, tokenAddress: tokenAddress, tokenId: tokenId); - final dynamic rawResponse = await call(request); - - final Map json = jsonDecode(rawResponse); - - return json; + chain: chain, + tokenAddress: tokenAddress, + tokenId: tokenId, + ); + return await call(request); } catch (e) { - log(e.toString(), - path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true); + log( + e.toString(), + path: 'Mm2ApiNft => RefreshNftMetadataRequest', + isError: true, + ).ignore(); throw TransportError(message: e.toString()); } } @@ -76,92 +82,99 @@ 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()); } } Future> getNftTxs( - NftTransactionsRequest request, bool withAdditionalInfo) async { + 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()); } } Future> getNftTxDetails( - NftTxDetailsRequest request) async { + NftTxDetailsRequest request, + ) async { try { final additionalTxInfo = await const ProxyApiNft() .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()); } } 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); + log( + enabledCoins.toString(), + 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); + log( + activeChains.toString(), + 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,22 +183,25 @@ 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']), - 'tx_hash': tx['transaction_hash'], - }) + .map( + (tx) => { + 'blockchain': convertChainForProxy(tx['chain'] as String), + 'tx_hash': tx['transaction_hash'], + }, + ) .toList(); final response = await Client().post( 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 +228,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..13986baf4f 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_type_utils.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..6b37851914 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_type_utils.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/convert_address/convert_address_request.dart b/lib/mm2/mm2_api/rpc/convert_address/convert_address_request.dart deleted file mode 100644 index 47202ee021..0000000000 --- a/lib/mm2/mm2_api/rpc/convert_address/convert_address_request.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; - -class ConvertAddressRequest implements BaseRequest { - ConvertAddressRequest({ - required this.from, - required this.coin, - required this.isErc, - }); - - @override - final String method = 'convertaddress'; - @override - late String userpass; - - final String from; - final String coin; - final bool isErc; - - @override - Map toJson() { - return { - 'method': method, - 'userpass': userpass, - 'from': from, - 'coin': coin, - 'to_address_format': { - 'format': isErc ? 'mixedcase' : 'cashaddress', - if (coin == 'BCH') 'network': 'bitcoincash', - } - }; - } -} 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..3f08c50e66 --- /dev/null +++ b/lib/mm2/mm2_api/rpc/directly_connected_peers/get_directly_connected_peers_response.dart @@ -0,0 +1,39 @@ +class GetDirectlyConnectedPeersResponse { + GetDirectlyConnectedPeersResponse({ + required this.peers, + }); + + factory GetDirectlyConnectedPeersResponse.fromJson( + Map json, + ) { + final peersMap = json['result'] as Map? ?? {}; + 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); + } + + 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/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/get_enabled_coins_req.dart deleted file mode 100644 index 026d3243ae..0000000000 --- a/lib/mm2/mm2_api/rpc/get_enabled_coins/get_enabled_coins_req.dart +++ /dev/null @@ -1,11 +0,0 @@ -class GetEnabledCoinsReq { - static const String method = 'get_enabled_coins'; - late String userpass; - - Map toJson() { - return { - 'method': method, - 'userpass': userpass, - }; - } -} 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/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/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_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/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/cex_price.dart b/lib/model/cex_price.dart index 8130765226..84d4b893b2 100644 --- a/lib/model/cex_price.dart +++ b/lib/model/cex_price.dart @@ -27,6 +27,34 @@ class CexPrice extends Equatable { return 'CexPrice(ticker: $ticker, price: $price)'; } + factory CexPrice.fromJson(Map json) { + return CexPrice( + ticker: json['ticker'] as String, + price: (json['price'] as num).toDouble(), + lastUpdated: json['lastUpdated'] == null + ? null + : DateTime.parse(json['lastUpdated'] as String), + priceProvider: cexDataProvider(json['priceProvider'] as String), + volume24h: (json['volume24h'] as num?)?.toDouble(), + volumeProvider: cexDataProvider(json['volumeProvider'] as String), + change24h: (json['change24h'] as num?)?.toDouble(), + changeProvider: cexDataProvider(json['changeProvider'] as String), + ); + } + + Map toJson() { + return { + 'ticker': ticker, + 'price': price, + 'lastUpdated': lastUpdated?.toIso8601String(), + 'priceProvider': priceProvider?.toString(), + 'volume24h': volume24h, + 'volumeProvider': volumeProvider?.toString(), + 'change24h': change24h, + 'changeProvider': changeProvider?.toString(), + }; + } + @override List get props => [ ticker, diff --git a/lib/model/coin.dart b/lib/model/coin.dart index 3e0a8e4011..27eb97518b 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -1,9 +1,11 @@ import 'package:collection/collection.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.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'; @@ -13,6 +15,7 @@ class Coin { Coin({ required this.type, required this.abbr, + required this.id, required this.name, required this.explorerUrl, required this.explorerTxUrl, @@ -20,98 +23,33 @@ class Coin { required this.protocolType, required this.protocolData, required this.isTestCoin, + required this.logoImageUrl, 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, this.coinpaprikaId, this.activeByDefault = false, + this.isCustomCoin = false, 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 AssetId id; + final String? logoImageUrl; 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 +57,26 @@ 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; + bool isCustomCoin; + + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset multi-address support instead. The wallet now works with multiple addresses per account.') String? address; + + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset account management instead.') List? accounts; - double _balance = 0; - String? _swapContractAddress; + + final double _balance; + final String? _swapContractAddress; String? fallbackSwapContract; + + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s WalletManager to determine wallet type.') WalletType? enabledType; - bool _walletOnly; + + final bool _walletOnly; final int priority; Coin? parentCoin; final CoinMode mode; @@ -138,35 +84,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; @@ -174,8 +91,10 @@ class Coin { bool get isActivating => state == CoinState.activating; bool get isInactive => state == CoinState.inactive; + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset.sendableBalance instead. This value is not updated after initial load and may be inaccurate.') double sendableBalance = 0; + @Deprecated('$_urgentDeprecationNotice Use the balance manager from the SDK. This balance value is not updated after initial load and may be inaccurate.') double get balance { switch (enabledType) { case WalletType.trezor: @@ -185,17 +104,7 @@ 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; - } - } - + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset balance tracking instead. This balance value is not updated after initial load and may be inaccurate.') double? get _totalHdBalance { if (accounts == null) return null; @@ -211,15 +120,26 @@ class Coin { return totalBalance; } + double calculateUsdAmount(double amount) { + if (usdPrice == null) return 0; + return amount * usdPrice!.price; + } + + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset price and balance methods instead. This value uses potentially outdated balance and price information.') 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 amountToFormattedUsd(double amount) { + if (usdPrice == null) return '\$0.00'; + return '\$${formatAmt(calculateUsdAmount(amount))}'; } - String get getFormattedUsdBalance => - usdBalance == null ? '\$0.00' : '\$${formatAmt(usdBalance!)}'; + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset balance methods. This getter uses outdated balance information.') + String get getFormattedUsdBalance => amountToFormattedUsd(balance); String get typeName => getCoinTypeName(type); String get typeNameWithTestnet => typeName + (isTestCoin ? ' (TESTNET)' : ''); @@ -233,6 +153,7 @@ class Coin { bool get isTxMemoSupported => type == CoinType.iris || type == CoinType.cosmos; + @Deprecated('TODO: Adapt SDK to cater for this use case and remove this method.') String? get defaultAddress { switch (enabledType) { case WalletType.trezor: @@ -249,7 +170,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; @@ -258,6 +178,7 @@ class Coin { return false; } + @Deprecated('TODO: Adapt SDK to cater for this use case and remove this method.') String? get _defaultTrezorAddress { if (enabledType != WalletType.trezor) return null; if (accounts == null) return null; @@ -267,6 +188,7 @@ class Coin { return accounts!.first.addresses.first.address; } + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset address management instead. This value is not updated after initial load and may be inaccurate.') List nonEmptyHdAddresses() { final List? allAddresses = accounts?.first.addresses; if (allAddresses == null) return []; @@ -276,11 +198,13 @@ class Coin { return nonEmpty; } + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset derivation methods instead. This method does not work for multiple addresses per coin.') String? getDerivationPath(String address) { final HdAddress? hdAddress = getHdAddress(address); return hdAddress?.derivationPath; } + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset address management instead. This method does not work for multiple addresses per coin.') HdAddress? getHdAddress(String? address) { if (address == null) return null; if (enabledType == WalletType.iguana) return null; @@ -290,7 +214,8 @@ class Coin { if (address.isEmpty) return null; return addresses.firstWhereOrNull( - (HdAddress hdAddress) => hdAddress.address == address); + (HdAddress hdAddress) => hdAddress.address == address, + ); } static bool checkSegwitByAbbr(String abbr) => abbr.contains('-segwit'); @@ -301,8 +226,8 @@ class Coin { return 'Coin($abbr);'; } + @Deprecated('$_urgentDeprecationNotice Use the SDK\'s Asset state management instead.') void reset() { - balance = 0; enabledType = null; accounts = null; state = CoinState.inactive; @@ -312,18 +237,17 @@ class Coin { return Coin( type: type, abbr: abbr, + id: assetId, name: name, explorerUrl: explorerUrl, explorerTxUrl: explorerTxUrl, explorerAddressUrl: explorerAddressUrl, protocolType: protocolType, isTestCoin: isTestCoin, + isCustomCoin: isCustomCoin, + logoImageUrl: logoImageUrl, coingeckoId: coingeckoId, fallbackSwapContract: fallbackSwapContract, - electrum: electrum, - nodes: nodes, - rpcUrls: rpcUrls, - bchdUrls: bchdUrls, priority: priority, state: state, swapContractAddress: swapContractAddress, @@ -331,7 +255,6 @@ class Coin { mode: mode, usdPrice: usdPrice, parentCoin: parentCoin, - trezorCoin: trezorCoin, derivationPath: derivationPath, accounts: accounts, coinpaprikaId: coinpaprikaId, @@ -339,210 +262,89 @@ 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(); - } - 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); -} + AssetId get assetId => AssetId( + id: abbr, + name: name, + symbol: AssetSymbol( + assetConfigId: abbr, + coinGeckoId: coingeckoId, + coinPaprikaId: coinpaprikaId, + ), + chainId: AssetChainId(chainId: 0), + derivationPath: derivationPath ?? '', + subClass: type.toCoinSubClass(), + ); -CoinType? getCoinType(String? jsonType, String coinAbbr) { - // anchor: protocols support - for (CoinType value in CoinType.values) { - switch (value) { - case CoinType.utxo: - if (jsonType == 'UTXO') { - return value; - } else { - continue; - } - case CoinType.smartChain: - if (jsonType == 'Smart Chain') { - return value; - } else { - continue; - } - case CoinType.erc20: - if (jsonType == 'ERC-20') { - return value; - } else { - continue; - } - case CoinType.bep20: - if (jsonType == 'BEP-20') { - return value; - } else { - continue; - } - case CoinType.qrc20: - if (jsonType == 'QRC-20') { - return value; - } else { - continue; - } - case CoinType.ftm20: - if (jsonType == 'FTM-20') { - return value; - } else { - continue; - } - case CoinType.arb20: - if (jsonType == 'Arbitrum') { - return value; - } else { - continue; - } - case CoinType.etc: - if (jsonType == 'Ethereum Classic') { - return value; - } else { - continue; - } - case CoinType.avx20: - if (jsonType == 'AVX-20') { - return value; - } else { - continue; - } - case CoinType.mvr20: - if (jsonType == 'Moonriver') { - return value; - } else { - continue; - } - case CoinType.hco20: - if (jsonType == 'HecoChain') { - return value; - } else { - continue; - } - case CoinType.plg20: - if (jsonType == 'Matic') { - return value; - } else { - continue; - } - case CoinType.sbch: - if (jsonType == 'SmartBCH') { - return value; - } else { - continue; - } - case CoinType.ubiq: - if (jsonType == 'Ubiq') { - return value; - } else { - continue; - } - case CoinType.hrc20: - if (jsonType == 'HRC-20') { - return value; - } else { - continue; - } - case CoinType.krc20: - if (jsonType == 'KRC-20') { - return value; - } else { - continue; - } - case CoinType.cosmos: - if (jsonType == 'TENDERMINT' && coinAbbr != 'IRIS') { - return value; - } else { - continue; - } - case CoinType.iris: - if (jsonType == 'TENDERMINTTOKEN' || coinAbbr == 'IRIS') { - return value; - } else { - continue; - } - case CoinType.slp: - if (jsonType == 'SLP') { - return value; - } else { - continue; - } - } + Coin copyWith({ + CoinType? type, + String? abbr, + AssetId? id, + String? name, + String? explorerUrl, + String? explorerTxUrl, + String? explorerAddressUrl, + String? protocolType, + String? logoImageUrl, + 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, + bool? isCustomCoin, + }) { + return Coin( + type: type ?? this.type, + abbr: abbr ?? this.abbr, + id: id ?? this.id, + name: name ?? this.name, + logoImageUrl: logoImageUrl ?? this.logoImageUrl, + 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, + isCustomCoin: isCustomCoin ?? this.isCustomCoin, + ) + ..address = address ?? this.address + ..enabledType = enabledType ?? this.enabledType + ..sendableBalance = sendableBalance ?? this.sendableBalance; } - return null; } -CoinMode _getCoinMode(Map json) { - if ((json['abbr'] as String).contains('-segwit')) { - return CoinMode.segwit; - } - return CoinMode.standard; +extension LegacyCoinToSdkAsset on Coin { + Asset toSdkAsset(KomodoDefiSdk sdk) => getSdkAsset(sdk, abbr); } class ProtocolData { @@ -570,8 +372,9 @@ class ProtocolData { class CoinNode { const CoinNode({required this.url, required this.guiAuth}); static CoinNode fromJson(Map json) => CoinNode( - url: json['url'], - guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false); + url: json['url'], + guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false, + ); final bool guiAuth; final String url; @@ -591,3 +394,11 @@ enum CoinState { suspended, hidden, } + +extension CoinListExtension on List { + Map toMap() { + return Map.fromEntries(map((coin) => MapEntry(coin.abbr, coin))); + } +} + +const String _urgentDeprecationNotice ='(URGENT) This must be fixed before the next release.'; \ No newline at end of file diff --git a/lib/model/coin_utils.dart b/lib/model/coin_utils.dart index a4e53e9b17..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'; @@ -27,6 +26,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); @@ -35,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/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/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/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/model/hd_account/hd_account.dart b/lib/model/hd_account/hd_account.dart index fdc4c2df94..23de67af28 100644 --- a/lib/model/hd_account/hd_account.dart +++ b/lib/model/hd_account/hd_account.dart @@ -17,6 +17,16 @@ class HdAccount { ); } + Map toJson() { + return { + 'account_index': accountIndex, + 'derivation_path': derivationPath, + 'total_balance': totalBalance?.toJson(), + 'addresses': + addresses.map((HdAddress address) => address.toJson()).toList(), + }; + } + final int accountIndex; final String? derivationPath; final HdBalance? totalBalance; @@ -40,6 +50,15 @@ class HdAddress { ); } + Map toJson() { + return { + 'address': address, + 'derivation_path': derivationPath, + 'chain': chain, + 'balance': balance.toJson(), + }; + } + final String address; final String derivationPath; final String chain; @@ -80,6 +99,13 @@ class HdBalance { ); } + Map toJson() { + return { + 'spendable': spendable, + 'unspendable': unspendable, + }; + } + double spendable; double unspendable; } diff --git a/lib/model/kdf_auth_metadata_extension.dart b/lib/model/kdf_auth_metadata_extension.dart new file mode 100644 index 0000000000..8cddf63379 --- /dev/null +++ b/lib/model/kdf_auth_metadata_extension.dart @@ -0,0 +1,43 @@ +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:web_dex/model/wallet.dart'; + +extension KdfAuthMetadataExtension 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', hasBackup); + } + + Future setWalletType(WalletType type) async { + await auth.setOrRemoveActiveUserKeyValue('type', type.name); + } +} 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/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/model/swap.dart b/lib/model/swap.dart index e37ff1f2c9..d600a5c0f5 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..a43abbc337 100644 --- a/lib/model/wallet.dart +++ b/lib/model/wallet.dart @@ -1,3 +1,8 @@ +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.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 +13,57 @@ 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.hdwallet, + 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 isHW => + config.type != WalletType.iguana && config.type != WalletType.hdwallet; + bool get isLegacyWallet => config.isLegacyWallet; + Future getLegacySeed(String password) async => await EncryptionTool().decryptData(password, config.seedPhrase) ?? ''; Map toJson() => { @@ -40,23 +84,34 @@ 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 +122,6 @@ class WalletConfig { }; } - String seedPhrase; - String? pubKey; - List activatedCoins; - bool hasBackup; - WalletType type; - WalletConfig copy() { return WalletConfig( activatedCoins: [...activatedCoins], @@ -86,6 +135,7 @@ class WalletConfig { enum WalletType { iguana, + hdwallet, trezor, metamask, keplr; @@ -98,8 +148,47 @@ enum WalletType { return WalletType.metamask; case 'keplr': return WalletType.keplr; + case 'hdwallet': + return WalletType.hdwallet; default: return WalletType.iguana; } } } + +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); +} 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/app_router_delegate.dart b/lib/router/navigators/app_router_delegate.dart index 2b18b3bf01..90f70b0f4d 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'; @@ -25,27 +30,46 @@ 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: () => runDropdownDismiss(context), + child: MainLayout( + key: ValueKey('${routingState.selectedMenu}'), + ), + ); + }, + ), + ); + + 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), ); } + 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/navigators/page_content/page_content_router_delegate.dart b/lib/router/navigators/page_content/page_content_router_delegate.dart index 8e12320a90..5f1fa6a5c9 100644 --- a/lib/router/navigators/page_content/page_content_router_delegate.dart +++ b/lib/router/navigators/page_content/page_content_router_delegate.dart @@ -32,7 +32,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/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/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/file_loader/file_loader.dart b/lib/services/file_loader/file_loader.dart index c2542ae6e2..6e3fb0fa23 100644 --- a/lib/services/file_loader/file_loader.dart +++ b/lib/services/file_loader/file_loader.dart @@ -1,15 +1,21 @@ 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, 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 +38,6 @@ enum LoadFileType { return 'application/zip'; case LoadFileType.text: return 'text/plain'; - default: - return '*/*'; } } 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_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_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 bc99d23bcc..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, @@ -16,10 +18,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; } } @@ -27,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')); - if (compressedData == null) return; + final response = web.Response(blob); + final compressedResponse = web.Response( + response.body!.pipeThrough( + web.CompressionStream('gzip') as web.ReadableWritablePair, + ), + ); - final anchor = AnchorElement(); - anchor.href = 'data:application/zip;base64,$compressedData'; - anchor.download = '$fileName.zip'; - anchor.click(); + final compressedBlob = await compressedResponse.blob().toDart; + final url = web.URL.createObjectURL(compressedBlob); + + 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/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/services/initializer/app_bootstrapper.dart b/lib/services/initializer/app_bootstrapper.dart index bb405a0216..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,22 +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), - KomodoCoinUpdater.ensureInitialized(appFolder) - .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 541866eeb9..c2afae8979 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'; @@ -28,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'); @@ -60,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, @@ -87,7 +77,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/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/ui/custom_numeric_text_form_field.dart b/lib/shared/ui/custom_numeric_text_form_field.dart index e921a218a2..468e75cf2b 100644 --- a/lib/shared/ui/custom_numeric_text_form_field.dart +++ b/lib/shared/ui/custom_numeric_text_form_field.dart @@ -29,7 +29,7 @@ class CustomNumericTextFormField extends StatelessWidget { final String filteringRegExp; final int? errorMaxLines; final InputValidationMode validationMode; - final void Function(String)? onChanged; + final void Function(String?)? onChanged; final FocusNode? focusNode; final void Function(FocusNode)? onFocus; diff --git a/lib/shared/utils/debug_utils.dart b/lib/shared/utils/debug_utils.dart index 6625728a6b..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,16 +20,23 @@ Future initDebugData(AuthBloc authBloc) async { return; } - final Wallet? debugWallet = await _createDebugWallet( - newWalletJson, - hasBackup: true, - ); - if (debugWallet == null) { - return; - } if (newWalletJson['automateLogin'] == true) { + final Wallet? debugWallet = await _createDebugWallet( + walletsRepository, + newWalletJson, + hasBackup: true, + ); + if (debugWallet == null) { + return; + } + authBloc.add( - AuthReLogInEvent(seed: newWalletJson['seed'], wallet: debugWallet)); + AuthRestoreRequested( + seed: newWalletJson['seed'], + wallet: debugWallet, + password: newWalletJson["password"], + ), + ); } } catch (e) { return; @@ -38,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..3d8a78ef71 100644 --- a/lib/shared/utils/extensions/async_extensions.dart +++ b/lib/shared/utils/extensions/async_extensions.dart @@ -6,18 +6,3 @@ extension WaitAllFutures on List> { /// See Dart docs on error handling in lists of futures: [Future.wait] 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/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/formatters.dart b/lib/shared/utils/formatters.dart index 26ce9e4212..f995dd540f 100644 --- a/lib/shared/utils/formatters.dart +++ b/lib/shared/utils/formatters.dart @@ -1,33 +1,36 @@ 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'; import 'package:rational/rational.dart'; import 'package:web_dex/shared/constants.dart'; +import 'package:komodo_defi_types/komodo_defi_types.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, required this.seconds, 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 +38,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 +53,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 +130,6 @@ class DecimalTextInputFormatter extends TextInputFormatter { return TextEditingValue( text: truncated, selection: newSelection, - composing: TextRange.empty, ); } } @@ -171,8 +175,13 @@ 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 +250,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 +275,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 +305,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, ); } @@ -326,11 +337,26 @@ void formatAmountInput(TextEditingController controller, Rational? value) { /// print(result2); // Output: "12...890" /// ``` /// unit tests: [testTruncateHash] -String truncateMiddleSymbols(String text, - [int? startSymbolsCount, int endCount = 7]) { - int startCount = startSymbolsCount ?? (text.startsWith('0x') ? 6 : 4); +String truncateMiddleSymbols( + String text, [ + 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); 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..1c9f7b1df2 100644 --- a/lib/shared/utils/password.dart +++ b/lib/shared/utils/password.dart @@ -3,7 +3,7 @@ import 'dart:math'; String generatePassword() { final List passwords = []; - final rng = Random(); + final rng = Random.secure(); const String lowerCase = 'abcdefghijklmnopqrstuvwxyz'; const String upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; @@ -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++) { @@ -55,7 +57,8 @@ bool validateRPCPassword(String src) { // Password must contain one digit, one lowercase letter, one uppercase letter, // one special character and its length must be between 8 and 32 characters final RegExp exp = RegExp( - r'^(?:(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])).{8,32}$'); + r'^(?:(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])).{8,32}$', + ); if (!src.contains(exp)) return false; // Password can't contain same character three time in a row, diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 98574b37f4..92ef91605b 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -1,63 +1,42 @@ import 'dart:async'; -import 'dart:convert'; 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'; import 'package:flutter/services.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: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'; 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 { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - duration: const Duration(seconds: 2), - content: Text( - LocaleKeys.clipBoard.tr(), - style: themeData.textTheme.bodyLarge!.copyWith( + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + LocaleKeys.clipBoard.tr(), + style: themeData.textTheme.bodyLarge!.copyWith( color: themeData.brightness == Brightness.dark ? themeData.hintColor - : themeData.primaryColor), + : themeData.primaryColor, + ), + ), ), - )); + ); } catch (_) {} Clipboard.setData(ClipboardData(text: str)); @@ -85,8 +64,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); @@ -168,8 +151,6 @@ Map? rat2fract(Rational? rat, [bool toLog = true]) { } } -String generateSeed() => bip39.generateMnemonic(); - String getTxExplorerUrl(Coin coin, String txHash) { final String explorerUrl = coin.explorerUrl; final String explorerTxUrl = coin.explorerTxUrl; @@ -190,6 +171,7 @@ String getAddressExplorerUrl(Coin coin, String address) { return '$explorerUrl$explorerAddressUrl$address'; } +@Deprecated('Use the Protocol class\'s explorer URL methods') void viewHashOnExplorer(Coin coin, String address, HashExplorerType type) { late String url; switch (type) { @@ -200,10 +182,34 @@ void viewHashOnExplorer(Coin coin, String address, HashExplorerType type) { url = getTxExplorerUrl(coin, address); break; } - launchURL(url); + launchURLString(url); } -Future launchURL( +extension AssetExplorerUrls on Asset { + Uri? txExplorerUrl(String? txHash) { + return txHash == null ? null : protocol.explorerTxUrl(txHash); + } + + Uri? addressExplorerUrl(String? address) { + return address == null ? null : protocol.explorerAddressUrl(address); + } +} + +Future openUrl(Uri uri, {bool? inSeparateTab}) async { + if (!await canLaunchUrl(uri)) { + throw Exception('Could not launch $uri'); + } + await launchUrl( + uri, + mode: inSeparateTab == null + ? LaunchMode.platformDefault + : inSeparateTab == true + ? LaunchMode.externalApplication + : LaunchMode.inAppWebView, + ); +} + +Future launchURLString( String url, { bool? inSeparateTab, }) async { @@ -223,7 +229,7 @@ Future launchURL( } } -void log( +Future log( String message, { String? path, StackTrace? trace, @@ -235,11 +241,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 +271,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 +620,7 @@ String? assertString(dynamic value) { case double: return value.toString(); default: - return value; + return value as String?; } } @@ -618,9 +629,33 @@ int? assertInt(dynamic value) { switch (value.runtimeType) { case String: - return int.parse(value); + return int.parse(value as String); default: - return value; + 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: + try { + return double.parse(value.toString()); + } catch (e, s) { + log('Error converting to double: $e', trace: s, isError: true); + return double.nan; + } } } @@ -636,17 +671,12 @@ 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, } + +Asset getSdkAsset(KomodoDefiSdk sdk, String abbr) { + // ignore: deprecated_member_use + return sdk.assets.assetsFromTicker(abbr).single; +} 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/shared/utils/zip.dart b/lib/shared/utils/zip.dart new file mode 100644 index 0000000000..c5d8ba1a76 --- /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; +} 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_balance.dart b/lib/shared/widgets/coin_balance.dart index bf20fe7dc7..c722f322aa 100644 --- a/lib/shared/widgets/coin_balance.dart +++ b/lib/shared/widgets/coin_balance.dart @@ -1,28 +1,74 @@ -import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +// TODO! Integrate this widget directly to the SDK and make it subscribe to +// the balance changes of the coin. class CoinBalance extends StatelessWidget { - const CoinBalance({required this.coin}); + const CoinBalance({ + super.key, + required this.coin, + this.isVertical = false, + }); + final Coin coin; + final bool isVertical; @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - doubleToString(coin.balance), - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + final baseFont = Theme.of(context).textTheme.bodySmall; + final balanceStyle = baseFont?.copyWith( + fontWeight: FontWeight.w500, + ); + + final children = [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: AutoScrollText( + key: Key('coin-balance-asset-${coin.abbr.toLowerCase()}'), + text: doubleToString(coin.balance), + style: balanceStyle, + textAlign: TextAlign.right, + ), + ), + Text( + ' ${Coin.normalizeAbbr(coin.abbr)}', + style: balanceStyle, + ), + ], + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 100, ), - const SizedBox(height: 2), - CoinFiatBalance( - coin, - style: TextStyle(color: theme.custom.increaseColor), + child: Row( + // mainAxisSize: MainAxisSize.min, + children: [ + Text('(', style: balanceStyle), + CoinFiatBalance( + coin, + isAutoScrollEnabled: true, + ), + Text(')', style: balanceStyle), + ], ), - ], - ); + ), + ]; + + return isVertical + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: children, + ); } } 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/disclaimer/constants.dart b/lib/shared/widgets/disclaimer/constants.dart index 5cf2cf8767..9d1e41935f 100644 --- a/lib/shared/widgets/disclaimer/constants.dart +++ b/lib/shared/widgets/disclaimer/constants.dart @@ -1,5 +1,5 @@ const String disclaimerEulaTitle1 = - 'End-User License Agreement (EULA) of Komodo Wallet:\n\n'; + 'End User License Agreement (EULA) of Komodo Wallet:\n\n'; const String disclaimerEulaTitle2 = 'TERMS and CONDITIONS: (APPLICATION USER AGREEMENT)\n\n'; @@ -25,9 +25,9 @@ const String disclaimerEulaTitle18 = 'TERMINATION\n\n'; const String disclaimerEulaTitle19 = 'THIRD PARTY RIGHTS\n\n'; const String disclaimerEulaTitle20 = 'OUR LEGAL OBLIGATIONS\n\n'; const String disclaimerEulaParagraph1 = - "This End-User License Agreement ('EULA') is a legal agreement between you and Komodo Platform.\n\nThis EULA agreement governs your acquisition and use of our Komodo Wallet software ('Software', 'Web Application', 'Application' or 'App') directly from Komodo Platform or indirectly through a Komodo Platform authorized entity, reseller or distributor (a 'Distributor').\nPlease read this EULA agreement carefully before completing the installation process and using the Komodo Wallet software. It provides a license to use the Komodo Wallet software and contains warranty information and liability disclaimers.\nIf you register for the beta program of the Komodo Wallet software, this EULA agreement will also govern that trial. By clicking 'accept' or installing and/or using the Komodo Wallet software, you are confirming your acceptance of the Software and agreeing to become bound by the terms of this EULA agreement.\nIf you are entering into this EULA agreement on behalf of a company or other legal entity, you represent that you have the authority to bind such entity and its affiliates to these terms and conditions. If you do not have such authority or if you do not agree with the terms and conditions of this EULA agreement, do not install or use the Software, and you must not accept this EULA agreement.\nThis EULA agreement shall apply only to the Software supplied by Komodo Platform herewith regardless of whether other software is referred to or described herein. The terms also apply to any Komodo Platform updates, supplements, Internet-based services, and support services for the Software, unless other terms accompany those items on delivery. If so, those terms apply.\nLicense Grant\nKomodo Platform hereby grants you a personal, non-transferable, non-exclusive license to use the Komodo Wallet software on your devices in accordance with the terms of this EULA agreement.\n\nYou are permitted to load the Komodo Wallet software (for example a PC, laptop, mobile or tablet) under your control. You are responsible for ensuring your device meets the minimum security and resource requirements of the Komodo Wallet software.\nYou are not permitted to:\nEdit, alter, modify, adapt, translate or otherwise change the whole or any part of the Software nor permit the whole or any part of the Software to be combined with or become incorporated in any other software, nor decompile, disassemble or reverse engineer the Software or attempt to do any such things\nReproduce, copy, distribute, resell or otherwise use the Software for any commercial purpose\nUse the Software in any way which breaches any applicable local, national or international law\nuse the Software for any purpose that Komodo Platform considers is a breach of this EULA agreement\nIntellectual Property and Ownership\nKomodo Platform shall at all times retain ownership of the Software as originally downloaded by you and all subsequent downloads of the Software by you. The Software (and the copyright, and other intellectual property rights of whatever nature in the Software, including any modifications made thereto) are and shall remain the property of Komodo Platform.\n\nKomodo Platform reserves the right to grant licences to use the Software to third parties.\nTermination\nThis EULA agreement is effective from the date you first use the Software and shall continue until terminated. You may terminate it at any time upon written notice to Komodo Platform.\nIt will also terminate immediately if you fail to comply with any term of this EULA agreement. Upon such termination, the licenses granted by this EULA agreement will immediately terminate and you agree to stop all access and use of the Software. The provisions that by their nature continue and survive will survive any termination of this EULA agreement.\nGoverning Law\nThis EULA agreement, and any dispute arising out of or in connection with this EULA agreement, shall be governed by and construed in accordance with the laws of Vietnam.\n\nThis document was last updated on January 31st, 2020\n\n"; + "This End User License Agreement ('EULA') is a legal agreement between you and Komodo Platform.\n\nThis EULA agreement governs your acquisition and use of our Komodo Wallet software ('Software', 'Web Application', 'Application' or 'App') directly from Komodo Platform or indirectly through a Komodo Platform authorized entity, reseller or distributor (a 'Distributor').\nPlease read this EULA agreement carefully before completing the installation process and using the Komodo Wallet software. It provides a license to use the Komodo Wallet software and contains warranty information and liability disclaimers.\nIf you register for the beta program of the Komodo Wallet software, this EULA agreement will also govern that trial. By clicking 'accept' or installing and/or using the Komodo Wallet software, you are confirming your acceptance of the Software and agreeing to become bound by the terms of this EULA agreement.\nIf you are entering into this EULA agreement on behalf of a company or other legal entity, you represent that you have the authority to bind such entity and its affiliates to these terms and conditions. If you do not have such authority or if you do not agree with the terms and conditions of this EULA agreement, do not install or use the Software, and you must not accept this EULA agreement.\nThis EULA agreement shall apply only to the Software supplied by Komodo Platform herewith regardless of whether other software is referred to or described herein. The terms also apply to any Komodo Platform updates, supplements, internet based services, and support services for the Software, unless other terms accompany those items on delivery. If so, those terms apply.\nLicense Grant\nKomodo Platform hereby grants you a personal, non-transferable, non-exclusive license to use the Komodo Wallet software on your devices in accordance with the terms of this EULA agreement.\n\nYou are permitted to load the Komodo Wallet software (for example a PC, laptop, mobile or tablet) under your control. You are responsible for ensuring your device meets the minimum security and resource requirements of the Komodo Wallet software.\nYou are not permitted to:\nEdit, alter, modify, adapt, translate or otherwise change the whole or any part of the Software nor permit the whole or any part of the Software to be combined with or become incorporated in any other software, nor decompile, disassemble or reverse engineer the Software or attempt to do any such things\nReproduce, copy, distribute, resell or otherwise use the Software for any commercial purpose\nUse the Software in any way which breaches any applicable local, national or international law\nuse the Software for any purpose that Komodo Platform considers is a breach of this EULA agreement\nIntellectual Property and Ownership\nKomodo Platform shall at all times retain ownership of the Software as originally downloaded by you and all subsequent downloads of the Software by you. The Software (and the copyright, and other intellectual property rights of whatever nature in the Software, including any modifications made thereto) are and shall remain the property of Komodo Platform.\n\nKomodo Platform reserves the right to grant licences to use the Software to third parties.\nTermination\nThis EULA agreement is effective from the date you first use the Software and shall continue until terminated. You may terminate it at any time upon written notice to Komodo Platform.\nIt will also terminate immediately if you fail to comply with any term of this EULA agreement. Upon such termination, the licenses granted by this EULA agreement will immediately terminate and you agree to stop all access and use of the Software. The provisions that by their nature continue and survive will survive any termination of this EULA agreement.\nGoverning Law\nThis EULA agreement, and any dispute arising out of or in connection with this EULA agreement, shall be governed by and construed in accordance with the laws of Vietnam.\n\nThis document was last updated on January 31st, 2020\n\n"; const String disclaimerEulaParagraph2 = - "This disclaimer applies to the contents and services of the app Komodo Wallet and is valid for all users of the β€œApplication” ('Software', β€œWeb Application”, β€œApplication” or β€œApp”).\n\nThe Application is owned by Komodo Platform.\n\nWe reserve the right to amend the following Terms and Conditions (governing the use of the application Komodo Wallet”) at any time without prior notice and at our sole discretion. It is your responsibility to periodically check this Terms and Conditions for any updates to these Terms, which shall come into force once published.\nYour continued use of the application shall be deemed as acceptance of the following Terms. \nWe are a company incorporated in Vietnam and these Terms and Conditions are governed by and subject to the laws of Vietnam. \nIf You do not agree with these Terms and Conditions, You must not use or access this software.\n\n"; + "This disclaimer applies to the contents and services of the app Komodo Wallet and is valid for all users of the β€œApplication” ('Software', β€œWeb Application”, β€œApplication” or β€œApp”).\n\nThe Application is owned by Komodo Platform.\n\nWe reserve the right to amend the following Terms and Conditions (governing the use of the application Komodo Wallet”) at any time without prior notice and at our sole discretion. It is your responsibility to periodically check these Terms and Conditions for any updates to these Terms, which shall come into force once published.\nYour continued use of the application shall be deemed as acceptance of the following Terms. \nWe are a company incorporated in Vietnam and these Terms and Conditions are governed by and subject to the laws of Vietnam. \nIf You do not agree with these Terms and Conditions, You must not use or access this software.\n\n"; const String disclaimerEulaParagraph3 = 'By entering into this User (each subject accessing or using the site) Agreement (this writing) You declare that You are an individual over the age of majority (at least 18 or older) and have the capacity to enter into this User Agreement and accept to be legally bound by the terms and conditions of this User Agreement, as incorporated herein and amended from time to time. \n\n'; const String disclaimerEulaParagraph4 = @@ -37,28 +37,28 @@ const String disclaimerEulaParagraph5 = const String disclaimerEulaParagraph6 = 'If you create an account in the Web Application, you are responsible for maintaining the security of your account and you are fully responsible for all activities that occur under the account and any other actions taken in connection with it. We will not be liable for any acts or omissions by you, including any damages of any kind incurred as a result of such acts or omissions. \n\n Komodo Wallet is a non-custodial wallet implementation and thus Komodo Platform can not access nor restore your account in case of (data) loss.\n\n'; const String disclaimerEulaParagraph7 = - 'We are not responsible for seed-phrases residing in the Web Application. In no event shall we be held liable for any loss of any kind. It is your sole responsibility to maintain appropriate backups of your accounts and their seedphrases.\n\n'; + 'We are not responsible for seed phrases residing in the Web Application. In no event shall we be held liable for any loss of any kind. It is your sole responsibility to maintain appropriate backups of your accounts and their seed phrases.\n\n'; const String disclaimerEulaParagraph8 = 'You should not act, or refrain from acting solely on the basis of the content of this application. \nYour access to this application does not itself create an adviser-client relationship between You and us. \nThe content of this application does not constitute a solicitation or inducement to invest in any financial products or services offered by us. \nAny advice included in this application has been prepared without taking into account your objectives, financial situation or needs. You should consider our Risk Disclosure Notice before making any decision on whether to acquire the product described in that document.\n\n'; const String disclaimerEulaParagraph9 = 'We do not guarantee your continuous access to the application or that your access or use will be error-free. \nWe will not be liable in the event that the application is unavailable to You for any reason (for example, due to computer downtime ascribable to malfunctions, upgrades, server problems, precautionary or corrective maintenance activities or interruption in telecommunication supplies). \n\n'; const String disclaimerEulaParagraph10 = - 'Komodo Platform is the owner and/or authorized user of all trademarks, service marks, design marks, patents, copyrights, database rights and all other intellectual property appearing on or contained within the application, unless otherwise indicated. All information, text, material, graphics, software and advertisements on the application interface are copyright of Komodo Platform, its suppliers and licensors, unless otherwise expressly indicated by Komodo Platform. \nExcept as provided in the Terms, use of the application does not grant You any right, title, interest or license to any such intellectual property You may have access to on the application. \nWe own the rights, or have permission to use, the trademarks listed in our application. You are not authorised to use any of those trademarks without our written authorization – doing so would constitute a breach of our or another party’s intellectual property rights. \nAlternatively, we might authorise You to use the content in our application if You previously contact us and we agree in writing.\n\n'; + 'Komodo Platform is the owner and/or authorized user of all trademarks, service marks, design marks, patents, copyrights, database rights, and all other intellectual property appearing on or contained within the application, unless otherwise indicated. All information, text, material, graphics, software, and advertisements on the application interface are copyright of Komodo Platform, its suppliers and licensors, unless otherwise expressly indicated by Komodo Platform. \nExcept as provided in the Terms, use of the application does not grant You any right, title, interest, or license to any such intellectual property You may have access to on the application. \nWe own the rights, or have permission to use, the trademarks listed in our application. You are not authorised to use any of those trademarks without our written authorization – doing so would constitute a breach of our or another party’s intellectual property rights. \nAlternatively, we might authorise You to use the content in our application if You previously contact us and we agree in writing.\n\n'; const String disclaimerEulaParagraph11 = - "Komodo Platform cannot guarantee the safety or security of your computer systems. We do not accept liability for any loss or corruption of electronically stored data or any damage to any computer system occurred in connection with the use of the application or of the user content.\nKomodo Platform makes no representation or warranty of any kind, express or implied, as to the operation of the application or the user content. You expressly agree that your use of the application is entirely at your sole risk.\nYou agree that the content provided in the application and the user content do not constitute financial product, legal or taxation advice, and You agree on not representing the user content or the application as such.\nTo the extent permitted by current legislation, the application is provided on an β€œas is, as available” basis.\n\nKomodo Platform expressly disclaims all responsibility for any loss, injury, claim, liability, or damage, or any indirect, incidental, special or consequential damages or loss of profits whatsoever resulting from, arising out of or in any way related to: \n(a) any errors in or omissions of the application and/or the user content, including but not limited to technical inaccuracies and typographical errors; \n(b) any third party website, application or content directly or indirectly accessed through links in the application, including but not limited to any errors or omissions; \n(c) the unavailability of the application or any portion of it; \n(d) your use of the application;\n(e) your use of any equipment or software in connection with the application. \nAny Services offered in connection with the Platform are provided on an 'as is' basis, without any representation or warranty, whether express, implied or statutory. To the maximum extent permitted by applicable law, we specifically disclaim any implied warranties of title, merchantability, suitability for a particular purpose and/or non-infringement. We do not make any representations or warranties that use of the Platform will be continuous, uninterrupted, timely, or error-free.\nWe make no warranty that any Platform will be free from viruses, malware, or other related harmful material and that your ability to access any Platform will be uninterrupted. Any defects or malfunction in the product should be directed to the third party offering the Platform, not to Komodo. \nWe will not be responsible or liable to You for any loss of any kind, from action taken, or taken in reliance on the material or information contained in or through the Platform.\nThis is experimental and unfinished software. Use at your own risk. No warranty for any kind of damage. By using this application you agree to this terms and conditions.\n\n"; + "Komodo Platform cannot guarantee the safety or security of your computer systems. We do not accept liability for any loss or corruption of electronically stored data or any damage to any computer system which occurrs in connection with the use of the application or user content.\nKomodo Platform makes no representation or warranty of any kind, express or implied, as to the operation of the application or the user content. You expressly agree that your use of the application is entirely at your sole risk.\nYou agree that the content provided in the application and the user content do not constitute financial product, legal, or taxation advice, and You agree on not representing the user content or the application as such.\nTo the extent permitted by current legislation, the application is provided on an β€œas is, as available” basis.\n\nKomodo Platform expressly disclaims all responsibility for any loss, injury, claim, liability, or damage, or any indirect, incidental, special, or consequential damages or loss of profits whatsoever resulting from, arising out of or in any way related to: \n(a) any errors in or omissions of the application and/or the user content, including but not limited to technical inaccuracies and typographical errors; \n(b) any third party website, application or content directly or indirectly accessed through links in the application, including but not limited to any errors or omissions; \n(c) the unavailability of the application or any portion of it; \n(d) your use of the application;\n(e) your use of any equipment or software in connection with the application. \nAny Services offered in connection with the Platform are provided on an 'as is' basis, without any representation or warranty, whether express, implied or statutory. To the maximum extent permitted by applicable law, we specifically disclaim any implied warranties of title, merchantability, suitability for a particular purpose and/or non-infringement. We do not make any representations or warranties that use of the Platform will be continuous, uninterrupted, timely, or error-free.\nWe make no warranty that any Platform will be free from viruses, malware, or other related harmful material and that your ability to access any Platform will be uninterrupted. Any defects or malfunction in the product should be directed to the third party offering the Platform, not to Komodo. \nWe will not be responsible or liable to You for any loss of any kind, from action taken, or taken in reliance on the material or information contained in or through the Platform.\nThis is experimental and unfinished software. Use at your own risk. No warranty for any kind of damage. By using this application you agree to this terms and conditions.\n\n"; const String disclaimerEulaParagraph12 = 'When accessing or using the Services, You agree that You are solely responsible for your conduct while accessing and using our Services. Without limiting the generality of the foregoing, You agree that You will not:\n(a) Use the Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying the Services, or that could damage, disable, overburden or impair the functioning of our Services in any manner;\n(b) Use the Services to pay for, support or otherwise engage in any illegal activities, including, but not limited to illegal gambling, fraud, money laundering, or terrorist activities;\n(c) Use any robot, spider, crawler, scraper or other automated means or interface not provided by us to access our Services or to extract data;\n(d) Use or attempt to use another user’s Wallet or credentials without authorization;\n(e) Attempt to circumvent any content filtering techniques we employ, or attempt to access any service or area of our Services that You are not authorized to access;\n(f) Introduce to the Services any virus, Trojan, worms, logic bombs or other harmful material;\n(g) Develop any third-party applications that interact with our Services without our prior written consent;\n(h) Provide false, inaccurate, or misleading information; \n(i) Encourage or induce any other person to engage in any of the activities prohibited under this Section.\n\n\n'; const String disclaimerEulaParagraph13 = 'You agree and understand that there are risks associated with utilizing Services involving Virtual Currencies including, but not limited to, the risk of failure of hardware, software and internet connections, the risk of malicious software introduction, and the risk that third parties may obtain unauthorized access to information stored within your Wallet, including but not limited to your public and private keys. You agree and understand that Komodo Platform will not be responsible for any communication failures, disruptions, errors, distortions or delays You may experience when using the Services, however caused.\nYou accept and acknowledge that there are risks associated with utilizing any virtual currency network, including, but not limited to, the risk of unknown vulnerabilities in or unanticipated changes to the network protocol. You acknowledge and accept that Komodo Platform has no control over any cryptocurrency network and will not be responsible for any harm occurring as a result of such risks, including, but not limited to, the inability to reverse a transaction, and any losses in connection therewith due to erroneous or fraudulent actions.\nThe risk of loss in using Services involving Virtual Currencies may be substantial and losses may occur over a short period of time. In addition, price and liquidity are subject to significant fluctuations that may be unpredictable.\nVirtual Currencies are not legal tender and are not backed by any sovereign government. In addition, the legislative and regulatory landscape around Virtual Currencies is constantly changing and may affect your ability to use, transfer, or exchange Virtual Currencies.\nCFDs are complex instruments and come with a high risk of losing money rapidly due to leverage. 80.6% of retail investor accounts lose money when trading CFDs with this provider. You should consider whether You understand how CFDs work and whether You can afford to take the high risk of losing your money.\n\n'; const String disclaimerEulaParagraph14 = - 'You agree to indemnify, defend and hold harmless Komodo Platform, its officers, directors, employees, agents, licensors, suppliers and any third party information providers to the application from and against all losses, expenses, damages and costs, including reasonable lawyer fees, resulting from any violation of the Terms by You.\nYou also agree to indemnify Komodo Platform against any claims that information or material which You have submitted to Komodo Platform is in violation of any law or in breach of any third party rights (including, but not limited to, claims in respect of defamation, invasion of privacy, breach of confidence, infringement of copyright or infringement of any other intellectual property right).\n\n'; + 'You agree to indemnify, defend, and hold harmless Komodo Platform, its officers, directors, employees, agents, licensors, suppliers, and any third party information providers to the application from and against all losses, expenses, damages and costs, including reasonable lawyer fees, resulting from any violation of the Terms by You.\nYou also agree to indemnify Komodo Platform against any claims that information or material which You have submitted to Komodo Platform is in violation of any law or in breach of any third party rights (including, but not limited to, claims in respect of defamation, invasion of privacy, breach of confidence, infringement of copyright or infringement of any other intellectual property right).\n\n'; const String disclaimerEulaParagraph15 = - 'In order to be completed, any Virtual Currency transaction created with the Komodo Platform must be confirmed and recorded in the Virtual Currency ledger associated with the relevant Virtual Currency network. Such networks are decentralized, peer-to-peer networks supported by independent third parties, which are not owned, controlled or operated by Komodo Platform.\nKomodo Platform has no control over any Virtual Currency network and therefore cannot and does not ensure that any transaction details You submit via our Services will be confirmed on the relevant Virtual Currency network. You agree and understand that the transaction details You submit via our Services may not be completed, or may be substantially delayed, by the Virtual Currency network used to process the transaction. We do not guarantee that the Wallet can transfer title or right in any Virtual Currency or make any warranties whatsoever with regard to title.\nOnce transaction details have been submitted to a Virtual Currency network, we cannot assist You to cancel or otherwise modify your transaction or transaction details. Komodo Platform has no control over any Virtual Currency network and does not have the ability to facilitate any cancellation or modification requests.\nIn the event of a Fork, Komodo Platform may not be able to support activity related to your Virtual Currency. You agree and understand that, in the event of a Fork, the transactions may not be completed, completed partially, incorrectly completed, or substantially delayed. Komodo Platform is not responsible for any loss incurred by You caused in whole or in part, directly or indirectly, by a Fork.\nIn no event shall Komodo Platform, its affiliates and service providers, or any of their respective officers, directors, agents, employees or representatives, be liable for any lost profits or any special, incidental, indirect, intangible, or consequential damages, whether based on contract, tort, negligence, strict liability, or otherwise, arising out of or in connection with authorized or unauthorized use of the services, or this agreement, even if an authorized representative of Komodo Platform has been advised of, has known of, or should have known of the possibility of such damages. \nFor example (and without limiting the scope of the preceding sentence), You may not recover for lost profits, lost business opportunities, or other types of special, incidental, indirect, intangible, or consequential damages. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so the above limitation may not apply to You. \nWe will not be responsible or liable to You for any loss and take no responsibility for damages or claims arising in whole or in part, directly or indirectly from: (a) user error such as forgotten passwords, incorrectly constructed transactions, or mistyped Virtual Currency addresses; (b) server failure or data loss; (c) corrupted or otherwise non-performing Wallets or Wallet files; (d) unauthorized access to applications; (e) any unauthorized activities, including without limitation the use of hacking, viruses, phishing, brute forcing or other means of attack against the Services.\n\n'; + 'In order to be completed, any Virtual Currency transaction created with the Komodo Platform must be confirmed and recorded in the Virtual Currency ledger associated with the relevant Virtual Currency network. Such networks are decentralized, peer-to-peer networks supported by independent third parties, which are not owned, controlled or operated by Komodo Platform.\nKomodo Platform has no control over any Virtual Currency network and therefore cannot and does not ensure that any transaction details You submit via our Services will be confirmed on the relevant Virtual Currency network. You agree and understand that the transaction details You submit via our Services may not be completed, or may be substantially delayed, by the Virtual Currency network used to process the transaction. We do not guarantee that the Wallet can transfer title or rights in any Virtual Currency or make any warranties whatsoever with regard to title.\nOnce transaction details have been submitted to a Virtual Currency network, we cannot assist You to cancel or otherwise modify your transaction or transaction details. Komodo Platform has no control over any Virtual Currency network and does not have the ability to facilitate any cancellation or modification requests.\nIn the event of a Fork, Komodo Platform may not be able to support activity related to your Virtual Currency. You agree and understand that, in the event of a Fork, the transactions may not be completed, completed partially, incorrectly completed, or substantially delayed. Komodo Platform is not responsible for any loss incurred by You caused in whole or in part, directly or indirectly, by a Fork.\nIn no event shall Komodo Platform, its affiliates and service providers, or any of their respective officers, directors, agents, employees, or representatives, be liable for any lost profits or any special, incidental, indirect, intangible, or consequential damages, whether based on contract, tort, negligence, strict liability, or otherwise, arising out of or in connection with authorized or unauthorized use of the services, or this agreement, even if an authorized representative of Komodo Platform has been advised of, has known of, or should have known of the possibility of such damages. \nFor example (and without limiting the scope of the preceding sentence), You may not recover for lost profits, lost business opportunities, or other types of special, incidental, indirect, intangible, or consequential damages. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so the above limitation may not apply to You. \nWe will not be responsible or liable to You for any loss and take no responsibility for damages or claims arising in whole or in part, directly or indirectly from: (a) user error such as forgotten passwords, incorrectly constructed transactions, or mistyped Virtual Currency addresses; (b) server failure or data loss; (c) corrupted or otherwise non-performing Wallets or Wallet files; (d) unauthorized access to applications; (e) any unauthorized activities, including without limitation the use of hacking, viruses, phishing, brute forcing, or any other means of attack against the Services.\n\n'; const String disclaimerEulaParagraph16 = - 'For the avoidance of doubt, Komodo Platform does not provide investment, tax or legal advice, nor does Komodo Platform broker trades on your behalf. All Komodo Platform trades are executed automatically, based on the parameters of your order instructions and in accordance with posted Trade execution procedures, and You are solely responsible for determining whether any investment, investment strategy or related transaction is appropriate for You based on your personal investment objectives, financial circumstances and risk tolerance. You should consult your legal or tax professional regarding your specific situation.Neither Komodo nor its owners, members, officers, directors, partners, consultants, nor anyone involved in the publication of this application, is a registered investment adviser or broker-dealer or associated person with a registered investment adviser or broker-dealer and none of the foregoing make any recommendation that the purchase or sale of crypto-assets or securities of any company profiled in the web Application is suitable or advisable for any person or that an investment or transaction in such crypto-assets or securities will be profitable. The information contained in the web Application is not intended to be, and shall not constitute, an offer to sell or the solicitation of any offer to buy any crypto-asset or security. The information presented in the web Application is provided for informational purposes only and is not to be treated as advice or a recommendation to make any specific investment or transaction. Please, consult with a qualified professional before making any decisions.The opinions and analysis included in this applications are based on information from sources deemed to be reliable and are provided β€œas is” in good faith. Komodo makes no representation or warranty, expressed, implied, or statutory, as to the accuracy or completeness of such information, which may be subject to change without notice. Komodo shall not be liable for any errors or any actions taken in relation to the above. Statements of opinion and belief are those of the authors and/or editors who contribute to this application, and are based solely upon the information possessed by such authors and/or editors. No inference should be drawn that Komodo or such authors or editors have any special or greater knowledge about the crypto-assets or companies profiled or any particular expertise in the industries or markets in which the profiled crypto-assets and companies operate and compete.Information on this application is obtained from sources deemed to be reliable; however, Komodo takes no responsibility for verifying the accuracy of such information and makes no representation that such information is accurate or complete. Certain statements included in this application may be forward-looking statements based on current expectations. Komodo makes no representation and provides no assurance or guarantee that such forward-looking statements will prove to be accurate.Persons using the Komodo application are urged to consult with a qualified professional with respect to an investment or transaction in any crypto-asset or company profiled herein. Additionally, persons using this application expressly represent that the content in this application is not and will not be a consideration in such persons’ investment or transaction decisions. Traders should verify independently information provided in the Komodo application by completing their own due diligence on any crypto-asset or company in which they are contemplating an investment or transaction of any kind and review a complete information package on that crypto-asset or company, which should include, but not be limited to, related blog updates and press releases.Past performance of profiled crypto-assets and securities is not indicative of future results. Crypto-assets and companies profiled on this site may lack an active trading market and invest in a crypto-asset or security that lacks an active trading market or trade on certain media, platforms and markets are deemed highly speculative and carry a high degree of risk. Anyone holding such crypto-assets and securities should be financially able and prepared to bear the risk of loss and the actual loss of his or her entire trade. The information in this application is not designed to be used as a basis for an investment decision. Persons using the Komodo application should confirm to their own satisfaction the veracity of any information prior to entering into any investment or making any transaction. The decision to buy or sell any crypto-asset or security that may be featured by Komodo is done purely and entirely at the reader’s own risk. As a reader and user of this application, You agree that under no circumstances will You seek to hold liable owners, members, officers, directors, partners, consultants or other persons involved in the publication of this application for any losses incurred by the use of information contained in this applicationKomodo and its contractors and affiliates may profit in the event the crypto-assets and securities increase or decrease in value. Such crypto-assets and securities may be bought or sold from time to time, even after Komodo has distributed positive information regarding the crypto-assets and companies. Komodo has no obligation to inform readers of its trading activities or the trading activities of any of its owners, members, officers, directors, contractors and affiliates and/or any companies affiliated with BC Relations’ owners, members, officers, directors, contractors and affiliates.Komodo and its affiliates may from time to time enter into agreements to purchase crypto-assets or securities to provide a method to reach their goals.\n\n'; + 'For the avoidance of doubt, Komodo Platform does not provide investment, tax, or legal advice, nor does Komodo Platform broker trades on your behalf. All Komodo Platform trades are executed automatically, based on the parameters of your order instructions and in accordance with posted Trade execution procedures, and You are solely responsible for determining whether any investment, investment strategy or related transaction is appropriate for You based on your personal investment objectives, financial circumstances and risk tolerance. You should consult your legal or tax professional regarding your specific situation. Neither Komodo nor its owners, members, officers, directors, partners, consultants, nor anyone involved in the publication of this application, is a registered investment adviser or broker-dealer or associated person with a registered investment adviser or broker-dealer and none of the foregoing make any recommendation that the purchase or sale of crypto-assets or securities of any company profiled in the web Application is suitable or advisable for any person or that an investment or transaction in such crypto-assets or securities will be profitable. The information contained in the web Application is not intended to be, and shall not constitute, an offer to sell or the solicitation of any offer to buy any crypto-asset or security. The information presented in the web Application is provided for informational purposes only and is not to be treated as advice or a recommendation to make any specific investment or transaction. Please consult with a qualified professional before making any decisions.The opinions and analysis included in this applications are based on information from sources deemed to be reliable and are provided β€œas is” in good faith. Komodo makes no representation or warranty, expressed, implied, or statutory, as to the accuracy or completeness of such information, which may be subject to change without notice. Komodo shall not be liable for any errors or any actions taken in relation to the above. Statements of opinion and belief are those of the authors and/or editors who contribute to this application, and are based solely upon the information possessed by such authors and/or editors. No inference should be drawn that Komodo or such authors or editors have any special or greater knowledge about the crypto-assets or companies profiled or any particular expertise in the industries or markets in which the profiled crypto-assets and companies operate and compete.Information on this application is obtained from sources deemed to be reliable; however, Komodo takes no responsibility for verifying the accuracy of such information and makes no representation that such information is accurate or complete. Certain statements included in this application may be forward-looking statements based on current expectations. Komodo makes no representation and provides no assurance or guarantee that such forward-looking statements will prove to be accurate. Persons using the Komodo application are urged to consult with a qualified professional with respect to an investment or transaction in any crypto-asset or company profiled herein. Additionally, persons using this application expressly represent that the content in this application is not and will not be a consideration in such persons’ investment or transaction decisions. Traders should verify independently information provided in the Komodo application by completing their own due diligence on any crypto-asset or company in which they are contemplating an investment or transaction of any kind and review a complete information package on that crypto-asset or company, which should include, but not be limited to, related blog updates and press releases. Past performance of profiled crypto-assets and securities is not indicative of future results. Crypto-assets and companies profiled on this site may lack an active trading market and invest in a crypto-asset or security that lacks an active trading market or trade on certain media, platforms and markets are deemed highly speculative and carry a high degree of risk. Anyone holding such crypto-assets and securities should be financially able and prepared to bear the risk of loss and the actual loss of his or her entire trade. The information in this application is not designed to be used as a basis for an investment decision. Persons using the Komodo application should confirm to their own satisfaction the veracity of any information prior to entering into any investment or making any transaction. The decision to buy or sell any crypto-asset or security that may be featured by Komodo is done purely and entirely at the reader’s own risk. As a reader and user of this application, You agree that under no circumstances will You seek to hold liable owners, members, officers, directors, partners, consultants or other persons involved in the publication of this application for any losses incurred by the use of information contained in this applicationKomodo and its contractors and affiliates may profit in the event the crypto-assets and securities increase or decrease in value. Such crypto-assets and securities may be bought or sold from time to time, even after Komodo has distributed positive information regarding the crypto-assets and companies. Komodo has no obligation to inform readers of its trading activities or the trading activities of any of its owners, members, officers, directors, contractors and affiliates and/or any companies affiliated with BC Relations’ owners, members, officers, directors, contractors and affiliates. Komodo and its affiliates may from time to time enter into agreements to purchase crypto-assets or securities to provide a method to reach their goals.\n\n'; const String disclaimerEulaParagraph17 = 'The Terms are effective until terminated by Komodo Platform. \nIn the event of termination, You are no longer authorized to access the Application, but all restrictions imposed on You and the disclaimers and limitations of liability set out in the Terms will survive termination. \nSuch termination shall not affect any legal right that may have accrued to Komodo Platform against You up to the date of termination. \nKomodo Platform may also remove the Application as a whole or any sections or features of the Application at any time. \n\n'; const String disclaimerEulaParagraph18 = 'The provisions of previous paragraphs are for the benefit of Komodo Platform and its officers, directors, employees, agents, licensors, suppliers, and any third party information providers to the Application. Each of these individuals or entities shall have the right to assert and enforce those provisions directly against You on its own behalf.\n\n'; const String disclaimerEulaParagraph19 = - 'Komodo Wallet is a non-custodial, decentralized and blockchain based application and as such does Komodo Platform never store any user-data (accounts and authentication data). \nWe also collect and process non-personal, anonymized data for statistical purposes and analysis and to help us provide a better service.\n\nThis document was last updated on January 31st, 2020\n\n'; + 'Komodo Wallet is a non-custodial, decentralized and blockchain based application and as such does Komodo Platform never store any user data (accounts and authentication data). \nWe also collect and process non-personal, anonymized data for statistical purposes and analysis and to help us provide a better service.\n\nThis document was last updated on January 31st, 2020\n\n'; diff --git a/lib/shared/widgets/disclaimer/eula_tos_checkboxes.dart b/lib/shared/widgets/disclaimer/eula_tos_checkboxes.dart index ebd7d88b2a..54d0937770 100644 --- a/lib/shared/widgets/disclaimer/eula_tos_checkboxes.dart +++ b/lib/shared/widgets/disclaimer/eula_tos_checkboxes.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -20,103 +21,76 @@ class EulaTosCheckboxes extends StatefulWidget { } class _EulaTosCheckboxesState extends State { - bool _checkBoxEULA = false; - bool _checkBoxTOC = false; + bool _checkBox = false; PopupDispatcher? _eulaPopupManager; PopupDispatcher? _disclaimerPopupManager; @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + final linkStyle = TextStyle( + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.primary, + ); + + return UiCheckbox( + checkboxKey: const Key('checkbox-eula-tos'), + value: _checkBox, + onChanged: (bool? value) { + setState(() { + _checkBox = value ?? false; + }); + _onCheck(); + }, + textWidget: Text.rich( + maxLines: 99, + TextSpan( children: [ - UiCheckbox( - checkboxKey: const Key('checkbox-eula'), - value: _checkBoxEULA, - onChanged: (bool? value) { - setState(() { - _checkBoxEULA = !_checkBoxEULA; - }); - _onCheck(); - }, + TextSpan(text: LocaleKeys.disclaimerAcceptDescription.tr()), + const TextSpan(text: ' '), + TextSpan( + text: LocaleKeys.disclaimerAcceptEulaCheckbox.tr(), + style: linkStyle, + recognizer: TapGestureRecognizer()..onTap = _showEula, ), - const SizedBox(width: 5), - Text(LocaleKeys.accept.tr(), style: const TextStyle(fontSize: 14)), - const SizedBox(width: 5), - InkWell( - onTap: _showEula, - child: Text(LocaleKeys.disclaimerAcceptEulaCheckbox.tr(), - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 14, - decoration: TextDecoration.underline)), - ) - ], - ), - const SizedBox(height: 10), - Row( - children: [ - UiCheckbox( - checkboxKey: const Key('checkbox-toc'), - value: _checkBoxTOC, - onChanged: (bool? value) { - setState(() { - _checkBoxTOC = !_checkBoxTOC; - _onCheck(); - }); - }, + const TextSpan(text: ', '), + TextSpan( + text: LocaleKeys.disclaimerAcceptTermsAndConditionsCheckbox.tr(), + style: linkStyle, + recognizer: TapGestureRecognizer()..onTap = _showDisclaimer, ), - const SizedBox(width: 5), - Text(LocaleKeys.accept.tr(), style: const TextStyle(fontSize: 14)), - const SizedBox(width: 5), - InkWell( - onTap: _showDisclaimer, - child: Text( - LocaleKeys.disclaimerAcceptTermsAndConditionsCheckbox.tr(), - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 14, - decoration: TextDecoration.underline)), - ) ], ), - const SizedBox(height: 8), - Text( - LocaleKeys.disclaimerAcceptDescription.tr(), - style: Theme.of(context).textTheme.bodySmall, - ), - ], + style: const TextStyle(fontSize: 14), + ), ); } @override void initState() { - _checkBoxEULA = widget.isChecked; - _checkBoxTOC = widget.isChecked; + _checkBox = widget.isChecked; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _disclaimerPopupManager = PopupDispatcher( - context: context, - popupContent: Disclaimer( - onClose: () { - _disclaimerPopupManager?.close(); - }, - )); + context: context, + popupContent: Disclaimer( + onClose: () { + _disclaimerPopupManager?.close(); + }, + ), + ); _eulaPopupManager = PopupDispatcher( - context: context, - popupContent: Eula( - onClose: () { - _eulaPopupManager?.close(); - }, - )); + context: context, + popupContent: Eula( + onClose: () { + _eulaPopupManager?.close(); + }, + ), + ); }); super.initState(); } void _onCheck() { - widget.onCheck(_checkBoxEULA && _checkBoxTOC); + widget.onCheck(_checkBox); } void _showDisclaimer() { 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/html_parser.dart b/lib/shared/widgets/html_parser.dart index f7f3cd4ddd..60751cef17 100644 --- a/lib/shared/widgets/html_parser.dart +++ b/lib/shared/widgets/html_parser.dart @@ -5,6 +5,7 @@ import 'package:web_dex/shared/utils/utils.dart'; class HtmlParser extends StatefulWidget { const HtmlParser( this.html, { + super.key, this.textStyle, this.linkStyle, }); @@ -42,18 +43,22 @@ class _HtmlParserState extends State { // ignore: unnecessary_string_escapes RegExp(']+href=\'(.*?)\'[^>]*>(.*)?<\/a>').firstMatch(chunk); if (linkMatch == null) { - children.add(TextSpan( - text: chunk, - style: textStyle, - )); + children.add( + TextSpan( + text: chunk, + style: textStyle, + ), + ); } else { children.add(_buildClickable(linkMatch)); } } - return SelectableText.rich(TextSpan( - children: children, - )); + return SelectableText.rich( + TextSpan( + children: children, + ), + ); } List _splitLinks(String text) { @@ -70,7 +75,8 @@ class _HtmlParserState extends State { } InlineSpan _buildClickable(RegExpMatch match) { - recognizers.add(TapGestureRecognizer()..onTap = () => launchURL(match[1]!)); + recognizers + .add(TapGestureRecognizer()..onTap = () => launchURLString(match[1]!)); return TextSpan( text: match[2], diff --git a/lib/shared/widgets/launch_native_explorer_button.dart b/lib/shared/widgets/launch_native_explorer_button.dart index e3fb75f9ff..35b1e0b485 100644 --- a/lib/shared/widgets/launch_native_explorer_button.dart +++ b/lib/shared/widgets/launch_native_explorer_button.dart @@ -20,7 +20,7 @@ class LaunchNativeExplorerButton extends StatelessWidget { width: 160, height: 30, onPressed: () { - launchURL(getNativeExplorerUrlByCoin(coin, address)); + launchURLString(getNativeExplorerUrlByCoin(coin, address)); }, text: LocaleKeys.viewOnExplorer.tr(), ); diff --git a/lib/shared/widgets/logout_popup.dart b/lib/shared/widgets/logout_popup.dart index 6b209b3f71..de0c777fdf 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,66 @@ 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 || + currentWallet?.config.type == WalletType.hdwallet) + 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/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/shared/widgets/simple_copyable_link.dart b/lib/shared/widgets/simple_copyable_link.dart index bdea41570f..1b0571e3e1 100644 --- a/lib/shared/widgets/simple_copyable_link.dart +++ b/lib/shared/widgets/simple_copyable_link.dart @@ -19,7 +19,7 @@ class SimpleCopyableLink extends StatelessWidget { return CopyableLink( text: text, valueToCopy: valueToCopy, - onLinkTap: link == null ? null : () => launchURL(link), + onLinkTap: link == null ? null : () => launchURLString(link), ); } } diff --git a/lib/views/bitrefill/bitrefill_button.dart b/lib/views/bitrefill/bitrefill_button.dart index d9165313e2..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'; @@ -9,10 +7,7 @@ import 'package:web_dex/bloc/bitrefill/models/bitrefill_event.dart'; import 'package:web_dex/bloc/bitrefill/models/bitrefill_event_factory.dart'; import 'package:web_dex/bloc/bitrefill/models/bitrefill_payment_intent_event.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/utils/utils.dart'; -import 'package:web_dex/views/bitrefill/bitrefill_button_view.dart'; -import 'package:web_dex/views/bitrefill/bitrefill_desktop_webview_button.dart'; -import 'package:web_dex/views/bitrefill/bitrefill_inappbrowser_button.dart'; +import 'package:web_dex/views/bitrefill/bitrefill_inappwebview_button.dart'; /// A button that opens the Bitrefill widget in a new window or tab. /// The Bitrefill widget is a web page that allows the user to purchase gift @@ -25,9 +20,9 @@ import 'package:web_dex/views/bitrefill/bitrefill_inappbrowser_button.dart'; /// The event is passed to the [onPaymentRequested] callback. class BitrefillButton extends StatefulWidget { const BitrefillButton({ - super.key, required this.coin, required this.onPaymentRequested, + super.key, this.windowTitle = 'Bitrefill', }); @@ -40,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 @@ -80,39 +72,18 @@ class _BitrefillButtonState extends State { 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..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'; @@ -11,16 +10,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 +31,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 @@ -43,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() { @@ -71,7 +65,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..4533476d8d --- /dev/null +++ b/lib/views/bitrefill/bitrefill_inappwebview_button.dart @@ -0,0 +1,152 @@ +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 { + if (kIsWeb) { + await _showWebDialog(); + } else { + await _showFullScreenDialog(); + } + } + + Future _showWebDialog() 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: 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(); + }, + ), + ], + ); + }, + ); + } + + 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; + } + + void _onConsoleMessage( + InAppWebViewController controller, + ConsoleMessage consoleMessage, + ) { + widget.onMessage(consoleMessage.message); + } + + URLRequest _createUrlRequest() { + return URLRequest(url: WebUri(widget.url)); + } +} 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/custom_token_import/custom_token_import_button.dart b/lib/views/custom_token_import/custom_token_import_button.dart new file mode 100644 index 0000000000..5f042bdc50 --- /dev/null +++ b/lib/views/custom_token_import/custom_token_import_button.dart @@ -0,0 +1,38 @@ +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_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_bloc.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_event.dart'; +import 'package:web_dex/bloc/custom_token_import/data/custom_token_import_repository.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/custom_token_import/custom_token_import_dialog.dart'; + +class CustomTokenImportButton extends StatelessWidget { + const CustomTokenImportButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return UiPrimaryButton( + onPressed: () { + final coinsRepo = RepositoryProvider.of(context); + final kdfSdk = RepositoryProvider.of(context); + showDialog( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (context) => CustomTokenImportBloc( + KdfCustomTokenImportRepository(kdfSdk, coinsRepo), + coinsRepo, + )..add(const ResetFormStatusEvent()), + child: const CustomTokenImportDialog(), + ); + }, + ); + }, + child: Text(LocaleKeys.importCustomToken.tr()), + ); + } +} diff --git a/lib/views/custom_token_import/custom_token_import_dialog.dart b/lib/views/custom_token_import/custom_token_import_dialog.dart new file mode 100644 index 0000000000..d62c06d5e2 --- /dev/null +++ b/lib/views/custom_token_import/custom_token_import_dialog.dart @@ -0,0 +1,359 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_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/coins_bloc/asset_coin_extension.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_bloc.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_event.dart'; +import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_state.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin_utils.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; + +class CustomTokenImportDialog extends StatefulWidget { + const CustomTokenImportDialog({Key? key}) : super(key: key); + + @override + CustomTokenImportDialogState createState() => CustomTokenImportDialogState(); +} + +class CustomTokenImportDialogState extends State { + final PageController _pageController = PageController(); + + Future navigateToPage(int pageIndex) async { + return _pageController.animateToPage( + pageIndex, + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + ); + } + + Future goToNextPage() async { + if (_pageController.page == null) return; + + await navigateToPage(_pageController.page!.toInt() + 1); + } + + Future goToPreviousPage() async { + if (_pageController.page == null) return; + + await navigateToPage(_pageController.page!.toInt() - 1); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: SizedBox( + width: 450, + height: 358, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + ImportFormPage( + onNextPage: goToNextPage, + ), + ImportSubmitPage( + onPreviousPage: goToPreviousPage, + ), + ], + ), + ), + ); + } +} + +class BasePage extends StatelessWidget { + final String title; + final Widget child; + final VoidCallback? onBackPressed; + + const BasePage({ + required this.title, + required this.child, + this.onBackPressed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (onBackPressed != null) + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: onBackPressed, + iconSize: 36, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + if (onBackPressed != null) const SizedBox(width: 16), + Text( + title, + style: const TextStyle( + fontSize: 18, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + splashRadius: 20, + ), + ], + ), + const SizedBox(height: 12), + Flexible(child: child), + ], + ), + ); + } +} + +class ImportFormPage extends StatelessWidget { + final VoidCallback onNextPage; + + const ImportFormPage({required this.onNextPage, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // keep controller outside of bloc consumer to prevent user inputs from + // being hijacked by state updates + final addressController = TextEditingController(text: ''); + return BlocConsumer( + listenWhen: (previous, current) => + previous.formStatus != current.formStatus, + listener: (context, state) { + if (state.formStatus == FormStatus.success || + state.formStatus == FormStatus.failure) { + onNextPage(); + } + }, + builder: (context, state) { + final initialState = state.formStatus == FormStatus.initial; + + final isSubmitEnabled = initialState && state.address.isNotEmpty; + + return BasePage( + title: LocaleKeys.importCustomToken.tr(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade300.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade300), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.orange.shade300), + const SizedBox(width: 8), + Expanded( + child: Text( + LocaleKeys.importTokenWarning.tr(), + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + DropdownButtonFormField( + value: state.network, + isExpanded: true, + decoration: InputDecoration( + labelText: LocaleKeys.selectNetwork.tr(), + border: const OutlineInputBorder(), + ), + items: state.evmNetworks.map((CoinSubClass coinType) { + return DropdownMenuItem( + value: coinType, + child: Text(getCoinTypeNameLong(coinType.toCoinType())), + ); + }).toList(), + onChanged: !initialState + ? null + : (CoinSubClass? value) { + context + .read() + .add(UpdateNetworkEvent(value)); + }, + ), + const SizedBox(height: 24), + TextFormField( + controller: addressController, + enabled: initialState, + onChanged: (value) { + context + .read() + .add(UpdateAddressEvent(value)); + }, + decoration: InputDecoration( + labelText: LocaleKeys.tokenContractAddress.tr(), + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + UiPrimaryButton( + onPressed: isSubmitEnabled + ? () { + context + .read() + .add(const SubmitFetchCustomTokenEvent()); + } + : null, + child: state.formStatus == FormStatus.initial + ? Text(LocaleKeys.importToken.tr()) + : const UiSpinner(color: Colors.white), + ), + ], + ), + ); + }, + ); + } +} + +class ImportSubmitPage extends StatelessWidget { + final VoidCallback onPreviousPage; + + const ImportSubmitPage({required this.onPreviousPage, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.importStatus != current.importStatus, + listener: (context, state) { + if (state.importStatus == FormStatus.success) { + Navigator.of(context).pop(); + } + }, + builder: (context, state) { + final newCoin = state.coin; + final newCoinBalance = formatAmt(state.coinBalance.toDouble()); + final newCoinUsdBalance = + '\$${formatAmt(state.coinBalanceUsd.toDouble())}'; + + final isSubmitEnabled = state.importStatus != FormStatus.submitting && + state.importStatus != FormStatus.success && + newCoin != null; + + return BasePage( + title: LocaleKeys.importCustomToken.tr(), + onBackPressed: () { + context + .read() + .add(const ResetFormStatusEvent()); + onPreviousPage(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: newCoin == null + ? [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + '$assetsPath/logo/not_found.png', + height: 250, + filterQuality: FilterQuality.high, + ), + Text( + LocaleKeys.tokenNotFound.tr(), + ), + ], + ), + ), + ] + : [ + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + CoinIcon.ofSymbol( + newCoin.id.id, + size: 80, + ), + const SizedBox(height: 12), + Text( + newCoin.id.id, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 32), + Text( + LocaleKeys.balance.tr(), + style: + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Text( + '$newCoinBalance ${newCoin.id.id} ($newCoinUsdBalance)', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ], + ), + ), + if (state.importErrorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + state.importErrorMessage, + textAlign: TextAlign.start, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + UiPrimaryButton( + onPressed: isSubmitEnabled + ? () { + context + .read() + .add(const SubmitImportCustomTokenEvent()); + } + : null, + child: state.importStatus == FormStatus.submitting || + state.importStatus == FormStatus.success + ? const UiSpinner(color: Colors.white) + : Text(LocaleKeys.importToken.tr()), + ), + ], + ), + ); + }, + ); + } +} 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 c0ad3b4b23..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,26 +163,6 @@ int getCoinPairsCountFromCoinAbbrMap(Map> coinAbbrMap, .length; } -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. @@ -214,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')); @@ -232,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: @@ -244,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..34ce41cde9 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'; @@ -39,9 +40,9 @@ class _DexListFilterCoinsListState extends State { children: [ UiTextFormField( hintText: LocaleKeys.searchAssets.tr(), - onChanged: (String searchPhrase) { + onChanged: (String? searchPhrase) { setState(() { - _searchPhrase = searchPhrase; + _searchPhrase = searchPhrase ?? ''; }); }, ), @@ -64,39 +65,47 @@ class _DexListFilterCoinsListState extends State { } Widget _buildSwapCoinList() { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( - stream: tradingEntitiesBloc.outSwaps, - initialData: tradingEntitiesBloc.swaps, - builder: (context, snapshot) { - final list = snapshot.data ?? []; - final filtered = widget.listType == DexListType.history - ? list.where((s) => s.isCompleted).toList() - : list.where((s) => !s.isCompleted).toList(); - final Map> coinAbbrMap = - getCoinAbbrMapFromSwapList(filtered, widget.isSellCoin); + stream: tradingEntitiesBloc.outSwaps, + initialData: tradingEntitiesBloc.swaps, + builder: (context, snapshot) { + final list = snapshot.data ?? []; + final filtered = widget.listType == DexListType.history + ? list.where((s) => s.isCompleted).toList() + : list.where((s) => !s.isCompleted).toList(); + final Map> coinAbbrMap = + getCoinAbbrMapFromSwapList(filtered, widget.isSellCoin); - return _buildCoinList(coinAbbrMap); - }); + return _buildCoinList(coinAbbrMap); + }, + ); } Widget _buildOrderCoinList() { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return StreamBuilder>( - stream: tradingEntitiesBloc.outMyOrders, - initialData: tradingEntitiesBloc.myOrders, - builder: (context, snapshot) { - final list = snapshot.data ?? []; - final Map> coinAbbrMap = - getCoinAbbrMapFromOrderList(list, widget.isSellCoin); + stream: tradingEntitiesBloc.outMyOrders, + initialData: tradingEntitiesBloc.myOrders, + builder: (context, snapshot) { + final list = snapshot.data ?? []; + final Map> coinAbbrMap = + getCoinAbbrMapFromOrderList(list, widget.isSellCoin); - return _buildCoinList(coinAbbrMap); - }); + return _buildCoinList(coinAbbrMap); + }, + ); } Widget _buildCoinList(Map> coinAbbrMap) { final List coinAbbrList = (_searchPhrase.isEmpty ? coinAbbrMap.keys.toList() - : coinAbbrMap.keys.where((String coinAbbr) => - coinAbbr.toLowerCase().contains(_searchPhrase))) + : coinAbbrMap.keys.where( + (String coinAbbr) => + coinAbbr.toLowerCase().contains(_searchPhrase), + )) .where((abbr) => abbr != widget.anotherCoin) .toList(); @@ -107,28 +116,29 @@ class _DexListFilterCoinsListState extends State { isMobile: isMobile, scrollController: scrollController, child: ListView.builder( - controller: scrollController, - shrinkWrap: true, - itemCount: coinAbbrList.length, - itemBuilder: (BuildContext context, int i) { - final coinAbbr = coinAbbrList[i]; - final String? anotherCoinAbbr = widget.anotherCoin; - final coinPairsCount = getCoinPairsCountFromCoinAbbrMap( - coinAbbrMap, - coinAbbr, - anotherCoinAbbr, - ); + controller: scrollController, + shrinkWrap: true, + itemCount: coinAbbrList.length, + itemBuilder: (BuildContext context, int i) { + final coinAbbr = coinAbbrList[i]; + final String? anotherCoinAbbr = widget.anotherCoin; + final coinPairsCount = getCoinPairsCountFromCoinAbbrMap( + coinAbbrMap, + coinAbbr, + anotherCoinAbbr, + ); - return Padding( - padding: EdgeInsets.fromLTRB( - 18, - 5.0, - 18, - lastIndex == i ? 20.0 : 0.0, - ), - child: _buildCoinListItem(coinAbbr, coinPairsCount), - ); - }), + return Padding( + padding: EdgeInsets.fromLTRB( + 18, + 5.0, + 18, + lastIndex == i ? 20.0 : 0.0, + ), + child: _buildCoinListItem(coinAbbr, coinPairsCount), + ); + }, + ), ); } @@ -148,7 +158,8 @@ class _DexListFilterCoinsListState extends State { Padding( padding: const EdgeInsets.only(left: 5.0), child: Text( - '${Coin.normalizeAbbr(coinAbbr)} ${isSegwit ? ' (segwit)' : ''}'), + '${Coin.normalizeAbbr(coinAbbr)} ${isSegwit ? ' (segwit)' : ''}', + ), ), const Spacer(), Text('($pairCount)'), 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 13084a0543..8d969a2ee2 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -2,11 +2,16 @@ 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/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'; +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'; @@ -14,27 +19,64 @@ 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) { 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( - DexTabBarState.initial(), - authRepo, - ), + RepositoryProvider.of(context), + tradingEntitiesBloc, + MarketMakerBotOrderListRepository( + myOrdersService, + SettingsRepository(), + coinsRepository, + ), + )..add(const ListenToOrdersRequested()), ), ], - child: routingState.dexState.isTradingDetails + child: isTradingDetails ? TradingDetails(uuid: routingState.dexState.uuid) : _DexContent(), ); } + + void _onRouteChange() { + setState( + () => isTradingDetails = routingState.dexState.isTradingDetails, + ); + } } class _DexContent extends StatefulWidget { @@ -58,7 +100,6 @@ class _DexContentState extends State<_DexContent> { ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const HiddenWithoutWallet( child: Padding( @@ -93,6 +134,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/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..00c1f52024 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,6 +16,7 @@ mixin SwapHistorySortingMixin { } List sortSwaps( + BuildContext context, List swaps, { required SortData sortData, }) { @@ -25,7 +28,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 +88,12 @@ mixin SwapHistorySortingMixin { } List _sortByPrice( + 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..824ebb4353 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}) @@ -140,9 +141,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.status.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.status.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Text(status), ], ); @@ -153,9 +157,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.orderId.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.orderId.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: InkWell( @@ -172,15 +179,20 @@ class _MakerOrderDetailsPageState extends State { TableRow _buildCreatedAt() { final String createdAt = DateFormat('dd MMM yyyy, HH:mm').format( - DateTime.fromMillisecondsSinceEpoch( - widget.makerOrderStatus.order.createdAt * 1000)); + DateTime.fromMillisecondsSinceEpoch( + widget.makerOrderStatus.order.createdAt * 1000, + ), + ); return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.createdAt.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.createdAt.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Text(createdAt), ], ); @@ -194,9 +206,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.price.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.price.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Row( children: [ Text( @@ -206,7 +221,7 @@ class _MakerOrderDetailsPageState extends State { const SizedBox( width: 5, ), - Text(order.rel) + Text(order.rel), ], ), ], @@ -219,6 +234,8 @@ 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..96ea3cd7ed 100644 --- a/lib/views/dex/entity_details/swap/swap_details_step.dart +++ b/lib/views/dex/entity_details/swap/swap_details_step.dart @@ -78,10 +78,11 @@ class SwapDetailsStep extends StatelessWidget { padding: const EdgeInsets.all(2), child: DecoratedBox( decoration: BoxDecoration( - shape: BoxShape.circle, - color: isProcessedStep || isFailedStep - ? Colors.transparent - : themeData.colorScheme.surface), + shape: BoxShape.circle, + color: isProcessedStep || isFailedStep + ? Colors.transparent + : themeData.colorScheme.surface, + ), ), ), ), @@ -91,7 +92,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, ), ], @@ -147,13 +149,13 @@ class SwapDetailsStep extends StatelessWidget { size: 20, ), onTap: () => - launchURL(getTxExplorerUrl(coin, txHash)), + launchURLString(getTxExplorerUrl(coin, txHash)), ), ), ), - ) + ), ], - ) + ), ], ), ), @@ -196,16 +198,18 @@ class SwapDetailsStep extends StatelessWidget { } String _getTimeSpent(BuildContext context) { - return LocaleKeys.swapDetailsStepStatusTimeSpent.tr(args: [ - durationFormat( - Duration(milliseconds: timeSpent), - DurationLocalization( - milliseconds: LocaleKeys.milliseconds.tr(), - seconds: LocaleKeys.seconds.tr(), - minutes: LocaleKeys.minutes.tr(), - hours: LocaleKeys.hours.tr(), + return LocaleKeys.swapDetailsStepStatusTimeSpent.tr( + args: [ + durationFormat( + Duration(milliseconds: timeSpent), + DurationLocalization( + milliseconds: LocaleKeys.milliseconds.tr(), + seconds: LocaleKeys.seconds.tr(), + minutes: LocaleKeys.minutes.tr(), + hours: LocaleKeys.hours.tr(), + ), ), - ), - ]); + ], + ); } } 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..e7d096bbe8 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(); } @@ -114,9 +119,10 @@ class _SwapRecoverButtonState extends State { padding: const EdgeInsets.only(top: 5.0), child: InkWell( child: Text( - '${LocaleKeys.transactionHash.tr()}: ${response.result.txHash}'), + '${LocaleKeys.transactionHash.tr()}: ${response.result.txHash}', + ), onTap: () { - launchURL(url); + launchURLString(url); }, ), ), 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 b80ad2e047..be5afce3a7 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 ?? '')} = ', @@ -82,7 +81,7 @@ class _Rates extends StatelessWidget { ), ), Text( - showDetails ? '($baseFiat)' : '', + showDetails ? '(${baseFiat(context)})' : '', style: theme.custom.tradingFormDetailsContent, ), ], @@ -92,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, @@ -103,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 69ef6b70c1..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) { @@ -79,7 +85,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/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 01158b139b..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,5 +1,7 @@ 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/bloc/settings/settings_bloc.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'; @@ -18,11 +20,14 @@ class CoinsTableContent extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder>( - stream: coinsBloc.outKnownCoins, - initialData: coinsBloc.knownCoins, - builder: (context, snapshot) { - final coins = prepareCoinsForTable(coinsBloc.knownCoins, searchString); + return BlocBuilder( + builder: (context, state) { + final coins = prepareCoinsForTable( + context, + state.coins.values.toList(), + searchString, + testCoinsEnabled: context.read().state.testCoinsEnabled, + ); if (coins.isEmpty) return const NothingFound(); return GroupedListView( diff --git a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart index c016cde038..534e420271 100644 --- a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart +++ b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart @@ -34,7 +34,7 @@ class CoinsTableItem extends StatelessWidget { subtitleText: subtitleText, ), const SizedBox(width: 8), - if (coin.isActive) CoinBalance(coin: coin), + if (coin.isActive) CoinBalance(coin: coin, isVertical: true), ], ), ); 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 771ef1ec3d..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 @@ -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,13 @@ 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( + context, + 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..5078398b89 100644 --- a/lib/views/dex/simple/form/tables/table_utils.dart +++ b/lib/views/dex/simple/form/tables/table_utils.dart @@ -1,38 +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(List coins, String? searchString) { +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(Map>? orders, - String? searchString, AuthorizeMode mode) { +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 []; - removeSuspendedCoinOrders(sorted, mode); + if (!testCoinsEnabled) { + removeTestCoinOrders(sorted, context); + if (sorted.isEmpty) return []; + } + + 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(); @@ -40,18 +61,20 @@ List prepareOrdersForTable(Map>? orders, 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); @@ -65,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..42fd7f6dbb 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,10 +22,10 @@ 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 { + const TakerFormContent({super.key}); + @override Widget build(BuildContext context) { return FormPlate( @@ -40,13 +42,19 @@ 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); + (element) => element.abbr == selectedOrder.coin, + ); if (buyCoin == null) return false; - takerBloc.add(TakerSetSellCoin(buyCoin, - autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr)); + takerBloc.add( + TakerSetSellCoin( + buyCoin, + autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr, + ), + ); return true; }, topWidget: const TakerFormSellItem(), @@ -94,7 +102,7 @@ class _FormControls extends StatelessWidget { } class ResetSwapFormButton extends StatelessWidget { - const ResetSwapFormButton(); + const ResetSwapFormButton({super.key}); @override Widget build(BuildContext context) { @@ -108,41 +116,43 @@ class ResetSwapFormButton extends StatelessWidget { } class TradeButton extends StatelessWidget { - const TradeButton(); + const TradeButton({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, systemHealthState) { - final bool isSystemClockValid = - systemHealthState is SystemHealthLoadSuccess && - systemHealthState.isValid; + builder: (context, systemHealthState) { + final bool isSystemClockValid = + systemHealthState is SystemHealthLoadSuccess && + systemHealthState.isValid; - return BlocSelector( - selector: (state) => state.inProgress, - builder: (context, inProgress) { - final bool disabled = inProgress || !isSystemClockValid; + return BlocSelector( + selector: (state) => state.inProgress, + builder: (context, inProgress) { + final bool disabled = inProgress || !isSystemClockValid; - return Opacity( - opacity: disabled ? 0.8 : 1, - child: UiPrimaryButton( - key: const Key('take-order-button'), - text: LocaleKeys.swapNow.tr(), - prefix: inProgress ? const TradeButtonSpinner() : null, - onPressed: disabled - ? null - : () => context.read().add(TakerFormSubmitClick()), - height: isMobile ? 52 : 40, - ), - ); - }, - ); - }); + return Opacity( + opacity: disabled ? 0.8 : 1, + child: UiPrimaryButton( + key: const Key('take-order-button'), + text: LocaleKeys.swapNow.tr(), + prefix: inProgress ? const TradeButtonSpinner() : null, + onPressed: disabled + ? null + : () => + context.read().add(TakerFormSubmitClick()), + height: isMobile ? 52 : 40, + ), + ); + }, + ); + }, + ); } } class TradeButtonSpinner extends StatelessWidget { - const TradeButtonSpinner(); + const TradeButtonSpinner({super.key}); @override Widget build(BuildContext context) { 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_form_layout.dart b/lib/views/dex/simple/form/taker/taker_form_layout.dart index 79572faf3c..89714a04d1 100644 --- a/lib/views/dex/simple/form/taker/taker_form_layout.dart +++ b/lib/views/dex/simple/form/taker/taker_form_layout.dart @@ -56,7 +56,7 @@ class _TakerFormDesktopLayout extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - TakerFormContent(), + const TakerFormContent(), Padding( padding: const EdgeInsets.fromLTRB(16, 52, 16, 0), child: TakerSellCoinsTable(), @@ -95,11 +95,11 @@ class _TakerFormMobileLayout extends StatelessWidget { constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), child: Stack( children: [ - Column( + const Column( children: [ TakerFormContent(), - const SizedBox(height: 22), - const TakerOrderbook(), + SizedBox(height: 22), + TakerOrderbook(), ], ), Padding( 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/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..133b13d42f --- /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.isEmpty ? '' : ' ($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..1dbab98145 100644 --- a/lib/views/fiat/fiat_form.dart +++ b/lib/views/fiat/fiat_form.dart @@ -1,101 +1,55 @@ 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/auth_bloc/auth_bloc.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/fiat_repository.dart'; -import 'package:web_dex/blocs/blocs.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/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; + bool _isLoggedIn = false; 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 +57,9 @@ class _FiatFormState extends State { if (isLoggedIn) { await fillAccountInformation(); } else { - await _clearAccountData(); + context + .read() + .add(const ClearAccountInformationRequested()); } } @@ -111,435 +67,73 @@ class _FiatFormState extends State { void initState() { super.initState(); - _walletCoinsListener = coinsBloc.outWalletCoins.listen((walletCoins) async { - _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); - }); - } - - String? getNonZeroFiatAmount() { - if (fiatAmount == null) return null; - final amount = double.tryParse(fiatAmount!); + _isLoggedIn = RepositoryProvider.of(context).state.isSignedIn; - 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(); - } - - 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; - } - }); + context.read().add(const AccountInformationChanged()); } - 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; + void _onConsoleMessage(String message) => context + .read() + .add(FiatOnRampPaymentStatusMessageReceived(message)); - final limit = getMinFiatAmount(); - if (limit == null) return false; - - 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 +150,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 +189,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 +205,121 @@ 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 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 - ? error != null - ? 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 645130d1cf..416945b863 100644 --- a/lib/views/fiat/fiat_inputs.dart +++ b/lib/views/fiat/fiat_inputs.dart @@ -1,44 +1,51 @@ 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'; // 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 { - 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(); } @@ -46,16 +53,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(); @@ -66,65 +63,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); } @@ -132,11 +107,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 @@ -146,16 +119,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, @@ -163,11 +139,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: @@ -176,29 +152,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, @@ -217,15 +196,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, @@ -239,23 +216,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, @@ -273,283 +254,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..e59e94616b 100644 --- a/lib/views/fiat/fiat_page.dart +++ b/lib/views/fiat/fiat_page.dart @@ -2,7 +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'; @@ -40,25 +45,85 @@ 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(), + final coinsRepository = RepositoryProvider.of(context); + final fiatRepository = FiatRepository( + [BanxaFiatProvider(), RampFiatProvider()], + coinsRepository, + ); + return BlocProvider( + create: (_) => FiatFormBloc( + repository: fiatRepository, + coinsRepository: coinsRepository, + ), + 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, + }); + + final int activeTabIndex; - Widget _buildFiatPage() { + @override + Widget build(BuildContext context) { return PageLayout( content: Expanded( child: Container( @@ -70,8 +135,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 +150,7 @@ class _FiatPageState extends State with TickerProviderStateMixin { // ), Flexible( child: _TabContent( - activeTabIndex: _activeTabIndex, - onCheckoutComplete: _onCheckoutComplete, + activeTabIndex: activeTabIndex, ), ), ], @@ -98,14 +160,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 +167,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..cf1af166e7 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 + .withValues(alpha: 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..38c7e1a131 --- /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.withValues( + alpha: 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..586f00ce16 --- /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.withValues(alpha: 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.withValues(alpha: 0.5) + : foregroundColor.withValues(alpha: 0.25), + ), + ), + ], + ), + const SizedBox(width: 4), + Icon( + Icons.keyboard_arrow_down, + size: 28, + color: foregroundColor.withValues(alpha: 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..10d12ac004 --- /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 launchURLString(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 3137ea659f..c82b8a336b 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,23 +1,21 @@ -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'; -import 'package:web_dex/blocs/startup_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/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 { + const MainLayout({super.key}); + @override State createState() => _MainLayoutState(); } @@ -25,12 +23,15 @@ class MainLayout extends StatefulWidget { class _MainLayoutState extends State { @override void initState() { + // TODO: localize + showMessageBeforeUnload('Are you sure you want to leave?'); + WidgetsBinding.instance.addPostFrameCallback((_) async { await AlphaVersionWarningService().run(); - updateBloc.init(); + await updateBloc.init(); - if (kDebugMode && !await _hasAgreedNoTrading()) { - _showDebugModeDialog().ignore(); + if (!kIsWalletOnly && !await _hasAgreedNoTrading()) { + _showNoTradingWarning().ignore(); } }); @@ -48,41 +49,34 @@ class _MainLayoutState extends State { 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 { + Future _showNoTradingWarning() async { await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( - title: const Text('Debug mode'), + title: const Text('Warning'), 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.', - ), + 'Trading features may not be used for actual trading. Only test ' + 'assets/networks may be used and only for development purposes. ' + 'You are solely responsible for any losses/damage that may occur.' + '\n\nKomodoPlatform does not condone the use of this app for ' + 'trading purposes and unequivocally forbids it.'), actions: [ TextButton( onPressed: () { @@ -99,11 +93,11 @@ class _MainLayoutState extends State { Future _saveAgreedState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool('wallet_only_agreed', true); + prefs.setInt('wallet_only_agreed', DateTime.now().millisecondsSinceEpoch); } Future _hasAgreedNoTrading() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('wallet_only_agreed') ?? false; + return prefs.getInt('wallet_only_agreed') != null; } } 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 e9a2f9a2c3..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,5 +1,7 @@ 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/bloc/settings/settings_bloc.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'; @@ -54,7 +56,12 @@ class _CoinSelectionAndAmountInputState } void _prepareItems() { - _items = prepareCoinsForTable(widget.coins, null) + _items = prepareCoinsForTable( + context, + widget.coins, + null, + testCoinsEnabled: context.read().state.testCoinsEnabled, + ) .map( (coin) => CoinSelectItem( name: coin.name, @@ -109,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 be6d6affa0..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'; @@ -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()); } @@ -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_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 97ce17b6b9..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,18 +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(), + coinsRepository, + ); + return MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => DexTabBarBloc( - DexTabBarState.initial(), - authRepo, - ), + RepositoryProvider.of(context), + tradingEntitiesBloc, + orderListRepository, + )..add(const ListenToOrdersRequested()), ), BlocProvider( create: (BuildContext context) => MarketMakerTradeFormBloc( - dexRepo: DexRepository(), - coinsRepo: coinsBloc, + dexRepo: RepositoryProvider.of(context), + coinsRepo: coinsRepository, ), ), BlocProvider( @@ -61,6 +72,7 @@ class _MarketMakerBotPageState extends State { MarketMakerBotOrderListRepository( myOrdersService, SettingsRepository(), + coinsRepository, ), ), ), 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_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..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, - orders, 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: @@ -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/market_maker_form_error_message_extensions.dart b/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart index 67af2d1b0b..8b837dca33 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,8 +18,6 @@ extension TradeMarginValidationErrorText on TradeMarginValidationError { return LocaleKeys.postitiveNumberRequired.tr(); case TradeMarginValidationError.greaterThanMaximum: return LocaleKeys.mustBeLessThan.tr(args: [maxValue.toString()]); - default: - return null; } } } @@ -56,8 +54,6 @@ 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/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/market_maker_bot/trade_pair_list_item.dart b/lib/views/market_maker_bot/trade_pair_list_item.dart index 948ffa9000..3d14913e87 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'; @@ -31,10 +32,12 @@ 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 tradingEntitiesBloc = + RepositoryProvider.of(context); final double fillProgress = order != null ? tradingEntitiesBloc.getProgressFillSwap(pair.order!) : 0; @@ -108,6 +111,8 @@ class _OrderItemDesktop extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingEntitiesBloc = + RepositoryProvider.of(context); return Row( mainAxisSize: MainAxisSize.max, children: [ @@ -194,6 +199,7 @@ class _OrderItemDesktop extends StatelessWidget { class TableActionsButtonList extends StatelessWidget { const TableActionsButtonList({ + super.key, required this.actions, }); diff --git a/lib/views/nfts/details_page/nft_details_page.dart b/lib/views/nfts/details_page/nft_details_page.dart index 3d3f58f66c..b5bba05302 100644 --- a/lib/views/nfts/details_page/nft_details_page.dart +++ b/lib/views/nfts/details_page/nft_details_page.dart @@ -3,11 +3,12 @@ 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_defi_sdk/komodo_defi_sdk.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'; @@ -40,9 +41,13 @@ class NftDetailsPage extends StatelessWidget { } final nfts = context.read().state.nfts; final NftToken? nft = nfts.values - .firstWhereOrNull((list) => - list?.firstWhereOrNull((token) => token.uuid == uuid) != null) + .firstWhereOrNull( + (list) => + list?.firstWhereOrNull((token) => token.uuid == uuid) != null, + ) ?.firstWhereOrNull((token) => token.uuid == uuid); + final mm2Api = RepositoryProvider.of(context); + final kdfSdk = RepositoryProvider.of(context); if (nft == null) { return Column( @@ -64,8 +69,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), + kdfSdk: kdfSdk, + 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..854902d627 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:komodo_defi_sdk/komodo_defi_sdk.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/views/nfts/nft_receive/nft_receive_view.dart'; class NftReceivePage extends StatelessWidget { @@ -14,8 +15,8 @@ class NftReceivePage extends StatelessWidget { builder: (context, state) { return BlocProvider( create: (context) => NftReceiveBloc( - coinsRepo: coinsBloc, - currentWalletBloc: currentWalletBloc, + coinsRepo: RepositoryProvider.of(context), + sdk: 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/qr_scanner.dart b/lib/views/qr_scanner.dart deleted file mode 100644 index 14db10ac8e..0000000000 --- a/lib/views/qr_scanner.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; - -class QrScanner extends StatefulWidget { - const QrScanner({super.key}); - - @override - State createState() => _QrScannerState(); -} - -class _QrScannerState extends State { - bool qrDetected = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(LocaleKeys.qrScannerTitle.tr()), - foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, - elevation: 0, - ), - body: MobileScanner( - controller: MobileScannerController( - detectionTimeoutMs: 1000, - formats: [BarcodeFormat.qrCode], - ), - errorBuilder: _buildQrScannerError, - onDetect: (capture) { - if (qrDetected) return; - - final List qrCodes = capture.barcodes; - - if (qrCodes.isNotEmpty) { - final r = qrCodes.first.rawValue; - qrDetected = true; - - // MRC: Guarantee that we don't try to close the current screen - // if it was already closed - if (!context.mounted) return; - Navigator.pop(context, r); - } - }, - placeholderBuilder: (context, _) => const Center( - child: CircularProgressIndicator(), - ), - ), - ); - } - - Widget _buildQrScannerError( - BuildContext context, MobileScannerException exception, _) { - late String errorMessage; - - switch (exception.errorCode) { - case MobileScannerErrorCode.controllerUninitialized: - errorMessage = LocaleKeys.qrScannerErrorControllerUninitialized.tr(); - break; - case MobileScannerErrorCode.permissionDenied: - errorMessage = LocaleKeys.qrScannerErrorPermissionDenied.tr(); - break; - case MobileScannerErrorCode.genericError: - default: - errorMessage = LocaleKeys.qrScannerErrorGenericError.tr(); - } - - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.warning, - color: Colors.yellowAccent, - size: 64, - ), - const SizedBox(height: 8), - Text( - LocaleKeys.qrScannerErrorTitle.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 32), - Text(errorMessage, style: Theme.of(context).textTheme.bodyLarge), - if (exception.errorDetails != null) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${LocaleKeys.errorCode.tr()}: ${exception.errorDetails!.code}'), - Text( - '${LocaleKeys.errorDetails.tr()}: ${exception.errorDetails!.details}'), - Text( - '${LocaleKeys.errorMessage.tr()}: ${exception.errorDetails!.message}'), - ], - ), - ], - ), - ); - } -} 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..8c862df4ad 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,12 +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, children: [ @@ -83,7 +76,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 +90,8 @@ class _ApiVersion extends StatelessWidget { @override Widget build(BuildContext context) { + final mm2Api = RepositoryProvider.of(context); + return Row( children: [ Flexible( @@ -107,8 +103,10 @@ class _ApiVersion extends StatelessWidget { final String? commitHash = _tryParseCommitHash(snapshot.data); if (commitHash == null) return const SizedBox.shrink(); - return SelectableText('${LocaleKeys.api.tr()}: $commitHash', - style: _textStyle); + return SelectableText( + '${LocaleKeys.api.tr()}: $commitHash', + style: _textStyle, + ); }, ), ), 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/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_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/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..bff46cf478 100644 --- a/lib/views/settings/widgets/security_settings/password_update_page.dart +++ b/lib/views/settings/widgets/security_settings/password_update_page.dart @@ -3,19 +3,19 @@ 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/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); + const PasswordUpdatePage({super.key}); @override State createState() => _PasswordUpdatePageState(); @@ -44,7 +44,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, @@ -101,7 +101,7 @@ class _PasswordUpdatePageState extends State { } class _FormView extends StatefulWidget { - const _FormView({Key? key, required this.onSuccess}) : super(key: key); + const _FormView({super.key, required this.onSuccess}); final VoidCallback onSuccess; @@ -170,26 +170,29 @@ class _FormViewState extends State<_FormView> { } Future _onUpdate() async { - if (!(_formKey.currentState?.validate() ?? false)) { - return; - } - final Wallet? wallet = currentWalletBloc.wallet; - if (wallet == null) return; - final String password = _newController.text; - - if (_oldController.text == password) { - setState(() { - _error = LocaleKeys.usedSamePassword.tr(); - }); - return; - } - - final bool isPasswordUpdated = await currentWalletBloc.updatePassword( - _oldController.text, - password, - wallet, - ); - + // TODO! re-implement along with password change feature + // 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; + + // if (_oldController.text == password) { + // setState(() { + // _error = LocaleKeys.usedSamePassword.tr(); + // }); + // return; + // } + + // final bool isPasswordUpdated = await currentWalletBloc.updatePassword( + // _oldController.text, + // password, + // wallet, + // ); + final isPasswordUpdated = true; + // ignore: dead_code if (!isPasswordUpdated) { setState(() { _error = LocaleKeys.passwordNotAccepted.tr(); @@ -233,6 +236,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 +252,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 +264,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(); } } @@ -361,7 +366,7 @@ class _PasswordField extends StatelessWidget { } class _SuccessView extends StatelessWidget { - const _SuccessView({Key? key, required this.back}) : super(key: key); + const _SuccessView({super.key, required this.back}); final VoidCallback back; 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..e955903d51 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,13 @@ 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/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 +103,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 +117,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 +175,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 authBloc = context.read(); + final hasBackup = currentWallet?.config.hasBackup == true; final text = hasBackup ? LocaleKeys.viewSeedPhrase.tr() : LocaleKeys.backupSeedPhrase.tr(); @@ -200,7 +206,7 @@ class _SaveAndRememberButtons extends StatelessWidget { final String? password = await walletPasswordDialog(context); if (password == null) return; - currentWalletBloc.downloadCurrentWallet(password); + authBloc.add(AuthWalletDownloadRequested(password: password)); }, width: isMobile ? double.infinity : 187, height: isMobile ? 52 : 40, 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..8aebef549d 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,27 @@ 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/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 +34,9 @@ class _SecuritySettingsPageState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SecuritySettingsBloc(SecuritySettingsState.initialState()), + create: (_) => SecuritySettingsBloc( + SecuritySettingsState.initialState(), + ), child: BlocBuilder( builder: (BuildContext context, SecuritySettingsState state) { final Widget content = _buildContent(state.step); @@ -47,19 +47,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 +86,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 +99,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 +124,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_confirmation/seed_confirmation.dart b/lib/views/settings/widgets/security_settings/seed_settings/seed_confirmation/seed_confirmation.dart index 995278e432..b398d2fd47 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/seed_confirmation/seed_confirmation.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/seed_confirmation/seed_confirmation.dart @@ -1,6 +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/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/common/screen.dart'; @@ -113,6 +114,7 @@ class _SeedConfirmationState extends State { if (result == widget.seedPhrase) { final settingsBloc = context.read(); settingsBloc.add(const SeedConfirmedEvent()); + context.read().add(AuthSeedBackupConfirmed()); return; } setState(() { 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..a17959b2c3 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 @@ -1,8 +1,9 @@ import 'package:app_theme/app_theme.dart'; -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_defi_sdk/komodo_defi_sdk.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 { @@ -78,8 +78,8 @@ class SeedShow extends StatelessWidget { class _PrivateKeysList extends StatelessWidget { const _PrivateKeysList({ required this.privKeys, - Key? key, - }) : super(key: key); + super.key, + }); final Map privKeys; @@ -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), ), @@ -308,7 +308,10 @@ class _SeedPlace extends StatelessWidget { @override Widget build(BuildContext context) { - final isCustom = !validateMnemonic(seedPhrase); + final isCustom = !context + .read() + .mnemonicValidator + .validateBip39(seedPhrase); if (isCustom) return _SeedField(seedPhrase: seedPhrase); return _WordsList(seedPhrase: seedPhrase); } @@ -397,11 +400,11 @@ class _WordsList extends StatelessWidget { class _SelectableSeedWord extends StatelessWidget { const _SelectableSeedWord({ - Key? key, + super.key, required this.isSeedShown, required this.initialValue, required this.index, - }) : super(key: key); + }); final bool isSeedShown; final String initialValue; @@ -412,22 +415,14 @@ 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 : 'β€’β€’β€’β€’β€’β€’'; + final text = isSeedShown ? initialValue : 'β€’β€’β€’β€’β€’β€’'; return Focus( descendantsAreFocusable: true, skipTraversal: true, - onFocusChange: (value) { - if (value) { - seedWordController.selection = TextSelection( - baseOffset: 0, - extentOffset: seedWordController.value.text.length, - ); - } - }, child: FractionallySizedBox( widthFactor: isMobile ? 0.5 : 0.25, child: Row( @@ -447,9 +442,9 @@ class _SelectableSeedWord extends StatelessWidget { constraints: const BoxConstraints(maxHeight: 31), child: DryIntrinsicWidth( child: UiTextFormField( + initialValue: text, obscureText: !isSeedShown, readOnly: true, - controller: seedWordController, ), ), ), @@ -469,10 +464,13 @@ class _SeedPhraseConfirmButton extends StatelessWidget { @override Widget build(BuildContext context) { final bloc = context.read(); - final isCustom = !validateMnemonic(seedPhrase); + final isCustom = !context + .read() + .mnemonicValidator + .validateBip39(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/settings/widgets/support_page/support_page.dart b/lib/views/settings/widgets/support_page/support_page.dart index 64fa74f7e7..e8dccd5f92 100644 --- a/lib/views/settings/widgets/support_page/support_page.dart +++ b/lib/views/settings/widgets/support_page/support_page.dart @@ -38,72 +38,82 @@ class SupportPage extends StatelessWidget { color: Theme.of(context).colorScheme.surface, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Visibility( - visible: !isMobile, - child: SelectableText(LocaleKeys.support.tr(), - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w700)), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Visibility( + visible: !isMobile, + child: SelectableText( + LocaleKeys.support.tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), ), - const SizedBox( - height: 16, + ), + const SizedBox( + height: 16, + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(18.0), ), - Container( - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(18.0)), - child: Stack( - children: [ - const _DiscordIcon(), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 18.0, horizontal: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: isMobile ? 0 : 160), - child: SelectableText( - LocaleKeys.supportAskSpan.tr(), - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), + child: Stack( + children: [ + const _DiscordIcon(), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 18.0, + horizontal: 5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: isMobile ? 0 : 160), + child: SelectableText( + LocaleKeys.supportAskSpan.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, ), ), - const SizedBox( - height: 12, + ), + const SizedBox( + height: 12, + ), + UiBorderButton( + backgroundColor: Theme.of(context).colorScheme.surface, + prefix: Icon( + Icons.discord, + color: Theme.of(context).textTheme.bodyMedium?.color, ), - UiBorderButton( - backgroundColor: - Theme.of(context).colorScheme.surface, - prefix: Icon( - Icons.discord, - color: - Theme.of(context).textTheme.bodyMedium?.color, - ), - text: LocaleKeys.supportDiscordButton.tr(), - fontSize: isMobile ? 13 : 14, - width: 400, - height: 40, - allowMultiline: true, - onPressed: () { - launchURL('https://komodoplatform.com/discord'); - }) - ], - ), - ) - ], - ), - ), - const SizedBox(height: 20), - SelectableText( - LocaleKeys.supportFrequentlyQuestionSpan.tr(), - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + text: LocaleKeys.supportDiscordButton.tr(), + fontSize: isMobile ? 13 : 14, + width: 400, + height: 40, + allowMultiline: true, + onPressed: () { + launchURLString( + 'https://komodoplatform.com/discord', + ); + }, + ), + ], + ), + ), + ], ), - const SizedBox(height: 30), - /* + ), + const SizedBox(height: 20), + SelectableText( + LocaleKeys.supportFrequentlyQuestionSpan.tr(), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 30), + /* if (!isMobile) Flexible( child: DexScrollbar( @@ -121,16 +131,18 @@ class SupportPage extends StatelessWidget { ), if (isMobile) */ - Container( - padding: const EdgeInsets.fromLTRB(0, 0, 12, 0), - child: Column( - children: supportInfo.asMap().entries.map((entry) { + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 12, 0), + child: Column( + children: supportInfo.asMap().entries.map((entry) { return SupportItem( data: entry.value, ); - }).toList()), - ) - ]), + }).toList(), + ), + ), + ], + ), ); } } @@ -161,42 +173,42 @@ class _DiscordIcon extends StatelessWidget { final List> supportInfo = [ { 'title': LocaleKeys.supportInfoTitle1.tr(), - 'content': LocaleKeys.supportInfoContent1.tr() + 'content': LocaleKeys.supportInfoContent1.tr(), }, { 'title': LocaleKeys.supportInfoTitle2.tr(), - 'content': LocaleKeys.supportInfoContent2.tr() + 'content': LocaleKeys.supportInfoContent2.tr(), }, { 'title': LocaleKeys.supportInfoTitle3.tr(), - 'content': LocaleKeys.supportInfoContent3.tr() + 'content': LocaleKeys.supportInfoContent3.tr(), }, { 'title': LocaleKeys.supportInfoTitle4.tr(), - 'content': LocaleKeys.supportInfoContent4.tr() + 'content': LocaleKeys.supportInfoContent4.tr(), }, { 'title': LocaleKeys.supportInfoTitle5.tr(), - 'content': LocaleKeys.supportInfoContent5.tr() + 'content': LocaleKeys.supportInfoContent5.tr(), }, { 'title': LocaleKeys.supportInfoTitle6.tr(), - 'content': LocaleKeys.supportInfoContent6.tr() + 'content': LocaleKeys.supportInfoContent6.tr(), }, { 'title': LocaleKeys.supportInfoTitle7.tr(), - 'content': LocaleKeys.supportInfoContent7.tr() + 'content': LocaleKeys.supportInfoContent7.tr(), }, { 'title': LocaleKeys.supportInfoTitle8.tr(), - 'content': LocaleKeys.supportInfoContent8.tr() + 'content': LocaleKeys.supportInfoContent8.tr(), }, { 'title': LocaleKeys.supportInfoTitle9.tr(), - 'content': LocaleKeys.supportInfoContent9.tr() + 'content': LocaleKeys.supportInfoContent9.tr(), }, { 'title': LocaleKeys.supportInfoTitle10.tr(), - 'content': LocaleKeys.supportInfoContent10.tr() + 'content': LocaleKeys.supportInfoContent10.tr(), } ]; diff --git a/lib/views/wallet/coin_details/coin_details.dart b/lib/views/wallet/coin_details/coin_details.dart index 04779ae0d7..5c38f2a827 100644 --- a/lib/views/wallet/coin_details/coin_details.dart +++ b/lib/views/wallet/coin_details/coin_details.dart @@ -1,8 +1,9 @@ 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/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'; @@ -37,25 +38,21 @@ class _CoinDetailsState extends State { void initState() { _txHistoryBloc = context.read(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - context - .read() - .add(TransactionHistorySubscribe(coin: widget.coin)); + _txHistoryBloc.add(TransactionHistorySubscribe(coin: widget.coin)); }); super.initState(); } @override void dispose() { - _txHistoryBloc.add(TransactionHistoryUnsubscribe(coin: widget.coin)); + // _txHistoryBloc.add(TransactionHistoryUnsubscribe(coin: widget.coin)); super.dispose(); } @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(); }, ); @@ -72,9 +69,9 @@ class _CoinDetailsState extends State { case CoinPageType.send: return WithdrawForm( - coin: widget.coin, + asset: widget.coin.toSdkAsset(context.read()), + onSuccess: _openInfo, onBackButtonPressed: _openInfo, - onSuccess: () => _setPageType(CoinPageType.info), ); case CoinPageType.receive: diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart b/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart new file mode 100644 index 0000000000..2f0912743a --- /dev/null +++ b/lib/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart @@ -0,0 +1,114 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.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:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.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'; + +class AnimatedPortfolioCharts extends StatefulWidget { + const AnimatedPortfolioCharts({ + required this.tabController, + required this.walletCoinsFiltered, + super.key, + }); + + final TabController tabController; + final List walletCoinsFiltered; + + @override + State createState() => + _AnimatedPortfolioChartsState(); +} + +class _AnimatedPortfolioChartsState extends State { + bool _userHasInteracted = false; + + @override + void initState() { + super.initState(); + widget.tabController.addListener(_onTabChanged); + } + + @override + void dispose() { + widget.tabController.removeListener(_onTabChanged); + super.dispose(); + } + + void _onTabChanged() { + if (!_userHasInteracted) { + setState(() { + _userHasInteracted = true; + }); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bool shouldExpand = + state is PortfolioGrowthChartLoadSuccess || _userHasInteracted; + + return Column( + children: [ + Card( + child: TabBar( + controller: widget.tabController, + tabs: [ + Tab(text: LocaleKeys.portfolioGrowth.tr()), + Tab(text: LocaleKeys.profitAndLoss.tr()), + ], + ), + ), + AnimatedContainer( + key: const Key('animated_portfolio_charts_container'), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + height: shouldExpand ? 340 : 0, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration(), + child: Stack( + children: [ + TabBarView( + controller: widget.tabController, + children: [ + SizedBox( + width: double.infinity, + child: PortfolioGrowthChart( + initialCoins: widget.walletCoinsFiltered, + ), + ), + SizedBox( + width: double.infinity, + child: PortfolioProfitLossChart( + initialCoins: widget.walletCoinsFiltered, + ), + ), + ], + ), + if (state is! PortfolioGrowthChartLoadSuccess && + _userHasInteracted) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ), + ], + ); + }, + ); + } +} 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..7e956bcb45 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 @@ -2,11 +2,13 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.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_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'; @@ -88,24 +90,33 @@ class _PortfolioGrowthChartState extends State { NumberFormat.currency(symbol: '\$', decimalDigits: 2) .format(totalValue), ), - availableCoins: - widget.initialCoins.map((coin) => coin.abbr).toList(), + availableCoins: widget.initialCoins + .map( + (coin) => getSdkAsset( + context.read(), + coin.abbr, + ).id, + ) + .toList(), selectedCoinId: _singleCoinOrNull?.abbr, onCoinSelected: _isCoinPage ? null : _showSpecificCoin, centreAmount: totalValue, 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 +190,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..4c8f1cc842 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 @@ -2,12 +2,13 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.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_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.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/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/utils/prominent_colors.dart'; +import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; class PortfolioProfitLossChart extends StatefulWidget { @@ -37,7 +38,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).state.currentUser?.walletId.name; @override Widget build(BuildContext context) { @@ -102,8 +104,14 @@ class PortfolioProfitLossChartState extends State { ), leadingText: Text(formattedValue), emptySelectAllowed: !_isCoinPage, - availableCoins: - widget.initialCoins.map((coin) => coin.abbr).toList(), + availableCoins: widget.initialCoins + .map( + (coin) => getSdkAsset( + context.read(), + coin.abbr, + ).id, + ) + .toList(), selectedCoinId: _singleCoinOrNull?.abbr, onCoinSelected: _isCoinPage ? null : _showSpecificCoin, centreAmount: totalValue, 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..562b3b9fe5 --- /dev/null +++ b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart @@ -0,0 +1,515 @@ +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/komodo_defi_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 StatefulWidget { + const CoinAddresses({ + super.key, + required this.coin, + }); + + final Coin coin; + + @override + State createState() => _CoinAddressesState(); +} + +class _CoinAddressesState extends State { + late final CoinAddressesBloc _addressesBloc; + + @override + void initState() { + super.initState(); + final kdfSdk = RepositoryProvider.of(context); + _addressesBloc = CoinAddressesBloc( + kdfSdk, + widget.coin.abbr, + )..add(const LoadAddressesEvent()); + } + + @override + void dispose() { + _addressesBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return BlocProvider.value( + value: _addressesBloc, + 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: widget.coin, + ); + }, + ), + 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({ + required this.status, + required this.createAddressStatus, + required this.hideZeroBalance, + required this.cantCreateNewAddressReasons, + }); + + 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({ + super.key, + required this.status, + required this.createAddressStatus, + required this.cantCreateNewAddressReasons, + }); + + final FormStatus status; + final FormStatus createAddressStatus; + final Set? cantCreateNewAddressReasons; + + @override + Widget build(BuildContext context) { + final tooltipMessage = _getTooltipMessage(); + + return Tooltip( + message: tooltipMessage, + child: UiPrimaryButton( + height: 40, + borderRadius: 20, + 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({ + super.key, + required this.address, + required this.coinAbbr, + }); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: QrImageView( + 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 e5299bae79..385e53f73c 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'; @@ -14,44 +15,166 @@ 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.onClickSwapButton, required this.coin, - }) : super(key: key); + super.key, + }); final bool isMobile; final Coin coin; final void Function(CoinPageType) selectWidget; - final VoidCallback? clickSwapButton; + final VoidCallback? onClickSwapButton; @override Widget build(BuildContext context) { return isMobile - ? _buildMobileButtons(context) - : _buildDesktopButtons(context); + ? CoinDetailsCommonButtonsMobileLayout( + coin: coin, + isMobile: isMobile, + selectWidget: selectWidget, + clickSwapButton: onClickSwapButton, + context: context, + ) + : CoinDetailsCommonButtonsDesktopLayout( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + clickSwapButton: onClickSwapButton, + 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, + }); - Widget _buildDesktopButtons(BuildContext context) { + 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, + onClickSwapButton: 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; + + @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 && !kIsWalletOnly) 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, + onClickSwapButton: clickSwapButton, + context: context, + ), + ), if (coin.hasFaucet) Container( margin: const EdgeInsets.only(left: 21), @@ -73,87 +196,117 @@ 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 + //TODO!.sdk || coin.balance == 0 ? null : () { - selectWidget(CoinPageType.receive); + selectWidget(CoinPageType.send); }, - text: LocaleKeys.receive.tr(), + text: LocaleKeys.send.tr(), ); } +} - Widget _buildSwapButton(BuildContext context) { - if (currentWalletBloc.wallet?.config.type != WalletType.iguana) { +class CoinDetailsSwapButton extends StatelessWidget { + const CoinDetailsSwapButton({ + required this.isMobile, + required this.coin, + required this.onClickSwapButton, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final VoidCallback? onClickSwapButton; + final BuildContext context; + + @override + Widget build(BuildContext context) { + final currentWallet = context.watch().state.currentUser?.wallet; + if (currentWallet?.config.type != WalletType.iguana && + currentWallet?.config.type != WalletType.hdwallet) { return const SizedBox.shrink(); } @@ -171,7 +324,7 @@ class CoinDetailsCommonButtons 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 878dd0b905..dd3e7e41f7 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,17 @@ 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/komodo_defi_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/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 +27,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'; @@ -34,11 +36,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; @@ -50,14 +52,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).state.currentUser?.walletId.name; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); const selectedDurationInitial = Duration(hours: 1); final growthBloc = context.read(); @@ -67,7 +68,6 @@ class _CoinDetailsInfoState extends State fiatCoinId: 'USDT', selectedPeriod: selectedDurationInitial, walletId: _walletId!, - updateFrequency: const Duration(minutes: 1), ), ); @@ -83,12 +83,6 @@ class _CoinDetailsInfoState extends State ); } - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return PageLayout( @@ -117,7 +111,6 @@ class _CoinDetailsInfoState extends State selectedTransaction: _selectedTransaction, setPageType: widget.setPageType, setTransaction: _selectTransaction, - tabController: _tabController, ); } return _DesktopContent( @@ -125,7 +118,6 @@ class _CoinDetailsInfoState extends State selectedTransaction: _selectedTransaction, setPageType: widget.setPageType, setTransaction: _selectTransaction, - tabController: _tabController, ); } @@ -134,7 +126,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(); }, ); @@ -168,14 +161,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) { @@ -192,12 +183,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, @@ -213,28 +207,16 @@ 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( - mainAxisSize: MainAxisSize.max, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -267,46 +249,14 @@ class _DesktopCoinDetails extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: false, selectWidget: setPageType, - clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() ? null : () => _goToSwap(context, coin), coin: coin, ), ), 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), ], ), ); @@ -319,14 +269,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) { @@ -334,11 +282,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, @@ -347,8 +303,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( @@ -356,8 +325,6 @@ class _MobileContent extends StatelessWidget { borderRadius: BorderRadius.circular(18.0), ), child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, children: [ CoinIcon( coin.abbr, @@ -379,50 +346,95 @@ class _MobileContent extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: true, selectWidget: setPageType, - clickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() - ? null - : () => _goToSwap(context, coin), + onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), - if (!coin.walletOnly) _SwapButton(coin: coin), if (coin.hasFaucet) 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, @@ -482,16 +494,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, @@ -568,9 +581,12 @@ 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) { + if (coin.abbr == 'KMD' && + (walletType == WalletType.iguana || + walletType == WalletType.hdwallet)) { return _GetRewardsButton( coin: coin, onTap: () => selectWidget(CoinPageType.claim), @@ -636,33 +652,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/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..fb7feb41bc 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 @@ -23,8 +23,9 @@ class ContractAddressButton extends StatelessWidget { onTap: coin.explorerUrl.isEmpty ? null : () { - launchURL( - '${coin.explorerUrl}address/${coin.protocolData?.contractAddress ?? ''}'); + launchURLString( + '${coin.explorerUrl}address/${coin.protocolData?.contractAddress ?? ''}', + ); }, child: isMobile ? _ContractAddressMobile(coin) @@ -91,7 +92,7 @@ class _ContractAddressDesktop extends StatelessWidget { height: 16, child: _ContractAddressCopyButton(coin), ), - ) + ), ], ), ), @@ -133,12 +134,14 @@ class _ContractAddressValue extends StatelessWidget { ?.copyWith(fontWeight: FontWeight.w500, fontSize: 11), ), Flexible( - child: TruncatedMiddleText(coin.protocolData?.contractAddress ?? '', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: Theme.of(context).textTheme.bodyMedium?.color, - )), + child: TruncatedMiddleText( + coin.protocolData?.contractAddress ?? '', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), ), ], ); @@ -176,8 +179,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..3a01846956 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/qr_code_address.dart b/lib/views/wallet/coin_details/receive/qr_code_address.dart index d54948275d..e0b8330c40 100644 --- a/lib/views/wallet/coin_details/receive/qr_code_address.dart +++ b/lib/views/wallet/coin_details/receive/qr_code_address.dart @@ -23,7 +23,7 @@ class QRCodeAddress extends StatelessWidget { return ClipRRect( borderRadius: borderRadius ?? BorderRadius.circular(18.0), - child: QrImage( + child: QrImageView( size: size, foregroundColor: foregroundColor, backgroundColor: backgroundColor, 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_reward_info_header.dart b/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart index 710872265e..fa2f246f09 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart @@ -74,7 +74,7 @@ class KmdRewardInfoHeader extends StatelessWidget { style: const TextStyle(color: Colors.blue), recognizer: TapGestureRecognizer() ..onTap = () { - launchURL('https://www.coingecko.com'); + launchURLString('https://www.coingecko.com'); }, ), const TextSpan(text: ', '), @@ -83,7 +83,7 @@ class KmdRewardInfoHeader extends StatelessWidget { style: const TextStyle(color: Colors.blue), recognizer: TapGestureRecognizer() ..onTap = () { - launchURL('https://exchangeratesapi.io'); + launchURLString('https://exchangeratesapi.io'); }, ), const TextSpan(text: ')'), 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..a1475be911 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({ @@ -127,65 +129,69 @@ class _KmdRewardsInfoState extends State { ), const Spacer(), Container( - width: 350, - height: 177, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: theme.custom.userRewardBoxColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 7), - blurRadius: 10, - color: theme.custom.rewardBoxShadowColor) - ]), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.rewardBoxTitle.tr(), - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 20, - ), - ), - Text( - LocaleKeys.rewardBoxSubTitle.tr(), - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withOpacity(0.4), - ), + width: 350, + height: 177, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: theme.custom.userRewardBoxColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, 7), + blurRadius: 10, + color: theme.custom.rewardBoxShadowColor, + ), + ], + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.rewardBoxTitle.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, ), - const SizedBox( - height: 30.0, + ), + Text( + LocaleKeys.rewardBoxSubTitle.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.4), ), - UiBorderButton( - width: 160, - height: 38, - text: LocaleKeys.rewardBoxReadMore.tr(), - onPressed: () { - launchURL( - 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know'); - }, - ) - ], - ), + ), + const SizedBox( + height: 30.0, + ), + UiBorderButton( + width: 160, + height: 38, + text: LocaleKeys.rewardBoxReadMore.tr(), + onPressed: () { + launchURLString( + 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know', + ); + }, + ), + ], ), - const Positioned( - bottom: 0, - right: 0, - child: RewardBackground(), - ) - ], - )) + ), + const Positioned( + bottom: 0, + right: 0, + child: RewardBackground(), + ), + ], + ), + ), ], ), const SizedBox(height: 20), @@ -221,79 +227,85 @@ class _KmdRewardsInfoState extends State { mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - color: Theme.of(context).colorScheme.surface), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - height: 177, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: theme.custom.userRewardBoxColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 7), - blurRadius: 10, - color: theme.custom.rewardBoxShadowColor) - ]), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.rewardBoxTitle.tr(), - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 20, - ), - ), - Text( - LocaleKeys.rewardBoxSubTitle.tr(), - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withOpacity(0.3), - ), - ), - const SizedBox(height: 24.0), - UiBorderButton( - width: 160, - height: 38, - text: LocaleKeys.rewardBoxReadMore.tr(), - onPressed: () { - launchURL( - 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know'); - }, - ) - ], + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 177, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: theme.custom.userRewardBoxColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, 7), + blurRadius: 10, + color: theme.custom.rewardBoxShadowColor, + ), + ], + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.rewardBoxTitle.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, + ), ), - ), - const Positioned( - bottom: 0, - right: 0, - child: RewardBackground(), - ) - ], - )), - const SizedBox(height: 20.0), - _buildTotal(), - _buildMessage(), - const SizedBox(height: 20), - _buildControls(context), - ], - )), - Flexible(child: _buildContent(context)) + Text( + LocaleKeys.rewardBoxSubTitle.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.3), + ), + ), + const SizedBox(height: 24.0), + UiBorderButton( + width: 160, + height: 38, + text: LocaleKeys.rewardBoxReadMore.tr(), + onPressed: () { + launchURLString( + 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know', + ); + }, + ), + ], + ), + ), + const Positioned( + bottom: 0, + right: 0, + child: RewardBackground(), + ), + ], + ), + ), + const SizedBox(height: 20.0), + _buildTotal(), + _buildMessage(), + const SizedBox(height: 20), + _buildControls(context), + ], + ), + ), + Flexible(child: _buildContent(context)), ], ); } @@ -301,27 +313,27 @@ class _KmdRewardsInfoState extends State { Widget _buildRewardList(BuildContext context) { final scrollController = ScrollController(); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: [ - isDesktop ? _buildRewardListHeader(context) : const SizedBox(), - const SizedBox(height: 10), - Flexible( - child: DexScrollbar( - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - children: - (_rewards ?? []).map(_buildRewardLstItem).toList(), - ), + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop ? _buildRewardListHeader(context) : const SizedBox(), + const SizedBox(height: 10), + Flexible( + child: DexScrollbar( + scrollController: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: Column( + children: (_rewards ?? []).map(_buildRewardLstItem).toList(), ), ), ), - ], - )); + ), + ], + ), + ); } Widget _buildRewardListHeader(BuildContext context) { @@ -403,6 +415,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 +428,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; @@ -429,7 +444,9 @@ class _KmdRewardsInfoState extends State { } bool _rewardsEquals( - List previous, List current) { + List previous, + List current, + ) { if (previous.length != current.length) return false; for (int i = 0; i < previous.length; i++) { @@ -460,10 +477,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..3989a64d6e 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/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.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/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'; @@ -27,10 +28,11 @@ class TransactionDetails extends StatelessWidget { @override Widget build(BuildContext context) { final EdgeInsets padding = EdgeInsets.only( - top: isMobile ? 16 : 0, - left: 16, - right: 16, - bottom: isMobile ? 20 : 30); + top: isMobile ? 16 : 0, + left: 16, + right: 16, + bottom: isMobile ? 20 : 30, + ); final scrollController = ScrollController(); return DexScrollbar( @@ -88,7 +90,7 @@ class TransactionDetails extends StatelessWidget { _buildSimpleData( context, title: LocaleKeys.date.tr(), - value: transaction.formattedTime, + value: formatTransactionDateTime(transaction), hasBackground: true, ), _buildFee(context), @@ -107,7 +109,7 @@ class TransactionDetails extends StatelessWidget { _buildSimpleData( context, title: LocaleKeys.transactionHash.tr(), - value: transaction.txHash, + value: transaction.txHash ?? '', isCopied: true, ), const SizedBox(height: 20), @@ -123,8 +125,11 @@ class TransactionDetails extends StatelessWidget { ); } - Widget _buildAddress(BuildContext context, - {required String title, required String address}) { + Widget _buildAddress( + BuildContext context, { + required String title, + required String address, + }) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Row( @@ -169,7 +174,7 @@ class TransactionDetails extends StatelessWidget { _buildAddress( context, title: LocaleKeys.to.tr(), - address: transaction.toAddress, + address: transaction.to.first, ), ], ) @@ -192,7 +197,7 @@ class TransactionDetails extends StatelessWidget { child: _buildAddress( context, title: LocaleKeys.to.tr(), - address: transaction.toAddress, + address: transaction.to.first, ), ), ), @@ -202,14 +207,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 +242,7 @@ class TransactionDetails extends StatelessWidget { color: theme.custom.defaultGradientButtonTextColor, ), onPressed: () { - launchURL(getTxExplorerUrl(coin, transaction.txHash)); + launchURLString(getTxExplorerUrl(coin, transaction.txHash ?? '')); }, text: LocaleKeys.viewOnExplorer.tr(), ), @@ -258,10 +263,11 @@ class TransactionDetails extends StatelessWidget { } Widget _buildFee(BuildContext context) { - final String? fee = transaction.feeDetails.feeValue; - final String formattedFee = - getNumberWithoutExponent(double.parse(fee ?? '').abs().toString()); - final double? usd = coinsBloc.getUsdPriceByAmount(formattedFee, _feeCoin); + final coinsRepository = RepositoryProvider.of(context); + + final String formattedFee = transaction.fee?.formatTotal() ?? ''; + final double? usd = + coinsRepository.getUsdPriceByAmount(formattedFee, _feeCoin); final String formattedUsd = formatAmt(usd ?? 0); final String title = LocaleKeys.fees.tr(); @@ -392,8 +398,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..f7e53820cc 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/komodo_defi_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..f69680a286 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/komodo_defi_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; @@ -53,15 +61,15 @@ class _TransactionListRowState extends State { }, hoverColor: Theme.of(context).primaryColor.withAlpha(20), child: Container( - color: _hasFocus - ? Theme.of(context).colorScheme.tertiary - : Colors.transparent, - margin: const EdgeInsets.symmetric(vertical: 5), - padding: isMobile - ? const EdgeInsets.fromLTRB(0, 12, 0, 12) - : const EdgeInsets.fromLTRB(12, 12, 12, 12), - child: - isMobile ? _buildMobileRow(context) : _buildNormalRow(context)), + color: _hasFocus + ? Theme.of(context).colorScheme.tertiary + : Colors.transparent, + margin: EdgeInsets.symmetric(vertical: isMobile ? 5 : 0), + padding: isMobile + ? 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,13 +100,14 @@ 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 - : theme.custom.decreaseColor, - fontSize: 14, - fontWeight: FontWeight.w500), + color: _isReceived + ? theme.custom.increaseColor + : theme.custom.decreaseColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), ], ); @@ -112,33 +121,18 @@ 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), ), ], - ) + ), ], ); } - 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 +143,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 +157,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,16 +192,17 @@ class _TransactionListRowState extends State { return Row( mainAxisSize: MainAxisSize.max, children: [ - const SizedBox(width: 4), + Expanded(flex: 4, child: _buildAddress()), Expanded( - flex: 4, - child: Text( - _isReceived ? LocaleKeys.receive.tr() : LocaleKeys.send.tr(), - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - )), + flex: 4, + child: Text( + _isReceived ? LocaleKeys.receive.tr() : LocaleKeys.send.tr(), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), Expanded(flex: 4, child: _buildBalanceChanges()), Expanded(flex: 4, child: _buildUsdChanges()), Expanded(flex: 3, child: _buildMemoAndDate()), @@ -230,47 +215,66 @@ class _TransactionListRowState extends State { if (memo == null || memo.isEmpty) return const SizedBox(); return CustomTooltip( - maxWidth: 200, - tooltip: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${LocaleKeys.memo.tr()}:', - style: theme.currentGlobal.textTheme.bodyLarge, - ), - const SizedBox(height: 6), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 120), - child: SingleChildScrollView( - controller: ScrollController(), - child: Text( - memo, - style: const TextStyle(fontSize: 14), - ), + maxWidth: 200, + tooltip: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.memo.tr()}:', + style: theme.currentGlobal.textTheme.bodyLarge, + ), + const SizedBox(height: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: SingleChildScrollView( + controller: ScrollController(), + child: Text( + memo, + style: const TextStyle(fontSize: 14), ), ), - ], - ), - child: Icon( - Icons.note, - size: 14, - color: theme.currentGlobal.colorScheme.onSurface, - )); + ), + ], + ), + child: Icon( + Icons.note, + size: 14, + color: theme.currentGlobal.colorScheme.onSurface, + ), + ); } 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( '$_sign \$${formatAmt((usdChanges ?? 0).abs())}', style: TextStyle( - color: _isReceived - ? theme.custom.increaseColor - : theme.custom.decreaseColor, - fontSize: 14, - fontWeight: FontWeight.w500), + color: _isReceived + ? theme.custom.increaseColor + : theme.custom.decreaseColor, + fontSize: 14, + 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 ec77e62c5e..a63779c674 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_table.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_table.dart @@ -6,7 +6,7 @@ import 'package:komodo_ui_kit/komodo_ui_kit.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/komodo_defi_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,13 +68,13 @@ class TransactionTable extends StatelessWidget { Widget _buildTransactionList(BuildContext context) { return BlocBuilder( builder: (BuildContext ctx, TransactionHistoryState state) { - if (coin.isActivating || state is TransactionHistoryInitialState) { + if (state.transactions.isEmpty && state.loading) { return const SliverToBoxAdapter( child: UiSpinnerList(), ); } - if (state is TransactionHistoryFailureState) { + if (state.error != null) { return SliverToBoxAdapter( child: _ErrorMessage( text: LocaleKeys.connectionToServersFailing.tr(args: [coin.name]), @@ -83,25 +83,11 @@ class TransactionTable extends StatelessWidget { ); } - if (state is TransactionHistoryInProgressState) { - return _TransactionsListWrapper( - coinAbbr: coin.abbr, - setTransaction: setTransaction, - transactions: state.transactions, - isInProgress: true, - ); - } - - 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..d2fbdf7ad3 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 @@ -6,12 +6,13 @@ import 'package:web_dex/bloc/withdraw_form/withdraw_form_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:web_dex/model/text_error.dart'; import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/coin_details/constants.dart'; class FailedPage extends StatelessWidget { - const FailedPage(); + const FailedPage({super.key}); @override Widget build(BuildContext context) { @@ -60,8 +61,10 @@ class _SendErrorHeader extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - Text(LocaleKeys.errorDescription.tr(), - style: Theme.of(context).textTheme.bodyMedium), + Text( + LocaleKeys.errorDescription.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), ], ); } @@ -72,17 +75,23 @@ class _SendErrorBody extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.sendError.message, - builder: (BuildContext context, String errorText) { - final iconColor = - Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(.7); + return BlocSelector( + // TODO: Confirm this is the correct error + selector: (state) => state.transactionError, + builder: (BuildContext context, error) { + final iconColor = Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: .7); return Material( color: theme.custom.buttonColorDefault, borderRadius: BorderRadius.circular(18), child: InkWell( - onTap: () => copyToClipBoard(context, errorText), + onTap: error == null + ? null + : () => copyToClipBoard(context, error.error), borderRadius: BorderRadius.circular(18), child: Padding( padding: const EdgeInsets.all(20.0), @@ -92,7 +101,7 @@ class _SendErrorBody extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _MultilineText(errorText)), + Expanded(child: _MultilineText(error?.error ?? '')), const SizedBox(width: 16), Icon( Icons.copy_rounded, diff --git a/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart b/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart deleted file mode 100644 index b979076a5b..0000000000 --- a/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/common/screen.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/views/wallet/coin_details/constants.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart'; - -class FillFormPage extends StatelessWidget { - const FillFormPage(); - - @override - Widget build(BuildContext context) { - final double maxWidth = isMobile ? double.infinity : withdrawWidth; - final state = context.watch().state; - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - isMobile ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FillFormTitle(state.coin.abbr), - const SizedBox(height: 28), - if (state.coin.enabledType == WalletType.trezor) - Padding( - padding: const EdgeInsets.only(bottom: 20.0), - child: FillFormTrezorSenderAddress( - coin: state.coin, - addresses: state.senderAddresses, - selectedAddress: state.selectedSenderAddress, - ), - ), - FillFormRecipientAddress(), - const SizedBox(height: 20), - FillFormAmount(), - if (state.coin.isTxMemoSupported) - const Padding( - padding: EdgeInsets.only(top: 20), - child: FillFormMemo(), - ), - if (state.coin.isCustomFeeSupported) - Padding( - padding: const EdgeInsets.only(top: 9.0), - child: FillFormCustomFee(), - ), - const SizedBox(height: 10), - const FillFormError(), - ], - ), - const SizedBox(height: 10), - FillFormFooter(), - ], - ), - ); - } -} diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart index 8025a5cf3b..6faa12e407 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart @@ -7,7 +7,7 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_primary_button.dart'; class ConvertAddressButton extends StatelessWidget { - const ConvertAddressButton(); + const ConvertAddressButton({super.key}); @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class ConvertAddressButton extends StatelessWidget { ), onPressed: () => context .read() - .add(const WithdrawFormConvertAddress()), + .add(const WithdrawFormConvertAddressRequested()), ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart index a724bae060..0b9ebf28a8 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart @@ -5,7 +5,7 @@ import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class SellMaxButton extends StatefulWidget { - const SellMaxButton(); + const SellMaxButton({super.key}); @override State createState() => _SellMaxButtonState(); @@ -27,7 +27,7 @@ class _SellMaxButtonState extends State { }), onTap: () => context .read() - .add(WithdrawFormMaxTapped(isEnabled: !state.isMaxAmount)), + .add(WithdrawFormMaxAmountEnabled(!state.isMaxAmount)), borderRadius: BorderRadius.circular(7), child: Container( width: 46, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart index 88e8cfd506..12708dd114 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart @@ -1,6 +1,8 @@ +import 'package:decimal/decimal.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/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -8,6 +10,8 @@ import 'package:web_dex/shared/constants.dart'; import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; class CustomFeeFieldEVM extends StatefulWidget { + const CustomFeeFieldEVM({super.key}); + @override State createState() => _CustomFeeFieldEVMState(); } @@ -35,32 +39,59 @@ class _CustomFeeFieldEVMState extends State { } Widget _buildGasLimitField() { - return BlocSelector( - selector: (state) { - return state.gasLimitError.message; + return CustomNumericTextFormField( + controller: _gasLimitController, + validationMode: InputValidationMode.aggressive, + validator: (_) { + const error = null; //TODO!.SDK + if (error.isEmpty) return null; + return error; + }, + onChanged: (_) { + _change(); + }, + filteringRegExp: r'^(|[1-9]\d*)$', + style: _style, + hintText: LocaleKeys.gasLimit.tr(), + hintTextStyle: _hintTextStyle, + ); + } + + Widget _buildGasPriceField() { + return BlocConsumer( + listenWhen: (previous, current) => + // TODO!.SDK: Add custom fee error property error to state and add here + previous.customFee != current.customFee, + listener: (context, state) { + // }, builder: (context, error) { - return BlocSelector( + return BlocSelector( selector: (state) { - return state.customFee.gas?.toString() ?? ''; + if (state.customFee is! FeeInfoEthGas) return null; + return (state.customFee as FeeInfoEthGas); }, - builder: (context, gasLimit) { - _gasLimitController - ..text = gasLimit - ..selection = _gasLimitSelection; + builder: (context, fee) { + // final price = fee?.gasPrice.toString() ?? ''; + + // _gasPriceController + // ..text = price + // ..selection = _gasPriceSelection; return CustomNumericTextFormField( - controller: _gasLimitController, + controller: _gasPriceController, validationMode: InputValidationMode.aggressive, validator: (_) { + const error = null; //TODO!.SDK if (error.isEmpty) return null; return error; }, onChanged: (_) { _change(); }, - filteringRegExp: r'^(|[1-9]\d*)$', + filteringRegExp: numberRegExp.pattern, style: _style, - hintText: LocaleKeys.gasLimit.tr(), + hintText: LocaleKeys.gasPriceGwei.tr(), hintTextStyle: _hintTextStyle, ); }, @@ -69,50 +100,21 @@ class _CustomFeeFieldEVMState extends State { ); } - Widget _buildGasPriceField() { - return BlocSelector( - selector: (state) { - return state.gasLimitError.message; - }, builder: (context, error) { - return BlocSelector( - selector: (state) { - return state.customFee.gasPrice ?? ''; - }, - builder: (context, gasPrice) { - final String price = gasPrice; - - _gasPriceController - ..text = price - ..selection = _gasPriceSelection; - return CustomNumericTextFormField( - controller: _gasPriceController, - validationMode: InputValidationMode.aggressive, - validator: (_) { - if (error.isEmpty) return null; - return error; - }, - onChanged: (_) { - _change(); - }, - filteringRegExp: numberRegExp.pattern, - style: _style, - hintText: LocaleKeys.gasPriceGwei.tr(), - hintTextStyle: _hintTextStyle, - ); - }, - ); - }); - } - void _change() { setState(() { _gasLimitSelection = _gasLimitController.selection; _gasPriceSelection = _gasPriceController.selection; }); + final asset = context.read().state.asset; + context.read().add( - WithdrawFormCustomEvmFeeChanged( - gas: double.tryParse(_gasLimitController.text)?.toInt(), - gasPrice: _gasPriceController.text, + WithdrawFormCustomFeeChanged( + FeeInfo.ethGas( + coin: asset.id.id, + gas: double.tryParse(_gasLimitController.text)?.toInt() ?? 0, + gasPrice: + Decimal.tryParse(_gasPriceController.text) ?? Decimal.zero, + ), ), ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart index b53c5b11e1..a539657ab3 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart @@ -1,15 +1,20 @@ +import 'package:decimal/decimal.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/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_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/model/coin.dart'; +import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/constants.dart'; import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; class CustomFeeFieldUtxo extends StatefulWidget { + const CustomFeeFieldUtxo({super.key}); + @override State createState() => _CustomFeeFieldUtxoState(); } @@ -21,18 +26,19 @@ class _CustomFeeFieldUtxoState extends State { @override Widget build(BuildContext context) { final style = TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - color: Theme.of(context).textTheme.bodyMedium?.color); + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).textTheme.bodyMedium?.color, + ); - return BlocSelector( + return BlocSelector( selector: (state) { - return state.utxoCustomFeeError; + return state.customFeeError; }, builder: (context, customFeeError) { return BlocSelector( selector: (state) { - return state.customFee.amount; + return state.customFee?.formatTotal(); }, builder: (context, feeAmount) { final amount = feeAmount ?? ''; @@ -44,24 +50,31 @@ class _CustomFeeFieldUtxoState extends State { controller: _feeController, validationMode: InputValidationMode.aggressive, validator: (_) { - if (customFeeError.message.isEmpty) return null; - return customFeeError.message; + if (customFeeError?.message.isEmpty ?? true) return null; + return customFeeError!.message; }, onChanged: (String? value) { setState(() { _previousTextSelection = _feeController.selection; }); + final asset = context.read().state.asset; + final feeInfo = FeeInfo.utxoFixed( + coin: asset.id.id, + amount: Decimal.tryParse(value ?? '0') ?? Decimal.zero, + ); context .read() - .add(WithdrawFormCustomFeeChanged(amount: value ?? '')); + .add(WithdrawFormCustomFeeChanged(feeInfo)); }, filteringRegExp: numberRegExp.pattern, style: style, - hintText: LocaleKeys.customFeeCoin.tr(args: [ - Coin.normalizeAbbr( - context.read().state.coin.abbr, - ) - ]), + hintText: LocaleKeys.customFeeCoin.tr( + args: [ + Coin.normalizeAbbr( + context.read().state.asset.id.id, + ), + ], + ), hintTextStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart index 07c7c35476..f83cea7e3d 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart @@ -2,14 +2,16 @@ 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/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/fee_type.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart'; class FillFormCustomFee extends StatefulWidget { + const FillFormCustomFee({super.key}); + @override State createState() => _FillFormCustomFeeState(); } @@ -19,7 +21,7 @@ class _FillFormCustomFeeState extends State { @override void initState() { - _isOpen = context.read().state.isCustomFeeEnabled; + _isOpen = context.read().state.isCustomFee; super.initState(); } @@ -29,20 +31,21 @@ class _FillFormCustomFeeState extends State { radius: 18, onTap: () { final bool newOpenState = !_isOpen; - context.read().add(newOpenState - ? const WithdrawFormCustomFeeEnabled() - : const WithdrawFormCustomFeeDisabled()); + context + .read() + .add(WithdrawFormCustomFeeEnabled(_isOpen)); setState(() { _isOpen = newOpenState; }); }, child: Container( - width: double.infinity, - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(18)), - color: Colors.transparent, - ), - child: _isOpen ? _Expanded() : _Collapsed()), + width: double.infinity, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(18)), + color: Colors.transparent, + ), + child: _isOpen ? _Expanded() : _Collapsed(), + ), ); } } @@ -56,9 +59,9 @@ class _Collapsed extends StatelessWidget { width: double.infinity, height: 25, decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: - Border.all(color: theme.custom.specificButtonBorderColor)), + borderRadius: const BorderRadius.all(Radius.circular(18)), + border: Border.all(color: theme.custom.specificButtonBorderColor), + ), child: const Padding( padding: EdgeInsets.only(left: 13, right: 13), child: _Header( @@ -79,9 +82,9 @@ class _Expanded extends StatelessWidget { Container( width: double.infinity, decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: - Border.all(color: theme.custom.specificButtonBorderColor)), + borderRadius: const BorderRadius.all(Radius.circular(18)), + border: Border.all(color: theme.custom.specificButtonBorderColor), + ), child: Padding( padding: const EdgeInsets.only(left: 13, right: 13), child: Column( @@ -167,10 +170,12 @@ class _FeeAmount extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (ctx, state) { - final isUtxo = state.customFee.type == feeType.utxoFixed; + builder: (ctx, state) { + // TODO! Handle both fixed and perkb + final isUtxo = state.customFee is FeeInfoUtxoFixed; - return isUtxo ? CustomFeeFieldUtxo() : CustomFeeFieldEVM(); - }); + return isUtxo ? const CustomFeeFieldUtxo() : const CustomFeeFieldEVM(); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart new file mode 100644 index 0000000000..4e6c976951 --- /dev/null +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart @@ -0,0 +1,632 @@ +// TODO! Separate out into individual files and remove unused fields +// form_fields.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; + +class ToAddressField extends StatelessWidget { + const ToAddressField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-recipient-address-input'), + autocorrect: false, + textInputAction: TextInputAction.next, + enableInteractiveSelection: true, + onChanged: (value) { + context + .read() + .add(WithdrawFormRecipientChanged(value ?? '')); + }, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter recipient address'; + } + return null; + }, + labelText: 'Recipient Address', + hintText: 'Enter recipient address', + suffixIcon: IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () async { + // TODO: Implement QR scanner + }, + ), + ); + }, + ); + } +} + +class AmountField extends StatelessWidget { + const AmountField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + UiTextFormField( + key: const Key('withdraw-amount-input'), + enabled: !state.isMaxAmount, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: currencyInputFormatters, + textInputAction: TextInputAction.next, + onChanged: (value) { + context + .read() + .add(WithdrawFormAmountChanged(value ?? '')); + }, + validator: (value) { + if (state.isMaxAmount) return null; + if (value?.isEmpty ?? true) return 'Please enter an amount'; + + final amount = Decimal.tryParse(value!); + if (amount == null) return 'Please enter a valid number'; + if (amount <= Decimal.zero) { + return 'Amount must be greater than 0'; + } + return null; + }, + labelText: 'Amount', + hintText: 'Enter amount to send', + suffix: Text(state.asset.id.id), + ), + CheckboxListTile( + value: state.isMaxAmount, + onChanged: (value) { + context + .read() + .add(WithdrawFormMaxAmountEnabled(value ?? false)); + }, + title: const Text('Send maximum amount'), + ), + ], + ); + }, + ); + } +} + +/// Fee configuration section +class FeeSection extends StatelessWidget { + const FeeSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Network Fee', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + const CustomFeeToggle(), + if (state.isCustomFee) ...[ + const SizedBox(height: 8), + _buildFeeFields(context, state), + const SizedBox(height: 8), + // Fee summary display + // if (state.customFee != null) ...[ + // const Divider(), + // _buildFeeSummary(context, state.customFee!, state.asset), + // ], + ], + ], + ); + }, + ); + } + + Widget _buildFeeSummary(BuildContext context, FeeInfo fee, Asset asset) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Fee Summary', + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: 4), + if (fee is FeeInfoEthGas) ...[ + Text( + 'Gas: ${fee.gas} units @ ${fee.gasPrice} Gwei', + style: theme.textTheme.bodySmall, + ), + ], + Text( + 'Total Fee: ${fee.totalFee} ${asset.id.id}', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + Widget _buildFeeFields(BuildContext context, WithdrawFormState state) { + final protocol = state.asset.protocol; + + if (protocol is Erc20Protocol) { + return const EvmFeeFields(); + } else if (protocol is UtxoProtocol) { + return const UtxoFeeFields(); + } + + return const SizedBox.shrink(); + } +} + +/// Toggle for enabling custom fee configuration +class CustomFeeToggle extends StatelessWidget { + const CustomFeeToggle({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: const Text('Custom fee'), + value: state.isCustomFee, + onChanged: (value) { + context.read().add( + WithdrawFormCustomFeeEnabled(value), + ); + }, + contentPadding: EdgeInsets.zero, + ); + }, + ); + } +} + +/// EVM-specific fee configuration fields (gas price & limit) +class EvmFeeFields extends StatelessWidget { + const EvmFeeFields({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final evmFee = state.customFee as FeeInfoEthGas?; + + return Column( + children: [ + UiTextFormField( + labelText: 'Gas Price (Gwei)', + keyboardType: TextInputType.number, + initialValue: evmFee?.gasPrice.toString(), + onChanged: (value) { + final gasPrice = Decimal.tryParse(value ?? ''); + if (gasPrice != null) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoEthGas( + coin: state.asset.id.id, + gasPrice: gasPrice, + gas: evmFee?.gas ?? 21000, + ), + ), + ); + } + }, + helperText: 'Higher gas price = faster confirmation', + ), + const SizedBox(height: 8), + UiTextFormField( + labelText: 'Gas Limit', + keyboardType: TextInputType.number, + initialValue: evmFee?.gas.toString() ?? '21000', + onChanged: (value) { + final gas = int.tryParse(value ?? ''); + if (gas != null) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoEthGas( + coin: state.asset.id.id, + gasPrice: evmFee?.gasPrice ?? Decimal.one, + gas: gas, + ), + ), + ); + } + }, + helperText: 'Estimated: 21000', + ), + ], + ); + }, + ); + } +} + +/// UTXO-specific fee configuration with predefined tiers +class UtxoFeeFields extends StatelessWidget { + const UtxoFeeFields({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final protocol = state.asset.protocol as UtxoProtocol; + final defaultFee = protocol.txFee ?? 10000; + final currentFee = state.customFee as FeeInfoUtxoFixed?; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SegmentedButton( + segments: [ + ButtonSegment( + value: defaultFee, + label: Text('Standard ($defaultFee)'), + ), + ButtonSegment( + value: defaultFee * 2, + label: Text('Fast (${defaultFee * 2})'), + ), + ButtonSegment( + value: defaultFee * 5, + label: Text('Urgent (${defaultFee * 5})'), + ), + ], + selected: { + currentFee?.amount.toBigInt().toInt() ?? defaultFee, + }, + onSelectionChanged: (values) { + if (values.isNotEmpty) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoUtxoFixed( + coin: state.asset.id.id, + amount: Decimal.fromInt(values.first), + ), + ), + ); + } + }, + ), + const SizedBox(height: 8), + Text( + 'Higher fee = faster confirmation', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + }, + ); + } +} + +/// Field for entering transaction memo +class MemoField extends StatelessWidget { + const MemoField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-memo-input'), + labelText: 'Memo (Optional)', + maxLines: 2, + onChanged: (value) { + context.read().add( + WithdrawFormMemoChanged(value ?? ''), + ); + }, + helperText: 'Required for some exchanges', + ); + }, + ); + } +} + +/// Preview button to initiate withdrawal confirmation +class PreviewButton extends StatelessWidget { + const PreviewButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + // Wrap with SizedBox + width: double.infinity, // Take full width + height: 48.0, // Fixed height + child: FilledButton.icon( + onPressed: state.isSending + ? null + : () => context.read().add( + const WithdrawFormPreviewSubmitted(), + ), + icon: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.send), + label: Text( + state.isSending ? 'Loading...' : 'Preview Withdrawal', + ), + ), + ); + }, + ); + } +} + +/// Page for confirming withdrawal details +class ConfirmationPage extends StatelessWidget { + const ConfirmationPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.preview == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ConfirmationItem( + label: 'From', + value: state.selectedSourceAddress?.address ?? + 'Default Wallet', + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'To', + value: state.recipientAddress, + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Amount', + value: + '${state.preview!.balanceChanges.netChange.abs()} ${state.asset.id.id}', + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Network Fee', + value: state.preview!.fee.formatTotal(), + ), + if (state.memo != null) ...[ + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Memo', + value: state.memo!, + ), + ], + ], + ), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context.read().add( + const WithdrawFormCancelled(), + ), + child: const Text('Back'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + //TODO! onPressed: state.submissionInProgress + onPressed: state.isSending + ? null + : () => context.read().add( + const WithdrawFormSubmitted(), + ), + //TODO! child: state.submissionInProgress + child: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Confirm'), + ), + ), + ], + ), + ], + ); + }, + ); + } +} + +/// Helper widget for displaying confirmation details +class _ConfirmationItem extends StatelessWidget { + final String label; + final String value; + + const _ConfirmationItem({ + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ); + } +} + +/// Page showing successful withdrawal +class SuccessPage extends StatelessWidget { + const SuccessPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + 'Withdrawal Successful', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Transaction Hash:', + style: Theme.of(context).textTheme.bodySmall, + ), + SelectableText( + state.result!.txHash, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Done'), + ), + ], + ); + }, + ); + } +} + +/// Page showing withdrawal failure +class FailurePage extends StatelessWidget { + const FailurePage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 24), + Text( + 'Withdrawal Failed', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(height: 16), + if (state.transactionError != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + state.transactionError!.error, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(height: 24), + OutlinedButton( + onPressed: () => context.read().add( + const WithdrawFormCancelled(), + ), + child: const Text('Try Again'), + ), + ], + ); + }, + ); + } +} + +class IbcTransferField extends StatelessWidget { + const IbcTransferField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: const Text('IBC Transfer'), + subtitle: const Text('Send to another Cosmos chain'), + value: state.isIbcTransfer, + onChanged: (value) { + context + .read() + .add(WithdrawFormIbcTransferEnabled(value)); + }, + ); + }, + ); + } +} + +class IbcChannelField extends StatelessWidget { + const IbcChannelField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-ibc-channel-input'), + labelText: 'IBC Channel', + hintText: 'Enter IBC channel ID', + onChanged: (value) { + context + .read() + .add(WithdrawFormIbcChannelChanged(value ?? '')); + }, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter IBC channel'; + } + return null; + }, + ); + }, + ); + } +} diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart index f1f6d8f5ab..a903af25c7 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart @@ -9,6 +9,8 @@ import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart'; class FillFormAmount extends StatefulWidget { + const FillFormAmount({super.key}); + @override State createState() => _FillFormAmountState(); } @@ -48,14 +50,10 @@ class _FillFormAmountState extends State { }); context .read() - .add(WithdrawFormAmountChanged(amount: amount ?? '')); + .add(WithdrawFormAmountChanged(amount ?? '')); }, validationMode: InputValidationMode.aggressive, - validator: (_) { - final String amountError = state.amountError.message; - if (amountError.isEmpty) return null; - return amountError; - }, + validator: (_) => state.amountError?.message, ); }, ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart index c21e4e6a95..778841d971 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart @@ -1,26 +1,29 @@ 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:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -class FillFormMemo extends StatelessWidget { - const FillFormMemo({Key? key}) : super(key: key); +class WithdrawMemoField extends StatelessWidget { + final String? memo; + final ValueChanged? onChanged; + + const WithdrawMemoField({ + required this.memo, + required this.onChanged, + super.key, + }); @override Widget build(BuildContext context) { return UiTextFormField( key: const Key('withdraw-form-memo-field'), + initialValue: memo, + maxLines: 2, + onChanged: onChanged == null ? null : (v) => onChanged!(v ?? ''), autocorrect: false, textInputAction: TextInputAction.next, enableInteractiveSelection: true, - onChanged: (String? memo) { - context - .read() - .add(WithdrawFormMemoUpdated(text: memo)); - }, inputFormatters: [LengthLimitingTextInputFormatter(256)], maxLength: 256, counterText: '', diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart index bbd3bcec6f..8388d1b889 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart @@ -6,14 +6,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/views/qr_scanner.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class FillFormRecipientAddress extends StatefulWidget { + const FillFormRecipientAddress({super.key}); + @override State createState() => _FillFormRecipientAddressState(); @@ -26,15 +28,14 @@ class _FillFormRecipientAddressState extends State { @override Widget build(BuildContext context) { - return BlocSelector( + return BlocSelector( selector: (state) { - return state.addressError; + //TODO! return state.addressError; + return state.amountError; }, builder: (context, addressError) { return BlocSelector( - selector: (state) { - return state.address; - }, + selector: (state) => state.recipientAddress, builder: (context, address) { _addressController ..text = address @@ -51,40 +52,45 @@ class _FillFormRecipientAddressState extends State { setState(() { _previousTextSelection = _addressController.selection; }); - context.read().add( - WithdrawFormAddressChanged(address: address ?? '')); + context + .read() + .add(WithdrawFormRecipientChanged(address ?? '')); }, validator: (String? value) { - if (addressError.message.isEmpty) return null; + if (addressError?.message.isEmpty ?? true) return null; if (addressError is MixedCaseAddressError) { return null; } - return addressError.message; + return addressError!.message; }, validationMode: InputValidationMode.aggressive, inputFormatters: [LengthLimitingTextInputFormatter(256)], hintText: LocaleKeys.recipientAddress.tr(), hintTextStyle: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), - suffixIcon: - (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) - ? IconButton( - icon: const Icon(Icons.qr_code_scanner), - onPressed: () async { - final address = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const QrScanner()), - ); + fontSize: 14, + fontWeight: FontWeight.w500, + ), + suffixIcon: (!kIsWeb && + (Platform.isAndroid || Platform.isIOS)) + ? IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () async { + final address = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const QrCodeReaderOverlay(), + ), + ); - if (context.mounted) { - context.read().add( - WithdrawFormAddressChanged( - address: address ?? '')); - } - }, - ) - : null, + if (context.mounted) { + context.read().add( + WithdrawFormRecipientChanged(address ?? ''), + ); + } + }, + ) + : null, ), if (addressError is MixedCaseAddressError) _ErrorAddressRow( @@ -121,7 +127,7 @@ class _ErrorAddressRow extends StatelessWidget { const Padding( padding: EdgeInsets.only(left: 6.0), child: ConvertAddressButton(), - ) + ), ], ), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart index fd980bdea6..5dbb5b5c92 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart @@ -8,6 +8,7 @@ import 'package:web_dex/views/wallet/coin_details/constants.dart'; class FillFormTrezorSenderAddress extends StatelessWidget { const FillFormTrezorSenderAddress({ + super.key, required this.coin, required this.addresses, required this.selectedAddress, @@ -26,7 +27,7 @@ class FillFormTrezorSenderAddress extends StatelessWidget { onChanged: (String address) { context .read() - .add(WithdrawFormSenderAddressChanged(address: address)); + .add(WithdrawFormRecipientChanged(address)); }, maxWidth: withdrawWidth, maxHeight: 300, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart index 66a932cc24..5c805de4e5 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart @@ -8,41 +8,43 @@ import 'package:web_dex/shared/widgets/copied_text.dart'; import 'package:web_dex/shared/widgets/details_dropdown.dart'; class FillFormError extends StatelessWidget { - const FillFormError(); + const FillFormError({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (ctx, state) { - if (!state.hasSendError) { - return const SizedBox(); - } - final BaseError sendError = state.sendError; - return Column( - children: [ - SizedBox( - width: double.infinity, - child: SelectableText( - sendError.message, - textAlign: TextAlign.left, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + builder: (ctx, state) { + if (!state.hasTransactionError) { + return const SizedBox(); + } + final BaseError sendError = state.transactionError!; + return Column( + children: [ + SizedBox( + width: double.infinity, + child: SelectableText( + sendError.message, + textAlign: TextAlign.left, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ), - ), - if (sendError is ErrorWithDetails) - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: DetailsDropdown( - summary: LocaleKeys.showMore.tr(), - content: SingleChildScrollView( - controller: ScrollController(), - child: CopiedText( - copiedValue: (sendError as ErrorWithDetails).details), + if (sendError is ErrorWithDetails) + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: DetailsDropdown( + summary: LocaleKeys.showMore.tr(), + content: SingleChildScrollView( + controller: ScrollController(), + child: CopiedText( + copiedValue: (sendError as ErrorWithDetails).details, + ), + ), ), ), - ) - ], - ); - }); + ], + ); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart index 367aeee033..ee2d590778 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart @@ -9,6 +9,8 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_for import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class FillFormFooter extends StatelessWidget { + const FillFormFooter({super.key}); + @override Widget build(BuildContext context) { return BlocBuilder( @@ -16,7 +18,10 @@ class FillFormFooter extends StatelessWidget { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: withdrawWidth), child: state.isSending - ? FillFormPreloader(state.trezorProgressStatus) + ? + //TODO(@takenagain): Trezor SDK support + // FillFormPreloader(state.trezorProgressStatus) + const FillFormPreloader('Sending') : UiBorderButton( key: const Key('send-enter-button'), backgroundColor: Theme.of(context).colorScheme.surface, 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_complete_form/send_complete_form.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart index 563de41da5..4a75d966a9 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart @@ -2,6 +2,7 @@ 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/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -13,7 +14,7 @@ import 'package:web_dex/views/wallet/coin_details/constants.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart'; class SendCompleteForm extends StatelessWidget { - const SendCompleteForm(); + const SendCompleteForm({super.key}); @override Widget build(BuildContext context) { @@ -24,6 +25,10 @@ class SendCompleteForm extends StatelessWidget { return BlocBuilder( builder: (context, WithdrawFormState state) { + final feeValue = state.result?.fee; + + if (state.result == null) return const SizedBox.shrink(); + return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -36,7 +41,7 @@ class SendCompleteForm extends StatelessWidget { children: [ SendConfirmItem( title: LocaleKeys.recipientAddress.tr(), - value: state.withdrawDetails.toAddress, + value: state.result!.toAddress, centerAlign: true, ), const SizedBox(height: 7), @@ -45,34 +50,37 @@ class SendCompleteForm extends StatelessWidget { children: [ const SizedBox(height: 10), SelectableText( - '-${state.amount} ${Coin.normalizeAbbr(state.coin.abbr)}', + '-${state.amount} ${Coin.normalizeAbbr(state.asset.id.id)}', style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.w700, - color: theme.custom.headerFloatBoxColor), + fontSize: 25, + fontWeight: FontWeight.w700, + color: theme.custom.headerFloatBoxColor, + ), ), const SizedBox(height: 5), SelectableText( '\$${state.usdAmountPrice ?? 0}', style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.custom.headerFloatBoxColor), + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.custom.headerFloatBoxColor, + ), ), ], ), - if (state.hasSendError) - _SendCompleteError(error: state.sendError), + if (state.hasTransactionError) + _SendCompleteError(error: state.transactionError!), ], ), ), - _TransactionHash( - feeValue: state.withdrawDetails.feeValue, - feeCoin: state.withdrawDetails.feeCoin, - txHash: state.withdrawDetails.txHash, - usdFeePrice: state.usdFeePrice, - isFeePriceExpensive: state.isFeePriceExpensive, - ), + if (state.result?.txHash != null) + _TransactionHash( + feeValue: feeValue!.formatTotal(), + feeCoin: feeValue.coin, + txHash: state.result!.txHash, + usdFeePrice: state.usdFeePrice, + isFeePriceExpensive: state.isFeePriceExpensive, + ), ], ); }, @@ -149,18 +157,20 @@ class _BuildMemo extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( - selector: (state) { - return state.memo; - }, builder: (context, memo) { - if (memo == null || memo.isEmpty) return const SizedBox.shrink(); + selector: (state) { + return state.memo; + }, + builder: (context, memo) { + if (memo == null || memo.isEmpty) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(bottom: 21), - child: SendConfirmItem( - title: '${LocaleKeys.memo.tr()}:', - value: memo, - ), - ); - }); + return Padding( + padding: const EdgeInsets.only(bottom: 21), + child: SendConfirmItem( + title: '${LocaleKeys.memo.tr()}:', + value: memo, + ), + ); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart index 40cfa81f11..420817c359 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.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/withdraw_form/withdraw_form_bloc.dart'; @@ -30,32 +31,36 @@ class _MobileButtons extends StatelessWidget { final WithdrawFormBloc withdrawFormBloc = context.read(); final WithdrawFormState state = withdrawFormBloc.state; - return Row(children: [ - Expanded( - child: AppDefaultButton( - key: const Key('send-complete-view-on-explorer'), - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: () => viewHashOnExplorer( - state.coin, - state.withdrawDetails.txHash, - HashExplorerType.tx, + final txHash = state.result?.txHash; + + final explorerUrl = + txHash == null ? null : state.asset.protocol.explorerTxUrl(txHash); + + return Row( + children: [ + if (explorerUrl != null) + Expanded( + child: AppDefaultButton( + key: const Key('send-complete-view-on-explorer'), + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: () => launchUrl(explorerUrl), + text: LocaleKeys.viewOnExplorer.tr(), + ), ), - text: LocaleKeys.viewOnExplorer.tr(), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: UiPrimaryButton( - key: const Key('send-complete-done'), - height: height, - onPressed: () => withdrawFormBloc.add(const WithdrawFormReset()), - text: LocaleKeys.done.tr(), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: UiPrimaryButton( + key: const Key('send-complete-done'), + height: height, + onPressed: () => withdrawFormBloc.add(const WithdrawFormReset()), + text: LocaleKeys.done.tr(), + ), ), ), - ), - ]); + ], + ); } } @@ -73,15 +78,16 @@ class _DesktopButtons extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - AppDefaultButton( - key: const Key('send-complete-view-on-explorer'), - width: width, - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: () => viewHashOnExplorer( - state.coin, state.withdrawDetails.txHash, HashExplorerType.tx), - text: LocaleKeys.viewOnExplorer.tr(), - ), + if (state.result?.txHash != null) + AppDefaultButton( + key: const Key('send-complete-view-on-explorer'), + width: width, + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: () => + openUrl(state.asset.txExplorerUrl(state.result?.txHash)!), + text: LocaleKeys.viewOnExplorer.tr(), + ), Padding( padding: const EdgeInsets.only(left: space), child: UiPrimaryButton( @@ -91,7 +97,7 @@ class _DesktopButtons extends StatelessWidget { onPressed: () => _sendCompleteDone(context), text: LocaleKeys.done.tr(), ), - ) + ), ], ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart index e219e1e065..27395b4d38 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart @@ -9,8 +9,11 @@ import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/views/wallet/coin_details/constants.dart'; class SendConfirmButtons extends StatelessWidget { - const SendConfirmButtons( - {required this.hasSendError, required this.onBackTap}); + const SendConfirmButtons({ + super.key, + required this.hasSendError, + required this.onBackTap, + }); final bool hasSendError; final VoidCallback onBackTap; @override @@ -33,31 +36,33 @@ class _MobileButtons extends StatelessWidget { Widget build(BuildContext context) { const height = 52.0; - return Row(children: [ - Expanded( - child: AppDefaultButton( - key: const Key('confirm-back-button'), - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: onBackTap, - text: LocaleKeys.back.tr(), - ), - ), - if (!hasError) + return Row( + children: [ Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: UiPrimaryButton( - key: const Key('confirm-agree-button'), - height: height, - onPressed: () => context - .read() - .add(const WithdrawFormSendRawTx()), - text: LocaleKeys.confirm.tr(), - ), + child: AppDefaultButton( + key: const Key('confirm-back-button'), + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: onBackTap, + text: LocaleKeys.back.tr(), ), ), - ]); + if (!hasError) + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: UiPrimaryButton( + key: const Key('confirm-agree-button'), + height: height, + onPressed: () => context + .read() + .add(const WithdrawFormSubmitted()), + text: LocaleKeys.confirm.tr(), + ), + ), + ), + ], + ); } } @@ -92,10 +97,10 @@ class _DesktopButtons extends StatelessWidget { height: height, onPressed: () => context .read() - .add(const WithdrawFormSendRawTx()), + .add(const WithdrawFormSubmitted()), text: LocaleKeys.confirm.tr(), ), - ) + ), ], ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart index 4a459d4541..2203cf6368 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart @@ -7,7 +7,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_con import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class SendConfirmFooter extends StatelessWidget { - const SendConfirmFooter(); + const SendConfirmFooter({super.key}); @override Widget build(BuildContext context) { @@ -21,10 +21,9 @@ class SendConfirmFooter extends StatelessWidget { child: Center(child: UiSpinner()), ) : SendConfirmButtons( - hasSendError: state.hasSendError, + hasSendError: state.hasTransactionError, onBackTap: () => context.read().add( - const WithdrawFormStepReverted( - step: WithdrawFormStep.confirm), + const WithdrawFormStepReverted(), ), ), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart index ae6b8304b2..f4a70a082c 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart @@ -2,6 +2,7 @@ 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/utils.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -13,7 +14,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_con import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart'; class SendConfirmForm extends StatelessWidget { - const SendConfirmForm(); + const SendConfirmForm({super.key}); @override Widget build(BuildContext context) { @@ -25,9 +26,15 @@ class SendConfirmForm extends StatelessWidget { return BlocBuilder( builder: (context, WithdrawFormState state) { final amountString = - '${truncateDecimal(state.amountToSendString, decimalRange)} ${Coin.normalizeAbbr(state.withdrawDetails.coin)}'; - final feeString = - '${truncateDecimal(state.withdrawDetails.feeValue, decimalRange)} ${Coin.normalizeAbbr(state.withdrawDetails.feeCoin)}'; + '${truncateDecimal(state.amount, decimalRange)} ${Coin.normalizeAbbr(state.result!.coin)}'; + + // Use the new formatting extension for fees + final feeString = state.preview?.fee.formatTotal( + precision: decimalRange, + ); + + // Use the isExpensive helper for warnings + final isFeePriceExpensive = state.preview?.fee.isHighFee ?? false; return Container( width: isMobile ? double.infinity : withdrawWidth, @@ -38,7 +45,7 @@ class SendConfirmForm extends StatelessWidget { children: [ SendConfirmItem( title: '${LocaleKeys.recipientAddress.tr()}:', - value: state.withdrawDetails.toAddress, + value: state.result!.toAddress, centerAlign: false, ), const SizedBox(height: 26), @@ -50,9 +57,9 @@ class SendConfirmForm extends StatelessWidget { const SizedBox(height: 26), SendConfirmItem( title: '${LocaleKeys.fee.tr()}:', - value: feeString, + value: feeString ?? '', usdPrice: state.usdFeePrice ?? 0, - isWarningShown: state.isFeePriceExpensive, + isWarningShown: isFeePriceExpensive, ), if (state.memo != null) Padding( diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart index dfd57bf00a..1105d2a541 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart @@ -1,28 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; class SendConfirmFormError extends StatelessWidget { - const SendConfirmFormError(); + const SendConfirmFormError({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, WithdrawFormState state) { - final BaseError sendError = state.sendError; + builder: (BuildContext context, WithdrawFormState state) { + final sendError = state.transactionError; - return Container( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - width: double.infinity, - child: Text( - sendError.message, - textAlign: TextAlign.left, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + return Container( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + width: double.infinity, + child: Text( + sendError?.message ?? 'Unknown error', + textAlign: TextAlign.left, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), - ), - ); - }); + ); + }, + ); } } 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/widgets/withdraw_form_header.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart index 4e15593f92..245d8d603b 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart @@ -1,34 +1,38 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/segwit_icon.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; class WithdrawFormHeader extends StatelessWidget { const WithdrawFormHeader({ - this.isIndicatorShown = true, - required this.coin, + required this.asset, + this.onBackButtonPressed, + super.key, }); - final bool isIndicatorShown; - final Coin coin; + + final Asset asset; + final VoidCallback? onBackButtonPressed; + + bool get _isSegwit => asset.id.id.toLowerCase().contains('segwit'); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, WithdrawFormState state) { + builder: (context, state) { return PageHeader( title: state.step.title, - widgetTitle: coin.mode == CoinMode.segwit + widgetTitle: _isSegwit ? const Padding( padding: EdgeInsets.only(left: 6.0), child: SegwitIcon(height: 22), ) : null, backText: LocaleKeys.backToWallet.tr(), - onBackButtonPressed: context.read().goBack, + onBackButtonPressed: onBackButtonPressed, ); }, ); 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..d585e843a6 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -1,79 +1,652 @@ 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:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/blocs/blocs.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 { +import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/model/text_error.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; +import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; + +class WithdrawForm extends StatefulWidget { + final Asset asset; + final VoidCallback onSuccess; + final VoidCallback? onBackButtonPressed; + const WithdrawForm({ - super.key, - required this.coin, - required this.onBackButtonPressed, + required this.asset, required this.onSuccess, + this.onBackButtonPressed, + super.key, }); - final Coin coin; - final VoidCallback onBackButtonPressed; - final VoidCallback onSuccess; + + @override + State createState() => _WithdrawFormState(); +} + +class _WithdrawFormState extends State { + late final WithdrawFormBloc _formBloc; + late final _sdk = context.read(); + + @override + void initState() { + super.initState(); + _formBloc = WithdrawFormBloc( + asset: widget.asset, + sdk: _sdk, + ); + } + + @override + void dispose() { + _formBloc.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => WithdrawFormBloc( - coin: coin, - coinsBloc: coinsBloc, - goBack: onBackButtonPressed, + return BlocProvider.value( + value: _formBloc, + child: BlocListener( + listenWhen: (prev, curr) => + prev.step != curr.step && curr.step == WithdrawFormStep.success, + listener: (_, __) => widget.onSuccess(), + child: WithdrawFormContent( + onBackButtonPressed: widget.onBackButtonPressed, + ), ), - child: isBitrefillIntegrationEnabled - ? BlocConsumer( - listener: (BuildContext context, BitrefillState state) { - if (state is BitrefillPaymentSuccess) { - onSuccess(); - _showBitrefillPaymentSuccessDialog(context, state); - } - }, - builder: (BuildContext context, BitrefillState state) { - final BitrefillPaymentInProgress? paymentState = - state is BitrefillPaymentInProgress ? state : null; - - final String? paymentAddress = - paymentState?.paymentIntent.paymentAddress; - final String? paymentAmount = - paymentState?.paymentIntent.paymentAmount.toString(); - - return WithdrawFormIndex( - coin: coin, - address: paymentAddress, - amount: paymentAmount, - ); - }, + ); + } +} + +class WithdrawFormContent extends StatelessWidget { + final VoidCallback? onBackButtonPressed; + + const WithdrawFormContent({ + this.onBackButtonPressed, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (prev, curr) => prev.step != curr.step, + builder: (context, state) { + return Column( + children: [ + WithdrawFormHeader( + asset: state.asset, + onBackButtonPressed: onBackButtonPressed, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(18), + ), + child: _buildStep(state.step), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildStep(WithdrawFormStep step) { + switch (step) { + case WithdrawFormStep.fill: + return const WithdrawFormFillSection(); + case WithdrawFormStep.confirm: + return const WithdrawFormConfirmSection(); + case WithdrawFormStep.success: + return const WithdrawFormSuccessSection(); + case WithdrawFormStep.failed: + return const WithdrawFormFailedSection(); + } + } +} + +class NetworkErrorDisplay extends StatelessWidget { + final TextError error; + final VoidCallback? onRetry; + + const NetworkErrorDisplay({ + required this.error, + this.onRetry, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ErrorDisplay( + message: error.message, + icon: Icons.cloud_off, + child: onRetry != null + ? TextButton( + onPressed: onRetry, + child: const Text('Retry'), ) - : WithdrawFormIndex( - coin: coin, + : null, + ); + } +} + +class TransactionErrorDisplay extends StatelessWidget { + final TextError error; + final VoidCallback? onDismiss; + + const TransactionErrorDisplay({ + required this.error, + this.onDismiss, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ErrorDisplay( + message: error.message, + icon: Icons.warning_amber_rounded, + child: onDismiss != null + ? IconButton( + icon: const Icon(Icons.close), + onPressed: onDismiss, + ) + : null, + ); + } +} + +class PreviewWithdrawButton extends StatelessWidget { + final VoidCallback? onPressed; + final bool isSending; + + const PreviewWithdrawButton({ + required this.onPressed, + required this.isSending, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: onPressed, + child: isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Preview Withdrawal'), + ), + ); + } +} + +class WithdrawPreviewDetails extends StatelessWidget { + final WithdrawalPreview preview; + + const WithdrawPreviewDetails({ + required this.preview, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRow('Amount', preview.balanceChanges.netChange.toString()), + const SizedBox(height: 8), + _buildRow('Fee', preview.fee.formatTotal()), + // Add more preview details as needed + ], + ), + ), + ); + } + + Widget _buildRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label), + Text(value), + ], + ); + } +} + +class WithdrawResultDetails extends StatelessWidget { + final WithdrawalResult result; + + const WithdrawResultDetails({ + required this.result, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + 'Transaction Hash:', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 4), + SelectableText(result.txHash), + // Add more result details as needed + ], + ), + ), + ); + } +} + +class WithdrawFormFillSection extends StatelessWidget { + const WithdrawFormFillSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (state.asset.supportsMultipleAddresses) ...[ + SourceAddressField( + asset: state.asset, + pubkeys: state.pubkeys, + selectedAddress: state.selectedSourceAddress, + onChanged: (address) => context + .read() + .add(WithdrawFormSourceChanged(address)), + ), + const SizedBox(height: 16), + ], + RecipientAddressField( + address: state.recipientAddress, + onChanged: (value) => context + .read() + .add(WithdrawFormRecipientChanged(value)), + onQrScanned: (value) => context + .read() + .add(WithdrawFormRecipientChanged(value)), + addressError: state.recipientAddressError?.message, + ), + const SizedBox(height: 16), + WithdrawAmountField( + asset: state.asset, + amount: state.amount, + isMaxAmount: state.isMaxAmount, + onChanged: (value) => context + .read() + .add(WithdrawFormAmountChanged(value)), + onMaxToggled: (value) => context + .read() + .add(WithdrawFormMaxAmountEnabled(value)), + amountError: state.amountError?.message, + ), + if (state.isCustomFeeSupported) ...[ + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: state.isCustomFee, + onChanged: (enabled) => context + .read() + .add(WithdrawFormCustomFeeEnabled(enabled ?? false)), + ), + const Text('Custom network fee'), + ], + ), + if (state.isCustomFee && state.customFee != null) ...[ + const SizedBox(height: 8), + + FeeInfoInput( + asset: state.asset, + selectedFee: state.customFee!, + isCustomFee: true, // indicates user can edit it + onFeeSelected: (newFee) { + context + .read() + .add(WithdrawFormCustomFeeChanged(newFee!)); + }, + ), + + // If the bloc has an error for custom fees: + if (state.customFeeError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + state.customFeeError!.message, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ], + ], + const SizedBox(height: 16), + WithdrawMemoField( + memo: state.memo, + onChanged: (value) => context + .read() + .add(WithdrawFormMemoChanged(value)), ), + const SizedBox(height: 24), + // TODO! Refactor to use Formz and replace with the appropriate + // error state value. + if (state.hasPreviewError) + ErrorDisplay(message: state.previewError!.message), + const SizedBox(height: 16), + PreviewWithdrawButton( + onPressed: state.isSending || state.hasValidationErrors + ? null + : () { + context + .read() + .add(const WithdrawFormPreviewSubmitted()); + }, + isSending: state.isSending, + ), + ], + ); + }, ); } +} + +class WithdrawFormConfirmSection extends StatelessWidget { + const WithdrawFormConfirmSection({super.key}); - void _showBitrefillPaymentSuccessDialog( - BuildContext context, - BitrefillPaymentSuccess state, - ) { - showDialog( - context: context, - builder: (BuildContext context) { - return BitrefillTransactionCompletedDialog( - title: LocaleKeys.bitrefillPaymentSuccessfull.tr(), - message: LocaleKeys.bitrefillPaymentSuccessfullInstruction.tr( - args: [state.invoiceId], + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.preview == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + WithdrawPreviewDetails(preview: state.preview!), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context + .read() + .add(const WithdrawFormCancelled()), + child: const Text('Back'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + onPressed: state.isSending + ? null + : () { + context + .read() + .add(const WithdrawFormSubmitted()); + }, + child: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Confirm'), + ), + ), + ], + ), + ], + ); + }, + ); + } +} + +class WithdrawFormSuccessSection extends StatelessWidget { + const WithdrawFormSuccessSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + 'Transaction Successful', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + WithdrawResultDetails(result: state.result!), + const SizedBox(height: 24), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Done'), + ), + ], + ); + }, + ); + } +} + +class WithdrawResultCard extends StatelessWidget { + final WithdrawalResult result; + final Asset asset; + + const WithdrawResultCard({ + required this.result, + required this.asset, + super.key, + }); + + @override + Widget build(BuildContext context) { + final maybeTxEplorer = asset.protocol.explorerTxUrl(result.txHash); + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHashSection(context), + const Divider(height: 32), + _buildNetworkSection(context), + if (maybeTxEplorer != null) ...[ + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: () => openUrl(maybeTxEplorer), + icon: const Icon(Icons.open_in_new), + label: const Text('View on Explorer'), + ), + ], + ], + ), + ), + ); + } + + Widget _buildHashSection(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Transaction Hash', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + SelectableText( + result.txHash, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'Mono', ), - onViewInvoicePressed: () {}, + ), + ], + ); + } + + Widget _buildNetworkSection(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Network', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + Row( + children: [ + AssetIcon(asset.id), + const SizedBox(width: 8), + Text( + asset.id.name, + style: theme.textTheme.bodyLarge, + ), + ], + ), + ], + ); + } +} + +class WithdrawFormFailedSection extends StatelessWidget { + const WithdrawFormFailedSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.error_outline, + size: 64, + color: theme.colorScheme.error, + ), + const SizedBox(height: 24), + Text( + 'Transaction Failed', + style: theme.textTheme.headlineMedium?.copyWith( + color: theme.colorScheme.error, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + if (state.transactionError != null) + WithdrawErrorCard( + error: state.transactionError!, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + onPressed: () => context + .read() + .add(const WithdrawFormStepReverted()), + child: const Text('Back'), + ), + const SizedBox(width: 16), + FilledButton( + onPressed: () => context + .read() + .add(const WithdrawFormReset()), + child: const Text('Try Again'), + ), + ], + ), + ], ); }, ); } } + +class WithdrawErrorCard extends StatelessWidget { + final BaseError error; + + const WithdrawErrorCard({ + required this.error, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error Details', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + SelectableText( + error.message, + style: theme.textTheme.bodyMedium, + ), + if (error is TextError) ...[ + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + ExpansionTile( + title: const Text('Technical Details'), + children: [ + SelectableText( + (error as TextError).error, + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'Mono', + ), + ), + ], + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart deleted file mode 100644 index 296f5d3b54..0000000000 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart +++ /dev/null @@ -1,92 +0,0 @@ -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/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/common/screen.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/views/common/pages/page_layout.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/complete_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/confirm_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/failed_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; - -class WithdrawFormIndex extends StatefulWidget { - const WithdrawFormIndex({ - required this.coin, - this.address, - this.amount, - }); - - final Coin coin; - final String? address; - final String? amount; - - @override - State createState() => _WithdrawFormIndexState(); -} - -class _WithdrawFormIndexState extends State { - @override - void initState() { - super.initState(); - - if (widget.address != null) { - context.read().add( - WithdrawFormAddressChanged( - address: widget.address!, - ), - ); - } - - if (widget.amount != null) { - context.read().add( - WithdrawFormAmountChanged( - amount: widget.amount!, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - final scrollController = ScrollController(); - return BlocSelector( - selector: (state) => state.step, - builder: (context, step) => PageLayout( - header: WithdrawFormHeader(coin: widget.coin), - content: Flexible( - child: DexScrollbar( - isMobile: isMobile, - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Container( - padding: - const EdgeInsets.symmetric(vertical: 20, horizontal: 15), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(18.0), - ), - child: Builder( - builder: (context) { - switch (step) { - case WithdrawFormStep.fill: - return const FillFormPage(); - case WithdrawFormStep.confirm: - return const ConfirmPage(); - case WithdrawFormStep.success: - return const CompletePage(); - case WithdrawFormStep.failed: - return const FailedPage(); - } - }, - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/views/wallet/coins_manager/coins_manager_controls.dart b/lib/views/wallet/coins_manager/coins_manager_controls.dart index 893fb61cb0..0b32f69d36 100644 --- a/lib/views/wallet/coins_manager/coins_manager_controls.dart +++ b/lib/views/wallet/coins_manager/coins_manager_controls.dart @@ -5,8 +5,8 @@ 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/custom_token_import/custom_token_import_button.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'; @@ -22,6 +22,8 @@ class CoinsManagerFilters extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSearchField(context), + const SizedBox(height: 8), + const CustomTokenImportButton(), Padding( padding: const EdgeInsets.only(top: 14.0), child: Row( @@ -43,7 +45,7 @@ class CoinsManagerFilters extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( @@ -51,6 +53,13 @@ class CoinsManagerFilters extends StatelessWidget { height: 45, child: _buildSearchField(context), ), + const SizedBox(width: 20), + Container( + constraints: const BoxConstraints(maxWidth: 240), + height: 45, + child: const CustomTokenImportButton(), + ), + const Spacer(), CoinsManagerFiltersDropdown(), ], ), @@ -74,9 +83,9 @@ class CoinsManagerFilters extends StatelessWidget { fontSize: 12, fontWeight: FontWeight.w500, ), - onChanged: (String text) => context + onChanged: (String? text) => context .read() - .add(CoinsManagerSearchUpdate(text: text)), + .add(CoinsManagerSearchUpdate(text: text ?? '')), ); } } 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..9d41497acd 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,17 @@ 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 + case WalletType.hdwallet: + 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 d5acf9aa2a..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,13 +1,14 @@ 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/blocs/blocs.dart'; +import 'package:web_dex/bloc/settings/settings_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'; +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'; @@ -16,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); @@ -49,7 +49,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( @@ -109,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..1c17d1a4a4 100644 --- a/lib/views/wallet/coins_manager/coins_manager_page.dart +++ b/lib/views/wallet/coins_manager/coins_manager_page.dart @@ -1,14 +1,14 @@ 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_manager/coins_manager_bloc.dart'; -import 'package:web_dex/blocs/blocs.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({ @@ -22,8 +22,9 @@ class CoinsManagerPage extends StatelessWidget { @override Widget build(BuildContext context) { - assert(action == CoinsManagerAction.add || - action == CoinsManagerAction.remove); + assert( + action == CoinsManagerAction.add || action == CoinsManagerAction.remove, + ); final title = action == CoinsManagerAction.add ? LocaleKeys.addAssets.tr() @@ -37,29 +38,26 @@ 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(), - ); - })), + padding: const EdgeInsets.only(top: 20.0), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.walletCoins != current.walletCoins, + listener: (context, state) => context + .read() + .add(CoinsManagerCoinsUpdate(action)), + builder: (context, state) { + if (!state.loginActivationFinished) { + return const Center( + child: Padding( + padding: EdgeInsets.fromLTRB(0, 100, 0, 100), + child: UiSpinner(), + ), + ); + } + return 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..1f3a103e8b 100644 --- a/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart +++ b/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart @@ -1,7 +1,10 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart' hide TextDirection; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/cex_market_data/price_chart/models/price_chart_data.dart'; import 'package:web_dex/bloc/cex_market_data/price_chart/models/time_period.dart'; @@ -34,22 +37,29 @@ 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( state.data.firstOrNull?.data.lastOrNull?.usdValue ?? 0, ), ), - availableCoins: state.availableCoins.keys.toList(), + availableCoins: state.availableCoins.keys + .map( + (e) => getSdkAsset(context.read(), e).id, + ) + .toList(), selectedCoinId: state.data.firstOrNull?.info.ticker, onCoinSelected: (coinId) { context.read().add( PriceChartCoinsSelected( - coinId == null ? [] : [coinId]), + coinId == null ? [] : [coinId], + ), ); }, centreAmount: @@ -65,13 +75,14 @@ class PriceChartPage extends StatelessWidget { }, customCoinItemBuilder: (coinId) { final coin = state.availableCoins[coinId]; - return CoinSelectItem( - coinId: coinId, + return SelectItem( + id: coinId.id, + value: coinId, trailing: TrendPercentageText( investmentReturnPercentage: coin?.selectedPeriodIncreasePercentage ?? 0, ), - name: coin?.name ?? coinId, + title: coin?.name ?? coinId.name, ); }, ), diff --git a/lib/views/wallet/wallet_page/common/coin_list_item.dart b/lib/views/wallet/wallet_page/common/coin_list_item.dart index 0cf8b32c92..828aa6f2fc 100644 --- a/lib/views/wallet/wallet_page/common/coin_list_item.dart +++ b/lib/views/wallet/wallet_page/common/coin_list_item.dart @@ -14,7 +14,7 @@ class CoinListItem extends StatelessWidget { final Coin coin; final Color backgroundColor; - final Function(Coin) onTap; + final void Function(Coin) onTap; @override Widget build(BuildContext context) { 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..393679caeb 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,19 +1,18 @@ 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'; -import 'package:web_dex/shared/utils/utils.dart'; -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_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 { @@ -82,7 +81,7 @@ class CoinListItemDesktop extends StatelessWidget { coin: coin, isReEnabling: coin.isActivating, ) - : _CoinBalance( + : CoinBalance( key: Key('balance-asset-${coin.abbr}'), coin: coin, ), @@ -119,80 +118,33 @@ class CoinListItemDesktop extends StatelessWidget { } } -class _CoinBalance extends StatelessWidget { - const _CoinBalance({ - Key? key, - required this.coin, - }) : super(key: key); - - final Coin coin; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Flexible( - flex: 2, - child: AutoScrollText( - text: doubleToString(coin.balance), - style: const TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - ), - ), - Text( - ' ${Coin.normalizeAbbr(coin.abbr)}', - style: const TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - ), - const Text(' (', - style: TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - )), - Flexible( - child: CoinFiatBalance( - coin, - isAutoScrollEnabled: true, - ), - ), - const Text(')', - style: TextStyle( - fontSize: _fontSize, - fontWeight: FontWeight.w500, - )), - ], - ); - } -} - class _SuspendedMessage extends StatelessWidget { const _SuspendedMessage({ super.key, 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: [ Opacity( - opacity: 0.6, - child: Text( - LocaleKeys.activationFailedMessage.tr(), - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: _fontSize, - fontWeight: FontWeight.w500, - ), - )), + opacity: 0.6, + child: Text( + LocaleKeys.activationFailedMessage.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: _fontSize, + fontWeight: FontWeight.w500, + ), + ), + ), const SizedBox(width: 12), Padding( padding: const EdgeInsets.only(top: 1.0), @@ -200,9 +152,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/common/coins_list_header.dart b/lib/views/wallet/wallet_page/common/coins_list_header.dart index c847479527..b15419f8de 100644 --- a/lib/views/wallet/wallet_page/common/coins_list_header.dart +++ b/lib/views/wallet/wallet_page/common/coins_list_header.dart @@ -4,63 +4,98 @@ import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class CoinsListHeader extends StatelessWidget { - const CoinsListHeader({Key? key}) : super(key: key); + const CoinsListHeader({ + super.key, + required this.isAuth, + }); + + final bool isAuth; @override Widget build(BuildContext context) { return isMobile - ? const _CoinsListHeaderMobile() - : const _CoinsListHeaderDesktop(); + ? const SizedBox.shrink() + : _CoinsListHeaderDesktop(isAuth: isAuth); } } class _CoinsListHeaderDesktop extends StatelessWidget { - const _CoinsListHeaderDesktop({Key? key}) : super(key: key); + const _CoinsListHeaderDesktop({ + required this.isAuth, + }); + + final bool isAuth; @override Widget build(BuildContext context) { - const style = TextStyle(fontSize: 14, fontWeight: FontWeight.w500); + // final style = Theme.of(context).textTheme.bodyMedium?.copyWith( + // fontWeight: FontWeight.w500, + // ); + final style = Theme.of(context).textTheme.labelSmall; - return Container( - padding: const EdgeInsets.fromLTRB(0, 0, 16, 4), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, + if (isAuth) { + return Row( children: [ - Expanded( - flex: 5, - child: Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Text(LocaleKeys.asset.tr(), style: style), - ), - ), - Expanded( - flex: 5, - child: Text(LocaleKeys.balance.tr(), style: style), + // Expand button space + SizedBox(width: 32), + + // Asset header + Container( + constraints: const BoxConstraints(maxWidth: 180), + width: double.infinity, + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.asset.tr(), style: style), ), + + const Spacer(), + + // Balance header Expanded( flex: 2, - child: Text(LocaleKeys.change24hRevert.tr(), style: style), + child: Align( + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.balance.tr(), style: style), + ), ), - Expanded( - flex: 2, - child: Text(LocaleKeys.price.tr(), style: style), + + // 24h change header + Container( + width: 68, + alignment: Alignment.centerLeft, + child: Text(LocaleKeys.change24hRevert.tr(), style: style), ), - Expanded( - flex: 2, - child: Text(LocaleKeys.trend.tr(), style: style), + + const Spacer(), + + // // More actions space + const SizedBox(width: 48), + ], + ); + } + + return Container( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), + child: Row( + children: [ + // Asset header + Text(LocaleKeys.asset.tr(), style: style), + + const Spacer(flex: 4), + + // Balance header + Text(LocaleKeys.balance.tr(), style: style), + + const Spacer(flex: 2), + + // 24h change header + Padding( + padding: const EdgeInsets.only(right: 48), + child: Text(LocaleKeys.change24hRevert.tr(), style: style), ), + + const Spacer(flex: 2), ], ), ); } } - -class _CoinsListHeaderMobile extends StatelessWidget { - const _CoinsListHeaderMobile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} diff --git a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart new file mode 100644 index 0000000000..0283fc9e18 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart @@ -0,0 +1,253 @@ +// lib/src/defi/asset/coin_list_item.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui/komodo_ui.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/coin.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_balance.dart'; +import 'package:web_dex/shared/widgets/coin_fiat_balance.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/wallet/common/wallet_helper.dart'; + +class ExpandableCoinListItem extends StatefulWidget { + final Coin coin; + final AssetPubkeys? pubkeys; + final bool isSelected; + final Color? backgroundColor; + final VoidCallback? onTap; + + const ExpandableCoinListItem({ + super.key, + required this.coin, + required this.pubkeys, + required this.isSelected, + this.onTap, + this.backgroundColor, + }); + + @override + State createState() => _ExpandableCoinListItemState(); +} + +class _ExpandableCoinListItemState extends State { + // Store the expansion state in the widget's state + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + // Attempt to restore state from PageStorage using a unique key + _isExpanded = PageStorage.of(context).readState( + context, + identifier: '${widget.coin.abbr}_expanded', + ) as bool? ?? + false; + } + + void _handleExpansionChanged(bool expanded) { + setState(() { + _isExpanded = expanded; + // Save state to PageStorage using a unique key + PageStorage.of(context).writeState( + context, + _isExpanded, + identifier: '${widget.coin.abbr}_expanded', + ); + }); + } + + @override + Widget build(BuildContext context) { + final hasAddresses = widget.pubkeys?.keys.isNotEmpty ?? false; + final sortedAddresses = hasAddresses + ? (List.of(widget.pubkeys!.keys) + ..sort((a, b) => b.balance.spendable.compareTo(a.balance.spendable))) + : null; + + return CollapsibleCard( + key: PageStorageKey('coin_${widget.coin.abbr}'), + borderRadius: BorderRadius.circular(12), + headerPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + onTap: widget.onTap, + childrenMargin: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + childrenDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + initiallyExpanded: _isExpanded, + onExpansionChanged: _handleExpansionChanged, + expansionControlPosition: ExpansionControlPosition.leading, + emptyChildrenBehavior: EmptyChildrenBehavior.disable, + isDense: true, + title: _buildTitle(context), + maintainState: true, + childrenDivider: const Divider(height: 1, indent: 16, endIndent: 16), + trailing: CoinMoreActionsButton(coin: widget.coin), + children: sortedAddresses + ?.map( + (pubkey) => _AddressRow( + pubkey: pubkey, + coin: widget.coin, + isSwapAddress: pubkey == sortedAddresses.first, + onTap: widget.onTap, + onCopy: () => copyToClipBoard(context, pubkey.address), + ), + ) + .toList(), + ); + } + + Widget _buildTitle(BuildContext context) { + final theme = Theme.of(context); + + return Container( + alignment: Alignment.centerLeft, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 180), + child: CoinItem(coin: widget.coin, size: CoinItemSize.large), + ), + const Spacer(), + Expanded( + flex: 2, + child: Align( + alignment: Alignment.centerLeft, + child: CoinBalance(coin: widget.coin), + ), + ), + TrendPercentageText( + investmentReturnPercentage: getTotal24Change([widget.coin]) ?? 0, + iconSize: 16, + spacing: 4, + textStyle: theme.textTheme.bodyMedium, + ), + const Spacer(), + ], + ), + ); + } +} + +class _AddressRow extends StatelessWidget { + final PubkeyInfo pubkey; + final Coin coin; + final bool isSwapAddress; + final VoidCallback? onTap; + final VoidCallback? onCopy; + + const _AddressRow({ + required this.pubkey, + required this.coin, + required this.isSwapAddress, + required this.onTap, + this.onCopy, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ListTile( + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + leading: CircleAvatar( + radius: 16, + backgroundColor: theme.colorScheme.surfaceContainerHigh, + child: const Icon(Icons.person_outline), + ), + title: Row( + children: [ + Text( + pubkey.addressShort, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: IconButton( + iconSize: 16, + icon: const Icon(Icons.copy), + onPressed: onCopy, + visualDensity: VisualDensity.compact, + // constraints: const BoxConstraints( + // minWidth: 32, + // minHeight: 32, + // ), + ), + ), + if (isSwapAddress && !kIsWalletOnly) ...[ + const SizedBox(width: 8), + const Chip( + label: Text( + 'Swap', + // style: theme.textTheme.labelSmall, + ), + // backgroundColor: theme.colorScheme.primaryContainer, + ), + ], + ], + ), + trailing: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${doubleToString(pubkey.balance.spendable.toDouble())} ${coin.abbr}', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + CoinFiatBalance( + coin.copyWith( + balance: pubkey.balance.spendable.toDouble(), + ), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +// This will be able to be removed in the near future when activation state +// is removed from the GUI because it is handled internall by the SDK. +class CoinMoreActionsButton extends StatelessWidget { + const CoinMoreActionsButton({required this.coin}); + + final Coin coin; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (action) { + switch (action) { + case CoinMoreActions.disable: + context.read().add(CoinsDeactivated([coin.abbr])); + break; + } + }, + itemBuilder: (context) { + return [ + const PopupMenuItem( + value: CoinMoreActions.disable, + child: Text('Disable'), + ), + ]; + }, + ); + } +} + +enum CoinMoreActions { + disable, +} 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..dfc270e354 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,10 +1,19 @@ 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_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/settings/settings_bloc.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/views/wallet/wallet_page/common/wallet_coins_list.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.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'; +import 'package:web_dex/views/wallet/wallet_page/common/expandable_coin_list_item.dart'; class ActiveCoinsList extends StatelessWidget { const ActiveCoinsList({ @@ -13,44 +22,62 @@ class ActiveCoinsList extends StatelessWidget { required this.withBalance, required this.onCoinItemTap, }) : super(key: key); + final String searchPhrase; final bool withBalance; final Function(Coin) onCoinItemTap; @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); - - if (displayedCoins.isEmpty && - (searchPhrase.isNotEmpty || withBalance)) { - return SliverToBoxAdapter( - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(8.0), - child: - SelectableText(LocaleKeys.walletPageNoSuchAsset.tr()), - ), - ); - } + 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()), + ), + ); + } - final sorted = sortFiatBalance(displayedCoins.toList()); + List sorted = sortFiatBalance(displayedCoins.toList()); + + if (!context.read().state.testCoinsEnabled) { + sorted = removeTestCoins(sorted); + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final coin = sorted[index]; + + // Fetch pubkeys if not already loaded + if (!state.pubkeys.containsKey(coin.abbr)) { + context.read().add(CoinsPubkeysRequested(coin.abbr)); + } - return WalletCoinsList( - coins: sorted, - onCoinItemTap: onCoinItemTap, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ExpandableCoinListItem( + // Changed from ExpandableCoinListItem + key: Key('coin-list-item-${coin.abbr.toLowerCase()}'), + coin: coin, + pubkeys: state.pubkeys[coin.abbr], + isSelected: false, + onTap: () => onCoinItemTap(coin), + ), ); }, - ); - }); + childCount: sorted.length, + ), + ); + }, + ); } Iterable _getDisplayedCoins(Iterable coins) => @@ -61,3 +88,202 @@ class ActiveCoinsList extends StatelessWidget { return true; }).toList(); } + +class AddressBalanceList extends StatelessWidget { + const AddressBalanceList({ + Key? key, + required this.coin, + required this.onCreateNewAddress, + required this.pubkeys, + required this.cantCreateNewAddressReasons, + }) : super(key: key); + + final Coin coin; + final AssetPubkeys pubkeys; + final VoidCallback onCreateNewAddress; + final Set? cantCreateNewAddressReasons; + + bool get canCreateNewAddress => cantCreateNewAddressReasons?.isEmpty ?? true; + + @override + Widget build(BuildContext context) { + if (pubkeys.keys.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + // Sort addresses by balance + final sortedAddresses = [...pubkeys.keys] + ..sort((a, b) => b.balance.spendable.compareTo(a.balance.spendable)); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Address list + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sortedAddresses.length, + itemBuilder: (context, index) { + final pubkey = sortedAddresses[index]; + return AddressBalanceCard( + pubkey: pubkey, + coin: coin, + ); + }, + ), + + // Create address button + if (canCreateNewAddress) + Padding( + padding: const EdgeInsets.all(16), + child: Tooltip( + message: _getTooltipMessage(), + child: ElevatedButton.icon( + onPressed: canCreateNewAddress ? onCreateNewAddress : null, + icon: const Icon(Icons.add), + label: const Text('Create New Address'), + ), + ), + ), + ], + ); + } + + String _getTooltipMessage() { + if (cantCreateNewAddressReasons?.isEmpty ?? true) { + return ''; + } + + return cantCreateNewAddressReasons!.map((reason) { + return switch (reason) { + // TODO: Localise and possibly also move localisations to the SDK. + CantCreateNewAddressReason.maxGapLimitReached => + 'Maximum gap limit reached - please use existing unused addresses first', + CantCreateNewAddressReason.maxAddressesReached => + 'Maximum number of addresses reached for this asset', + CantCreateNewAddressReason.missingDerivationPath => + 'Missing derivation path configuration', + CantCreateNewAddressReason.protocolNotSupported => + 'Protocol does not support multiple addresses', + CantCreateNewAddressReason.derivationModeNotSupported => + 'Current wallet mode does not support multiple addresses', + CantCreateNewAddressReason.noActiveWallet => + 'No active wallet - please sign in first', + }; + }).join('\n'); + } +} + +class AddressBalanceCard extends StatelessWidget { + const AddressBalanceCard({ + Key? key, + required this.pubkey, + required this.coin, + }) : super(key: key); + + final PubkeyInfo pubkey; + final Coin coin; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Address row + Row( + children: [ + AddressIcon(address: pubkey.address), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AddressText(address: pubkey.address), + AddressCopyButton(address: pubkey.address), + if (pubkey.isActiveForSwap) + Chip( + label: const Text('Swap Address'), + backgroundColor: Theme.of(context) + .primaryColor + .withOpacity(0.1), + ), + ], + ), + if (pubkey.derivationPath != null) + Text( + 'Derivation: ${pubkey.derivationPath}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + + const Divider(), + + // Balance row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${formatBalance(pubkey.balance.spendable.toBigInt())} ${coin.abbr}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + CoinFiatBalance( + coin.copyWith( + balance: pubkey.balance.spendable.toDouble(), + ), + style: Theme.of(context).textTheme.bodySmall, + // customBalance: pubkey.balance.spendable.toDouble(), + ), + ], + ), + IconButton( + icon: const Icon(Icons.qr_code), + onPressed: () { + showDialog( + context: context, + builder: (context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrCode( + address: pubkey.address, + coinAbbr: coin.abbr, + ), + const SizedBox(height: 16), + SelectableText(pubkey.address), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } + + String formatBalance(BigInt balance) { + return doubleToString(balance.toDouble()); + } +} 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..df8a1079cd 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,13 @@ 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/settings/settings_bloc.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 { +class AllCoinsList extends StatefulWidget { const AllCoinsList({ Key? key, required this.searchPhrase, @@ -17,23 +19,66 @@ class AllCoinsList extends StatelessWidget { final Function(Coin) onCoinItemTap; @override - Widget build(BuildContext context) { - return StreamBuilder>( - initialData: coinsBloc.knownCoins, - stream: coinsBloc.outKnownCoins, - builder: (context, AsyncSnapshot> snapshot) { - final List coins = snapshot.data ?? []; + _AllCoinsListState createState() => _AllCoinsListState(); +} + +class _AllCoinsListState extends State { + List displayedCoins = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateDisplayedCoins(); + } - if (coins.isEmpty) { - return const SliverToBoxAdapter(child: UiSpinner()); - } + @override + void didUpdateWidget(AllCoinsList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.searchPhrase != widget.searchPhrase) { + _updateDisplayedCoins(); + } + } - final displayedCoins = - sortByPriority(filterCoinsByPhrase(coins, searchPhrase)); - return WalletCoinsList( - coins: displayedCoins.toList(), - onCoinItemTap: onCoinItemTap, - ); - }); + void _updateDisplayedCoins() { + final coins = context.read().state.coins.values.toList(); + if (coins.isNotEmpty) { + List filteredCoins = + sortByPriority(filterCoinsByPhrase(coins, widget.searchPhrase)) + .toList(); + if (!context.read().state.testCoinsEnabled) { + filteredCoins = removeTestCoins(filteredCoins); + } + setState(() { + displayedCoins = filteredCoins; + }); + } + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => previous.coins != current.coins, + listener: (context, state) { + _updateDisplayedCoins(); + }, + builder: (context, state) { + return state.coins.isEmpty + ? const SliverToBoxAdapter(child: UiSpinner()) + : displayedCoins.isEmpty + ? SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No coins found', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ) + : WalletCoinsList( + coins: displayedCoins, + onCoinItemTap: widget.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 18e4451a81..c2f2646293 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,9 @@ 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/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'; @@ -26,8 +26,7 @@ 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/dex/dex_helpers.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/charts/animated_portfolio_charts.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/active_coins_list.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/all_coins_list.dart'; @@ -37,7 +36,7 @@ import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dar import 'package:web_dex/views/wallets_manager/wallets_manager_wrapper.dart'; class WalletMain extends StatefulWidget { - const WalletMain({Key? key = const Key('wallet-page')}) : super(key: key); + const WalletMain({super.key = const Key('wallet-page')}); @override State createState() => _WalletMainState(); @@ -55,18 +54,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,94 +73,91 @@ 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( - 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), ), - ), - SizedBox( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart( - initialCoins: walletCoinsFiltered.toList(), + const Gap(8), + ], + if (authStateMode != AuthorizeMode.logIn) + const SizedBox( + width: double.infinity, + height: 340, + child: PriceChartPage(key: Key('price-chart')), + ) + else + AnimatedPortfolioCharts( + key: const Key('animated_portfolio_charts'), + tabController: _tabController, + walletCoinsFiltered: 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 walletCoins = + context.read().state.walletCoins.values.toList(); portfolioGrowthBloc.add( PortfolioGrowthLoadRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, fiatCoinId: 'USDT', updateFrequency: const Duration(minutes: 1), selectedPeriod: portfolioGrowthBloc.state.selectedPeriod, @@ -178,7 +167,7 @@ class _WalletMainState extends State profitLossBloc.add( ProfitLossPortfolioChartLoadRequested( - coins: coinsBloc.walletCoins, + coins: walletCoins, selectedPeriod: profitLossBloc.state.selectedPeriod, fiatCoinId: 'USDT', walletId: walletId, @@ -188,13 +177,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), ), @@ -265,7 +254,7 @@ class _WalletMainState extends State onSuccess: (_) async { takerBloc.add(TakerReInit()); bridgeBloc.add(const BridgeReInit()); - await reInitTradingForms(); + await reInitTradingForms(context); _popupDispatcher?.close(); }, ), @@ -287,13 +276,18 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { }); @override - double get minExtent => 120; + final double minExtent = 110; @override - double get maxExtent => 120; + final double maxExtent = 114; @override Widget build( - BuildContext context, double shrinkOffset, bool overlapsContent) { + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + // return SizedBox.expand(); + return WalletManageSection( withBalance: withBalance, onSearchChange: onSearchChange, 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..6ed3018c77 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 @@ -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: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/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/authorize_mode.dart'; @@ -34,91 +35,52 @@ class WalletManageSection extends StatelessWidget { } Widget _buildDesktopSection(BuildContext context) { - final ThemeData themeData = Theme.of(context); + final ThemeData theme = Theme.of(context); return Card( clipBehavior: Clip.antiAlias, - color: Theme.of(context).colorScheme.surface, + color: theme.colorScheme.surface, margin: const EdgeInsets.all(0), - elevation: pinned ? 4 : 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + elevation: pinned ? 2 : 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + children: [ + Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + WalletManagerSearchField(onChange: onSearchChange), + Spacer(), HiddenWithoutWallet( - child: Container( - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - color: theme.custom.walletEditButtonsBackgroundColor, - borderRadius: BorderRadius.circular(18.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - UiPrimaryButton( - buttonKey: const Key('add-assets-button'), - onPressed: _onAddAssetsPress, - text: LocaleKeys.addAssets.tr(), - height: 30.0, - width: 110, - backgroundColor: themeData.colorScheme.surface, - textStyle: TextStyle( - color: themeData.colorScheme.primary, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 3.0), - child: UiPrimaryButton( - buttonKey: const Key('remove-assets-button'), - onPressed: _onRemoveAssetsPress, - text: LocaleKeys.removeAssets.tr(), - height: 30.0, - width: 125, - backgroundColor: themeData.colorScheme.surface, - textStyle: TextStyle( - color: themeData.textTheme.labelLarge?.color - ?.withOpacity(0.7), - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - ), - ], - ), + child: CoinsWithBalanceCheckbox( + withBalance: withBalance, + onWithBalanceChange: onWithBalanceChange, ), ), - Row( - children: [ - HiddenWithoutWallet( - child: Padding( - padding: const EdgeInsets.only(right: 30.0), - child: CoinsWithBalanceCheckbox( - withBalance: withBalance, - onWithBalanceChange: onWithBalanceChange, - ), - ), - ), - WalletManagerSearchField(onChange: onSearchChange), - ], + SizedBox(width: 24), + HiddenWithoutWallet( + child: UiPrimaryButton( + buttonKey: const Key('add-assets-button'), + onPressed: () => _onAddAssetsPress(context), + text: LocaleKeys.addAssets.tr(), + height: 36, + width: 147, + borderRadius: 10, + textStyle: theme.textTheme.bodySmall, + ), ), ], ), - ), - const CoinsListHeader(), - ], + Spacer(), + CoinsListHeader(isAuth: mode == AuthorizeMode.logIn), + ], + ), ), ); } Widget _buildMobileSection(BuildContext context) { - final ThemeData themeData = Theme.of(context); + final ThemeData theme = Theme.of(context); return Container( padding: const EdgeInsets.fromLTRB(2, 20, 2, 10), @@ -140,40 +102,19 @@ class WalletManageSection extends StatelessWidget { Container( padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( - color: themeData.colorScheme.surface, + color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(18.0), ), child: Row( children: [ UiPrimaryButton( buttonKey: const Key('add-assets-button'), - onPressed: _onAddAssetsPress, + onPressed: () => _onAddAssetsPress(context), text: LocaleKeys.addAssets.tr(), - height: 25.0, - width: 110, - backgroundColor: themeData.colorScheme.onSurface, - textStyle: TextStyle( - color: themeData.colorScheme.secondary, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 3.0), - child: UiPrimaryButton( - buttonKey: const Key('remove-assets-button'), - onPressed: _onRemoveAssetsPress, - text: LocaleKeys.remove.tr(), - height: 25.0, - width: 80, - backgroundColor: themeData.colorScheme.onSurface, - textStyle: TextStyle( - color: themeData.textTheme.labelLarge?.color - ?.withOpacity(0.7), - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), + height: 36, + width: 147, + borderRadius: 10, + textStyle: theme.textTheme.bodySmall, ), ], ), @@ -199,11 +140,17 @@ class WalletManageSection extends StatelessWidget { ); } - void _onAddAssetsPress() { + void _onAddAssetsPress(BuildContext context) { + context + .read() + .add(const CoinsManagerCoinsListReset(CoinsManagerAction.add)); routingState.walletState.action = coinsManagerRouteAction.addAssets; } - void _onRemoveAssetsPress() { + void _onRemoveAssetsPress(BuildContext context) { + context + .read() + .add(const CoinsManagerCoinsListReset(CoinsManagerAction.remove)); routingState.walletState.action = coinsManagerRouteAction.removeAssets; } } diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart index 105fc64fe7..7dfb5f3a9b 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manager_search_field.dart @@ -6,8 +6,8 @@ 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'; -const double _hiddenSearchFieldWidth = 38; -const double _normalSearchFieldWidth = 150; +const double _hiddenSearchFieldWidth = 285; +const double _normalSearchFieldWidth = 285; class WalletManagerSearchField extends StatefulWidget { const WalletManagerSearchField({required this.onChange}); @@ -21,9 +21,12 @@ class WalletManagerSearchField extends StatefulWidget { class _WalletManagerSearchFieldState extends State { double _searchFieldWidth = _normalSearchFieldWidth; final TextEditingController _searchController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + @override void initState() { _searchController.addListener(_onChange); + _focusNode.addListener(_onFocusChange); if (isMobile) { _changeSearchFieldWidth(false); } @@ -33,47 +36,69 @@ class _WalletManagerSearchFieldState extends State { @override void dispose() { _searchController.removeListener(_onChange); + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return AnimatedContainer( duration: const Duration(milliseconds: 200), constraints: BoxConstraints.tightFor( width: _searchFieldWidth, - height: isMobile ? _hiddenSearchFieldWidth : 30, + height: isMobile ? _hiddenSearchFieldWidth : 40, ), - child: UiTextFormField( + child: TextFormField( key: const Key('wallet-page-search-field'), controller: _searchController, + focusNode: _focusNode, autocorrect: false, - onFocus: (FocusNode node) { - _searchController.text = _searchController.text.trim(); - if (!isMobile) return; - _changeSearchFieldWidth(node.hasFocus); - }, textInputAction: TextInputAction.none, enableInteractiveSelection: true, - prefixIcon: Icon( - Icons.search, - size: isMobile ? 25 : 18, - ), inputFormatters: [LengthLimitingTextInputFormatter(40)], - hintText: LocaleKeys.searchAssets.tr(), - hintTextStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - height: 1.3), - inputContentPadding: const EdgeInsets.fromLTRB(0, 0, 12, 0), - maxLines: 1, - style: const TextStyle(fontSize: 12), - fillColor: _searchFieldColor, + decoration: InputDecoration( + filled: true, + // fillColor: theme.colorScheme.surfaceContainer, + // hintText: LocaleKeys.searchAssets.tr(), + hintText: LocaleKeys.search.tr(), + // hintStyle: theme.textTheme.bodyMedium?.copyWith( + // color: theme.colorScheme.onSurfaceVariant, + // ), + prefixIcon: Icon( + Icons.search, + size: 20, + // color: theme.colorScheme.onSurfaceVariant, + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + // enabledBorder: OutlineInputBorder( + // borderRadius: BorderRadius.circular(12), + // borderSide: BorderSide.none, + // ), + // focusedBorder: OutlineInputBorder( + // borderRadius: BorderRadius.circular(12), + // borderSide: BorderSide.none, + // ), + // ), + // style: theme.textTheme.bodyMedium?.copyWith( + // color: theme.colorScheme.onSurface, + ), ), ); } + void _onFocusChange() { + if (!isMobile) return; + _changeSearchFieldWidth(_focusNode.hasFocus); + } + void _changeSearchFieldWidth(bool hasFocus) { if (hasFocus) { setState(() => _searchFieldWidth = _normalSearchFieldWidth); @@ -85,8 +110,4 @@ class _WalletManagerSearchFieldState extends State { void _onChange() { widget.onChange(_searchController.text.trim()); } - - Color? get _searchFieldColor { - return isMobile ? theme.custom.searchFieldMobile : null; - } } 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..2e44a3fba9 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -1,15 +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/komodo_ui.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({ + super.key, this.onPortfolioGrowthPressed, this.onPortfolioProfitLossPressed, }); @@ -19,17 +20,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(); - - int assetCount = coins.length; - + final int assetCount = state.walletCoins.length; final stateWithData = portfolioAssetsOverviewBloc.state is PortfolioAssetsOverviewLoadSuccess ? portfolioAssetsOverviewBloc.state @@ -42,6 +38,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), @@ -61,7 +58,7 @@ class WalletOverview extends StatelessWidget { size: 16, ), const SizedBox(width: 4), - Text('$assetCount ${LocaleKeys.asset.tr()}'), + Text('$assetCount ${LocaleKeys.assets.tr()}'), ], ), ), diff --git a/lib/views/wallet/wallet_page/wallet_page.dart b/lib/views/wallet/wallet_page/wallet_page.dart index 384f5920ed..a8cb32eaf1 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,28 @@ 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) { + 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_events_factory.dart b/lib/views/wallets_manager/wallets_manager_events_factory.dart index 7c5653f53f..4877dfaa05 100644 --- a/lib/views/wallets_manager/wallets_manager_events_factory.dart +++ b/lib/views/wallets_manager/wallets_manager_events_factory.dart @@ -44,7 +44,7 @@ class WalletsManagerEvent extends AnalyticsEventData { } @override - Map get parameters => { + Map get parameters => { 'source': source, 'method': method, }; 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/custom_seed_checkbox.dart b/lib/views/wallets_manager/widgets/custom_seed_checkbox.dart new file mode 100644 index 0000000000..6bfa22f656 --- /dev/null +++ b/lib/views/wallets_manager/widgets/custom_seed_checkbox.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/wallets_manager/widgets/custom_seed_dialog.dart'; + +class CustomSeedCheckbox extends StatelessWidget { + const CustomSeedCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + final bool value; + final void Function(bool) onChanged; + + @override + Widget build(BuildContext context) { + return UiCheckbox( + checkboxKey: const Key('checkbox-custom-seed'), + value: value, + text: LocaleKeys.allowCustomFee.tr(), + onChanged: (newValue) async { + if (!value && newValue) { + final confirmed = await customSeedDialog(context); + if (!confirmed) return; + } + + onChanged(newValue); + }, + ); + } +} 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/hdwallet_mode_switch.dart b/lib/views/wallets_manager/widgets/hdwallet_mode_switch.dart new file mode 100644 index 0000000000..3cab7d3405 --- /dev/null +++ b/lib/views/wallets_manager/widgets/hdwallet_mode_switch.dart @@ -0,0 +1,38 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; + +class HDWalletModeSwitch extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const HDWalletModeSwitch({ + Key? key, + required this.value, + required this.onChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: Row( + children: [ + Text(LocaleKeys.hdWalletModeSwitchTitle.tr()), + const SizedBox(width: 8), + Tooltip( + message: LocaleKeys.hdWalletModeSwitchTooltip.tr(), + child: const Icon(Icons.info, size: 16), + ), + ], + ), + subtitle: Text( + LocaleKeys.hdWalletModeSwitchSubtitle.tr(), + style: const TextStyle( + fontSize: 12, + ), + ), + value: value, + onChanged: onChanged, + ); + } +} diff --git a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart index bf308d11cc..c136e26edb 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, ), - ) + ), ], ), ); @@ -124,7 +126,6 @@ class _IguanaWalletsManagerState extends State { return WalletImportWrapper( key: const Key('wallet-import'), onImport: _importWallet, - onCreate: _createWallet, onCancel: _cancel, ); case WalletsManagerAction.create: @@ -185,65 +186,40 @@ 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( + WalletType? walletType, + }) { + setState(() => _isLoading = true); + final Wallet newWallet = Wallet.fromName( name: name, - password: password, - seed: seed, + walletType: walletType ?? WalletType.iguana, ); - if (newWallet == null) { - setState(() { - _errorText = - LocaleKeys.walletsManagerStepBuilderCreationWalletError.tr(); - }); - - return; - } - - await _reLogin( - seed, - newWallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.create), - ); + context.read().add( + AuthRegisterRequested(wallet: newWallet, password: password), + ); } - 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, - ); - - if (newWallet == null) { - setState(() { - _errorText = - LocaleKeys.walletsManagerStepBuilderCreationWalletError.tr(); - }); + final Wallet newWallet = + Wallet.fromConfig(name: name, config: walletConfig); - return; - } - - await _reLogin( - walletConfig.seedPhrase, - newWallet, - walletsManagerEventsFactory.createEvent( - widget.eventType, WalletsManagerEventMethod.import)); + context.read().add( + AuthRestoreRequested( + wallet: newWallet, + password: password, + seed: walletConfig.seedPhrase, + ), + ); } Future _logInToWallet(String password, Wallet wallet) async { @@ -252,29 +228,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), + 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(() { @@ -283,14 +246,15 @@ class _IguanaWalletsManagerState extends State { } } - Future _reLogin( - String seed, Wallet wallet, AnalyticsEventData analyticsEventData) 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)); + 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..0b63b40497 100644 --- a/lib/views/wallets_manager/widgets/wallet_creation.dart +++ b/lib/views/wallets_manager/widgets/wallet_creation.dart @@ -1,14 +1,17 @@ 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/wallet.dart'; import 'package:web_dex/model/wallets_manager_models.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/views/wallets_manager/widgets/creation_password_fields.dart'; +import 'package:web_dex/views/wallets_manager/widgets/hdwallet_mode_switch.dart'; class WalletCreation extends StatefulWidget { const WalletCreation({ @@ -22,7 +25,7 @@ class WalletCreation extends StatefulWidget { final void Function({ required String name, required String password, - required String seed, + WalletType? walletType, }) onCreate; final void Function() onCancel; @@ -37,6 +40,7 @@ class _WalletCreationState extends State { final GlobalKey _formKey = GlobalKey(); bool _eulaAndTosChecked = false; bool _inProgress = false; + bool _isHdMode = true; @override Widget build(BuildContext context) { @@ -109,11 +113,18 @@ class _WalletCreationState extends State { }, ), const SizedBox(height: 16), + HDWalletModeSwitch( + value: _isHdMode, + onChanged: (value) { + setState(() => _isHdMode = value); + }, + ), ], ); } Widget _buildNameField() { + final walletsRepository = RepositoryProvider.of(context); return UiTextFormField( key: const Key('name-wallet-field'), controller: _nameController, @@ -122,7 +133,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(), ); @@ -134,12 +145,10 @@ class _WalletCreationState extends State { setState(() => _inProgress = true); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final String seed = generateSeed(); - widget.onCreate( name: _nameController.text, password: _passwordController.text, - seed: seed, + walletType: _isHdMode ? WalletType.hdwallet : WalletType.iguana, ); }); } 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..0b2d68cb17 100644 --- a/lib/views/wallets_manager/widgets/wallet_import_by_file.dart +++ b/lib/views/wallets_manager/widgets/wallet_import_by_file.dart @@ -1,16 +1,19 @@ 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_defi_sdk/komodo_defi_sdk.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/disclaimer/eula_tos_checkboxes.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; +import 'package:web_dex/views/wallets_manager/widgets/custom_seed_checkbox.dart'; +import 'package:web_dex/views/wallets_manager/widgets/hdwallet_mode_switch.dart'; class WalletFileData { const WalletFileData({required this.content, required this.name}); @@ -20,11 +23,11 @@ class WalletFileData { class WalletImportByFile extends StatefulWidget { const WalletImportByFile({ - Key? key, + super.key, required this.fileData, required this.onImport, required this.onCancel, - }) : super(key: key); + }); final WalletFileData fileData; final void Function({ @@ -43,6 +46,9 @@ class _WalletImportByFileState extends State { TextEditingController(text: ''); final GlobalKey _formKey = GlobalKey(); bool _isObscured = true; + bool _isHdMode = true; + bool _eulaAndTosChecked = false; + bool _allowCustomSeed = false; String? _filePasswordError; String? _commonError; @@ -51,6 +57,8 @@ class _WalletImportByFileState extends State { return _filePasswordError == null; } + bool get _isButtonEnabled => _eulaAndTosChecked; + @override Widget build(BuildContext context) { return Column( @@ -59,17 +67,18 @@ class _WalletImportByFileState extends State { Text( LocaleKeys.walletImportByFileTitle.tr(), style: Theme.of(context).textTheme.titleLarge!.copyWith( - fontSize: 18, + fontSize: 24, ), ), - const SizedBox(height: 36), + const SizedBox(height: 20), Text(LocaleKeys.walletImportByFileDescription.tr(), style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 22), + const SizedBox(height: 20), Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ UiTextFormField( key: const Key('file-password-field'), @@ -95,8 +104,9 @@ class _WalletImportByFileState extends State { Row(children: [ const UiGradientIcon( icon: Icons.folder, + size: 32, ), - const SizedBox(width: 12), + const SizedBox(width: 8), Expanded( child: Text( widget.fileData.name, @@ -116,13 +126,38 @@ class _WalletImportByFileState extends State { ), ), const SizedBox(height: 30), - const UiDivider(), + HDWalletModeSwitch( + value: _isHdMode, + onChanged: (value) { + setState(() => _isHdMode = value); + }, + ), + const SizedBox(height: 15), + if (!_isHdMode) + CustomSeedCheckbox( + value: _allowCustomSeed, + onChanged: (value) { + setState(() { + _allowCustomSeed = value; + }); + }, + ), + const SizedBox(height: 15), + EulaTosCheckboxes( + key: const Key('import-wallet-eula-checks'), + isChecked: _eulaAndTosChecked, + onCheck: (isChecked) { + setState(() { + _eulaAndTosChecked = isChecked; + }); + }, + ), const SizedBox(height: 30), UiPrimaryButton( key: const Key('confirm-password-button'), height: 50, text: LocaleKeys.import.tr(), - onPressed: _onImport, + onPressed: _isButtonEnabled ? _onImport : null, ), const SizedBox(height: 20), UiUnderlineTextButton( @@ -143,6 +178,10 @@ class _WalletImportByFileState extends State { super.dispose(); } + // TODO? Investigate if using this instead of a getter may have limitations + // or issues with multi-instance support + late final KomodoDefiSdk _sdk = context.read(); + Future _onImport() async { final EncryptionTool encryptionTool = EncryptionTool(); final String? fileData = await encryptionTool.decryptData( @@ -164,16 +203,27 @@ class _WalletImportByFileState extends State { try { final WalletConfig walletConfig = WalletConfig.fromJson(json.decode(fileData)); + walletConfig.type = _isHdMode ? WalletType.hdwallet : WalletType.iguana; + final String? decryptedSeed = await encryptionTool.decryptData( _filePasswordController.text, walletConfig.seedPhrase); if (decryptedSeed == null) return; if (!_isValidData) return; - walletConfig.seedPhrase = decryptedSeed; + if ((_isHdMode || !_allowCustomSeed) && + !_sdk.mnemonicValidator.validateBip39(decryptedSeed)) { + setState(() { + _commonError = LocaleKeys.walletCreationBip39SeedError.tr(); + }); + 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_import_wrapper.dart b/lib/views/wallets_manager/widgets/wallet_import_wrapper.dart index 026e338e2e..7a4b8864d4 100644 --- a/lib/views/wallets_manager/widgets/wallet_import_wrapper.dart +++ b/lib/views/wallets_manager/widgets/wallet_import_wrapper.dart @@ -6,16 +6,10 @@ import 'package:web_dex/views/wallets_manager/widgets/wallet_simple_import.dart' class WalletImportWrapper extends StatefulWidget { const WalletImportWrapper({ Key? key, - required this.onCreate, required this.onImport, required this.onCancel, }) : super(key: key); - final void Function({ - required String name, - required String password, - required String seed, - }) onCreate; final void Function({ required String name, required String password, 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..94fe9f65fc 100644 --- a/lib/views/wallets_manager/widgets/wallet_login.dart +++ b/lib/views/wallets_manager/widgets/wallet_login.dart @@ -1,13 +1,15 @@ -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:collection/collection.dart'; +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: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'; +import 'package:web_dex/views/wallets_manager/widgets/hdwallet_mode_switch.dart'; class WalletLogIn extends StatefulWidget { const WalletLogIn({ @@ -33,6 +35,27 @@ class _WalletLogInState extends State { final _backKeyButton = GlobalKey(); final TextEditingController _passwordController = TextEditingController(); bool _inProgress = false; + bool _isHdMode = true; + KdfUser? _user; + + @override + void initState() { + super.initState(); + _fetchKdfUser(); + } + + Future _fetchKdfUser() async { + final kdfSdk = RepositoryProvider.of(context); + final users = await kdfSdk.auth.getUsers(); + final user = users + .firstWhereOrNull((user) => user.walletId.name == widget.wallet.name); + + if (user != null) { + setState(() { + _user = user; + }); + } + } @override void dispose() { @@ -41,19 +64,20 @@ 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; }); WidgetsBinding.instance.addPostFrameCallback((_) { + widget.wallet.config.type = + _isHdMode && _user != null && _user!.isBip39Seed == true + ? WalletType.hdwallet + : WalletType.iguana; + widget.onLogin( _passwordController.text, - wallet, + widget.wallet, ); if (mounted) setState(() => _inProgress = false); @@ -75,6 +99,14 @@ class _WalletLogInState extends State { ), _buildPasswordField(), const SizedBox(height: 20), + if (_user != null && _user!.isBip39Seed == true) + HDWalletModeSwitch( + value: _isHdMode, + onChanged: (value) { + setState(() => _isHdMode = value); + }, + ), + const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 2.0), child: UiPrimaryButton( diff --git a/lib/views/wallets_manager/widgets/wallet_simple_import.dart b/lib/views/wallets_manager/widgets/wallet_simple_import.dart index 569d8bfab1..26223fc24a 100644 --- a/lib/views/wallets_manager/widgets/wallet_simple_import.dart +++ b/lib/views/wallets_manager/widgets/wallet_simple_import.dart @@ -1,27 +1,30 @@ -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_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart' + show MnemonicFailedReason; +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'; -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'; import 'package:web_dex/views/wallets_manager/widgets/creation_password_fields.dart'; -import 'package:web_dex/views/wallets_manager/widgets/custom_seed_dialog.dart'; +import 'package:web_dex/views/wallets_manager/widgets/custom_seed_checkbox.dart'; +import 'package:web_dex/views/wallets_manager/widgets/hdwallet_mode_switch.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, @@ -53,7 +56,8 @@ class _WalletImportWrapperState extends State { bool _isSeedHidden = true; bool _eulaAndTosChecked = false; bool _inProgress = false; - bool? _allowCustomSeed; + bool _allowCustomSeed = false; + bool _isHdMode = true; bool get _isButtonEnabled { return _eulaAndTosChecked && !_inProgress; @@ -70,18 +74,18 @@ class _WalletImportWrapperState extends State { : LocaleKeys.walletImportCreatePasswordTitle .tr(args: [_nameController.text]), style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontSize: 18, + fontSize: 20, ), textAlign: TextAlign.center, ), - const SizedBox(height: 36), + const SizedBox(height: 20), Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildFields(), - const SizedBox(height: 32), + const SizedBox(height: 20), UiPrimaryButton( key: const Key('confirm-seed-button'), text: _inProgress @@ -117,19 +121,11 @@ class _WalletImportWrapperState extends State { } Widget _buildCheckBoxCustomSeed() { - return UiCheckbox( - checkboxKey: const Key('checkbox-custom-seed'), - value: _allowCustomSeed!, - text: LocaleKeys.allowCustomFee.tr(), - onChanged: (bool? data) async { - if (data == null) return; - if (!_allowCustomSeed!) { - final bool confirmed = await customSeedDialog(context); - if (!confirmed) return; - } - + return CustomSeedCheckbox( + value: _allowCustomSeed, + onChanged: (value) { setState(() { - _allowCustomSeed = !_allowCustomSeed!; + _allowCustomSeed = value; }); if (_seedController.text.isNotEmpty && @@ -160,7 +156,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, @@ -181,19 +177,25 @@ class _WalletImportWrapperState extends State { Widget _buildNameAndSeed() { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildNameField(), const SizedBox(height: 16), _buildSeedField(), - if (_allowCustomSeed != null) ...[ - const SizedBox(height: 15), - _buildCheckBoxCustomSeed(), - ], - const SizedBox(height: 25), - UiDivider(text: LocaleKeys.or.tr()), + const SizedBox(height: 16), + HDWalletModeSwitch( + value: _isHdMode, + onChanged: (value) { + setState(() => _isHdMode = value); + }, + ), + const SizedBox(height: 20), + UiDivider(text: LocaleKeys.seedOr.tr()), const SizedBox(height: 20), _buildImportFileButton(), - const SizedBox(height: 22), + const SizedBox(height: 15), + if (!_isHdMode) _buildCheckBoxCustomSeed(), + const SizedBox(height: 15), EulaTosCheckboxes( key: const Key('import-wallet-eula-checks'), isChecked: _eulaAndTosChecked, @@ -208,18 +210,17 @@ class _WalletImportWrapperState extends State { } Widget _buildNameField() { + final walletsRepository = RepositoryProvider.of(context); return UiTextFormField( key: const Key('name-wallet-field'), controller: _nameController, autofocus: true, autocorrect: false, 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(), - validationMode: InputValidationMode.eager, ); } @@ -232,7 +233,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, @@ -275,6 +275,7 @@ class _WalletImportWrapperState extends State { } final WalletConfig config = WalletConfig( + type: _isHdMode ? WalletType.hdwallet : WalletType.iguana, activatedCoins: enabledByDefaultCoins, hasBackup: true, seedPhrase: _seedController.text, @@ -292,18 +293,31 @@ class _WalletImportWrapperState extends State { } String? _validateSeed(String? seed) { - if (seed == null || seed.isEmpty) { - return LocaleKeys.walletCreationEmptySeedError.tr(); - } else if ((_allowCustomSeed != true) && !bip39.validateMnemonic(seed)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _allowCustomSeed = false; - }); - } - }); - return LocaleKeys.walletCreationBip39SeedError.tr(); + final maybeFailedReason = + context.read().mnemonicValidator.validateMnemonic( + seed ?? '', + minWordCount: 12, + maxWordCount: 24, + isHd: _isHdMode, + allowCustomSeed: _allowCustomSeed, + ); + + if (maybeFailedReason == null) { + return null; } - return null; + + return switch (maybeFailedReason) { + MnemonicFailedReason.empty => + LocaleKeys.walletCreationEmptySeedError.tr(), + MnemonicFailedReason.customNotSupportedForHd => + LocaleKeys.walletCreationBip39SeedError.tr(), + MnemonicFailedReason.customNotAllowed => + LocaleKeys.customSeedWarningText.tr(), + MnemonicFailedReason.invalidLength => + // TODO: Add this string has placeholders for min/max counts, which we + // specify as "12" and "24" + // LocaleKeys.seedPhraseCheckingEnterWord.tr(args: ['12', '24']), + LocaleKeys.walletCreationBip39SeedError.tr(), + }; } } diff --git a/lib/views/wallets_manager/widgets/wallet_type_list_item.dart b/lib/views/wallets_manager/widgets/wallet_type_list_item.dart index 1de5e7682f..5ccac0e8b7 100644 --- a/lib/views/wallets_manager/widgets/wallet_type_list_item.dart +++ b/lib/views/wallets_manager/widgets/wallet_type_list_item.dart @@ -18,7 +18,8 @@ class WalletTypeListItem extends StatelessWidget { @override Widget build(BuildContext context) { - final bool needAttractAttention = type == WalletType.iguana; + final bool needAttractAttention = + type == WalletType.iguana || type == WalletType.hdwallet; final bool isSupported = _checkWalletSupport(type); return Column( @@ -35,7 +36,7 @@ class WalletTypeListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (type != WalletType.iguana) + if (type != WalletType.iguana && type != WalletType.hdwallet) SvgPicture.asset( _iconPath, width: 25, @@ -73,6 +74,7 @@ class WalletTypeListItem extends StatelessWidget { String get _iconPath { switch (type) { case WalletType.iguana: + case WalletType.hdwallet: return '$assetsPath/ui_icons/atomic_dex.svg'; case WalletType.metamask: return '$assetsPath/ui_icons/metamask.svg'; @@ -90,6 +92,7 @@ class WalletTypeListItem extends StatelessWidget { String get _walletTypeName { switch (type) { case WalletType.iguana: + case WalletType.hdwallet: return LocaleKeys.komodoWalletSeed.tr(); case WalletType.metamask: return LocaleKeys.metamask.tr(); @@ -103,6 +106,7 @@ class WalletTypeListItem extends StatelessWidget { bool _checkWalletSupport(WalletType type) { switch (type) { case WalletType.iguana: + case WalletType.hdwallet: case WalletType.trezor: return true; case WalletType.keplr: diff --git a/lib/views/wallets_manager/widgets/wallets_list.dart b/lib/views/wallets_manager/widgets/wallets_list.dart index f8b2d6d0e7..136bc9c5ab 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,13 +15,18 @@ 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 = - wallets.where((w) => w.config.type == walletType).toList(); + final List filteredWallets = wallets + .where((w) => + w.config.type == walletType || + (walletType == WalletType.iguana && + w.config.type == WalletType.hdwallet)) + .toList(); if (wallets.isEmpty) { return const SizedBox(width: 0, height: 0); } diff --git a/lib/views/wallets_manager/widgets/wallets_manager.dart b/lib/views/wallets_manager/widgets/wallets_manager.dart index e9d4dea83a..431a1621c7 100644 --- a/lib/views/wallets_manager/widgets/wallets_manager.dart +++ b/lib/views/wallets_manager/widgets/wallets_manager.dart @@ -21,6 +21,7 @@ class WalletsManager extends StatelessWidget { Widget build(BuildContext context) { switch (walletType) { case WalletType.iguana: + case WalletType.hdwallet: return IguanaWalletsManager( close: close, onSuccess: onSuccess, diff --git a/lib/views/wallets_manager/widgets/wallets_type_list.dart b/lib/views/wallets_manager/widgets/wallets_type_list.dart index 77b9735e92..445c0aadcd 100644 --- a/lib/views/wallets_manager/widgets/wallets_type_list.dart +++ b/lib/views/wallets_manager/widgets/wallets_type_list.dart @@ -11,6 +11,7 @@ class WalletsTypeList extends StatelessWidget { Widget build(BuildContext context) { return Column( children: WalletType.values + .where((type) => type != WalletType.hdwallet) .map((type) => Padding( padding: const EdgeInsets.only(bottom: 12.0), child: WalletTypeListItem( diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 6e9c958168..502858c4fc 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -136,8 +136,3 @@ if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() - -install(CODE " - configure_file(\"${CMAKE_CURRENT_SOURCE_DIR}/mm2/mm2\" \"${CMAKE_INSTALL_PREFIX}/mm2\" COPYONLY) - " -) \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 55e53518fd..906a05dda2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,14 @@ #include "generated_plugin_registrant.h" -#include +#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) 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 8d2c737526..5731285cba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,12 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window + 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/linux/mm2/.gitkeep b/linux/mm2/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/macos/.gitignore b/macos/.gitignore index d2fd377230..88b612aed7 100644 --- a/macos/.gitignore +++ b/macos/.gitignore @@ -4,3 +4,5 @@ # Xcode-related **/xcuserdata/ + +build/* diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c287d6c379..a7a6478b6d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation -import desktop_webview_window +import file_picker import firebase_analytics import firebase_core import flutter_inappwebview_macos +import flutter_secure_storage_darwin +import local_auth_darwin import mobile_scanner import package_info_plus import path_provider_foundation @@ -19,12 +21,14 @@ import video_player_avfoundation import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/macos/Podfile b/macos/Podfile index 1560be84db..fb9b4184a4 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,13 @@ -platform :osx, '10.14' +require 'xcodeproj' + +def deployment_target + project_path = 'Runner.xcodeproj' + project = Xcodeproj::Project.open(project_path) + target = project.targets.first + target.build_configurations.first.build_settings['MACOSX_DEPLOYMENT_TARGET'] +end + +platform :osx, deployment_target # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -40,7 +49,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) target.build_configurations.each do |config| - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.0' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = deployment_target end end end diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 156a72c5ad..f793cd6771 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,109 +1,116 @@ PODS: - - desktop_webview_window (0.0.1): + - file_picker (0.0.1): - FlutterMacOS - - Firebase/Analytics (10.25.0): + - Firebase/Analytics (11.8.0): - Firebase/Core - - Firebase/Core (10.25.0): + - Firebase/Core (11.8.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.25.0) - - Firebase/CoreOnly (10.25.0): - - FirebaseCore (= 10.25.0) - - firebase_analytics (10.10.5): - - Firebase/Analytics (= 10.25.0) + - FirebaseAnalytics (~> 11.8.0) + - Firebase/CoreOnly (11.8.0): + - FirebaseCore (~> 11.8.0) + - firebase_analytics (11.4.4): + - Firebase/Analytics (= 11.8.0) - firebase_core - FlutterMacOS - - firebase_core (2.31.0): - - Firebase/CoreOnly (~> 10.25.0) + - firebase_core (3.12.1): + - Firebase/CoreOnly (~> 11.8.0) - FlutterMacOS - - 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.30911.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.25.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 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.30911.0, >= 2.30908.0) - - FirebaseCore (10.25.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.28.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.28.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) + - FirebaseAnalytics (11.8.0): + - FirebaseAnalytics/AdIdSupport (= 11.8.0) + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/AdIdSupport (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseCore (11.8.1): + - FirebaseCoreInternal (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.8.0): + - FirebaseCore (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - flutter_inappwebview_macos (0.0.1): - FlutterMacOS - - OrderedSet (~> 5.0) + - OrderedSet (~> 6.0.3) + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS - FlutterMacOS (1.0.0) - - 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.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.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.30911.0, >= 2.30908.0) - - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleAppMeasurement (11.8.0): + - GoogleAppMeasurement/AdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/AdIdSupport (11.8.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/WithoutAdIdSupport (11.8.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (7.13.3): + - GoogleUtilities/MethodSwizzler (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.3)": + - "GoogleUtilities/NSData+zlib (8.0.2)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - mobile_scanner (5.1.1): + - komodo_defi_framework (0.0.1): + - FlutterMacOS + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS + - mobile_scanner (6.0.2): - 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) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - OrderedSet (6.0.3) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -124,11 +131,14 @@ PODS: - FlutterMacOS DEPENDENCIES: - - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/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`) + - flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - 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`) @@ -152,16 +162,22 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: - desktop_webview_window: - :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos firebase_analytics: :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos firebase_core: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos flutter_inappwebview_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos + flutter_secure_storage_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin 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: @@ -180,30 +196,33 @@ 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 + file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af + Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf + firebase_analytics: 75b9d9ea8b21ce77898a3a46910e2051e93db8e1 + firebase_core: 1b573eac37729348cdc472516991dd7e269ae37e + FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b + FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d + FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 + FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 + flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b + flutter_secure_storage_darwin: 12d2375c690785d97a4e586f15f11be5ae35d5b0 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b - nanopb: 438bc412db1928dac798aa6fd75726007be04262 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + komodo_defi_framework: f0b88d0dfa7907a9ece425bf4f1435cd94cfdc73 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + mobile_scanner: 07710d6b9b2c220ae899de2d7ecf5d77ffa56333 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 -PODFILE CHECKSUM: 837d51985fe358f89b82d0f3805fc2fd357bd915 +PODFILE CHECKSUM: d064900e78ded0efef7fcc0db57cbf4bc2487624 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 2119f7e23a..c3b081112e 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; @@ -361,8 +354,11 @@ "${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}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_macos/flutter_inappwebview_macos.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_darwin/flutter_secure_storage_darwin.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,8 +377,11 @@ "${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}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_darwin.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", @@ -405,9 +404,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; @@ -473,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -489,11 +487,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", @@ -507,8 +505,8 @@ "$(inherited)", "$(PROJECT_DIR)", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -565,7 +563,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -612,7 +610,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -628,10 +626,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", @@ -646,8 +645,9 @@ "$(PROJECT_DIR)", ); PRODUCT_BUNDLE_IDENTIFIER = com.komodo.komodowallet; + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -661,10 +661,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", @@ -679,8 +680,8 @@ "$(PROJECT_DIR)", ); PRODUCT_BUNDLE_IDENTIFIER = com.komodo.komodowallet; + MACOSX_DEPLOYMENT_TARGET = 15.0; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 170663764b..d0f2c56865 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -58,6 +58,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 77cae2b3ff..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, *) -@NSApplicationMain -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 - } +@main +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/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 ae583c6f10..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": "npx webpack --config=webpack.config.js --mode=production", - "build:dev": "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" - } -} 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_wallet_build_transformer/pubspec.lock b/packages/komodo_cex_market_data/pubspec.lock similarity index 60% rename from packages/komodo_wallet_build_transformer/pubspec.lock rename to packages/komodo_cex_market_data/pubspec.lock index 6a2de45493..3acd3419b9 100644 --- a/packages/komodo_wallet_build_transformer/pubspec.lock +++ b/packages/komodo_cex_market_data/pubspec.lock @@ -5,95 +5,98 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "68.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.1.0" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "7.3.0" args: - dependency: "direct main" + dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.11.1" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" - csslib: - dependency: transitive + version: "3.0.6" + equatable: + dependency: "direct main" description: - name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.7" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "5.0.0" frontend_server_client: dependency: transitive description: @@ -106,106 +109,99 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" - html: + version: "2.1.3" + hive: dependency: "direct main" description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" + 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: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" lints: - dependency: "direct dev" + dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "5.1.1" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 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" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -218,18 +214,18 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: - dependency: "direct main" + dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" pool: dependency: transitive description: @@ -242,18 +238,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -266,146 +262,146 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: d11b55850c68c1f6c0cf00eabded4e66c4043feaf6c0d7ce4a36785137df6331 + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.5" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.8" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vm_service: dependency: transitive description: name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.2" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: name: web_socket - sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -418,9 +414,9 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index 8f73e78c17..949792242d 100644 --- a/packages/komodo_cex_market_data/pubspec.yaml +++ b/packages/komodo_cex_market_data/pubspec.yaml @@ -4,17 +4,14 @@ version: 0.0.1 publish_to: none # publishable packages should not have git dependencies environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.6.0 <4.0.0" # Add regular dependencies here. dependencies: - http: 0.13.6 # dart.dev + http: 1.3.0 # dart.dev + + equatable: 2.0.7 - equatable: - git: - url: https://github.com/KomodoPlatform/equatable.git - ref: 2117551ff3054f8edb1a58f63ffe1832a8d25623 #2.0.5 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: git: @@ -23,5 +20,5 @@ dependencies: ref: 470473ffc1ba39f6c90f31ababe0ee63b76b69fe #2.2.3 dev_dependencies: - flutter_lints: ^2.0.0 # flutter.dev + flutter_lints: ^5.0.0 # flutter.dev test: ^1.24.0 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 6dbf468023..0000000000 --- a/packages/komodo_coin_updates/lib/src/komodo_coin_updater.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; - -import 'models/coin_info.dart'; -import 'models/models.dart'; - -class KomodoCoinUpdater { - static Future ensureInitialized(String appFolder) async { - await Hive.initFlutter(appFolder); - 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 eda1a1fc7d..0000000000 --- a/packages/komodo_coin_updates/pubspec.yaml +++ /dev/null @@ -1,45 +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: 0.13.6 # dart.dev - - 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 - - # 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_persistence_layer/pubspec.yaml b/packages/komodo_persistence_layer/pubspec.yaml index 8a1ad6c74e..9b0136644a 100644 --- a/packages/komodo_persistence_layer/pubspec.yaml +++ b/packages/komodo_persistence_layer/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.6.0 <4.0.0" # Add regular dependencies here. dependencies: @@ -16,5 +16,5 @@ dependencies: ref: 470473ffc1ba39f6c90f31ababe0ee63b76b69fe #2.2.3 dev_dependencies: - lints: ^2.1.0 + lints: ^5.1.1 test: ^1.24.0 diff --git a/packages/komodo_ui_kit/lib/komodo_ui_kit.dart b/packages/komodo_ui_kit/lib/komodo_ui_kit.dart index 090a753745..e134fd30b0 100644 --- a/packages/komodo_ui_kit/lib/komodo_ui_kit.dart +++ b/packages/komodo_ui_kit/lib/komodo_ui_kit.dart @@ -6,7 +6,6 @@ library komodo_ui_kit; // Buttons // This category includes various button widgets used throughout the UI, // providing different styles and functionalities. -export 'src/buttons/divided_button.dart'; // New button export 'src/buttons/hyperlink.dart'; export 'src/buttons/language_switcher/language_switcher.dart'; export 'src/buttons/multiselect_dropdown/filter_container.dart'; @@ -36,7 +35,6 @@ export 'src/custom_icons/custom_icons.dart'; // Display // Widgets primarily focused on displaying data and information. export 'src/display/statistic_card.dart'; -export 'src/display/trend_percentage_text.dart'; // Dividers // Widgets for dividing content or adding scrollbars. export 'src/dividers/ui_divider.dart'; diff --git a/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart b/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart deleted file mode 100644 index 9d4bab2063..0000000000 --- a/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -class DividedButton extends StatelessWidget { - final List children; - final EdgeInsetsGeometry? childPadding; - final VoidCallback? onPressed; - - const DividedButton({ - required this.children, - this.childPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - super.key, - this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return FilledButton( - style: - (Theme.of(context).segmentedButtonTheme.style ?? const ButtonStyle()) - .copyWith( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - textStyle: WidgetStatePropertyAll( - Theme.of(context).textTheme.labelMedium, - ), - padding: const WidgetStatePropertyAll(EdgeInsets.zero), - backgroundColor: WidgetStatePropertyAll( - Theme.of(context) - .segmentedButtonTheme - .style - ?.backgroundColor - ?.resolve({WidgetState.focused}) ?? - Theme.of(context).colorScheme.surface, - ), - ), - onPressed: onPressed, - child: Row( - children: [ - for (int i = 0; i < children.length; i++) ...[ - Padding( - padding: childPadding!, - child: children[i], - ), - if (i < children.length - 1) - const SizedBox( - height: 32, - child: VerticalDivider( - width: 1, - thickness: 1, - indent: 2, - endIndent: 2, - ), - ), - ], - ], - ), - ); - } -} 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/ui_checkbox.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_checkbox.dart index 0afcbb7c5c..c9f19f8bab 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_checkbox.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_checkbox.dart @@ -7,6 +7,7 @@ class UiCheckbox extends StatelessWidget { this.checkboxKey, this.onChanged, this.text = '', + this.textWidget, this.size = 18, this.textColor, this.borderColor, @@ -16,6 +17,7 @@ class UiCheckbox extends StatelessWidget { final Key? checkboxKey; final bool value; final String text; + final Text? textWidget; final double size; final Color? borderColor; final Color? textColor; @@ -34,6 +36,7 @@ class UiCheckbox extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(2), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( @@ -61,17 +64,18 @@ class UiCheckbox extends StatelessWidget { ) : const SizedBox.shrink(), ), - if (text.isNotEmpty) + if (textWidget != null || text.isNotEmpty) Flexible( child: Padding( padding: const EdgeInsets.only(left: 8, right: 2), - child: Text( - text, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontSize: 14, color: textColor), - ), + child: textWidget ?? + Text( + text, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(fontSize: 14, color: textColor), + ), ), ), ], 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/controls/market_chart_header_controls.dart b/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart index 9dc0f266f6..0424ee4a9d 100644 --- a/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart +++ b/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/src/controls/selected_coin_graph_control.dart'; -import 'package:komodo_ui_kit/src/inputs/coin_search_dropdown.dart'; import 'package:komodo_ui_kit/src/inputs/time_period_selector.dart'; import 'package:komodo_ui_kit/src/utils/gap.dart'; @@ -8,7 +9,7 @@ class MarketChartHeaderControls extends StatelessWidget { final Widget title; final Widget? leadingIcon; final Widget leadingText; - final List availableCoins; + final List availableCoins; final String? selectedCoinId; final void Function(String?)? onCoinSelected; final double centreAmount; @@ -16,7 +17,7 @@ class MarketChartHeaderControls extends StatelessWidget { final List timePeriods; final Duration selectedPeriod; final void Function(Duration?) onPeriodChanged; - final CoinSelectItem Function(String coinId)? customCoinItemBuilder; + final SelectItem Function(AssetId coinId)? customCoinItemBuilder; final bool emptySelectAllowed; const MarketChartHeaderControls({ diff --git a/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart b/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart index 227a9a6c7c..ed16b95cd9 100644 --- a/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart +++ b/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:komodo_ui_kit/src/buttons/divided_button.dart'; -import 'package:komodo_ui_kit/src/display/trend_percentage_text.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/src/images/coin_icon.dart'; -import 'package:komodo_ui_kit/src/inputs/coin_search_dropdown.dart'; class SelectedCoinGraphControl extends StatelessWidget { const SelectedCoinGraphControl({ @@ -26,9 +25,9 @@ class SelectedCoinGraphControl extends StatelessWidget { /// A list of coin IDs that are available for selection. /// /// Must be non-null and not empty if [onCoinSelected] is non-null. - final List? availableCoins; + final List? availableCoins; - final CoinSelectItem Function(String)? customCoinItemBuilder; + final SelectItem Function(AssetId)? customCoinItemBuilder; @override Widget build(BuildContext context) { @@ -50,7 +49,7 @@ class SelectedCoinGraphControl extends StatelessWidget { customCoinItemBuilder: customCoinItemBuilder, ); if (selectedCoin != null) { - onCoinSelected?.call(selectedCoin.coinId); + onCoinSelected?.call(selectedCoin.id); } }, children: [ 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/display/trend_percentage_text.dart b/packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart deleted file mode 100644 index 966be6440d..0000000000 --- a/packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -class TrendPercentageText extends StatelessWidget { - const TrendPercentageText({ - super.key, - required this.investmentReturnPercentage, - }); - - final double investmentReturnPercentage; - - @override - Widget build(BuildContext context) { - final iconTextColor = investmentReturnPercentage > 0 - ? Colors.green - : investmentReturnPercentage == 0 - ? Theme.of(context).disabledColor - : Theme.of(context).colorScheme.error; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - investmentReturnPercentage > 0 - ? Icons.trending_up - : (investmentReturnPercentage == 0) - ? Icons.trending_flat - : Icons.trending_down, - color: iconTextColor, - ), - const SizedBox(width: 2), - Text( - '${(investmentReturnPercentage).toStringAsFixed(2)}%', - style: (Theme.of(context).textTheme.bodyLarge ?? - const TextStyle( - fontSize: 12, - )) - .copyWith(color: iconTextColor), - ), - ], - ); - } -} 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 fff1790764..2cbf40f50d 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,11 +2,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -const coinImagesFolder = 'assets/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 = {}; final Map _cdnExistenceCache = {}; +final Map _customIconsCache = {}; List? _cachedFileList; String _getImagePath(String abbr) { @@ -66,7 +69,12 @@ Future checkIfAssetExists(String abbr) async { } } +const _deprecatedCoinIconMessage = + 'CoinIcon is deprecated. Use AssetIcon from the SDK\'s `komodo_ui` package instead.'; + +@Deprecated(_deprecatedCoinIconMessage) class CoinIcon extends StatelessWidget { + @Deprecated(_deprecatedCoinIconMessage) const CoinIcon( this.coinAbbr, { this.size = 20, @@ -76,6 +84,7 @@ class CoinIcon extends StatelessWidget { /// Convenience constructor for creating a coin icon from a symbol aka /// abbreviation. This avoids having to call [abbr2Ticker] manually. + @Deprecated(_deprecatedCoinIconMessage) CoinIcon.ofSymbol( String symbol, { this.size = 20, @@ -87,6 +96,41 @@ class CoinIcon extends StatelessWidget { final double size; final bool suspended; + /// Registers a custom icon for a given coin abbreviation. + /// + /// The [imageProvider] will be used instead of the default asset or CDN images + /// when displaying the icon for the specified [coinAbbr]. + /// + /// Example: + /// ```dart + /// // Register a custom icon from an asset + /// CoinIcon.registerCustomIcon( + /// 'MYCOIN', + /// AssetImage('assets/my_custom_coin.png'), + /// ); + /// + /// // Register a custom icon from memory + /// CoinIcon.registerCustomIcon( + /// 'MYCOIN', + /// MemoryImage(customIconBytes), + /// ); + /// ``` + static void registerCustomIcon(String coinAbbr, ImageProvider imageProvider) { + final normalizedAbbr = abbr2Ticker(coinAbbr).toLowerCase(); + _customIconsCache[normalizedAbbr] = imageProvider; + } + + /// Removes a custom icon registration for the specified coin abbreviation. + static void removeCustomIcon(String coinAbbr) { + final normalizedAbbr = abbr2Ticker(coinAbbr).toLowerCase(); + _customIconsCache.remove(normalizedAbbr); + } + + /// Clears all custom icon registrations. + static void clearCustomIcons() { + _customIconsCache.clear(); + } + @override Widget build(BuildContext context) { return Opacity( @@ -106,6 +150,7 @@ class CoinIcon extends StatelessWidget { _assetExistenceCache.clear(); _cdnExistenceCache.clear(); _cachedFileList = null; + _customIconsCache.clear(); } /// Pre-loads the coin icon image into the cache. @@ -121,6 +166,26 @@ class CoinIcon extends StatelessWidget { bool throwExceptions = false, }) async { try { + final normalizedAbbr = abbr2Ticker(abbr).toLowerCase(); + + // Check for custom icon first + if (_customIconsCache.containsKey(normalizedAbbr)) { + if (context.mounted) { + await precacheImage( + _customIconsCache[normalizedAbbr]!, + context, + onError: (e, stackTrace) { + if (throwExceptions) { + throw Exception( + 'Failed to pre-cache custom image for coin $abbr: $e', + ); + } + }, + ); + } + return; + } + bool? assetExists, cdnExists; final filePath = _getImagePath(abbr); @@ -176,10 +241,22 @@ class CoinIconResolverWidget extends StatelessWidget { @override Widget build(BuildContext context) { - // Check local asset first - final filePath = _getImagePath(coinAbbr); + final normalizedAbbr = abbr2Ticker(coinAbbr).toLowerCase(); + + // Check for custom icon first + if (_customIconsCache.containsKey(normalizedAbbr)) { + return Image( + image: _customIconsCache[normalizedAbbr]!, + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) { + debugPrint('Error loading custom icon for $coinAbbr: $error'); + return Icon(Icons.monetization_on_outlined, size: size); + }, + ); + } - // if (await checkIfAssetExists(coinAbbr)) { + // Check local asset + final filePath = _getImagePath(coinAbbr); _assetExistenceCache[filePath] = true; return Image.asset( @@ -201,7 +278,6 @@ class CoinIconResolverWidget extends StatelessWidget { ); } } - // DUPLICATED FROM MAIN PROJECT in `lib/shared/utils/utils.dart`. // NB: ENSURE IT STAYS IN SYNC. diff --git a/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart b/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart index 06c058219f..8b5578f333 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart @@ -1,389 +1,66 @@ import 'package:flutter/material.dart'; -import 'dart:async'; - +import 'package:komodo_ui/komodo_ui.dart'; import 'package:komodo_ui_kit/src/images/coin_icon.dart'; -class CoinSelectItem { - CoinSelectItem({ - required this.name, - required this.coinId, - this.leading, - this.trailing, +/// A specialized version of [SearchableSelect] for cryptocurrency selection. +class CoinSelect extends StatelessWidget { + const CoinSelect({ + super.key, + required this.coins, + required this.onCoinSelected, + this.customCoinItemBuilder, + this.initialCoin, + this.controller, }); - final String name; - final String coinId; - final Widget? trailing; - final Widget? leading; -} + /// List of coin IDs to show in the selector + final List coins; -class CryptoSearchDelegate extends SearchDelegate { - CryptoSearchDelegate(this.items); + /// Callback when a coin is selected + final Function(String coinId) onCoinSelected; - final Iterable items; + /// Optional custom builder for coin items + final SelectItem Function(String coinId)? customCoinItemBuilder; - @override - List buildActions(BuildContext context) { - return [ - IconButton(icon: const Icon(Icons.clear), onPressed: () => query = ''), - ]; - } + /// Optional initial selected coin + final String? initialCoin; - @override - Widget buildLeading(BuildContext context) { - return IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => close(context, null), - ); - } - - @override - Widget buildResults(BuildContext context) { - final results = items - .where((item) => item.name.toLowerCase().contains(query.toLowerCase())) - .toList(); + /// Optional controller for external state management + final SearchableSelectController? controller; - return ListView.builder( - itemCount: results.length, - itemBuilder: (context, index) { - final item = results[index]; - return CoinListTile( - item: item, - onTap: () => close(context, item), - ); - }, + SelectItem _defaultCoinItemBuilder(String coin) { + return SelectItem( + id: coin, + title: coin, + value: coin, + leading: CoinIcon.ofSymbol(coin), ); } - @override - Widget buildSuggestions(BuildContext context) { - final suggestions = items - .where((item) => item.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - - return ListView.builder( - itemCount: suggestions.length, - itemBuilder: (context, index) { - final item = suggestions[index]; - return CoinListTile( - item: item, - onTap: () => query = item.name, - ); - }, - ); - } -} - -class CoinListTile extends StatelessWidget { - const CoinListTile({ - Key? key, - required this.item, - this.onTap, - }) : super(key: key); - - final CoinSelectItem item; - final VoidCallback? onTap; - @override Widget build(BuildContext context) { - return ListTile( - leading: item.leading ?? CoinIcon.ofSymbol(item.coinId), - title: Text(item.name), - trailing: item.trailing, - onTap: onTap, - ); - } -} - -Future showCoinSearch( - BuildContext context, { - required List coins, - CoinSelectItem Function(String coinId)? customCoinItemBuilder, -}) async { - final isMobile = MediaQuery.of(context).size.width < 600; - - final items = coins.map( - (coin) => - customCoinItemBuilder?.call(coin) ?? _defaultCoinItemBuilder(coin), - ); - - if (isMobile) { - return await showSearch( - context: context, - delegate: CryptoSearchDelegate(items), - ); - } else { - return await showDropdownSearch(context, items); - } -} - -CoinSelectItem _defaultCoinItemBuilder(String coin) { - return CoinSelectItem( - name: coin, - coinId: coin, - leading: CoinIcon.ofSymbol(coin), - ); -} - -OverlayEntry? _overlayEntry; -Completer? _completer; - -Future showDropdownSearch( - BuildContext context, - Iterable items, -) async { - final renderBox = context.findRenderObject() as RenderBox; - final offset = renderBox.localToGlobal(Offset.zero); - - void clearOverlay() { - _overlayEntry?.remove(); - _overlayEntry = null; - _completer = null; - } - - void onItemSelected(CoinSelectItem? item) { - _completer?.complete(item); - clearOverlay(); - } - - clearOverlay(); - - _completer = Completer(); - _overlayEntry = OverlayEntry( - builder: (context) { - return GestureDetector( - onTap: () => onItemSelected(null), - behavior: HitTestBehavior.translucent, - child: Stack( - children: [ - Positioned( - left: offset.dx, - top: offset.dy + renderBox.size.height, - width: 300, - child: _DropdownSearch( - items: items, - onSelected: onItemSelected, - ), - ), - ], - ), - ); - }, - ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - Overlay.of(context).insert(_overlayEntry!); - }); - - return _completer!.future; -} - -class _DropdownSearch extends StatefulWidget { - final Iterable items; - final ValueChanged onSelected; - - const _DropdownSearch({required this.items, required this.onSelected}); - - @override - State<_DropdownSearch> createState() => __DropdownSearchState(); -} - -class __DropdownSearchState extends State<_DropdownSearch> { - late Iterable filteredItems; - String query = ''; - final FocusNode _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - filteredItems = widget.items; - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _focusNode.requestFocus(); - } - }); - } - - void updateSearchQuery(String newQuery) { - setState(() { - query = newQuery; - filteredItems = widget.items.where( - (item) => item.name.toLowerCase().contains(query.toLowerCase()), - ); - }); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - color: Theme.of(context).colorScheme.surfaceContainerLow, - child: Container( - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: TextField( - focusNode: _focusNode, - autofocus: true, - decoration: InputDecoration( - hintText: 'Search', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - prefixIcon: const Icon(Icons.search), - ), - onChanged: updateSearchQuery, - ), - ), - Flexible( - child: ListView.builder( - itemCount: filteredItems.length, - itemBuilder: (context, index) { - final item = filteredItems.elementAt(index); - return CoinListTile( - item: item, - onTap: () => widget.onSelected(item), - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -class CoinDropdown extends StatefulWidget { - final List items; - final Function(CoinSelectItem) onItemSelected; - - const CoinDropdown({ - super.key, - required this.items, - required this.onItemSelected, - }); - - @override - State createState() => _CoinDropdownState(); -} - -class _CoinDropdownState extends State { - CoinSelectItem? selectedItem; - - void _showSearch(BuildContext context) async { - final selected = await showCoinSearch( - context, - coins: widget.items.map((e) => e.coinId).toList(), - customCoinItemBuilder: (coinId) { - return widget.items.firstWhere((e) => e.coinId == coinId); - }, - ); - if (selected != null) { - setState(() { - selectedItem = selected; - }); - widget.onItemSelected(selected); - } - } + final items = coins + .map( + (coin) => + customCoinItemBuilder?.call(coin) ?? + _defaultCoinItemBuilder(coin), + ) + .toList(); - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () => _showSearch(context), - child: InputDecorator( - isEmpty: selectedItem == null, - decoration: const InputDecoration( - hintText: 'Select a Coin', - border: OutlineInputBorder(), - ), - child: selectedItem == null - ? null - : Row( - children: [ - Text(selectedItem!.name), - const Spacer(), - selectedItem?.trailing ?? const SizedBox(), - ], - ), - ), + // Find initial value if provided + final initialValue = initialCoin != null + ? items.firstWhere( + (item) => item.value == initialCoin, + orElse: () => items.first, + ) + : null; + + return SearchableSelect( + items: items, + onItemSelected: (item) => onCoinSelected(item.value), + hint: 'Select a coin', + initialValue: initialValue?.value, + controller: controller, ); } } - -// Example usage - -// void main() { -// runApp(const MyApp()); -// } - -// class MyApp extends StatelessWidget { -// const MyApp({super.key}); - -// @override -// Widget build(BuildContext context) { -// final items = [ -// CoinSelectItem( -// name: "KMD", -// coinId: "KMD", -// trailing: const Text('+2.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "SecondLive", -// coinId: "SL", -// trailing: const Text('+322.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "KiloEx", -// coinId: "KE", -// trailing: const Text('-2.09%', style: TextStyle(color: Colors.red)), -// ), -// CoinSelectItem( -// name: "Native", -// coinId: "NT", -// trailing: const Text('+225.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "XY Finance", -// coinId: "XY", -// trailing: const Text('+62.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "KMD", -// coinId: "KMD", -// trailing: const Text('+2.9%', style: TextStyle(color: Colors.green)), -// ), -// ]; - -// return MaterialApp( -// home: Scaffold( -// appBar: AppBar(title: const Text('Crypto Selector')), -// body: Padding( -// padding: const EdgeInsets.all(16.0), -// child: CoinDropdown( -// items: items, -// onItemSelected: (item) { -// // Handle item selection -// print('Selected item: ${item.name}'); -// }, -// ), -// ), -// ), -// ); -// } -// } diff --git a/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart b/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart index 8787b4101d..ffbff35f09 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart @@ -28,7 +28,7 @@ class PercentageInput extends StatefulWidget { class _PercentageInputState extends State { late TextEditingController _controller; - String _lastEmittedValue = ''; + String? _lastEmittedValue; bool _shouldUpdateText = true; @override @@ -60,11 +60,11 @@ class _PercentageInputState extends State { } } - void _handlePercentageChanged(String value) { + void _handlePercentageChanged(String? value) { if (value != _lastEmittedValue) { _lastEmittedValue = value; _shouldUpdateText = false; - widget.onChanged?.call(value); + widget.onChanged?.call(value ?? ''); _shouldUpdateText = true; } } @@ -97,7 +97,7 @@ class _PercentageInputState extends State { widget.maxFractionDigits.toString() + r'})?$', ), - replacementString: _lastEmittedValue, + replacementString: _lastEmittedValue ?? '', ), _DecimalInputFormatter(), ], diff --git a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart index 4b840a9b10..3a0dc26b60 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart @@ -15,16 +15,19 @@ import 'package:komodo_ui_kit/komodo_ui_kit.dart'; /// /// The `UiTextFormField` can be customized using various parameters such as /// `hintText`, `controller`, `inputFormatters`, `textInputAction`, and more. + class UiTextFormField extends StatefulWidget { const UiTextFormField({ super.key, this.initialValue, this.hintText, + this.labelText, this.controller, this.inputFormatters, this.textInputAction, this.style, this.hintTextStyle, + this.labelStyle, this.inputContentPadding, this.keyboardType, this.validator, @@ -48,7 +51,7 @@ class UiTextFormField extends StatefulWidget { this.maxLength, this.maxLengthEnforcement, this.counterText, - this.labelStyle, + this.helperText, this.enabledBorder, this.focusedBorder, this.errorStyle, @@ -57,6 +60,8 @@ class UiTextFormField extends StatefulWidget { final String? initialValue; final String? hintText; + final String? labelText; + final String? helperText; final TextEditingController? controller; final List? inputFormatters; final TextInputAction? textInputAction; @@ -79,7 +84,7 @@ class UiTextFormField extends StatefulWidget { final FocusNode? focusNode; final void Function(FocusNode)? onFocus; final Color? fillColor; - final void Function(String)? onChanged; + final void Function(String?)? onChanged; final void Function(String)? onFieldSubmitted; final String? Function(String?)? validator; final Widget? suffix; @@ -96,17 +101,18 @@ class UiTextFormField extends StatefulWidget { } class _UiTextFormFieldState extends State { - String? _hintText; String? _errorText; String? _displayedErrorText; - FocusNode _focusNode = FocusNode(); + late FocusNode _focusNode; bool _hasFocusExitedOnce = false; bool _shouldValidate = false; + TextEditingController? _controller; @override void initState() { super.initState(); - _hintText = widget.hintText; + _controller = + widget.controller ?? TextEditingController(text: widget.initialValue); _errorText = widget.errorText; _displayedErrorText = widget.errorText; @@ -115,10 +121,8 @@ class _UiTextFormFieldState extends State { _hasFocusExitedOnce = true; _shouldValidate = true; } - if (widget.focusNode != null) { - _focusNode = widget.focusNode!; - } + _focusNode = widget.focusNode ?? FocusNode(); _focusNode.addListener(_handleFocusChange); } @@ -127,41 +131,45 @@ class _UiTextFormFieldState extends State { super.didUpdateWidget(oldWidget); if (widget.errorText != oldWidget.errorText) { - setState(() { - _errorText = widget.errorText; - _displayedErrorText = widget.errorText; - if (_errorText?.isNotEmpty == true) { - _hasFocusExitedOnce = true; - _shouldValidate = true; - } - }); + _errorText = widget.errorText; + _displayedErrorText = widget.errorText; + if (_errorText?.isNotEmpty == true) { + _hasFocusExitedOnce = true; + _shouldValidate = true; + } } - } - @override - void dispose() { - _focusNode.removeListener(_handleFocusChange); - _focusNode.dispose(); - super.dispose(); + if (widget.initialValue != oldWidget.initialValue && + widget.controller == null) { + _controller?.text = widget.initialValue ?? ''; + } } - /// Handles the focus change events. void _handleFocusChange() { + if (!mounted) return; + + final shouldUpdate = !_focusNode.hasFocus && + (widget.validationMode == InputValidationMode.eager || + widget.validationMode == InputValidationMode.passive); + + if (shouldUpdate) { + _hasFocusExitedOnce = true; + _shouldValidate = true; + // Schedule validation for the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _validateAndUpdateError(_controller?.text); + }); + } + }); + } + setState(() { - _hintText = _focusNode.hasFocus ? null : widget.hintText; if (widget.onFocus != null) { widget.onFocus!(_focusNode); } - if (!_focusNode.hasFocus) { - if (!_hasFocusExitedOnce) { - _hasFocusExitedOnce = true; - } - if (widget.validationMode == InputValidationMode.eager || - widget.validationMode == InputValidationMode.lazy) { - _shouldValidate = true; - _performValidation(); - } - } + if (_focusNode.hasFocus && widget.validationMode == InputValidationMode.aggressive) { _shouldValidate = true; @@ -169,46 +177,75 @@ class _UiTextFormFieldState extends State { }); } + // Separate validation logic from state updates + String? _validateAndUpdateError(String? value) { + final error = widget.validator?.call(value) ?? widget.errorText; + _errorText = error; + _displayedErrorText = + _hasFocusExitedOnce || _focusNode.hasFocus ? _errorText : null; + return error; + } + @override Widget build(BuildContext context) { - final widgetStyle = widget.style; - var style = TextStyle( + final theme = Theme.of(context); + + final defaultStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: theme.textTheme.bodyMedium?.color, ); - if (widgetStyle != null) { - style = style.merge(widgetStyle); - } + final style = widget.style?.merge(defaultStyle) ?? defaultStyle; - final TextStyle? hintTextStyle = Theme.of(context) - .inputDecorationTheme - .hintStyle - ?.merge(widget.hintTextStyle); + final defaultLabelStyle = theme.inputDecorationTheme.labelStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.8), + ); + final labelStyle = + widget.labelStyle?.merge(defaultLabelStyle) ?? defaultLabelStyle; - final TextStyle? labelStyle = Theme.of(context) - .inputDecorationTheme - .labelStyle - ?.merge(widget.labelStyle); + final defaultHintStyle = theme.inputDecorationTheme.hintStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5), + ); + final hintStyle = + widget.hintTextStyle?.merge(defaultHintStyle) ?? defaultHintStyle; - final TextStyle? errorStyle = Theme.of(context) - .inputDecorationTheme - .errorStyle - ?.merge(widget.errorStyle); + final defaultErrorStyle = theme.inputDecorationTheme.errorStyle ?? + TextStyle( + fontSize: 12, + color: theme.colorScheme.error, + ); + final errorStyle = + widget.errorStyle?.merge(defaultErrorStyle) ?? defaultErrorStyle; + + final fillColor = widget.fillColor ?? theme.inputDecorationTheme.fillColor; return TextFormField( + controller: _controller, maxLength: widget.maxLength, maxLengthEnforcement: widget.maxLengthEnforcement, - initialValue: widget.initialValue, - controller: widget.controller, inputFormatters: widget.inputFormatters, - validator: (value) => _performValidation(value), + validator: (value) { + // Don't update state during build, just return the validation result + final error = widget.validator?.call(value) ?? widget.errorText; + return _shouldValidate ? error : null; + }, onChanged: (value) { - if (widget.onChanged != null) { - widget.onChanged!(value); - } + widget.onChanged?.call(value); if (_shouldValidate) { - _performValidation(value); + // Schedule state update for the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _validateAndUpdateError(value); + }); + } + }); } }, onFieldSubmitted: widget.onFieldSubmitted, @@ -227,14 +264,16 @@ class _UiTextFormFieldState extends State { focusNode: _focusNode, enabled: widget.enabled, decoration: InputDecoration( - fillColor: widget.fillColor, - hintText: _hintText, - hintStyle: hintTextStyle, - contentPadding: widget.inputContentPadding, + fillColor: fillColor, + filled: fillColor != null, + hintText: widget.hintText, + hintStyle: hintStyle, + contentPadding: widget.inputContentPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), counterText: widget.counterText, - labelText: widget.hintText, - labelStyle: - _hintText != null && !_hasValue ? hintTextStyle : labelStyle, + labelText: widget.labelText ?? widget.hintText, + labelStyle: labelStyle, + helperText: widget.helperText, errorText: _displayedErrorText, errorStyle: errorStyle, prefixIcon: widget.prefixIcon, @@ -246,25 +285,4 @@ class _UiTextFormFieldState extends State { ), ); } - - /// Checks if the field has a value. - bool get _hasValue => - (widget.controller?.text.isNotEmpty ?? false) || - (widget.initialValue?.isNotEmpty ?? false); - - /// Performs validation based on the validator function and updates error state. - String? _performValidation([String? value]) { - final error = widget.validator?.call(value ?? widget.controller?.text) ?? - widget.errorText; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _errorText = error; - _displayedErrorText = - _hasFocusExitedOnce || _focusNode.hasFocus ? _errorText : null; - }); - } - }); - return error; - } } 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/lib/src/tips/ui_spinner.dart b/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart index f5758f7462..cb3e39a377 100644 --- a/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart +++ b/packages/komodo_ui_kit/lib/src/tips/ui_spinner.dart @@ -17,9 +17,10 @@ class UiSpinner extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( + return Container( height: height, width: width, + alignment: Alignment.center, child: CircularProgressIndicator( color: color, strokeWidth: strokeWidth, diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index 4423bfaba0..eba5b6bbf3 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -12,26 +12,42 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + decimal: + dependency: transitive + description: + name: decimal + sha256: "28239b8b929c1bd8618702e6dbc96e2618cf99770bbe9cb040d6cf56a11e4ec3" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "3.2.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter: dependency: "direct main" description: flutter @@ -41,50 +57,106 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "5.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" intl: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "4.9.0" + komodo_defi_rpc_methods: + dependency: transitive + description: + path: "packages/komodo_defi_rpc_methods" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_types: + dependency: "direct main" + description: + path: "packages/komodo_defi_types" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_ui: + dependency: "direct main" + description: + path: "packages/komodo_ui" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "5.1.1" material_color_utilities: 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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mobile_scanner: + dependency: transitive + description: + name: mobile_scanner + sha256: "91d28b825784e15572fdc39165c5733099ce0e69c6f6f0964ebdbf98a62130fd" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "6.0.6" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -93,11 +165,19 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" vector_math: dependency: transitive description: @@ -106,6 +186,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: - dart: ">=3.3.0-0 <4.0.0" - flutter: ">=2.5.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/packages/komodo_ui_kit/pubspec.yaml b/packages/komodo_ui_kit/pubspec.yaml index b20c8ab055..d511fc5298 100644 --- a/packages/komodo_ui_kit/pubspec.yaml +++ b/packages/komodo_ui_kit/pubspec.yaml @@ -1,19 +1,34 @@ name: komodo_ui_kit -description: Komodo AtomicDEX's UIKit Flutter package. +description: Komodo Wallet's UI Kit Flutter package. publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.6.0 <4.0.0" + flutter: ^3.29.0 dependencies: flutter: sdk: flutter - intl: 0.19.0 # flutter.dev + intl: ^0.20.2 # flutter.dev app_theme: path: ../../app_theme/ + komodo_defi_types: + # path: ../../sdk/packages/komodo_defi_types # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_defi_types + ref: dev + + komodo_ui: + # path: ../../sdk/packages/komodo_ui # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_ui + ref: dev + dev_dependencies: - flutter_lints: ^2.0.0 # flutter.dev + flutter_lints: ^5.0.0 # flutter.dev flutter: uses-material-design: true @@ -22,6 +37,6 @@ flutter: - lib/src/custom_icons/Custom.ttf fonts: - - family: Custom - fonts: - - asset: lib/src/custom_icons/Custom.ttf \ No newline at end of file + - family: Custom + fonts: + - asset: lib/src/custom_icons/Custom.ttf 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.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..090a76de17 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "80.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "2350805d7afefb0efe7acd325cb19d3ae8ba4039b906eade3807ffb69938a01f" + sha256: "7fd72d77a7487c26faab1d274af23fb008763ddc10800261abbfb2c067f183d5" url: "https://pub.dev" source: hosted - version: "1.3.33" + version: "1.3.53" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "7.3.0" app_theme: dependency: "direct main" description: @@ -36,148 +36,146 @@ packages: dependency: "direct main" description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "068190d6c99c436287936ba5855af2e1fa78d8083ae65b4db6a281780da727ae" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.6.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" - badges: - dependency: "direct main" + version: "2.12.0" + aws_client: + dependency: transitive description: - path: "." - ref: "69958a3a2d6d5dd108393832acde6bda06bd10bc" - resolved-ref: "69958a3a2d6d5dd108393832acde6bda06bd10bc" - url: "https://github.com/yako-dev/flutter_badges.git" - source: git - version: "3.1.1" - bip39: + name: aws_client + sha256: c8f9860e0fbe515bcc88ef7a1b2c7a4ea8db16805c1d36bbe447ceab643bb6da + url: "https://pub.dev" + source: hosted + version: "0.6.1" + badges: dependency: "direct main" description: - path: "." - ref: "3633daa2026b98c523ae9a091322be2903f7a8ab" - resolved-ref: "3633daa2026b98c523ae9a091322be2903f7a8ab" - url: "https://github.com/KomodoPlatform/bip39-dart.git" - source: git - version: "1.0.6" + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" bloc: dependency: transitive description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_concurrency: dependency: "direct main" description: name: bloc_concurrency - sha256: "456b7a3616a7c1ceb975c14441b3f198bf57d81cb95b7c6de5cb0c60201afcd8" + sha256: "86b7b17a0a78f77fca0d7c030632b59b593b22acea2d96972588f40d4ef53a94" url: "https://pub.dev" source: hosted - version: "0.2.5" + version: "0.3.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.11.1" cross_file: 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: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "0.17.3" - desktop_webview_window: + version: "1.0.2" + decimal: dependency: "direct main" description: - name: desktop_webview_window - sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + name: decimal + sha256: "28239b8b929c1bd8618702e6dbc96e2618cf99770bbe9cb040d6cf56a11e4ec3" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "3.2.1" dragon_charts_flutter: dependency: "direct main" description: @@ -190,18 +188,18 @@ packages: dependency: "direct main" description: name: dragon_logs - sha256: "00bec36566176f7f6c243142530a1abf22416d728a283949b92408a0d1f6a442" + sha256: e697f25bd0f27b0b85af42aff7f55f003fe045c0e3eeda6164bf97aab1525804 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" easy_localization: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.7+1" easy_logger: dependency: transitive description: @@ -213,110 +211,115 @@ packages: encrypt: dependency: "direct main" description: - path: "." - ref: "3a42d25b0c356606c26a238384b9f2189572d954" - resolved-ref: "3a42d25b0c356606c26a238384b9f2189572d954" - url: "https://github.com/KomodoPlatform/encrypt" - source: git - version: "5.0.2" + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" equatable: dependency: "direct main" description: - path: "." - ref: "2117551ff3054f8edb1a58f63ffe1832a8d25623" - resolved-ref: "2117551ff3054f8edb1a58f63ffe1832a8d25623" - url: "https://github.com/KomodoPlatform/equatable.git" - source: git - version: "2.0.5" + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" 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: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111" + url: "https://pub.dev" + source: hosted + version: "9.0.1" file_system_access_api: dependency: transitive description: name: file_system_access_api - sha256: bcbf061ce180dffcceed9faefab513e87bff1eef38c3ed99cf7c3bbbc65a34e1 + sha256: c961c5020ab4e5f05200dbdd9809c5256c3dc4a1fe5746ca7d8cf8e8cc11c47d url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.0" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - sha256: "51afa4751e8d17d1484c193b7e9759abbae324e1b8f5cc93e2a08daac4d55928" + sha256: "81a582e9348216fcf6b30878487369325bf78b8ddd752ed176949c8e4fd4aaac" url: "https://pub.dev" source: hosted - version: "10.10.5" + version: "11.4.4" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: ad7f6b70304e2b81c6079a5830355edc87496527d5b104d34c3e50b5b744da83 + sha256: "5ae7bd4a551b67009cd0676f5407331b202eaf16e0a80dcf7b40cd0a34a18746" url: "https://pub.dev" source: hosted - version: "3.10.6" + version: "4.3.4" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "63ed03d229d1c2ec2b1be037cd4760c3516cc8ecf6598a6b2fb8ca29586bf464" + sha256: "15fd7459fea2a00958dbf9b86cd8ad14d3ce2db13950308af7c7717e89ccc5c2" url: "https://pub.dev" source: hosted - version: "0.5.7+5" + version: "0.5.10+10" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "372d94ced114b9c40cb85e18c50ac94a7e998c8eec630c50d7aec047847d27bf" + sha256: f4d8f49574a4e396f34567f3eec4d38ab9c3910818dec22ca42b2a467c685d8b url: "https://pub.dev" source: hosted - version: "2.31.0" + version: "3.12.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" + sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493 + url: "https://pub.dev" + source: hosted + version: "2.21.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "2.17.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -325,12 +328,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: "153856bdaac302bbdc58a1d1403d50c40557254aa05eaeed40515d88a25a526b" + url: "https://pub.dev" + source: hosted + version: "9.0.0" flutter_driver: dependency: transitive description: flutter @@ -340,66 +342,74 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.5" flutter_inappwebview_android: dependency: transitive description: name: flutter_inappwebview_android - sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.3" flutter_inappwebview_internal_annotations: dependency: transitive description: name: flutter_inappwebview_internal_annotations - sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" flutter_inappwebview_ios: dependency: transitive description: name: flutter_inappwebview_ios - sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.2" flutter_inappwebview_macos: dependency: transitive description: name: flutter_inappwebview_macos - sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.1.2" flutter_inappwebview_platform_interface: dependency: transitive description: name: flutter_inappwebview_platform_interface - sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 url: "https://pub.dev" source: hosted - version: "1.0.10" + version: "1.3.0+1" flutter_inappwebview_web: dependency: transitive description: name: flutter_inappwebview_web - sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "0.6.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -409,35 +419,82 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f" + sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 url: "https://pub.dev" source: hosted - version: "0.6.14" + version: "0.7.6+2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf" + url: "https://pub.dev" + source: hosted + version: "2.0.26" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: f7eceb0bc6f4fd0441e29d43cab9ac2a1c5ffd7ea7b64075136b718c46954874 url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "10.0.0-beta.4" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: f226f2a572bed96bc6542198ebaec227150786e34311d455a7e2d3d06d951845 + url: "https://pub.dev" + source: hosted + version: "0.1.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21 + url: "https://pub.dev" + source: hosted + version: "4.0.0" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: ab7dbb16f783307c9d7762ede2593ce32c220ba2ba0fd540a3db8e9a3acba71a url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" flutter_svg: dependency: "direct main" description: - path: "packages/flutter_svg" - ref: d7b5c23a79dcb5425548879bdb79a5e7f5097ce5 - resolved-ref: d7b5c23a79dcb5425548879bdb79a5e7f5097ce5 - url: "https://github.com/dnfield/flutter_svg.git" - source: git - version: "2.0.7" + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" flutter_test: dependency: transitive description: flutter @@ -452,10 +509,18 @@ packages: dependency: "direct main" description: name: formz - sha256: a58eb48d84685b7ffafac1d143bf47d585bf54c7db89fe81c175dfd6e53201c7 + sha256: "382c7be452ff76833f9efa0b2333fec3a576393f6d2c7801725bed502f3d40c3" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -473,18 +538,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hex: - dependency: transitive - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "2.1.3" hive: dependency: "direct main" description: @@ -507,34 +564,34 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" integration_test: dependency: "direct dev" description: flutter @@ -544,26 +601,34 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: "direct main" description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "4.9.0" komodo_cex_market_data: dependency: "direct main" description: @@ -571,13 +636,60 @@ packages: relative: true source: path version: "0.0.1" - komodo_coin_updates: + komodo_coins: + dependency: transitive + description: + path: "packages/komodo_coins" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_framework: + dependency: transitive + description: + path: "packages/komodo_defi_framework" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0" + komodo_defi_local_auth: + dependency: transitive + description: + path: "packages/komodo_defi_local_auth" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_rpc_methods: + dependency: transitive + description: + path: "packages/komodo_defi_rpc_methods" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_sdk: dependency: "direct main" description: - path: "packages/komodo_coin_updates" - relative: true - source: path - version: "1.0.0" + path: "packages/komodo_defi_sdk" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_types: + dependency: "direct main" + description: + path: "packages/komodo_defi_types" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" komodo_persistence_layer: dependency: "direct main" description: @@ -585,6 +697,15 @@ packages: relative: true source: path version: "0.0.1" + komodo_ui: + dependency: "direct main" + description: + path: "packages/komodo_ui" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" komodo_ui_kit: dependency: "direct main" description: @@ -593,28 +714,30 @@ packages: source: path version: "0.0.0" komodo_wallet_build_transformer: - dependency: "direct dev" + dependency: transitive description: path: "packages/komodo_wallet_build_transformer" - relative: true - source: path - version: "0.0.1" + ref: dev + resolved-ref: af79b7d09ab3f3b45ff6767b9556c514f09b25ce + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -627,66 +750,114 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "5.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: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" + url: "https://pub.dev" + source: hosted + version: "1.4.3" + 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: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" markdown: dependency: transitive description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: 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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mobile_scanner: - dependency: "direct main" + dependency: transitive description: name: mobile_scanner - sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 + sha256: "91d28b825784e15572fdc39165c5733099ce0e69c6f6f0964ebdbf98a62130fd" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.6" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" nested: dependency: transitive description: @@ -707,67 +878,66 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: - path: "packages/package_info_plus/package_info_plus" - ref: "263469d796d769cb73b655fa8c97d4985bce5029" - resolved-ref: "263469d796d769cb73b655fa8c97d4985bce5029" - url: "https://github.com/KomodoPlatform/plus_plugins.git" - source: git - version: "4.0.2" + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.2.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -780,34 +950,34 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -836,10 +1006,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" provider: dependency: transitive description: @@ -852,116 +1022,114 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" qr: dependency: transitive description: name: qr - sha256: "5c4208b4dc0d55c3184d10d83ee0ded6212dc2b5e2ba17c5a0c0aab279128d21" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.2" qr_flutter: dependency: "direct main" description: - path: "." - ref: e3f8d3d4bbe8661f6c941acde8c9815a876756a3 - resolved-ref: e3f8d3d4bbe8661f6c941acde8c9815a876756a3 - url: "https://github.com/KomodoPlatform/qr.flutter.git" - source: git - version: "4.0.0" + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" 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: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" share_plus: dependency: "direct main" description: name: share_plus - sha256: ed3fcea4f789ed95913328e629c0c53e69e80e08b6c24542f1b3576046c614e8 + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "5.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.5.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.6" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.4" 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: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.3" 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: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -974,79 +1142,87 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -1059,51 +1235,50 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.8" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_html: dependency: "direct main" description: - path: "." - ref: "6a1bc7d9e6ed735ab9f7b319f9eedb138ce8b0e5" - resolved-ref: "6a1bc7d9e6ed735ab9f7b319f9eedb138ce8b0e5" - url: "https://github.com/KomodoPlatform/universal_html.git" - source: git - version: "2.2.2" + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" universal_io: dependency: transitive description: @@ -1116,98 +1291,98 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: 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: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: "direct main" description: name: uuid - sha256: "2469694ad079893e3b434a627970c33f2fa5adc46dfe03c9617546969a9a8afc" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.5.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1220,82 +1395,90 @@ packages: dependency: "direct main" description: name: video_player - sha256: "3fd106c74da32f336dc7feb65021da9b0207cb3124392935f1552834f7cce822" + sha256: "48941c8b05732f9582116b1c01850b74dbee1d8520cd7e34ad4609d6df666845" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.9.3" video_player_android: dependency: transitive description: name: video_player_android - sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.7.17" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.7.0" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" + sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.4" vm_service: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.1" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.2" webdriver: 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: @@ -1308,10 +1491,10 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.11.0" window_size: dependency: "direct main" description: @@ -1325,10 +1508,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1341,10 +1524,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.4.0 <3.5.0" - flutter: ">=3.19.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 99938c9ea7..f4a70d132c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,10 +15,14 @@ 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+1 +version: 0.9.0+0 environment: - sdk: ">=3.4.0 <3.5.0" # The recent 3.5.0 breaks the build. We will resolve this after this release. + # TODO: Upgrade mininum Dart version to 3.7.0 only after the release is concluded because + # the new formatting style may cause conflicts. This allows to run 3.7.0, but it will not + # enforce the new formatting style until the mininum Dart version is updated. + sdk: ">=3.6.0 <4.0.0" + flutter: ^3.29.0 dependencies: ## ---- Flutter SDK @@ -40,29 +44,23 @@ 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 ## ---- KomodoPlatform pub.dev packages (First-party) - dragon_logs: 1.0.3 # Secure code review PR URL: TBD + dragon_logs: 1.1.0 ## ---- Dart.dev, Flutter.dev - - args: 2.5.0 # dart.dev - flutter_markdown: 0.6.14 # flutter.dev - http: 0.13.6 # dart.dev - 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 - crypto: 3.0.3 # dart.dev - path_provider: 2.1.1 # flutter.dev - cross_file: 0.3.3+4 # flutter.dev - video_player: 2.7.0 # flutter.dev + args: 2.6.0 # dart.dev + flutter_markdown: 0.7.6+2 # flutter.dev + http: 1.3.0 # dart.dev + intl: 0.20.2 # dart.dev + js: ">=0.6.7 <=0.7.2" # dart.dev + url_launcher: 6.3.1 # flutter.dev + crypto: 3.0.6 # dart.dev + cross_file: 0.3.4+2 # flutter.dev + video_player: 2.9.3 # flutter.dev ## ---- google.com @@ -77,94 +75,34 @@ dependencies: # Upgraded Firebase, needs secure code review - firebase_analytics: 10.10.5 - firebase_core: 2.31.0 + firebase_analytics: 11.4.4 + firebase_core: 3.12.1 ## ---- 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.7 # sdk depends on hosted version, and not from git - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - package_info_plus: - git: - url: https://github.com/KomodoPlatform/plus_plugins.git - path: packages/package_info_plus/package_info_plus/ - ref: 263469d796d769cb73b655fa8c97d4985bce5029 #4.0.2 + # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 (Outdated) + package_info_plus: 8.3.0 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - share_plus: - 7.0.2 - # git: - # url: https://github.com/KomodoPlatform/plus_plugins.git - # path: packages/share_plus/share_plus/ - # ref: 052dcbb90aa2120c8f8384c05e17a82ad78a1758 #7.0.2 + # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 (Outdated) + share_plus: 10.1.4 ## ---- 3d party - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - bip39: - git: - url: https://github.com/KomodoPlatform/bip39-dart.git - ref: 3633daa2026b98c523ae9a091322be2903f7a8ab #1.0.6 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - encrypt: - git: - url: https://github.com/KomodoPlatform/encrypt - ref: 3a42d25b0c356606c26a238384b9f2189572d954 #5.0.1 - - # Consider newly added, needs secure code review - flutter_svg: - git: - url: https://github.com/dnfield/flutter_svg.git - path: packages/flutter_svg/ - ref: d7b5c23a79dcb5425548879bdb79a5e7f5097ce5 #2.0.7 - - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - qr_flutter: - git: - 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 - 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 (Outdated) + encrypt: 5.0.3 + flutter_svg: 2.0.17 - # 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 (Outdated) + qr_flutter: 4.1.0 - # 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 + easy_localization: 3.0.7+1 # last reviewed 3.0.2 via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - # 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 (Outdated) + universal_html: 2.2.4 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 hive: @@ -180,50 +118,66 @@ dependencies: path: hive_flutter/ ref: 0cbaab793be77b19b4740bc85d7ea6461b9762b4 #1.1.0 - # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - badges: - git: - url: https://github.com/yako-dev/flutter_badges.git - ref: 69958a3a2d6d5dd108393832acde6bda06bd10bc + # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 (Outdated) + badges: 3.1.2 - flutter_slidable: # last reviewed 27bbe0dfa9866ae01e8001267e873221ef5fbd67 - ^3.1.0 - # git: - # url: https://github.com/KomodoPlatform/flutter_slidable.git - # ref: 175b0735f5577dd7d378e60cfe2fe1ca607df9fa #1.1.0 + flutter_slidable: 4.0.0 # 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.5 # Android, iOS, macOS, Web (currently broke, open issue) # Newly added, not yet reviewed - # TODO: review required - # 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 - formz: 0.7.0 + formz: 0.8.0 # TODO: review required - dragon_charts_flutter: ^0.1.1-dev.1 - bloc_concurrency: ^0.2.5 + dragon_charts_flutter: 0.1.1-dev.1 + bloc_concurrency: 0.3.0 + file_picker: 9.0.1 + + # TODO: review required - SDK integration + path_provider: 2.1.5 # flutter.dev + shared_preferences: 2.5.2 # flutter.dev + decimal: 3.2.1 # transitive dependency that is required to fix breaking changes in rational package + rational: 2.2.3 # sdk depends on decimal ^3.0.2, which depends on rational ^2.0.0 + uuid: 4.5.1 # sdk depends on this version + flutter_bloc: 9.0.0 # sdk depends on this version, and hosted instead of git reference + komodo_defi_sdk: # TODO: change to pub.dev version? + # path: sdk/packages/komodo_defi_sdk # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_defi_sdk + ref: dev + + komodo_defi_types: + # path: sdk/packages/komodo_defi_types # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_defi_types + ref: dev + komodo_ui: + # path: sdk/packages/komodo_ui # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_ui + ref: dev dev_dependencies: integration_test: # SDK sdk: flutter test: ^1.24.1 # dart.dev - komodo_wallet_build_transformer: - path: packages/komodo_wallet_build_transformer - # 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 # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.1 # flutter.dev + flutter_lints: ^5.0.0 # flutter.dev + +dependency_overrides: + # Temporary until Flutter's pinned version is updated + intl: ^0.20.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -239,10 +193,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/ @@ -258,29 +209,8 @@ flutter: - assets/fiat/fiat_icons_square/ - assets/fiat/providers/ - assets/packages/flutter_inappwebview_web/assets/web/ + - app_build/build_config.json - # 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, - ] - # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -315,7 +245,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 ad676310d1..66235a0c64 100644 --- a/run_integration_tests.dart +++ b/run_integration_tests.dart @@ -1,269 +1,210 @@ -// 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' -]; - -//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 suspendedCoin = 'KMD'; -File? _configFile; +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 { - // 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 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 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(false); //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); } - // Run tests - if (testToRunArg.isNotEmpty) { - 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(); - } - } + if (testArgs.testToRun.isNotEmpty) { + testsList + ..clear() + ..add(testArgs.testToRun); } -} -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') { - 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); + 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); } - print('Starting process for test: ' + test); - - ProcessResult result; try { - if (deviceFromArg == 'web-server') { - //Run integration tests for web app - result = await Process.run( - '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 - ], - runInShell: true, - ); - } else { - //Clear app data before tests for Desktop native app - _clearNativeAppsData(); - - //Run integration tests for native apps (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, - '-d', - deviceFromArg, - '--' + runModeFromArg - ], - runInShell: true, - ); - } - } catch (e) { - 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(); - } - - 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 (result.stdout.toString().contains('failure')) { - throw ProcessException('flutter', ['test ' + test], - 'Failure details are in chromedriver output.\n', -1); - } - print('\n---\n'); -} + final testRunner = IntegrationTestRunner( + testArgs, + testsDirectory: 'test_integration/tests', + ); + await driver?.start(); -Map _readConfig() { - Map json; + final testFutures = testsList.map((test) async { + await testRunner.runTest(test); + await driver?.reset(); // reset configuration changes + }); - try { - final String jsonStr = _configFile!.readAsStringSync(); - json = jsonDecode(jsonStr); - } catch (e) { - print('Unable to load json from ${_configFile!.path}:\n$e'); - rethrow; + 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(); } - return json; + exit(didTestFail ? 1 : 0); } -void _writeConfig(Map config) { - final String spaces = ' ' * 4; - final JsonEncoder encoder = JsonEncoder.withIndent(spaces); +void _registerProcessSignalHandlers(WebBrowserDriver? driver) { + ProcessSignal.sigint.watch().listen((_) { + print('Caught SIGINT, shutting down...'); + if (driver != null) cleanup(driver); + }); - _configFile!.writeAsStringSync(encoder.convert(config)); + ProcessSignal.sigterm.watch().listen((_) { + print('Caught SIGTERM, shutting down...'); + if (driver != null) cleanup(driver); + }); } -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); +// 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( + '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'], + ) + ..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 _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', + }; } -void _clearNativeAppsData() async { - ProcessResult deleteResult; - if (Platform.isWindows) { - var homeDir = Platform.environment['UserProfile']; - if (await Directory('$homeDir$windowsAppData').exists()) { - 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) { - var 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) { - var 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..ad371477c7 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 $key'); } diff --git a/test_integration/common/pump_and_settle.dart b/test_integration/common/pump_and_settle.dart deleted file mode 100644 index b89fcc6ffa..0000000000 --- a/test_integration/common/pump_and_settle.dart +++ /dev/null @@ -1,22 +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(); -} diff --git a/test_integration/common/tester_utils.dart b/test_integration/common/tester_utils.dart deleted file mode 100644 index a58cd9f181..0000000000 --- a/test_integration/common/tester_utils.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'pause.dart'; - -Future testerTap(WidgetTester tester, Finder finder) async { - await tester.tap(finder); - await tester.pumpAndSettle(); - 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 5e756438a6..8bed3a3218 100644 --- a/test_integration/helpers/restore_wallet.dart +++ b/test_integration/helpers/restore_wallet.dart @@ -1,14 +1,23 @@ -import 'package:bip39/bip39.dart' as bip39; +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart' + show MnemonicValidator; 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'); + + final validator = MnemonicValidator(); + await validator.init(); + // Restores wallet to be used in following tests final String testSeed = getFundedWif(); const String walletName = 'my-wallet'; @@ -32,40 +41,49 @@ 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(); + 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(); - if (!bip39.validateMnemonic(testSeed)) { - await tester.tap(allowCustomSeedCheckbox); - await tester.pumpAndSettle(); + print('πŸ” RESTORE WALLET: Accepting terms'); + await tester.tapAndPump(eulaCheckBox); + await tester.tapAndPump(tocCheckBox); + + final isCustomSeed = validator.validateBip39(testSeed); + + if (isCustomSeed) { + print('πŸ” RESTORE WALLET: Handling custom seed input'); + await tester.tapAndPump(allowCustomSeedCheckbox); await tester.enterText(customSeedDialogInput, confirmCustomSeedText); - await tester.pumpAndSettle(); - await tester.tap(customSeedDialogOkButton); - await tester.pumpAndSettle(); + await tester.pumpNFrames(5); + await tester.tapAndPump(customSeedDialogOkButton); } - await tester.tap(eulaCheckBox); - await tester.pumpAndSettle(); - await tester.tap(tocCheckBox); + print('πŸ” RESTORE WALLET: Confirming wallet creation'); await tester.dragUntilVisible( importConfirmButton, walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpAndSettle(); - await tester.tap(importConfirmButton); - await tester.pumpAndSettle(); + 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( @@ -73,7 +91,10 @@ Future restoreWalletToTest(WidgetTester tester) async { walletsManagerWrapper, const Offset(0, -15), ); - await tester.pumpAndSettle(); + await tester.pumpNFrames(10); await tester.tap(importConfirmButton); - await pumpUntilDisappear(tester, walletsManagerWrapper); + + print('πŸ” RESTORE WALLET: Waiting for completion'); + await tester.pumpUntilDisappear(walletsManagerWrapper); + print('πŸ” RESTORE WALLET: Wallet restoration completed'); } 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/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..ab87725ba0 --- /dev/null +++ b/test_integration/runners/drivers/chrome_driver.dart @@ -0,0 +1,60 @@ +// ignore_for_file: avoid_print + +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 { + try { + chromeConfigManager.restoreChromeConfiguration(); + } catch (e) { + print('Failed to restore Chrome configuration: $e'); + } + 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 30a392e093..de22662608 100644 --- a/test_integration/tests/dex_tests/dex_tests.dart +++ b/test_integration/tests/dex_tests/dex_tests.dart @@ -4,24 +4,39 @@ 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); + 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(); + 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, + 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 2ec907215c..c2c99490d3 100644 --- a/test_integration/tests/dex_tests/maker_orders_test.dart +++ b/test_integration/tests/dex_tests/maker_orders_test.dart @@ -4,13 +4,20 @@ 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'; 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'; @@ -27,7 +34,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 +43,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 = @@ -44,31 +51,40 @@ 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.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( @@ -76,41 +92,147 @@ 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); } +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('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 3ce6b46313..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('orders-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,148 +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(); + expect(swapSuccess, findsOneWidget); + print('πŸ” SWAP VERIFY: Found success status'); - // 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(); + expect( + find.descendant( + of: takerFeeSentEventStep, matching: find.byType(CopiedText)), + findsOneWidget); + print('πŸ” SWAP VERIFY: Taker fee sent verified'); - // 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( + find.descendant( + of: makerPaymentReceivedEventStep, matching: find.byType(CopiedText)), + findsOneWidget); + print('πŸ” SWAP VERIFY: Maker payment received verified'); - await pause(); + await tester.dragUntilVisible(takerPaymentSentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); + print('πŸ” SWAP VERIFY: Scrolled to taker payment sent'); + expect( + find.descendant( + of: takerPaymentSentEventStep, matching: find.byType(CopiedText)), + findsOneWidget, + ); - // Select infinite bid if it exists - if (infiniteBids.evaluate().isNotEmpty) { - await tester.tap(infiniteBids.first); - await tester.pumpAndSettle(); - } + await tester.dragUntilVisible(takerPaymentSpentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); + expect( + find.descendant( + of: takerPaymentSpentEventStep, matching: find.byType(CopiedText)), + findsOneWidget, + ); + + await tester.dragUntilVisible(makerPaymentSpentEventStep, + tradingDetailsScrollable, const Offset(0, -10)); + expect( + find.descendant( + of: makerPaymentSpentEventStep, matching: find.byType(CopiedText)), + findsOneWidget, + ); - // Create order await tester.dragUntilVisible( + 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, - find.byKey(const Key('taker-form-layout-scroll')), - const Offset(0, -150), + // 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.tap(takeOrderButton); + 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( - takeOrderConfirmButton, - find.byKey(const Key('taker-order-confirmation-scroll')), - const Offset(0, -150), + 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')), ); - 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'; - }, + 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')), ); - expect( - swapSuccess, - findsOneWidget, - reason: 'Test error: Taker Swap was not successful (probably failed)', - ); + await tester.tapAndPump(sellCoinSelectButton); + print('πŸ” SELL CONFIG: Opened coin selector'); - expect( - 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'); + await tester.enterText(sellCoinSearchField, sellCoin); + print('πŸ” SELL CONFIG: Entered search text: $sellCoin'); + await tester.pumpNFrames(10); - await tester.dragUntilVisible( - takerPaymentSentEventStep, - tradingDetailsScrollable, - const Offset(0, -10), - ); - expect( - find.descendant( - of: takerPaymentSentEventStep, matching: find.byType(CopiedText)), - findsOneWidget, - reason: - 'Test error: \'takerPaymentSent\' event tx copied text not found'); + await tester.tapAndPump(sellCoinItem); + print('πŸ” SELL CONFIG: Selected coin'); - 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'); + await tester.enterText(sellAmountField, sellAmount); + print('πŸ” SELL CONFIG: Entered amount: $sellAmount'); + await tester.pumpNFrames(10); +} - await tester.dragUntilVisible( - makerPaymentSpentEventStep, - tradingDetailsScrollable, - const Offset(0, -10), - ); - expect( - find.descendant( - of: makerPaymentSpentEventStep, matching: find.byType(CopiedText)), - findsOneWidget, - reason: - 'Test error: \'makerPaymentSpent\' event tx copied text not found'); +Future _selectBuyCoin(WidgetTester tester, + {required String buyCoin}) async { + print('πŸ” BUY CONFIG: Setting up buy parameters'); - await tester.dragUntilVisible( - backButton, - tradingDetailsScrollable, - const Offset(0, 10), + 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.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.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 { + 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('END TAKER ORDER TESTS'); + 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 new file mode 100644 index 0000000000..d0ed7338d2 --- /dev/null +++ b/test_integration/tests/fiat_onramp_tests/fiat_onramp_tests.dart @@ -0,0 +1,38 @@ +// 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(); + 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(); + await app.main(); + await tester.pumpAndSettle(); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testFiatFormInputs(tester); + + 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 new file mode 100644 index 0000000000..cde1105510 --- /dev/null +++ b/test_integration/tests/fiat_onramp_tests/form_tests.dart @@ -0,0 +1,149 @@ +// 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/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(); + + await _testFiatAmountField(tester); + await _testFiatSelection(tester); + await _testCoinSelection(tester); + await _testPaymentMethodSelection(tester); + await _textSubmit(tester); +} + +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.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 + + await _testPaymentMethodSelection(tester); +} + +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 = + find.byKey(const Key('fiat-onramp-currency-item-USD')); + final Finder eurIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-EUR')); + + await tester.tapAndPump(fiatDropdownFinder); + expect(usdIconFinder, findsOneWidget, reason: 'USD icon not found'); + expect(eurIconFinder, findsOneWidget, reason: 'EUR icon not found'); + 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 = + find.byKey(const Key('fiat-onramp-currency-item-BTC')); + final Finder maticIconFinder = + find.byKey(const Key('fiat-onramp-currency-item-LTC')); + + await tester.tapAndPump(coinDropdownFinder); + expect(btcIconFinder, findsOneWidget, reason: 'BTC icon not found'); + print('πŸ” FIAT FORM TEST: Verified BTC option'); + await _tapCurrencyItem(tester, maticIconFinder); + 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')); + + expect( + dialog, + findsOneWidget, + 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.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 = + 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', + ); + print('πŸ” FIAT FORM TEST: Verified Ramp and Banxa payment methods'); + + await tester.tapAndPump(rampPaymentMethodFinder); + print('πŸ” FIAT FORM TEST: Selected Ramp payment method'); +} + +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, + ); +} 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 7d678f53f0..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,17 +35,16 @@ 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); - 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 +52,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..7615a6ab45 100644 --- a/test_integration/tests/misc_tests/misc_tests.dart +++ b/test_integration/tests/misc_tests/misc_tests.dart @@ -4,27 +4,42 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:web_dex/main.dart' as app; -import './feedback_tests.dart'; -import './menu_tests.dart'; -import './theme_test.dart'; import '../../helpers/accept_alpha_warning.dart'; import '../../helpers/restore_wallet.dart'; +import 'feedback_tests.dart'; +import 'menu_tests.dart'; +import 'theme_test.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); + 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(); + 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, + 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 b8698170cd..8bb1e92eab 100644 --- a/test_integration/tests/misc_tests/theme_test.dart +++ b/test_integration/tests/misc_tests/theme_test.dart @@ -6,51 +6,74 @@ 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')); - // Check default theme (dark) - checkTheme(tester, themeSwitcherFinder, Brightness.dark); + final currentBrightness = + Theme.of(tester.element(themeSwitcherFinder)).brightness; + print( + 'πŸ” 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(); - 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); - - 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)); + 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.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() { 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/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..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,12 +1,13 @@ // 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'; 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 +15,39 @@ 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(); - - 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-$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); + testWidgets( + 'Run suspended asset tests:', + (WidgetTester tester) async { + 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_manager_tests/wallets_manager_create_test.dart b/test_integration/tests/wallets_manager_tests/wallets_manager_create_test.dart index 30264005db..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,43 +32,60 @@ 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.pumpAndSettle(); - await tester.tap(eulaCheckBox); - await tester.pumpAndSettle(); - await tester.tap(tocCheckBox); - await tester.pumpAndSettle(); - 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() { 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 { + print('πŸ” WALLET TESTS: Starting wallet creation test suite'); + tester.testTextInput.register(); + await app.main(); + await tester.pumpAndSettle(); + + 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'); - }, semanticsEnabled: false); + 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 c1a1976945..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,74 +19,92 @@ 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 = 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 = - 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.pumpAndSettle(); - await tester.tap(allowCustomSeedCheckbox); - await tester.pumpAndSettle(); + 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.tapAndPump(customSeedDialogOkButton); + await tester.tapAndPump(importConfirmButton); await tester.pumpAndSettle(); - await tester.tap(customSeedDialogOkButton); - await tester.pumpAndSettle(); - await tester.tap(eulaCheckbox); - await tester.pumpAndSettle(); - await tester.tap(tocCheckbox); - await tester.pumpAndSettle(); - await tester.tap(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() { 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_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 9adf6cc060..cdcc78dd9a 100644 --- a/test_integration/tests/wallets_tests/test_cex_prices.dart +++ b/test_integration/tests/wallets_tests/test_cex_prices.dart @@ -7,35 +7,30 @@ 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'), - ); - final Finder docCoinActive = find.byKey( - const Key('active-coin-item-doc'), - ); + + // 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'), ); 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'), ); @@ -51,60 +46,97 @@ 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 - final hasKmdBep20 = await filterAsset(tester, - asset: kmdBep20CoinActive, - text: kmdBep20ByTicker, - searchField: searchCoinsField); + 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) - await testerTap(tester, docCoinActive); - expect(docPrice, findsNothing); + // TODO: re-enable this after the doc/marty changes have been decided on + // 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() { 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(); - - print('END CEX PRICES TESTS'); - }, semanticsEnabled: false); + 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('πŸ” MAIN: Accepting alpha warning'); + await acceptAlphaWarning(tester); + + await restoreWalletToTest(tester); + print('πŸ” MAIN: Wallet restored'); + + await testCexPrices(tester); + await tester.pumpAndSettle(); + + 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 eb3ad068c7..72be8b2c64 100644 --- a/test_integration/tests/wallets_tests/test_withdraw.dart +++ b/test_integration/tests/wallets_tests/test_withdraw.dart @@ -6,51 +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/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'), ); @@ -61,13 +88,6 @@ Future testWithdraw(WidgetTester tester) async { const Key('coin-details-address-field'), ); - await addAsset(tester, asset: martyCoinItem, search: 'marty'); - - expect(martyCoinActive, findsOneWidget); - await testerTap(tester, martyCoinActive); - - expect(coinBalance, findsOneWidget); - final AutoScrollText text = coinBalance.evaluate().single.widget as AutoScrollText; @@ -75,53 +95,97 @@ 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 tester.tapAndPump(exitButton); + print('πŸ” COPY ADDRESS: Copy address test completed'); +} - await testerTap(tester, exitButton); - expect(sendButton, findsOneWidget); - - 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 enterText(tester, finder: amountInput, text: '0.01'); - await testerTap(tester, sendEnterButton); + 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.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.tapAndPump(doneButton); + print('πŸ” CONFIRM SEND: Tapped done button'); + await tester.pumpAndSettle(); expect(exitButton, findsOneWidget); - await testerTap(tester, exitButton); + 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'); + + 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'); - await removeAsset(tester, asset: martyCoinItem, search: 'marty'); + expect(sendButton, findsOneWidget); + await tester.tapAndPump(sendButton); + print('πŸ” SEND AMOUNT: Tapped send button'); - print('END TEST WITHDRAW'); + 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() { 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 { + print('πŸ” MAIN: Starting withdraw 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 testWithdraw(tester); + await tester.pumpAndSettle(); + + 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 38c039039a..fb22693fb8 100644 --- a/test_integration/tests/wallets_tests/wallet_tools.dart +++ b/test_integration/tests/wallets_tests/wallet_tools.dart @@ -5,10 +5,16 @@ import 'package:flutter_test/flutter_test.dart'; import '../../common/goto.dart' as goto; import '../../common/pause.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'); -Future removeAsset(WidgetTester tester, - {required Finder asset, required String search}) async { final Finder removeAssetsButton = find.byKey( const Key('remove-assets-button'), ); @@ -16,15 +22,17 @@ 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'), ); await goto.walletPage(tester); + print('πŸ” REMOVE ASSET: Navigated to wallet page'); - await testerTap(tester, removeAssetsButton); + await tester.tapAndPump(removeAssetsButton); + print('πŸ” REMOVE ASSET: Tapped remove assets button'); expect(list, findsOneWidget); try { @@ -34,28 +42,38 @@ Future removeAsset(WidgetTester tester, } 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); } -Future addAsset(WidgetTester tester, - {required Finder asset, required String search}) async { +Future addAsset( + WidgetTester tester, { + required Finder asset, + required String search, +}) async { + print('πŸ” ADD ASSET: Starting add asset flow'); + final Finder list = find.byKey( const Key('coins-manager-list'), ); @@ -66,19 +84,24 @@ 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); + 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.tap(addAssetsButton); + await tester.pumpAndSettle(); // wait for page switch and list loading + print('πŸ” ADD ASSET: Tapped add assets button'); + try { expect(searchCoinsField, findsOneWidget); } on TestFailure { @@ -86,46 +109,60 @@ Future addAsset(WidgetTester tester, } 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 testerTap(tester, 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 testerTap(tester, 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; } -Future enterText(WidgetTester tester, - {required Finder finder, required String text}) async { +Future enterText( + WidgetTester tester, { + required Finder finder, + required String text, + int frames = 60, +}) async { await tester.enterText(finder, text); - await tester.pumpAndSettle(); + 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 14b41f6be0..ad8ed75491 100644 --- a/test_integration/tests/wallets_tests/wallets_tests.dart +++ b/test_integration/tests/wallets_tests/wallets_tests.dart @@ -15,25 +15,32 @@ import 'test_withdraw.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Run wallet tests:', (WidgetTester tester) async { - tester.testTextInput.register(); - await app.main(); - await tester.pumpAndSettle(); + walletsWidgetTests(); +} + +void walletsWidgetTests({ + bool skip = false, +}) { + 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); + await acceptAlphaWarning(tester); + await restoreWalletToTest(tester); + await testCoinIcons(tester); + await testActivateCoins(tester); + await testCexPrices(tester); + await testWithdraw(tester); + await testFilters(tester); - print('END WALLET TESTS'); - }, semanticsEnabled: false); + // Disabled until the bitrefill feature is re-enabled + // await tester.pumpAndSettle(); + // await testBitrefillIntegration(tester); + }, + semanticsEnabled: false, + skip: skip, + ); } 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..386692f8ad 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,107 @@ 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 += tx.balanceChanges.netChange.toDouble(); + } + + for (final tx in badTransactions) { + badBalance += tx.balanceChanges.netChange.toDouble(); + } + + 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.assetId.id, equals('BTC')); + expect(tx.confirmations, inInclusiveRange(1, 3)); + expect(tx.from, isNotEmpty); + expect(tx.to, isNotEmpty); + expect(tx.internalId, isNotEmpty); + expect(tx.txHash, isNotEmpty); + } + }); + + 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..6af0b2f51f 100644 --- a/test_units/tests/cex_market_data/transaction_generation.dart +++ b/test_units/tests/cex_market_data/transaction_generation.dart @@ -1,55 +1,71 @@ -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 +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; Transaction createBuyTransaction( double balanceChange, { - int timeStamp = 1708646400, + int timeStamp = 1708646400, // $50,740.50 usd }) { 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, // $50,740.50 usd - to: ['1XYZ...'], - totalAmount: value, + timestamp: DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000), + to: const ['1XYZ...'], txHash: 'hash1', - txHex: 'hex1', memo: 'Buy 1 BTC', ); } 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( + 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: balanceChange.abs().toString(), - timestamp: timeStamp, // $60,666.60 usd - 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/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/test_units/tests/utils/test_util.dart b/test_units/tests/utils/test_util.dart index 4977e49596..0696fc9f13 100644 --- a/test_units/tests/utils/test_util.dart +++ b/test_units/tests/utils/test_util.dart @@ -1,18 +1,35 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/cex_price.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; -Coin setCoin( - {double? usdPrice, double? change24h, String? coinAbbr, double? balance}) { - final coin = Coin( +Coin setCoin({ + double? usdPrice, + double? change24h, + String? coinAbbr, + double? balance, +}) { + return Coin( abbr: coinAbbr ?? 'KMD', + id: AssetId( + id: coinAbbr ?? 'KMD', + name: 'Komodo', + parentId: null, + symbol: AssetSymbol( + assetConfigId: coinAbbr ?? 'KMD', + coinGeckoId: 'komodo', + coinPaprikaId: 'kmd-komodo', + ), + derivationPath: "m/44'/141'/0'", + chainId: AssetChainId(chainId: 0), + subClass: CoinSubClass.smartChain, + ), accounts: null, activeByDefault: true, - bchdUrls: [], + logoImageUrl: null, coingeckoId: "komodo", coinpaprikaId: "kmd-komodo", derivationPath: "m/44'/141'/0'", - electrum: [], explorerUrl: "https://kmdexplorer.io/address/", explorerAddressUrl: "address/", explorerTxUrl: "tx/", @@ -20,16 +37,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 +55,4 @@ Coin setCoin( ) : null, ); - if (balance != null) { - coin.balance = balance; - } - return coin; } 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/kdf/res/kdf_wrapper.dart b/web/kdf/res/kdf_wrapper.dart new file mode 100644 index 0000000000..b6793c4a31 --- /dev/null +++ b/web/kdf/res/kdf_wrapper.dart @@ -0,0 +1,114 @@ +// 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_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:web/web.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 = (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; + + document.head!.appendChild(script); + + return completer.future; + } + + Future _mm2Main(String conf, Function logCallback) async { + await _ensureLoaded(); + + 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.'); + } + + final jsResult = globalContext.callMethod('mm2_main_status'.toJS); + return jsResult.dartify()! as int; + } + + Future _mm2Stop() async { + await _ensureLoaded(); + final jsResult = globalContext.callMethod('mm2_stop'.toJS); + return jsResult.dartify()! 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/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/src/index.js b/web/src/index.js deleted file mode 100644 index 1e6ae09152..0000000000 --- a/web/src/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 "./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/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 deleted file mode 100644 index df340f2a9f..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/src/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/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 3f71e1736d..efb62ebe7d 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3e41859bd6..da8127b416 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,17 +6,23 @@ #include "generated_plugin_registrant.h" -#include #include +#include +#include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FirebaseCorePluginCApiRegisterWithRegistrar( 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 b36f8e33be..1117ebfecf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,14 +3,17 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window 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) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index dced3b4adf..2041a04410 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -16,11 +16,6 @@ add_executable(${BINARY_NAME} WIN32 "runner.exe.manifest" ) -add_custom_command(TARGET ${BINARY_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - "${CMAKE_CURRENT_SOURCE_DIR}/exe/mm2.exe" - "$/mm2.exe") - # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) diff --git a/windows/runner/exe/.gitkeep b/windows/runner/exe/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 490813deb2..282cc512d0 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; }