diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 84c680dda..47b08c44d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -55,7 +55,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.27.4" + flutter-version: "3.27.0" channel: stable - name: Install package dependencies @@ -153,8 +153,8 @@ jobs: echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -168,6 +168,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -178,7 +179,8 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/no_http_imports.yaml b/.github/workflows/no_http_imports.yaml new file mode 100644 index 000000000..dad6821ac --- /dev/null +++ b/.github/workflows/no_http_imports.yaml @@ -0,0 +1,21 @@ +name: No http imports + +on: [pull_request] + +jobs: + PR_test_build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for http package usage + if: github.event_name == 'pull_request' + run: | + GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) || true)" + [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 + echo "$GIT_GREP_OUT" + echo "There are .dart files which use http imports" + echo "Using http package breaks proxy integration" + echo "Please use ProxyWrapper.getHttpClient() from package:cw_core/utils/proxy_wrapper.dart" + exit 1 + \ No newline at end of file diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 9c3d82bc2..507793bd8 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -15,5 +15,5 @@ jobs: [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 echo "$GIT_GREP_OUT" echo "There are .dart files which use print() statements" - echo "Please use printV from package: cw_core/utils/print_verbose.dart" + echo "Please use printV from package:cw_core/utils/print_verbose.dart" exit 1 diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 8f6139747..f7c226ce4 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -98,8 +98,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -113,6 +113,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -124,7 +125,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart @@ -253,6 +255,11 @@ jobs: - name: Build generated code run: | + flutter --version + flutter clean + rm -rf .dart_tool + rm pubspec.lock + flutter pub get ./model_generator.sh async - name: Generate key properties diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 476a033a0..f057b19e5 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -91,8 +91,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -106,6 +106,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -117,7 +118,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/Dockerfile b/Dockerfile index 84179d645..151b7af20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -15,11 +15,11 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.27.4 +ENV FLUTTER_VERSION=3.27.0 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools -ENV ANDROID_SDK_TOOLS_VERSION=11076708 +ENV ANDROID_SDK_TOOLS_VERSION=13114758 # Comes from https://developer.android.com/studio/releases/build-tools ENV ANDROID_PLATFORM_VERSION=35 ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 @@ -164,9 +164,12 @@ RUN (addgroup kvm || true) && \ ENV PATH=${HOME}/.cargo/bin:${PATH} RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ cargo install cargo-ndk && \ + for toolchain in stable nightly; \ + do \ for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu; \ do \ - rustup target add --toolchain stable $target; \ + rustup target add --toolchain $toolchain $target; \ + done \ done # Download and install Flutter @@ -175,8 +178,11 @@ ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} ENV FLUTTER_ROOT=$FLUTTER_HOME ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin -RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \ - && yes | flutter doctor --android-licenses \ +RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} && \ + cd ${FLUTTER_HOME} && \ + git fetch -a + +RUN yes | flutter doctor --android-licenses \ && flutter doctor \ && chown -R root:root ${FLUTTER_HOME} diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 280a45b3c..8283a7c8c 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -111,6 +111,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/2fa_warning_light.svg b/assets/images/2fa_warning_light.svg new file mode 100644 index 000000000..087d8e99b --- /dev/null +++ b/assets/images/2fa_warning_light.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/birthday_cake.png b/assets/images/birthday_cake.png index 84b084fba..293cd10f6 100644 Binary files a/assets/images/birthday_cake.png and b/assets/images/birthday_cake.png differ diff --git a/assets/images/birthday_cake.svg b/assets/images/birthday_cake.svg deleted file mode 100644 index b5e31dddb..000000000 --- a/assets/images/birthday_cake.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/btc_lock_dark.png b/assets/images/btc_lock_dark.png new file mode 100644 index 000000000..f5b3d7e27 Binary files /dev/null and b/assets/images/btc_lock_dark.png differ diff --git a/assets/images/btc_lock_light.png b/assets/images/btc_lock_light.png new file mode 100644 index 000000000..4320d96e3 Binary files /dev/null and b/assets/images/btc_lock_light.png differ diff --git a/assets/images/buy.png b/assets/images/buy.png index ff4549d5a..32c116e6b 100644 Binary files a/assets/images/buy.png and b/assets/images/buy.png differ diff --git a/assets/images/cake_logo_dark.svg b/assets/images/cake_logo_dark.svg new file mode 100644 index 000000000..095077443 --- /dev/null +++ b/assets/images/cake_logo_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/cake_logo_light.svg b/assets/images/cake_logo_light.svg new file mode 100644 index 000000000..767e205d1 --- /dev/null +++ b/assets/images/cake_logo_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/contact.png b/assets/images/contact.png new file mode 100644 index 000000000..5cf96694b Binary files /dev/null and b/assets/images/contact.png differ diff --git a/assets/images/hero/cw_welcome_dark.svg b/assets/images/hero/cw_welcome_dark.svg new file mode 100644 index 000000000..5479cb1ee --- /dev/null +++ b/assets/images/hero/cw_welcome_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/hero/cw_welcome_light.svg b/assets/images/hero/cw_welcome_light.svg new file mode 100644 index 000000000..ece7d1f84 --- /dev/null +++ b/assets/images/hero/cw_welcome_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/history.svg b/assets/images/history.svg new file mode 100644 index 000000000..f308ab7e3 --- /dev/null +++ b/assets/images/history.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/home_screen_setting_icon.svg b/assets/images/home_screen_setting_icon.svg new file mode 100644 index 000000000..7b3aa7b4c --- /dev/null +++ b/assets/images/home_screen_setting_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/menu.svg b/assets/images/menu.svg new file mode 100644 index 000000000..0a4cc3784 --- /dev/null +++ b/assets/images/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/notif.svg b/assets/images/notif.svg new file mode 100644 index 000000000..b1ff5b4fa --- /dev/null +++ b/assets/images/notif.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/notification_icon.svg b/assets/images/notification_icon.svg index 099039e67..360d0b4e6 100644 --- a/assets/images/notification_icon.svg +++ b/assets/images/notification_icon.svg @@ -1,69 +1,3 @@ - - - -image/svg+xml \ No newline at end of file + + + diff --git a/assets/images/passphrase_dark.png b/assets/images/passphrase_dark.png new file mode 100644 index 000000000..f72d1e1a1 Binary files /dev/null and b/assets/images/passphrase_dark.png differ diff --git a/assets/images/passphrase_light.png b/assets/images/passphrase_light.png new file mode 100644 index 000000000..f86f68156 Binary files /dev/null and b/assets/images/passphrase_light.png differ diff --git a/assets/images/qr-cake.png b/assets/images/qr-cake.png new file mode 100644 index 000000000..7c54dedb0 Binary files /dev/null and b/assets/images/qr-cake.png differ diff --git a/assets/images/receive.png b/assets/images/receive.png new file mode 100644 index 000000000..180a4e5b3 Binary files /dev/null and b/assets/images/receive.png differ diff --git a/assets/images/seed_verified_dark.png b/assets/images/seed_verified_dark.png new file mode 100644 index 000000000..cbefa5d8c Binary files /dev/null and b/assets/images/seed_verified_dark.png differ diff --git a/assets/images/seed_verified_light.png b/assets/images/seed_verified_light.png new file mode 100644 index 000000000..a51eddcd7 Binary files /dev/null and b/assets/images/seed_verified_light.png differ diff --git a/assets/images/seed_warning_dark.svg b/assets/images/seed_warning_dark.svg new file mode 100644 index 000000000..0a47254ff --- /dev/null +++ b/assets/images/seed_warning_dark.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/seed_warning_light.svg b/assets/images/seed_warning_light.svg new file mode 100644 index 000000000..cab27ec31 --- /dev/null +++ b/assets/images/seed_warning_light.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/send.png b/assets/images/send.png deleted file mode 100644 index aef504999..000000000 Binary files a/assets/images/send.png and /dev/null differ diff --git a/assets/images/send2.png b/assets/images/send2.png new file mode 100644 index 000000000..85fc570a8 Binary files /dev/null and b/assets/images/send2.png differ diff --git a/assets/images/swap.png b/assets/images/swap.png new file mode 100644 index 000000000..fe3fc0893 Binary files /dev/null and b/assets/images/swap.png differ diff --git a/assets/images/tor_logo.svg b/assets/images/tor_logo.svg new file mode 100644 index 000000000..ebd00324d --- /dev/null +++ b/assets/images/tor_logo.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/usdtbsc_icon.png b/assets/images/usdtbsc_icon.png new file mode 100644 index 000000000..9f2cda237 Binary files /dev/null and b/assets/images/usdtbsc_icon.png differ diff --git a/assets/images/wallet_group_bright.png b/assets/images/wallet_group_bright.png deleted file mode 100644 index 263361db6..000000000 Binary files a/assets/images/wallet_group_bright.png and /dev/null differ diff --git a/assets/images/wallet_group_confirmed_dark.png b/assets/images/wallet_group_confirmed_dark.png new file mode 100644 index 000000000..a047cb29c Binary files /dev/null and b/assets/images/wallet_group_confirmed_dark.png differ diff --git a/assets/images/wallet_group_confirmed_light.png b/assets/images/wallet_group_confirmed_light.png new file mode 100644 index 000000000..851d32300 Binary files /dev/null and b/assets/images/wallet_group_confirmed_light.png differ diff --git a/assets/images/wallet_group_dark.png b/assets/images/wallet_group_dark.png deleted file mode 100644 index 7cd08d2cd..000000000 Binary files a/assets/images/wallet_group_dark.png and /dev/null differ diff --git a/assets/images/wallet_group_empty_dark.png b/assets/images/wallet_group_empty_dark.png new file mode 100644 index 000000000..e613d876e Binary files /dev/null and b/assets/images/wallet_group_empty_dark.png differ diff --git a/assets/images/wallet_group_empty_light.png b/assets/images/wallet_group_empty_light.png new file mode 100644 index 000000000..f795648ae Binary files /dev/null and b/assets/images/wallet_group_empty_light.png differ diff --git a/assets/images/wallet_group_light.png b/assets/images/wallet_group_light.png deleted file mode 100644 index 7827971e7..000000000 Binary files a/assets/images/wallet_group_light.png and /dev/null differ diff --git a/assets/images/wallet_group_options_dark.png b/assets/images/wallet_group_options_dark.png new file mode 100644 index 000000000..479aac57c Binary files /dev/null and b/assets/images/wallet_group_options_dark.png differ diff --git a/assets/images/wallet_group_options_light.png b/assets/images/wallet_group_options_light.png new file mode 100644 index 000000000..308930520 Binary files /dev/null and b/assets/images/wallet_group_options_light.png differ diff --git a/assets/images/wallet_name.png b/assets/images/wallet_name.png deleted file mode 100644 index f586682bd..000000000 Binary files a/assets/images/wallet_name.png and /dev/null differ diff --git a/assets/images/wallet_name_light.png b/assets/images/wallet_name_light.png deleted file mode 100644 index 0199c1b30..000000000 Binary files a/assets/images/wallet_name_light.png and /dev/null differ diff --git a/assets/images/wallet_type.png b/assets/images/wallet_type.png deleted file mode 100644 index 4e0eba8b5..000000000 Binary files a/assets/images/wallet_type.png and /dev/null differ diff --git a/assets/images/wallet_type_light.png b/assets/images/wallet_type_light.png deleted file mode 100644 index e36c0d3aa..000000000 Binary files a/assets/images/wallet_type_light.png and /dev/null differ diff --git a/assets/images/wallet_type_wallet_dark.png b/assets/images/wallet_type_wallet_dark.png new file mode 100644 index 000000000..b840f5547 Binary files /dev/null and b/assets/images/wallet_type_wallet_dark.png differ diff --git a/assets/images/wallet_type_wallet_light.png b/assets/images/wallet_type_wallet_light.png new file mode 100644 index 000000000..ee759a109 Binary files /dev/null and b/assets/images/wallet_type_wallet_light.png differ diff --git a/assets/images/wallets.png b/assets/images/wallets.png new file mode 100644 index 000000000..62ea20039 Binary files /dev/null and b/assets/images/wallets.png differ diff --git a/assets/images/welcome.png b/assets/images/welcome.png deleted file mode 100644 index f1132d253..000000000 Binary files a/assets/images/welcome.png and /dev/null differ diff --git a/assets/images/welcome_dark_theme.svg b/assets/images/welcome_dark_theme.svg new file mode 100644 index 000000000..8f60c931a --- /dev/null +++ b/assets/images/welcome_dark_theme.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/welcome_light.png b/assets/images/welcome_light.png deleted file mode 100644 index 6feff85d1..000000000 Binary files a/assets/images/welcome_light.png and /dev/null differ diff --git a/assets/images/welcome_light_theme.svg b/assets/images/welcome_light_theme.svg new file mode 100644 index 000000000..178b2853d --- /dev/null +++ b/assets/images/welcome_light_theme.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/welcome_wallet_dark.png b/assets/images/welcome_wallet_dark.png new file mode 100644 index 000000000..771a600d3 Binary files /dev/null and b/assets/images/welcome_wallet_dark.png differ diff --git a/assets/images/welcome_wallet_light.png b/assets/images/welcome_wallet_light.png new file mode 100644 index 000000000..2a738be0b Binary files /dev/null and b/assets/images/welcome_wallet_light.png differ diff --git a/assets/images/wyre-icon.png b/assets/images/wyre-icon.png deleted file mode 100644 index a2810948e..000000000 Binary files a/assets/images/wyre-icon.png and /dev/null differ diff --git a/assets/images/wyre.png b/assets/images/wyre.png deleted file mode 100644 index a16bbdc8b..000000000 Binary files a/assets/images/wyre.png and /dev/null differ diff --git a/assets/images/yat_crypto.png b/assets/images/yat_crypto.png deleted file mode 100644 index fbd5d2483..000000000 Binary files a/assets/images/yat_crypto.png and /dev/null differ diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 852e6ad0d..faf57258a 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,4 +1,4 @@ -Background sync improvements -Payment notifications +Add built-in Tor support (experimental) +Ledger improvements UI/UX improvements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index c766c39ff..c49b895e3 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,5 +1,9 @@ -Background sync improvements -Payment notifications -WalletConnect enhancements +Add built-in Tor support (experimental) +Add dEuro investments +Solana fixes/enhancements +Polygon fixes/enhancements +WalletConnect improvements +Ledger improvements +Payjoin improvements UI/UX improvements Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index e726217f5..0d985b237 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -20,6 +20,7 @@ BitcoinBaseAddress addressFromScript(Script script, return P2pkhAddress.fromScriptPubkey( script: script, network: BitcoinNetwork.mainnet); case P2shAddressType.p2pkhInP2sh: + case P2shAddressType.p2pkInP2sh: return P2shAddress.fromScriptPubkey( script: script, network: BitcoinNetwork.mainnet); case SegwitAddresType.p2wpkh: diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index a23b72660..9231022f6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -266,6 +266,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { derivationPath: walletInfo.derivationInfo!.derivationPath!); } + @override + Future close({bool shouldCleanup = false}) async { + payjoinManager.cleanupSessions(); + super.close(shouldCleanup: shouldCleanup); + } + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 0fefe4e57..d84d958be 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -31,12 +31,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final PayjoinManager payjoinManager; - @observable payjoin.Receiver? currentPayjoinReceiver; - @computed - String? get payjoinEndpoint => - currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint(); + @observable + String? payjoinEndpoint = null; @override String getAddress( @@ -59,16 +57,32 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return generateP2WPKHAddress(hd: hd, index: index, network: network); } + @action Future initPayjoin() async { - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - - payjoinManager.resumeSessions(); + try { + await payjoinManager.initPayjoin(); + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + + payjoinManager.resumeSessions(); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } } + @action Future newPayjoinReceiver() async { - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); + try { + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); - printV("Initializing new Payjoin Receiver"); - payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); + payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } } } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 1f5c369e3..2ddd30df6 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -42,7 +44,7 @@ class ElectrumClient { static const aliveTimerDuration = Duration(seconds: 4); bool get isConnected => _isConnected; - Socket? socket; + ProxySocket? socket; void Function(ConnectionStatus)? onConnectionStatusChange; int _id; final Map _tasks; @@ -72,18 +74,11 @@ class ElectrumClient { } catch (_) {} socket = null; + final ssl = !(useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))); try { - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); - } + socket = await ProxyWrapper().getSocksSocket(ssl, host, port, connectionTimeout: connectionTimeout); } catch (e) { + printV("connect: $e"); if (e is HandshakeException) { useSSL = !(useSSL ?? false); } @@ -105,7 +100,6 @@ class ElectrumClient { // use ping to determine actual connection status since we could've just not timed out yet: // _setConnectionStatus(ConnectionStatus.connected); - socket!.listen( (Uint8List event) { try { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 35c15682c..bb9cea1bc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -49,7 +50,6 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -493,10 +493,9 @@ abstract class ElectrumWalletBase Future updateFeeRates() async { if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = await http - .get(Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) - .timeout(Duration(seconds: 5)); - + final response = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 15)); final result = json.decode(response.body) as Map; final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; @@ -1176,20 +1175,18 @@ abstract class ElectrumWalletBase } }); - return PendingBitcoinTransaction( - transaction, - type, - electrumClient: electrumClient, - amount: estimatedTx.amount, - fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), - network: network, - hasChange: estimatedTx.hasChange, - isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: hasTaprootInputs, - utxos: estimatedTx.utxos, - publicKeys: estimatedTx.publicKeys - )..addListener((transaction) async { + return PendingBitcoinTransaction(transaction, type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys) + ..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { @@ -1880,20 +1877,17 @@ abstract class ElectrumWalletBase if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { try { - final blockHash = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block-height/$height", - ), - ); + final blockHash = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) + .timeout(Duration(seconds: 15)); if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty && jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", - ), - ); + final blockResponse = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) + .timeout(Duration(seconds: 15)); + if (blockResponse.statusCode == 200 && blockResponse.body.isNotEmpty && jsonDecode(blockResponse.body)['timestamp'] != null) { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 662d70d67..08c56c600 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -464,7 +464,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final oldBox = await CakeHive.openBox(oldBoxName); mwebUtxosBox = await CakeHive.openBox(newBoxName); for (final key in oldBox.keys) { - await mwebUtxosBox.put(key, oldBox.get(key)!); + final value = oldBox.get(key); + await oldBox.delete(key); + await mwebUtxosBox.put(key, value!); } oldBox.deleteFromDisk(); diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index b80fa777c..95a523d89 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_persister.dart'; import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; @@ -16,6 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/config.dart' as pj_config; import 'package:payjoin_flutter/uri.dart' as PayjoinUri; class PayjoinManager { @@ -28,13 +30,16 @@ class PayjoinManager { static const List ohttpRelayUrls = [ 'https://pj.bobspacebkk.com', 'https://ohttp.achow101.com', + 'https://ohttp.cakewallet.com', ]; - static Future randomOhttpRelayUrl() => PayjoinUri.Url.fromStr( - ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]); + static String randomOhttpRelayUrl() => + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]; static const payjoinDirectoryUrl = 'https://payjo.in'; + Future initPayjoin() => pj_config.PConfig.initializeApp(); + Future resumeSessions() async { final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); @@ -42,13 +47,13 @@ class PayjoinManager { if (session.isSenderSession) { printV("Resuming Payjoin Sender Session ${session.pjUri!}"); return _spawnSender( - sender: Sender.fromJson(session.sender!), + sender: Sender.fromJson(json: session.sender!), pjUri: session.pjUri!, ); } - final receiver = Receiver.fromJson(session.receiver!); + final receiver = Receiver.fromJson(json: session.receiver!); printV("Resuming Payjoin Receiver Session ${receiver.id()}"); - return _spawnReceiver(receiver: receiver); + return spawnReceiver(receiver: receiver); }); printV("Resumed ${spawnedSessions.length} Payjoin Sessions"); @@ -65,7 +70,12 @@ class PayjoinManager { psbtBase64: originalPsbt, pjUri: pjUri, ); - return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final persister = PayjoinSenderPersister.impl(); + final newSender = + await senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final senderToken = await newSender.persist(persister: persister); + + return Sender.load(token: senderToken, persister: persister); } catch (e) { throw Exception('Error initializing Payjoin Sender: $e'); } @@ -111,15 +121,13 @@ class PayjoinManager { } } catch (e) { _cleanupSession(pjUri); - printV(e); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); - completer.completeError(e); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString()); + completer.complete(); } } else if (message is PayjoinSessionError) { _cleanupSession(pjUri); if (message is UnrecoverableError) { - printV(message.message); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message); completer.complete(); } else if (message is RecoverableError) { completer.complete(); @@ -139,42 +147,41 @@ class PayjoinManager { return completer.future; } - Future initReceiver(String address, + Future getUnusedReceiver(String address, [bool isTestnet = false]) async { - try { - final payjoinDirectory = - await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); + final session = _payjoinStorage.getUnusedActiveReceiverSession(_wallet.id); - final ohttpKeys = await PayjoinUri.fetchOhttpKeys( - ohttpRelay: await randomOhttpRelayUrl(), - payjoinDirectory: payjoinDirectory, - ); + if (session != null) { + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); - final receiver = await Receiver.create( - address: address, - network: isTestnet ? Network.testnet : Network.bitcoin, - directory: payjoinDirectory, - ohttpKeys: ohttpKeys, - ohttpRelay: await randomOhttpRelayUrl(), - ); - - await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - - return receiver; - } catch (e) { - throw Exception('Error initializing Payjoin Receiver: $e'); + return Receiver.fromJson(json: session.receiver!); } + + return initReceiver(address); } - Future spawnNewReceiver({ - required Receiver receiver, - bool isTestnet = false, - }) async { + Future initReceiver(String address, [bool isTestnet = false]) async { + final ohttpKeys = await PayjoinUri.fetchOhttpKeys( + ohttpRelay: await randomOhttpRelayUrl(), + payjoinDirectory: payjoinDirectoryUrl, + ); + + final newReceiver = await NewReceiver.create( + address: address, + network: isTestnet ? Network.testnet : Network.bitcoin, + directory: payjoinDirectoryUrl, + ohttpKeys: ohttpKeys, + ); + final persister = PayjoinReceiverPersister.impl(); + final receiverToken = await newReceiver.persist(persister: persister); + final receiver = await Receiver.load(persister: persister, token: receiverToken); + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - return _spawnReceiver(isTestnet: isTestnet, receiver: receiver); + + return receiver; } - Future _spawnReceiver({ + Future spawnReceiver({ required Receiver receiver, bool isTestnet = false, }) async { @@ -194,7 +201,8 @@ class PayjoinManager { rawAmount = getOutputAmountFromTx(tx, _wallet); break; case PayjoinReceiverRequestTypes.checkIsOwned: - (_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver(); + (_wallet.walletAddresses as BitcoinWalletAddresses) + .newPayjoinReceiver(); _payjoinStorage.markReceiverSessionInProgress(receiver.id()); final inputScript = message['input_script'] as Uint8List; @@ -218,6 +226,10 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.getCandidateInputs: utxos = _wallet.getUtxoWithPrivateKeys(); + if (utxos.isEmpty) { + await _wallet.updateAllUnspents(); + utxos = _wallet.getUtxoWithPrivateKeys(); + } mainToIsolateSendPort?.send({ 'requestId': message['requestId'], 'result': utxos, diff --git a/cw_bitcoin/lib/payjoin/payjoin_persister.dart b/cw_bitcoin/lib/payjoin/payjoin_persister.dart new file mode 100644 index 000000000..4e395e36a --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_persister.dart @@ -0,0 +1,66 @@ +import 'package:payjoin_flutter/src/generated/api/receive.dart'; +import 'package:payjoin_flutter/src/generated/api/send.dart'; + +class PayjoinSenderPersister implements DartSenderPersister { + static DartSenderPersister impl() { + final impl = PayjoinSenderPersister(); + return DartSenderPersister( + save: (sender) => impl.save(sender: sender), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiSender sender}) async { + final token = sender.key(); + _store[token.toBytes().toString()] = sender; + return token; + } + + Future load({required SenderToken token}) async { + final sender = _store[token.toBytes().toString()]; + if (sender == null) { + throw Exception('Sender not found for the provided token.'); + } + return sender; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} + +class PayjoinReceiverPersister implements DartReceiverPersister { + static DartReceiverPersister impl() { + final impl = PayjoinReceiverPersister(); + return DartReceiverPersister( + save: (receiver) => impl.save(receiver: receiver), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiReceiver receiver}) async { + final token = receiver.key(); + _store[token.toBytes().toString()] = receiver; + return token; + } + + Future load({required ReceiverToken token}) async { + final receiver = _store[token.toBytes().toString()]; + if (receiver == null) { + throw Exception('Receiver not found for the provided token.'); + } + return receiver; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index a499660b0..c56148de2 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -4,14 +4,16 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/bitcoin_ffi.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:http/http.dart' as very_insecure_http_do_not_use; // for errors enum PayjoinReceiverRequestTypes { processOriginalTx, @@ -27,7 +29,7 @@ class PayjoinReceiverWorker { final pendingRequests = >{}; PayjoinReceiverWorker._(this.sendPort); - + static final client = ProxyWrapper().getHttpIOClient(); static Future run(List args) async { await pj.core.init(); @@ -41,11 +43,10 @@ class PayjoinReceiverWorker { receivePort.listen(worker.handleMessage); try { - final httpClient = http.Client(); - final receiver = Receiver.fromJson(receiverJson); + final receiver = Receiver.fromJson(json: receiverJson); final uncheckedProposal = - await worker.receiveUncheckedProposal(httpClient, receiver); + await worker.receiveUncheckedProposal(receiver); final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast(); sendPort.send({ @@ -56,14 +57,14 @@ class PayjoinReceiverWorker { final payjoinProposal = await worker.processPayjoinProposal( uncheckedProposal, ); - final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal); + final psbt = await worker.sendFinalProposal(payjoinProposal); sendPort.send({ 'type': PayjoinReceiverRequestTypes.proposalSent, 'psbt': psbt, }); } catch (e) { if (e is HttpException || - (e is http.ClientException && + (e is very_insecure_http_do_not_use.ClientException && e.message.contains("Software caused connection abort"))) { sendPort.send(PayjoinSessionError.recoverable(e.toString())); } else { @@ -97,15 +98,16 @@ class PayjoinReceiverWorker { return completer.future; } - Future receiveUncheckedProposal( - http.Client httpClient, Receiver session) async { + Future receiveUncheckedProposal(Receiver session) async { while (true) { printV("Polling for Proposal (${session.id()})"); - final extractReq = await session.extractReq(); + final extractReq = await session.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final request = extractReq.$1; final url = Uri.parse(request.url.asString()); - final httpRequest = await httpClient.post(url, + final httpRequest = await client.post(url, headers: {'Content-Type': request.contentType}, body: request.body); final proposal = await session.processRes( @@ -114,13 +116,14 @@ class PayjoinReceiverWorker { } } - Future sendFinalProposal( - http.Client httpClient, PayjoinProposal finalProposal) async { - final req = await finalProposal.extractV2Req(); + Future sendFinalProposal(PayjoinProposal finalProposal) async { + final req = await finalProposal.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final proposalReq = req.$1; final proposalCtx = req.$2; - final request = await httpClient.post( + final request = await client.post( Uri.parse(proposalReq.url.asString()), headers: {"Content-Type": proposalReq.contentType}, body: proposalReq.body, @@ -171,7 +174,7 @@ class PayjoinReceiverWorker { final listUnspent = await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs); final unspent = listUnspent as List; - if (unspent.isEmpty) throw Exception('No unspent outputs available'); + if (unspent.isEmpty) throw RecoverableError('No unspent outputs available'); final selectedUtxo = await _inputPairFromUtxo(unspent[0]); final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]); @@ -214,6 +217,6 @@ class PayjoinReceiverWorker { sequence: 0, ); - return InputPair.newInstance(txin, psbtin); + return InputPair.newInstance(txin: txin, psbtin: psbtin); } } diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart index f720bac01..7e85cc773 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -5,10 +5,12 @@ import 'dart:isolate'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/send.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:payjoin_flutter/src/generated/api/send/error.dart' as pj_error; +import 'package:payjoin_flutter/uri.dart' as pj_uri; enum PayjoinSenderRequestTypes { requestPosted, @@ -29,7 +31,7 @@ class PayjoinSenderWorker { final senderJson = args[1] as String; final pjUrl = args[2] as String; - final sender = Sender.fromJson(senderJson); + final sender = Sender.fromJson(json: senderJson); final worker = PayjoinSenderWorker._(sendPort, pjUrl); try { @@ -42,19 +44,17 @@ class PayjoinSenderWorker { sendPort.send(e); } } + final client = ProxyWrapper().getHttpIOClient(); /// Run a payjoin sender (V2 protocol first, fallback to V1). Future runSender(Sender sender) async { - final httpClient = http.Client(); try { - return await _runSenderV2(sender, httpClient); + return await _runSenderV2(sender); } catch (e) { printV(e); - if (e is PayjoinException && - // TODO condition on error type instead of message content - e.message?.contains('parse receiver public key') == true) { - return await _runSenderV1(sender, httpClient); + if (e is pj_error.FfiCreateRequestError) { + return await _runSenderV1(sender); } else if (e is HttpException) { printV(e); throw Exception(PayjoinSessionError.recoverable(e.toString())); @@ -65,13 +65,14 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V2 of the protocol. - Future _runSenderV2(Sender sender, http.Client httpClient) async { + Future _runSenderV2(Sender sender) async { try { final postRequest = await sender.extractV2( - ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(), + ohttpProxyUrl: + await pj_uri.Url.fromStr(PayjoinManager.randomOhttpRelayUrl()), ); - final postResult = await _postRequest(httpClient, postRequest.$1); + final postResult = await _postRequest(postRequest.$1); final getContext = await postRequest.$2.processResponse(response: postResult); @@ -83,7 +84,7 @@ class PayjoinSenderWorker { final getRequest = await getContext.extractReq( ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), ); - final getRes = await _postRequest(httpClient, getRequest.$1); + final getRes = await _postRequest(getRequest.$1); final proposalPsbt = await getContext.processResponse( response: getRes, ohttpCtx: getRequest.$2, @@ -97,20 +98,20 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V1 of the protocol. - Future _runSenderV1(Sender sender, http.Client httpClient) async { + Future _runSenderV1(Sender sender) async { try { final postRequest = await sender.extractV1(); - final response = await _postRequest(httpClient, postRequest.$1); + final response = await _postRequest(postRequest.$1); sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); return await postRequest.$2.processResponse(response: response); - } catch (e) { - throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e'); + } catch (e, stack) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e, $stack'); } } - Future> _postRequest(http.Client client, Request req) async { + Future> _postRequest(Request req) async { final httpRequest = await client.post(Uri.parse(req.url.asString()), headers: {'Content-Type': req.contentType}, body: req.body); diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart index 9c1c83253..5fb9d5716 100644 --- a/cw_bitcoin/lib/payjoin/storage.dart +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -23,6 +23,14 @@ class PayjoinStorage { ), ); + PayjoinSession? getUnusedActiveReceiverSession(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + session.status == PayjoinSessionStatus.created.name && + !session.isSenderSession) + .firstOrNull; + Future markReceiverSessionComplete( String sessionId, String txId, String amount) async { final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; @@ -76,10 +84,11 @@ class PayjoinStorage { await session.save(); } - Future markSenderSessionUnrecoverable(String pjUrl) async { + Future markSenderSessionUnrecoverable(String pjUrl, String reason) async { final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; await session.save(); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index e21da4f1d..c2987894c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -80,7 +85,7 @@ packages: description: path: "." ref: cake-update-v9 - resolved-ref: "86969a14e337383e14965f5fb45a72a63e5009bc" + resolved-ref: bb4318511312a454fd91bf49042e25ecc855e4ac url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" @@ -121,10 +126,10 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_cli_annotations: dependency: transitive description: @@ -137,42 +142,42 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -185,10 +190,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: transitive description: @@ -296,10 +301,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" dart_varuint_bitcoin: dependency: transitive description: @@ -389,10 +394,10 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.9.0" flutter_test: dependency: "direct dev" description: flutter @@ -402,10 +407,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -415,10 +420,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -439,18 +444,18 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.3+1" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "2.0.0" graphs: dependency: transitive description: @@ -463,10 +468,10 @@ packages: dependency: "direct main" description: name: grpc - sha256: "5b99b7a420937d4361ece68b798c9af8e04b5bc128a7859f2a4be87427694813" + sha256: "30e1edae6846b163a64f6d8716e3443980fe1f7d2d1f086f011d24ea186f2582" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.4" hex: dependency: transitive description: @@ -487,18 +492,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http2: dependency: transitive description: @@ -519,10 +524,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -592,10 +597,10 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31" + sha256: "531da5daba5731d9eca2732881ef2f039b97bf8aa3564e7098dfa99a9b07a8e6" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.3" ledger_litecoin: dependency: "direct main" description: @@ -621,6 +626,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -665,10 +678,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" nested: dependency: transitive description: @@ -690,10 +703,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -714,10 +727,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -754,11 +767,11 @@ packages: dependency: "direct main" description: path: "." - ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c" - resolved-ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c" - url: "https://github.com/konstantinullrich/payjoin-flutter" + ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff + resolved-ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff + url: "https://github.com/OmarHatem28/payjoin-flutter" source: git - version: "0.21.0" + version: "0.23.0" petitparser: dependency: transitive description: @@ -811,26 +824,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: @@ -859,18 +872,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -915,18 +928,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -935,27 +948,37 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: "direct main" + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -973,14 +996,6 @@ packages: url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" stack_trace: dependency: transitive description: @@ -1033,10 +1048,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -1057,10 +1081,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098" + sha256: "35d210e93a5938c6a6d1fd3c710cf4ac90b1bdd1b11c8eb2beeb32600672e6e6" url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.17.0" universal_platform: dependency: transitive description: @@ -1077,14 +1101,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" vector_math: dependency: transitive description: @@ -1121,18 +1137,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -1166,5 +1182,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index f45258f92..c24732c3a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -42,8 +42,8 @@ dependencies: url: https://github.com/cake-tech/bech32.git payjoin_flutter: git: - url: https://github.com/konstantinullrich/payjoin-flutter - ref: 6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c #cake-v1 + url: https://github.com/OmarHatem28/payjoin-flutter + ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff #cake-v2 ledger_flutter_plus: ^1.4.1 ledger_bitcoin: git: @@ -54,14 +54,18 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-litecoin + socks_socket: + git: + url: https://github.com/sneurlax/socks_socket + ref: e6232c53c1595469931ababa878759a067c02e94 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9a5c4f14f..e78261f9a 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -7,7 +7,7 @@ homepage: https://cakewallet.com environment: sdk: '>=2.19.0 <3.0.0' - flutter: ">=1.17.0" + flutter: ">=1.20.0" dependencies: flutter: @@ -33,9 +33,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index cb8485ec5..0d6ab9f3a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -111,7 +111,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.zano, CryptoCurrency.ton, CryptoCurrency.flip, - CryptoCurrency.deuro + CryptoCurrency.deuro, + CryptoCurrency.usdtbsc, ]; static const havenCurrencies = [ @@ -232,7 +233,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); - static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Digital Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const usdtbsc = CryptoCurrency(title: 'USDT', tag: 'BSC', fullName: 'USDT Binance coin', raw: 99, name: 'usdtbsc', iconPath: 'assets/images/usdtbsc_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index aee12b423..4786336af 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:intl/intl.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; // FIXME: Hardcoded values; Works only for monero @@ -234,10 +234,14 @@ int getHavenHeightByDate({required DateTime date}) { } Future getHavenCurrentHeight() async { - final response = await http.get(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo')); + final req = await ProxyWrapper().getHttpClient() + .getUrl(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo')) + .timeout(Duration(seconds: 15)); + final response = await req.close(); + final stringResponse = await response.transform(utf8.decoder).join(); if (response.statusCode == 200) { - final info = jsonDecode(response.body); + final info = jsonDecode(stringResponse); return info['data']['height'] as int; } else { throw Exception('Failed to load current blockchain height'); @@ -269,13 +273,13 @@ const bitcoinDates = { }; Future getBitcoinHeightByDateAPI({required DateTime date}) async { - final response = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", - ), - ); + final req = await ProxyWrapper().getHttpClient() + .getUrl(Uri.parse("https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}")) + .timeout(Duration(seconds: 15)); + final response = await req.close(); + final stringResponse = await response.transform(utf8.decoder).join(); - return jsonDecode(response.body)['height'] as int; + return jsonDecode(stringResponse)['height'] as int; } int getBitcoinHeightByDate({required DateTime date}) { diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 38fcde9e1..fdffb844b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:cw_core/keyable.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:http/io_client.dart' as ioc; import 'dart:math' as math; import 'package:convert/convert.dart'; @@ -184,23 +184,17 @@ class Node extends HiveObject with Keyable { final body = {'jsonrpc': '2.0', 'id': '0', 'method': "getinfo"}; try { - final authenticatingClient = HttpClient(); - authenticatingClient.badCertificateCallback = - ((X509Certificate cert, String host, int port) => true); - - final http.Client client = ioc.IOClient(authenticatingClient); - final jsonBody = json.encode(body); - final response = await client.post( - rpcUri, + final response = await ProxyWrapper().post( + clearnetUri: rpcUri, headers: {'Content-Type': 'application/json'}, body: jsonBody, ); - printV("node check response: ${response.body}"); - + final resBody = json.decode(response.body) as Map; + return resBody['result']['height'] != null; } catch (e) { printV("error: $e"); @@ -218,11 +212,7 @@ class Node extends HiveObject with Keyable { final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName}; try { - final authenticatingClient = HttpClient(); - authenticatingClient.badCertificateCallback = - ((X509Certificate cert, String host, int port) => true); - - final http.Client client = ioc.IOClient(authenticatingClient); + final client = ProxyWrapper().getHttpIOClient(); final jsonBody = json.encode(body); @@ -242,15 +232,15 @@ class Node extends HiveObject with Keyable { return !(response['offline'] as bool); } - printV("node check response: ${response.body}"); + final responseString = await response.body; - if ((response.body.contains("400 Bad Request") // Some other generic error + if ((responseString.contains("400 Bad Request") // Some other generic error || - response.body.contains("plain HTTP request was sent to HTTPS port") // Cloudflare + responseString.contains("plain HTTP request was sent to HTTPS port") // Cloudflare || response.headers["location"] != null // Generic reverse proxy || - response.body + responseString .contains("301 Moved Permanently") // Poorly configured generic reverse proxy ) && !(useSSL ?? false)) { @@ -277,15 +267,16 @@ class Node extends HiveObject with Keyable { } Future requestNodeWithProxy() async { - if (!isValidProxyAddress /* && !Tor.instance.enabled*/) { + if (!isValidProxyAddress && !CakeTor.instance.enabled) { return false; } String? proxy = socksProxyAddress; - // if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { - // proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; - // } + if ((proxy?.isEmpty ?? true) && CakeTor.instance.enabled) { + proxy = "${InternetAddress.loopbackIPv4.address}:${CakeTor.instance.port}"; + } + printV("proxy: $proxy"); if (proxy == null) { return false; } @@ -305,13 +296,9 @@ class Node extends HiveObject with Keyable { // you try to communicate with it Future requestElectrumServer() async { try { - final Socket socket; - if (useSSL == true) { - socket = await SecureSocket.connect(uri.host, uri.port, - timeout: Duration(seconds: 5), onBadCertificate: (_) => true); - } else { - socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); - } + final ProxySocket socket; + socket = await ProxyWrapper().getSocksSocket(useSSL ?? false, uri.host, uri.port); + socket.destroy(); return true; @@ -322,8 +309,8 @@ class Node extends HiveObject with Keyable { Future requestNanoNode() async { try { - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: {"Content-Type": "application/json", "nano-app": "cake-wallet"}, body: jsonEncode( { @@ -332,7 +319,8 @@ class Node extends HiveObject with Keyable { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body); if (response.statusCode != 200 || data["error"] != null || data["balance"] == null || @@ -348,13 +336,14 @@ class Node extends HiveObject with Keyable { Future requestEthereumServer() async { try { - final response = await http.get( - uri, - headers: {'Content-Type': 'application/json'}, - ); + final req = await ProxyWrapper().getHttpClient() + .getUrl(uri,) + .timeout(Duration(seconds: 15)); + final response = await req.close(); return response.statusCode >= 200 && response.statusCode < 300; - } catch (_) { + } catch (err) { + printV("Failed to request ethereum server: $err"); return false; } } @@ -462,7 +451,7 @@ class DaemonRpc { /// Perform a JSON-RPC call with Digest Authentication. Future> call(String method, Map params) async { - final http.Client client = http.Client(); + final client = ProxyWrapper().getHttpIOClient(); final DigestAuth digestAuth = DigestAuth(username, password); // Initial request to get the `WWW-Authenticate` header. diff --git a/cw_core/lib/solana_rpc_http_service.dart b/cw_core/lib/solana_rpc_http_service.dart index fbe9a29dc..1c6e975aa 100644 --- a/cw_core/lib/solana_rpc_http_service.dart +++ b/cw_core/lib/solana_rpc_http_service.dart @@ -1,20 +1,19 @@ import 'dart:convert'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:on_chain/solana/solana.dart'; class SolanaRPCHTTPService implements SolanaJSONRPCService { SolanaRPCHTTPService( - {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) - : client = client ?? Client(); + {required this.url, + this.defaultRequestTimeout = const Duration(seconds: 30)}); @override final String url; - final Client client; final Duration defaultRequestTimeout; - @override - Future> call(SolanaRequestDetails params, [Duration? timeout]) async { - final response = await client.post( - Uri.parse(url), + Future> call(SolanaRequestDetails params, + [Duration? timeout]) async { + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(url), body: params.toRequestBody(), headers: { 'Content-Type': 'application/json', diff --git a/cw_core/lib/utils/proxy_logger/abstract.dart b/cw_core/lib/utils/proxy_logger/abstract.dart new file mode 100644 index 000000000..303b640f1 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/abstract.dart @@ -0,0 +1,29 @@ +import 'dart:typed_data'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +enum RequestNetwork { + clearnet, + tor, +} + +enum RequestMethod { + get, + post, + put, + delete, + + newHttpClient, + newHttpIOClient, + newProxySocket, +} + +abstract class ProxyLogger { + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }); +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart b/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart new file mode 100644 index 000000000..e2929da12 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart @@ -0,0 +1,63 @@ +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +class MemoryProxyLoggerEntry { + MemoryProxyLoggerEntry({ + required this.trace, + required this.uri, + required this.body, + required this.network, + required this.method, + required this.response, + required this.error, + }) : time = DateTime.now(); + + final StackTrace trace; + final Uri? uri; + final Uint8List body; + final RequestNetwork network; + final very_insecure_http_do_not_use.Response? response; + final RequestMethod method; + final String? error; + final DateTime time; + @override + String toString() => """MemoryProxyLoggerEntry( + uri: $uri, + body: $body, + network: $network, + method: $method, + response: + code: ${response?.statusCode}, + headers: ${response?.headers}, + body: ${response?.body}, + error: $error, + time: $time, + trace: ${trace} +);"""; +} + +class MemoryProxyLogger implements ProxyLogger { + static List logs = []; + @override + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }) { + final trace = StackTrace.current; + logs.add(MemoryProxyLoggerEntry( + method: method, + trace: trace, + uri: uri, + body: body, + network: network, + response: response, + error: error,), + ); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_logger/silent_logger.dart b/cw_core/lib/utils/proxy_logger/silent_logger.dart new file mode 100644 index 000000000..1cea0d011 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/silent_logger.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +// we are not doing anything +class SilentProxyLogger implements ProxyLogger { + @override + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }) {} +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/abstract.dart b/cw_core/lib/utils/proxy_socket/abstract.dart new file mode 100644 index 000000000..b4b628f74 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/abstract.dart @@ -0,0 +1,47 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/insecure.dart'; +import 'package:cw_core/utils/proxy_socket/secure.dart'; +import 'package:cw_core/utils/proxy_socket/socks.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:socks_socket/socks_socket.dart'; + +class ProxyAddress { + final String host; + final int port; + + ProxyAddress({required this.host, required this.port}); +} + +abstract class ProxySocket { + static Future connect(bool sslEnabled, ProxyAddress address, {Duration? connectionTimeout}) async { + if (CakeTor.instance.started) { + var socksSocket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: CakeTor.instance.port, + sslEnabled: sslEnabled, + ); + await socksSocket.connect(); + await socksSocket.connectTo(address.host, address.port); + return ProxySocketSocks(socksSocket); + } + if (sslEnabled == false) { + return ProxySocketInsecure(await Socket.connect(address.host, address.port, timeout: connectionTimeout)); + } else { + return ProxySocketSecure(await SecureSocket.connect( + address.host, + address.port, + timeout: connectionTimeout, + onBadCertificate: (_) => true, + )); + } + } + + Future close(); + Future destroy(); + Future write(String data); + StreamSubscription> listen(Function(Uint8List event) onData, {Function (Object error)? onError, Function ()? onDone, bool cancelOnError = true}); + ProxyAddress get address; +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/insecure.dart b/cw_core/lib/utils/proxy_socket/insecure.dart new file mode 100644 index 000000000..aeac474d7 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/insecure.dart @@ -0,0 +1,34 @@ + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:io'; + +class ProxySocketInsecure implements ProxySocket { + final Socket socket; + + ProxySocketInsecure(this.socket); + + ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort); + + @override + Future close() => socket.close(); + + @override + Future destroy() async => socket.destroy(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/secure.dart b/cw_core/lib/utils/proxy_socket/secure.dart new file mode 100644 index 000000000..2efd13ee4 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/secure.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; + +class ProxySocketSecure implements ProxySocket { + final SecureSocket socket; + + ProxySocketSecure(this.socket); + + ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort); + + @override + Future close() => socket.close(); + + @override + Future destroy() async => socket.destroy(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} diff --git a/cw_core/lib/utils/proxy_socket/socks.dart b/cw_core/lib/utils/proxy_socket/socks.dart new file mode 100644 index 000000000..a4e5ddeb6 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/socks.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:socks_socket/socks_socket.dart'; + +class ProxySocketSocks implements ProxySocket { + final SOCKSSocket socket; + + ProxySocketSocks(this.socket); + + @override + ProxyAddress get address => ProxyAddress(host: socket.proxyHost, port: socket.proxyPort); + + @override + Future close() => socket.close(); + + @override + Future destroy() => close(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + diff --git a/cw_core/lib/utils/proxy_wrapper.dart b/cw_core/lib/utils/proxy_wrapper.dart new file mode 100644 index 000000000..e43f34ff1 --- /dev/null +++ b/cw_core/lib/utils/proxy_wrapper.dart @@ -0,0 +1,447 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:cw_core/utils/tor/android.dart'; +import 'package:cw_core/utils/tor/disabled.dart'; +import 'package:http/http.dart'; +import 'package:socks5_proxy/socks_client.dart'; +import 'package:http/io_client.dart' as ioc; + +class ProxyWrapper { + static final ProxyWrapper _proxyWrapper = ProxyWrapper._internal(); + static ProxyLogger? logger; + + factory ProxyWrapper() { + return _proxyWrapper; + } + + ProxyWrapper._internal(); + Future getSocksSocket(bool sslEnabled, String host, int port, {Duration? connectionTimeout}) async { + logger?.log( + uri: Uri( + scheme: sslEnabled ? "https" : "http", + host: host, + port: port, + ), + method: RequestMethod.newProxySocket, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null + ); + return ProxySocket.connect(sslEnabled, ProxyAddress(host: host, port: port), connectionTimeout: connectionTimeout); + } + + RequestNetwork requestNetwork() { + return CakeTor.instance.started ? RequestNetwork.tor : RequestNetwork.clearnet; + } + + ioc.IOClient getHttpIOClient({int? portOverride, bool internal = false}) { + if (!internal) { + logger?.log( + uri: null, + method: RequestMethod.newHttpIOClient, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null, + ); + } + // ignore: deprecated_member_use_from_same_package + final httpClient = ProxyWrapper().getHttpClient(portOverride: portOverride, internal: true); + return ioc.IOClient(httpClient); + } + + int getPort() => CakeTor.instance.port; + + @Deprecated('Use ProxyWrapper().get/post/put methods instead, and provide proper clearnet and onion uri.') + HttpClient getHttpClient({int? portOverride, bool internal = false}) { + if (!internal) { + logger?.log( + uri: null, + method: RequestMethod.newProxySocket, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null + ); + } + if (CakeTor.instance.started) { + // Assign connection factory. + final client = HttpClient(); + SocksTCPClient.assignToHttpClient(client, [ + ProxySettings( + InternetAddress.loopbackIPv4, + CakeTor.instance.port, + password: null, + ), + ]); + return client; + } else { + return HttpClient(); + } + } + + + + Future _make({ + required RequestMethod method, + required ioc.IOClient client, + required Uri uri, + required Map? headers, + String? body, + }) async { + Object? error; + Response? resp; + try { + switch (method) { + case RequestMethod.get: + resp = await client. get( + uri, + headers: headers, + ); + break; + case RequestMethod.delete: + resp = await client.delete( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.post: + resp = await client.post( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.put: + resp = await client.put( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.newHttpClient: + case RequestMethod.newHttpIOClient: + case RequestMethod.newProxySocket: + throw UnimplementedError(); + } + return resp; + } catch (e) { + error = e; + rethrow; + } finally { + logger?.log( + uri: uri, + method: RequestMethod.get, + body: utf8.encode(body ?? ''), + response: resp, + network: requestNetwork(), + error: error?.toString(), + ); + } + } + + Future get({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (CakeTor.instance.started) { + torEnabled = true; + } else { + torEnabled = false; + } + + // if tor is enabled, try to connect to the onion url first: + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) { + rethrow; + } + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.get, + client: torClient, + uri: onionUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.get, + client: torClient, + uri: clearnetUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.get, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + + Future post({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + String? body, + bool allowMitmMoneroBypassSSLCheck = false, + }) async { + HttpClient? torHttpClient; + HttpClient cleatnetHttpClient = HttpClient(); + if (allowMitmMoneroBypassSSLCheck) { + cleatnetHttpClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + } + + ioc.IOClient clearnetClient = ioc.IOClient(cleatnetHttpClient); + + + bool torEnabled = CakeTor.instance.started; + + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torHttpClient = await getHttpClient(portOverride: portOverride); + } catch (_) { + rethrow; + } + if (allowMitmMoneroBypassSSLCheck) { + torHttpClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + } + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.post, + client: ioc.IOClient(torHttpClient), + uri: onionUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.post, + client: ioc.IOClient(torHttpClient), + uri: clearnetUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.post, + client: clearnetClient, + uri: clearnetUri, + headers: headers, + body: body, + ); + }, + ); + } catch (_) { + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + Future put({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + String? body, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) {} + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.put, + client: torClient!, + uri: onionUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.put, + client: torClient!, + uri: clearnetUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.put, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + body: body, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + Future delete({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (CakeTor.instance.started) { + torEnabled = true; + } else { + torEnabled = false; + } + + // if tor is enabled, try to connect to the onion url first: + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) { + rethrow; + } + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.delete, + client: torClient, + uri: onionUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.delete, + client: torClient, + uri: clearnetUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.delete, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } +} + + +class CakeTor { + static final CakeTorInstance instance = CakeTorInstance.getInstance(); +} diff --git a/cw_core/lib/utils/tor/abstract.dart b/cw_core/lib/utils/tor/abstract.dart new file mode 100644 index 000000000..aaecabc56 --- /dev/null +++ b/cw_core/lib/utils/tor/abstract.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/tor/android.dart'; +import 'package:cw_core/utils/tor/disabled.dart'; +import 'package:cw_core/utils/tor/tails.dart'; + +abstract class CakeTorInstance { + bool get started; + + int get port => -1; + + bool get enabled => false; + + bool get bootstrapped => false; + + Future start(); + Future stop(); + + static CakeTorInstance getInstance() { + if (Platform.isAndroid) { + return CakeTorAndroid(); + } + if (Platform.isLinux) { + try { + final os = File("/etc/os-release").readAsLinesSync(); + for (var line in os) { + if (!line.startsWith("ID=")) continue; + if (!line.contains("tails")) continue; + return CakeTorTails(); + } + } catch (e) { + printV("Failed to identify linux version - /etc/os-release missing"); + } + } + return CakeTorDisabled(); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/android.dart b/cw_core/lib/utils/tor/android.dart new file mode 100644 index 000000000..dfc1bfbe4 --- /dev/null +++ b/cw_core/lib/utils/tor/android.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; +import 'package:tor_binary/tor_binary_platform_interface.dart'; + +class CakeTorAndroid implements CakeTorInstance { + @override + bool get bootstrapped => _proc != null; + + @override + bool get enabled => _proc != null; + + @override + int get port => 42142; + + @override + Future start() async { + await _runEmbeddedTor(); + } + + @override + bool get started => _proc != null; + + @override + Future stop() async { + _proc?.kill(); + await _proc?.exitCode; + _proc = null; + } + + static Process? _proc; + + Future _runEmbeddedTor() async { + final dir = await getApplicationCacheDirectory(); + + final torBinPath = p.join((await TorBinaryPlatform.instance.getBinaryPath())!, "libtor.so"); + printV("torPath: $torBinPath"); + + if (started) { + printV("Proxy is running"); + return; + } + + printV("Starting embedded tor"); + printV("app docs: $dir"); + final torrc = """ +SocksPort $port +Log notice file ${p.join(dir.path, "tor.log")} +RunAsDaemon 0 +DataDirectory ${p.join(dir.path, "tor-data")} +"""; + final torrcPath = p.join(dir.absolute.path, "torrc"); + File(torrcPath).writeAsStringSync(torrc); + + if (_proc != null) { + try { + _proc?.kill(); + await _proc?.exitCode; + _proc = null; + } catch (e) { + printV(e); + } + } + printV("path: $torBinPath -f $torrcPath"); + _proc = await Process.start(torBinPath, ["-f", torrcPath]); + _proc?.stdout.transform(utf8.decoder).forEach(printV); + _proc?.stderr.transform(utf8.decoder).forEach(printV); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/disabled.dart b/cw_core/lib/utils/tor/disabled.dart new file mode 100644 index 000000000..8bd3d837e --- /dev/null +++ b/cw_core/lib/utils/tor/disabled.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/utils/tor/abstract.dart'; + +class CakeTorDisabled implements CakeTorInstance { + @override + bool get bootstrapped => false; + + @override + bool get enabled => false; + + @override + int get port => -1; + + @override + Future start() => throw UnimplementedError(); + + @override + bool get started => false; + + @override + Future stop() => throw UnimplementedError(); +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/tails.dart b/cw_core/lib/utils/tor/tails.dart new file mode 100644 index 000000000..c37ee72aa --- /dev/null +++ b/cw_core/lib/utils/tor/tails.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/utils/tor/abstract.dart'; + +class CakeTorTails implements CakeTorInstance { + @override + bool get bootstrapped => true; + + @override + bool get enabled => true; + + @override + int get port => 9150; + + @override + Future start() async {} + + @override + bool get started => true; + + @override + Future stop() async {} +} \ No newline at end of file diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 265eaa9bc..d7bfdbe2a 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -26,18 +26,18 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -67,50 +67,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -123,10 +123,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: "direct main" description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -326,10 +326,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -342,10 +342,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -462,10 +462,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" nested: dependency: transitive description: @@ -479,7 +479,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273" url: "https://github.com/cake-tech/on_chain.git" source: git version: "3.7.0" @@ -487,10 +487,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -583,26 +583,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" rational: dependency: transitive description: @@ -615,18 +615,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -635,11 +635,21 @@ packages: socks5_proxy: dependency: "direct main" description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: "direct main" + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -716,10 +726,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: "direct main" + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -780,18 +799,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -809,5 +828,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 9b38b61a7..07f87475a 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -25,22 +25,29 @@ dependencies: url: https://github.com/cake-tech/cake_backup.git ref: main version: 1.0.0 - socks5_proxy: ^1.0.4 + socks5_proxy: + git: + url: https://github.com/LacticWhale/socks_dart + ref: 27ad7c2efae8d7460325c74b90f660085cbd0685 unorm_dart: ^0.3.0 on_chain: git: url: https://github.com/cake-tech/on_chain.git ref: cake-update-v2 -# tor: -# git: -# url: https://github.com/cake-tech/tor.git -# ref: main + socks_socket: + git: + url: https://github.com/sneurlax/socks_socket + ref: e6232c53c1595469931ababa878759a067c02e94 + tor_binary: + git: + url: https://github.com/MrCyjaneK/flutter-tor_binary + ref: cb811c610871a9517d47134b87c2f590c15c96c5 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 hive_generator: ^2.0.1 diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index ac70a4aaa..97aee775d 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -5,6 +5,8 @@ import 'package:path/path.dart' as p; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/pending_transaction.dart'; import 'package:cw_decred/transaction_credentials.dart'; @@ -307,9 +309,10 @@ abstract class DecredWalletBase persistantPeer = addr; await _libwallet.closeWallet(walletInfo.name); final network = isTestnet ? "testnet" : "mainnet"; + final dirPath = await pathForWalletDir(name: walletInfo.name, type: WalletType.decred); final config = { "name": walletInfo.name, - "datadir": walletInfo.dirPath, + "datadir": dirPath, "net": network, "unsyncedaddrs": true, }; @@ -605,22 +608,22 @@ abstract class DecredWalletBase final sourceDir = Directory(currentDirPath); final targetDir = Directory(newDirPath); - + if (!targetDir.existsSync()) { await targetDir.create(recursive: true); } - + await for (final entity in sourceDir.list(recursive: true)) { - final relativePath = entity.path.substring(sourceDir.path.length+1); + final relativePath = entity.path.substring(sourceDir.path.length + 1); final targetPath = p.join(targetDir.path, relativePath); - + if (entity is File) { await entity.rename(targetPath); } else if (entity is Directory) { await Directory(targetPath).create(recursive: true); } } - + await sourceDir.delete(recursive: true); } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index e2313904e..93c708886 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -8,6 +8,7 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:path/path.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -57,42 +58,93 @@ class DecredWalletService extends WalletService< @override Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { await this.init(); + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + final network = isTestnet == true ? testnet : mainnet; final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pass": credentials.password!, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + // ios will move our wallet directory when updating. Since we must + // recalculate the new path every time we open the wallet, ensure this path + // is not used. An older wallet will have a directory here which is a + // condition for moving the wallet when opening, so this must be kept blank + // going forward. + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); return wallet; } + void copyDirectorySync(Directory source, Directory destination) { + /// create destination folder if not exist + if (!destination.existsSync()) { + destination.createSync(recursive: true); + } + + /// get all files from source (recursive: false is important here) + source.listSync(recursive: false).forEach((entity) { + final newPath = destination.path + Platform.pathSeparator + basename(entity.path); + if (entity is File) { + entity.rename(newPath); + } else if (entity is Directory) { + copyDirectorySync(entity, Directory(newPath)); + } + }); + } + + Future moveWallet(String fromPath, String toPath) async { + final oldWalletDir = new Directory(fromPath); + final newWalletDir = new Directory(toPath); + copyDirectorySync(oldWalletDir, newWalletDir); + // It would be ideal to delete the old directory here, but ios will error + // sometimes with "OS Error: No such file or directory, errno = 2" even + // after checking if it exists. + } + @override Future openWallet(String name, String password) async { final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; - final network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || - walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet - ? testnet - : mainnet; + if (walletInfo.network == null || walletInfo.network == "") { + walletInfo.network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + } await this.init(); - final walletDirExists = Directory(walletInfo.dirPath).existsSync(); - if (!walletDirExists) { - walletInfo.dirPath = await pathForWalletDir(name: name, type: getType()); + + // Cake wallet version 4.27.0 and earlier gave a wallet dir that did not + // match the name. Move those to the correct place. + final dirPath = await pathForWalletDir(name: name, type: getType()); + if (walletInfo.path != "") { + // On ios the stored dir no longer exists. We can only trust the basename. + // dirPath may already be updated and lost the basename, so look at path. + final randomBasename = basename(walletInfo.path); + final oldDir = await pathForWalletDir(name: randomBasename, type: getType()); + if (oldDir != dirPath) { + await this.moveWallet(oldDir, dirPath); + } + // Clear the path so this does not trigger again. + walletInfo.dirPath = ""; + walletInfo.path = ""; + await walletInfo.save(); } final config = { - "name": walletInfo.name, - "datadir": walletInfo.dirPath, - "net": network, + "name": name, + "datadir": dirPath, + "net": walletInfo.network, "unsyncedaddrs": true, }; await libwallet!.loadWallet(jsonEncode(config)); @@ -127,12 +179,11 @@ class DecredWalletService extends WalletService< await currentWallet.renameWalletFiles(newName); - final newDirPath = await pathForWalletDir(name: newName, type: getType()); final newWalletInfo = currentWalletInfo; newWalletInfo.id = WalletBase.idFor(newName, getType()); newWalletInfo.name = newName; - newWalletInfo.dirPath = newDirPath; - newWalletInfo.network = network; + newWalletInfo.dirPath = ""; + newWalletInfo.path = ""; await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @@ -141,18 +192,23 @@ class DecredWalletService extends WalletService< Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pass": credentials.password!, "mnemonic": credentials.mnemonic, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); @@ -165,17 +221,22 @@ class DecredWalletService extends WalletService< Future restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials, {bool? isTestnet}) async { await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pubkey": credentials.pubkey, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWatchOnlyWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 007d9cfb2..f7a5f335d 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -91,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -666,11 +666,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -751,6 +761,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -848,5 +867,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index fcb2ac5ec..989831a89 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -19,8 +19,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 hive_generator: ^2.0.1 ffigen: ^16.1.0 diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index 8381744d6..630424967 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -18,7 +18,7 @@ class DefaultEthereumErc20Tokens { enabled: true, ), Erc20Token( - name: "Digital Euro", + name: "Decentralized Euro", symbol: "DEURO", contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea", decimal: 18, diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_ethereum/lib/deuro/deuro_savings.dart new file mode 100644 index 000000000..bce05905c --- /dev/null +++ b/cw_ethereum/lib/deuro/deuro_savings.dart @@ -0,0 +1,170 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_evm/contract/erc20.dart'; +import 'package:cw_evm/evm_chain_exceptions.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; + +const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; +const String frontendCode = "0x00000000000000000000000000000000000000000043616b652057616c6c6574"; + +class DEuro { + final SavingsGateway _savingsGateway; + final ERC20 _dEuro; + final EthereumWallet _wallet; + + DEuro(EthereumWallet wallet) + : _wallet = wallet, + _savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!), + _dEuro = _getDEuroToken(wallet.getWeb3Client()!); + + static SavingsGateway _getSavingsGateway(Web3Client client) => SavingsGateway( + address: EthereumAddress.fromHex(savingsGatewayAddress), + client: client, + ); + + static ERC20 _getDEuroToken(Web3Client client) => ERC20( + address: EthereumAddress.fromHex(dEuroAddress), + client: client, + ); + + EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); + + Future get savingsBalance async => + (await _savingsGateway.savings(accountOwner: _address)).saved; + + Future get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address); + + Future get interestRate => _savingsGateway.currentRatePPM(); + + Future get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address); + + Future _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + final currentBalance = ethBalance.getInWei; + + final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: BigInt.zero, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17] + .encodeCall([BigInt.zero, hexToBytes(frontendCode)]), + ); + + final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee); + final requiredBalance = estimatedGasFee; + + if (currentBalance < requiredBalance) { + throw DeuroGasFeeException( + requiredGasFee: requiredBalance, + currentBalance: currentBalance, + ); + } + } + + Future depositSavings( + BigInt amount, EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + final signedTransaction = await _savingsGateway.save( + (amount: amount, frontendCode: hexToBytes(frontendCode)), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + ); + + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: BigInt.from(fee.estimatedGasFee), + amount: amount.toString(), + exponent: 18, + ); + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } + } + + Future withdrawSavings( + BigInt amount, EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + final signedTransaction = await _savingsGateway.withdraw( + (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + ); + + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: BigInt.from(fee.estimatedGasFee), + amount: amount.toString(), + exponent: 18); + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } + } + + // Set an infinite approval to save gas in the future + Future enableSavings(EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + return (await _wallet.createApprovalTransaction( + BigInt.parse( + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + radix: 16, + ), + _savingsGateway.self.address.hexEip55, + CryptoCurrency.deuro, + priority, + )) as PendingEVMChainTransaction; + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } + } +} diff --git a/cw_ethereum/lib/deuro/deuro_savings_contract.dart b/cw_ethereum/lib/deuro/deuro_savings_contract.dart new file mode 100644 index 000000000..ca2eb5dcc --- /dev/null +++ b/cw_ethereum/lib/deuro/deuro_savings_contract.dart @@ -0,0 +1,543 @@ +// ignore_for_file: type=lint +// ignore_for_file: unused_local_variable, unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:web3dart/web3dart.dart' as _i1; +import 'dart:typed_data' as _i2; + +final _contractAbi = _i1.ContractAbi.fromJson( + '[{"inputs":[{"internalType":"contract IDecentralizedEURO","name":"deuro_","type":"address"},{"internalType":"uint24","name":"initialRatePPM","type":"uint24"},{"internalType":"address","name":"gateway_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ChangeNotReady","type":"error"},{"inputs":[],"name":"ModuleDisabled","type":"error"},{"inputs":[],"name":"NoPendingChange","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint24","name":"newRate","type":"uint24"}],"name":"RateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"uint24","name":"nextRate","type":"uint24"},{"indexed":false,"internalType":"uint40","name":"nextChange","type":"uint40"}],"name":"RateProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Saved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"GATEWAY","outputs":[{"internalType":"contract IFrontendGateway","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"applyChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"internalType":"struct Savings.Account","name":"account","type":"tuple"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"name":"calculateInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentTicks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deuro","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"equity","outputs":[{"internalType":"contract IReserve","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextChange","outputs":[{"internalType":"uint40","name":"","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint24","name":"newRatePPM_","type":"uint24"},{"internalType":"address[]","name":"helpers","type":"address[]"}],"name":"proposeChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"refreshBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refreshMyBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savings","outputs":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ticks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]', + 'SavingsGateway', +); + +class SavingsGateway extends _i1.GeneratedContract { + SavingsGateway({ + required _i1.EthereumAddress address, + required _i1.Web3Client client, + int? chainId, + }) : super( + _i1.DeployedContract( + _contractAbi, + address, + ), + client, + chainId, + ); + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[1]; + assert(checkSignature(function, '338c5371')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[2]; + assert(checkSignature(function, '77267ec3')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest$2( + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[3]; + assert(checkSignature(function, 'a696399d')); + final params = [ + args.accountOwner, + args.timestamp, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future adjust( + ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[4]; + assert(checkSignature(function, '753ef93c')); + final params = [ + args.targetAmount, + args.frontendCode, + ]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future calculateInterest( + ({dynamic account, BigInt ticks}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[7]; + assert(checkSignature(function, '7915ce20')); + final params = [ + args.account, + args.ticks, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[8]; + assert(checkSignature(function, '06a7b376')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentTicks({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[9]; + assert(checkSignature(function, 'b079f163')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> deuro({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[10]; + assert(checkSignature(function, '82b8eaf5')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> equity({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[11]; + assert(checkSignature(function, '91a0ac6a')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextChange({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[12]; + assert(checkSignature(function, 'b6f83c17')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[13]; + assert(checkSignature(function, '2e4b20ab')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future refreshBalance( + ({_i1.EthereumAddress owner}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[15]; + assert(checkSignature(function, 'b77cd1c7')); + final params = [args.owner]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future refreshMyBalance({ + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[16]; + assert(checkSignature(function, '85bd12d1')); + final params = []; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> save( + ({BigInt amount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[17]; + assert(checkSignature(function, '9e2363dc')); + final params = [ + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> saveTo( + ({ + _i1.EthereumAddress owner, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[19]; + assert(checkSignature(function, 'cbcf9676')); + final params = [ + args.owner, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future savings({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[21]; + assert(checkSignature(function, '1f7cdd5f')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return Savings(response); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> withdraw( + ({ + _i1.EthereumAddress target, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[24]; + assert(checkSignature(function, '829a0476')); + final params = [ + args.target, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// Returns a live stream of all InterestCollected events emitted by this contract. + Stream interestCollectedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('InterestCollected'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return InterestCollected( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateChanged events emitted by this contract. + Stream rateChangedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateChanged'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateChanged( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateProposed events emitted by this contract. + Stream rateProposedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateProposed'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateProposed( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Saved events emitted by this contract. + Stream savedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Saved'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Saved( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Withdrawn events emitted by this contract. + Stream withdrawnEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Withdrawn'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Withdrawn( + decoded, + result, + ); + }); + } +} + +class Savings { + Savings(List response) + : saved = (response[0] as BigInt), + ticks = (response[1] as BigInt); + + final BigInt saved; + + final BigInt ticks; +} + +class InterestCollected { + InterestCollected( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + interest = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt interest; + + final _i1.FilterEvent event; +} + +class RateChanged { + RateChanged( + List response, + this.event, + ) : newRate = (response[0] as BigInt); + + final BigInt newRate; + + final _i1.FilterEvent event; +} + +class RateProposed { + RateProposed( + List response, + this.event, + ) : who = (response[0] as _i1.EthereumAddress), + nextRate = (response[1] as BigInt), + nextChange = (response[2] as BigInt); + + final _i1.EthereumAddress who; + + final BigInt nextRate; + + final BigInt nextChange; + + final _i1.FilterEvent event; +} + +class Saved { + Saved( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} + +class Withdrawn { + Withdrawn( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index 9d50fdd5b..259e7d11d 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -19,7 +19,8 @@ class EthereumClient extends EVMChainClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", "module": "account", "action": contractAddress != null ? "tokentx" : "txlist", if (contractAddress != null) "contractaddress": contractAddress, @@ -50,7 +51,8 @@ class EthereumClient extends EVMChainClient { @override Future> fetchInternalTransactions(String address) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", "module": "account", "action": "txlistinternal", "address": address, diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 7cc140c5a..c22be3cb2 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -31,11 +31,14 @@ class EthereumWallet extends EVMChainWallet { }) : super(nativeCurrency: CryptoCurrency.eth); @override - void addInitialTokens() { + void addInitialTokens([bool isMigration = false]) { final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens; - for (var token in initialErc20Tokens) { - evmChainErc20TokensBox.put(token.contractAddress, token); + for (final token in initialErc20Tokens) { + if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { + if (isMigration) token.enabled = false; + evmChainErc20TokensBox.put(token.contractAddress, token); + } } } diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 858416055..06ba07bfc 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -53,6 +53,7 @@ class EthereumWalletService extends EVMChainWalletService { ); await wallet.init(); + wallet.addInitialTokens(true); await wallet.save(); saveBackup(name); return wallet; diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index 462e1d77e..1da1e3b3b 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -6,7 +6,7 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: '>=2.18.2 <3.0.0' + sdk: ^3.5.0 flutter: ">=1.17.0" dependencies: @@ -29,7 +29,7 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 flutter: # assets: diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 7e6caf374..1fecc9c1e 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; @@ -12,13 +13,12 @@ import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets; import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart' as hex; -import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; import 'contract/erc20.dart'; abstract class EVMChainClient { - final httpClient = Client(); + late final client = ProxyWrapper().getHttpIOClient(); Web3Client? _client; //! To be overridden by all child classes @@ -47,7 +47,7 @@ abstract class EVMChainClient { } _client = - Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), httpClient); + Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), client); return true; } catch (e) { @@ -76,7 +76,7 @@ abstract class EVMChainClient { Future getGasUnitPrice() async { try { final gasPrice = await _client!.getGasPrice(); - + return gasPrice.getInWei.toInt(); } catch (_) { return 0; @@ -101,6 +101,7 @@ abstract class EVMChainClient { String? contractAddress, EtherAmount? gasPrice, EtherAmount? maxFeePerGas, + Uint8List? data, }) async { try { if (contractAddress == null) { @@ -108,7 +109,6 @@ abstract class EVMChainClient { sender: senderAddress, to: toAddress, value: value, - // maxFeePerGas: maxFeePerGas, ); return estimatedGas.toInt(); @@ -124,7 +124,7 @@ abstract class EVMChainClient { final gasEstimate = await _client!.estimateGas( sender: senderAddress, to: EthereumAddress.fromHex(contractAddress), - data: transfer.encodeCall([ + data: data ?? transfer.encodeCall([ toAddress, value.getInWei, ]), @@ -137,6 +137,21 @@ abstract class EVMChainClient { } } + Uint8List getEncodedDataForApprovalTransaction({ + required EthereumAddress toAddress, + required EtherAmount value, + required EthereumAddress contractAddress, + }) { + final contract = DeployedContract(ethereumContractAbi, contractAddress); + + final approve = contract.function('approve'); + + return approve.encodeCall([ + toAddress, + value.getInWei, + ]); + } + Future signTransaction({ required Credentials privateKey, required String toAddress, @@ -149,6 +164,7 @@ abstract class EVMChainClient { required int exponent, String? contractAddress, String? data, + int? gasPrice, }) async { assert(currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || @@ -164,6 +180,7 @@ abstract class EVMChainClient { data: data != null ? hexToBytes(data) : null, maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, ); Uint8List signedTransaction; @@ -198,6 +215,52 @@ abstract class EVMChainClient { ); } + Future signApprovalTransaction({ + required Credentials privateKey, + required String spender, + required BigInt amount, + required BigInt gasFee, + required int estimatedGasUnits, + required int maxFeePerGas, + required EVMChainTransactionPriority priority, + required int exponent, + required String contractAddress, + int? gasPrice, + }) async { + + final Transaction transaction = createTransaction( + from: privateKey.address, + to: EthereumAddress.fromHex(contractAddress), + maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + amount: EtherAmount.zero(), + maxGas: estimatedGasUnits, + maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, + ); + + final erc20 = ERC20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress), + chainId: chainId, + ); + + final signedTransaction = await erc20.approve( + EthereumAddress.fromHex(spender), + amount, + credentials: privateKey, + transaction: transaction, + ); + + return PendingEVMChainTransaction( + signedTransaction: prepareSignedTransactionForSending(signedTransaction), + amount: amount.toString(), + fee: gasFee, + sendTransaction: () => sendTransaction(signedTransaction), + exponent: exponent, + isInfiniteApproval: amount.toRadixString(16) == 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + ); + } + Transaction createTransaction({ required EthereumAddress from, required EthereumAddress to, @@ -293,7 +356,7 @@ abstract class EVMChainClient { }, ); - final response = await httpClient.get( + final response = await client.get( uri, headers: { "Accept": "application/json", diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index c7509a17f..19bb047ef 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -22,3 +22,32 @@ class EVMChainTransactionFeesException implements Exception { @override String toString() => exceptionMessage; } + +class DeuroGasFeeException implements Exception { + final String exceptionMessage; + final BigInt? requiredGasFee; + final BigInt? currentBalance; + + DeuroGasFeeException({ + this.requiredGasFee, + this.currentBalance, + }) : exceptionMessage = _buildMessage(requiredGasFee, currentBalance); + + static String _buildMessage(BigInt? requiredGasFee, BigInt? currentBalance) { + const baseMessage = 'Insufficient ETH for gas fees.'; + const addEthMessage = ' Please add ETH to your wallet to cover transaction fees.'; + + if (requiredGasFee != null) { + final requiredEth = (requiredGasFee / BigInt.from(10).pow(18)).toStringAsFixed(8); + final balanceInfo = currentBalance != null + ? ', Available: ${(currentBalance / BigInt.from(10).pow(18)).toStringAsFixed(8)} ETH' + : ''; + return '$baseMessage Required: ~$requiredEth ETH$balanceInfo.$addEthMessage'; + } + + return '$baseMessage$addEthMessage'; + } + + @override + String toString() => exceptionMessage; +} diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index d640f8c14..a1285f0e4 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; @@ -136,7 +137,7 @@ abstract class EVMChainWalletBase //! Methods to be overridden by every child - void addInitialTokens(); + void addInitialTokens([bool isMigration]); // Future open({ // required String name, @@ -197,8 +198,8 @@ abstract class EVMChainWalletBase for (var token in erc20Currencies) { bool isPotentialScam = false; - bool isWhitelisted = - getDefaultTokenContractAddresses.any((element) => element == token.contractAddress); + bool isWhitelisted = getDefaultTokenContractAddresses + .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase()); final tokenSymbol = token.title.toUpperCase(); @@ -213,6 +214,16 @@ abstract class EVMChainWalletBase token.iconPath = null; await token.save(); } + + // For fixing wrongly classified tokens + if (!isPotentialScam && token.isPotentialScam) { + token.isPotentialScam = false; + final iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + token.iconPath = iconPath; + await token.save(); + } } } @@ -255,36 +266,72 @@ abstract class EVMChainWalletBase required String? contractAddress, required String receivingAddressHex, required TransactionPriority priority, + Uint8List? data, }) async { try { if (priority is EVMChainTransactionPriority) { final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); int maxFeePerGas; + int adjustedGasPrice; + + bool isPolygon = _client.chainId == 137; + if (gasBaseFee != null) { // MaxFeePerGas with EIP1559; maxFeePerGas = gasBaseFee! + priorityFee; } else { // MaxFeePerGas with gasPrice - maxFeePerGas = gasPrice; + maxFeePerGas = gasPrice + priorityFee; + } + + adjustedGasPrice = maxFeePerGas; + + // Polygon has a minimum priority fee of 25 gwei + if (isPolygon) { + int minPriorityFee = 25; + int minPriorityFeeWei = + EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); + + // Calculate user selected priority-based additional fee on top of minimum + int additionalPriorityFee = 0; + switch (priority) { + case EVMChainTransactionPriority.slow: + // We use minimum priority fee only + additionalPriorityFee = 0; + break; + case EVMChainTransactionPriority.medium: + // We add 15 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); + break; + case EVMChainTransactionPriority.fast: + // We add 35 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); + break; + } + + int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; + adjustedGasPrice = gasPrice + totalPriorityFee; + maxFeePerGas = gasPrice + totalPriorityFee; } final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( contractAddress: contractAddress, senderAddress: _evmChainPrivateKey.address, value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), - gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), toAddress: EthereumAddress.fromHex(receivingAddressHex), maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + data: data, ); - final totalGasFee = estimatedGas * maxFeePerGas; + final totalGasFee = estimatedGas * adjustedGasPrice; return GasParamsHandler( estimatedGasUnits: estimatedGas, estimatedGasFee: totalGasFee, maxFeePerGas: maxFeePerGas, - gasPrice: gasPrice, + gasPrice: adjustedGasPrice, ); } return GasParamsHandler.zero(); @@ -473,11 +520,46 @@ abstract class EVMChainWalletBase contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, data: hexOpReturnMemo, + gasPrice: maxFeePerGasForTransaction, ); return pendingEVMChainTransaction; } + Future createApprovalTransaction(BigInt amount, String spender, + CryptoCurrency token, EVMChainTransactionPriority priority) async { + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == token.title); + assert(transactionCurrency is Erc20Token); + + final data = _client.getEncodedDataForApprovalTransaction( + contractAddress: EthereumAddress.fromHex((transactionCurrency as Erc20Token).contractAddress), + value: EtherAmount.fromBigInt(EtherUnit.wei, amount), + toAddress: EthereumAddress.fromHex(spender), + ); + + final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + receivingAddressHex: spender, + priority: priority, + contractAddress: transactionCurrency.contractAddress, + data: data, + ); + + return _client.signApprovalTransaction( + privateKey: _evmChainPrivateKey, + spender: spender, + amount: amount, + priority: priority, + gasFee: BigInt.from(gasFeesModel.estimatedGasFee), + maxFeePerGas: gasFeesModel.maxFeePerGas, + estimatedGasUnits: gasFeesModel.estimatedGasUnits, + exponent: transactionCurrency.decimal, + contractAddress: transactionCurrency.contractAddress, + gasPrice: gasFeesModel.gasPrice, + ); + } + Future _updateTransactions() async { try { if (_isTransactionUpdating) { diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 6861b41f8..61b406470 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -11,6 +11,7 @@ class PendingEVMChainTransaction with PendingTransaction { final BigInt fee; final String amount; final int exponent; + final bool isInfiniteApproval; PendingEVMChainTransaction({ required this.sendTransaction, @@ -18,10 +19,12 @@ class PendingEVMChainTransaction with PendingTransaction { required this.fee, required this.amount, required this.exponent, + this.isInfiniteApproval = false, }); @override String get amountFormatted { + if (isInfiniteApproval) return "∞"; final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); return _amount.substring(0, min(10, _amount.length)); } diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 326ff4dc9..80788ca06 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-ethereum + ref: f4761cd5171d4c1e2e42fd3298261650539fb2db dependency_overrides: web3dart: @@ -41,9 +42,9 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 flutter_lints: ^2.0.0 flutter: diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 7f6a3f1aa..b753b4656 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -25,8 +25,8 @@ bool isUpdating = false; void refreshAccounts() { try { isUpdating = true; - subaddressAccount = currentWallet!.subaddressAccount(); - subaddressAccount!.refresh(); + subaddressAccount = currentWallet?.subaddressAccount(); + subaddressAccount?.refresh(); isUpdating = false; } catch (e) { isUpdating = false; diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index dc5fbddd0..22f974f4b 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -5,13 +5,11 @@ class PendingTransactionDescription { required this.fee, required this.hash, required this.hex, - required this.txKey, required this.pointerAddress}); final int amount; final int fee; final String hash; final String hex; - final String txKey; final int pointerAddress; } \ No newline at end of file diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 4360149d2..fd6004140 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; @@ -194,14 +195,12 @@ Future createTransactionSync( final rFee = pendingTx.fee(); final rHash = pendingTx.txid(''); final rHex = pendingTx.hex(''); - final rTxKey = rHash; return PendingTransactionDescription( amount: rAmt, fee: rFee, hash: rHash, hex: rHex, - txKey: rTxKey, pointerAddress: pendingTx.ffiAddress(), ); } @@ -246,23 +245,23 @@ Future createTransactionMultDest( fee: tx.fee(), hash: tx.txid(''), hex: tx.hex(''), - txKey: tx.txid(''), pointerAddress: tx.ffiAddress(), ); } -String? commitTransactionFromPointerAddress({required int address, required bool useUR}) => +Future commitTransactionFromPointerAddress({required int address, required bool useUR}) => commitTransaction(tx: MoneroPendingTransaction(Pointer.fromAddress(address)), useUR: useUR); -String? commitTransaction({required Wallet2PendingTransaction tx, required bool useUR}) { +Future commitTransaction({required Wallet2PendingTransaction tx, required bool useUR}) async { final txCommit = useUR ? tx.commitUR(120) - : Isolate.run(() { + : await Isolate.run(() { monero.PendingTransaction_commit( Pointer.fromAddress(tx.ffiAddress()), filename: '', overwrite: false, ); + return null; }); String? error = (() { @@ -285,11 +284,12 @@ String? commitTransaction({required Wallet2PendingTransaction tx, required bool if (error != null && error != "no tx keys found for this txid") { throw CreationTransactionException(message: error); } - if (useUR) { - return txCommit as String?; - } else { - return null; - } + unawaited(() async { + storeSync(force: true); + await Future.delayed(Duration(seconds: 5)); + storeSync(force: true); + }()); + return Future.value(txCommit); } class Transaction { diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 8c5ab2d41..148b271ff 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -116,12 +116,17 @@ String getSeedLegacy(String? language) { } String getPassphrase() { - return currentWallet!.getCacheAttribute(key: "cakewallet.passphrase"); + return currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? ""; } Map>> addressCache = {}; String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // this is a workaround for when we switch the wallet pointer, + // it should never reach UI but should be good enough to prevent gray screen + // or other errors because of forced null check. + if (currentWallet == null) return ""; + // printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); // this could be a while loop, but I'm in favor of making it if to not cause freezes if (currentWallet!.numSubaddresses(accountIndex: accountIndex)-1 < addressIndex) { @@ -146,7 +151,25 @@ int getUnlockedBalance({int accountIndex = 0}) => int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0; -int getNodeHeightSync() => currentWallet?.daemonBlockChainHeight() ?? 0; + +int cachedNodeHeight = 0; +bool isHeightRefreshing = false; +int getNodeHeightSync() { + if (isHeightRefreshing == false) { + (() async { + try { + isHeightRefreshing = true; + final wptrAddress = currentWallet!.ffiAddress(); + cachedNodeHeight = await Isolate.run(() async { + return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress)); + }); + } finally { + isHeightRefreshing = false; + } + })(); + } + return cachedNodeHeight; +} bool isConnectedSync() => currentWallet?.connected() != 0; @@ -202,7 +225,6 @@ Future setupNodeSync( } void startRefreshSync() { - currentWallet!.refreshAsync(); currentWallet!.startRefresh(); } @@ -228,7 +250,7 @@ void storeSync({bool force = false}) async { return monero.Wallet_synchronized(Pointer.fromAddress(addr)); }); if (lastStorePointer == addr && - lastStoreHeight + 5000 > currentWallet!.blockChainHeight() && + lastStoreHeight + 75000 > currentWallet!.blockChainHeight() && !synchronized && !force) { return; @@ -255,13 +277,13 @@ void closeCurrentWallet() { currentWallet!.stop(); } -String getSecretViewKey() => currentWallet!.secretViewKey(); +String getSecretViewKey() => currentWallet?.secretViewKey() ?? ""; -String getPublicViewKey() => currentWallet!.publicViewKey(); +String getPublicViewKey() => currentWallet?.publicViewKey() ?? ""; -String getSecretSpendKey() => currentWallet!.secretSpendKey(); +String getSecretSpendKey() => currentWallet?.secretSpendKey() ?? ""; -String getPublicSpendKey() => currentWallet!.publicSpendKey(); +String getPublicSpendKey() => currentWallet?.publicSpendKey() ?? ""; class SyncListener { SyncListener(this.onNewBlock, this.onNewTransaction) diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 9b369c155..97f1de275 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -388,9 +388,7 @@ Future loadWallet( if (gLedger == null) { throw Exception("Tried to open a ledger wallet with no ledger connected"); } - final dummyWPtr = (currentWallet ?? - wmPtr.openWallet(path: '', password: '')); - enableLedgerExchange(dummyWPtr, gLedger!); + enableLedgerExchange(gLedger!); } final addr = wmPtr.ffiAddress(); diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index d931b58d0..6e32d4d68 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -2,75 +2,50 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; -import 'package:collection/collection.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart'; -import 'package:monero/src/wallet2.dart'; +import 'package:monero/src/monero.dart' as api; LedgerConnection? gLedger; +String? latestLedgerCommand; -Timer? _ledgerExchangeTimer; -Timer? _ledgerKeepAlive; +typedef LedgerCallback = Void Function(Pointer, UnsignedInt); +NativeCallable? callable; -void enableLedgerExchange(Wallet2Wallet wallet, LedgerConnection connection) { - _ledgerExchangeTimer?.cancel(); - _ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async { - final ledgerRequestLength = wallet.getSendToDeviceLength(); - final ledgerRequest = wallet.getSendToDevice() - .cast() - .asTypedList(ledgerRequestLength); - if (ledgerRequestLength > 0) { - _ledgerKeepAlive?.cancel(); +void enableLedgerExchange(LedgerConnection connection) { + callable?.close(); - final Pointer emptyPointer = malloc(0); - wallet.setDeviceSendData( - emptyPointer.cast(), 0); - malloc.free(emptyPointer); + void callback(Pointer request, int requestLength) async { + final ledgerRequest = request.cast().asTypedList(requestLength); - _logLedgerCommand(ledgerRequest, false); - final response = await exchange(connection, ledgerRequest); - _logLedgerCommand(response, true); + _logLedgerCommand(ledgerRequest, false); + final response = await exchange(connection, ledgerRequest); + _logLedgerCommand(response, true); - if (ListEquality().equals(response, [0x55, 0x15])) { - await connection.disconnect(); - // // TODO: Show POPUP pls unlock your device - // await Future.delayed(Duration(seconds: 15)); - // response = await exchange(connection, ledgerRequest); - } - - final Pointer result = malloc(response.length); - for (var i = 0; i < response.length; i++) { - result.asTypedList(response.length)[i] = response[i]; - } - - wallet.setDeviceReceivedData( - result.cast(), response.length); - malloc.free(result); - keepAlive(connection); + final Pointer result = malloc(response.length); + for (var i = 0; i < response.length; i++) { + result.asTypedList(response.length)[i] = response[i]; } - }); -} -void keepAlive(LedgerConnection connection) { - if (connection.connectionType == ConnectionType.ble) { - _ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async { - UniversalBle.setNotifiable( - connection.device.id, - connection.device.deviceInfo.serviceId, - connection.device.deviceInfo.notifyCharacteristicKey, - BleInputProperty.notification, - ).onError((_, __) async {}); - }); + latestLedgerCommand = _ledgerMoneroCommands[ledgerRequest[1]]; + + api.MoneroWallet.setDeviceReceivedData( + result.cast(), response.length); + api.MoneroFree().free(result.cast()); } + + callable = NativeCallable.listener(callback); + api.MoneroWallet.setLedgerCallback(callable!.nativeFunction); } void disableLedgerExchange() { - _ledgerExchangeTimer?.cancel(); - _ledgerKeepAlive?.cancel(); + callable?.close(); gLedger?.disconnect(); gLedger = null; + latestLedgerCommand = null; } Future exchange(LedgerConnection connection, Uint8List data) async => @@ -135,8 +110,6 @@ void _logLedgerCommand(Uint8List command, [bool isResponse = true]) { String toHexString(Uint8List data) => data.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); - - if (isResponse) { printV("< ${toHexString(command)}"); } else { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 7a4d943fe..9a8cb70b4 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -17,6 +17,7 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; @@ -34,12 +35,12 @@ import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; +import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; -import 'package:monero/src/monero.dart' as m; import 'package:monero/monero.dart' as monero; part 'monero_wallet.g.dart'; @@ -193,19 +194,7 @@ abstract class MoneroWalletBase extends WalletBase connectToNode({required Node node}) async { + String socksProxy = node.socksProxyAddress ?? ''; + printV("bootstrapped: ${CakeTor.instance.bootstrapped}"); + printV(" enabled: ${CakeTor.instance.enabled}"); + printV(" port: ${CakeTor.instance.port}"); + printV(" started: ${CakeTor.instance.started}"); + if (CakeTor.instance.enabled) { + socksProxy = "127.0.0.1:${CakeTor.instance.port}"; + } try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNodeSync( @@ -228,7 +225,7 @@ abstract class MoneroWalletBase extends WalletBase closeWalletAwaitIfShould(int wmaddr, int waddr) async { + if (Platform.isWindows) { + await Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); + }); + } else { + unawaited(Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); + })); + } +} \ No newline at end of file diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index 1c01a60dc..9909a3021 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' @@ -7,6 +9,7 @@ import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/monero_wallet.dart'; class DoubleSpendException implements Exception { DoubleSpendException(); @@ -17,9 +20,10 @@ class DoubleSpendException implements Exception { } class PendingMoneroTransaction with PendingTransaction { - PendingMoneroTransaction(this.pendingTransactionDescription); + PendingMoneroTransaction(this.pendingTransactionDescription, this.wallet); final PendingTransactionDescription pendingTransactionDescription; + final MoneroWalletBase wallet; @override String get id => pendingTransactionDescription.hash; @@ -27,8 +31,6 @@ class PendingMoneroTransaction with PendingTransaction { @override String get hex => pendingTransactionDescription.hex; - String get txKey => pendingTransactionDescription.txKey; - @override String get amountFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.amount); @@ -42,7 +44,7 @@ class PendingMoneroTransaction with PendingTransaction { @override Future commit() async { try { - monero_transaction_history.commitTransactionFromPointerAddress( + await monero_transaction_history.commitTransactionFromPointerAddress( address: pendingTransactionDescription.pointerAddress, useUR: false); } catch (e) { @@ -55,14 +57,23 @@ class PendingMoneroTransaction with PendingTransaction { rethrow; } storeSync(force: true); + unawaited(() async { + await Future.delayed(const Duration(milliseconds: 250)); + await wallet.fetchTransactions(); + }()); } @override Future commitUR() async { try { - final ret = monero_transaction_history.commitTransactionFromPointerAddress( + final ret = await monero_transaction_history.commitTransactionFromPointerAddress( address: pendingTransactionDescription.pointerAddress, useUR: true); + storeSync(force: true); + unawaited(() async { + await Future.delayed(const Duration(milliseconds: 250)); + await wallet.fetchTransactions(); + }()); return ret; } catch (e) { final message = e.toString(); diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 0140c39aa..2de22ed11 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -66,11 +66,11 @@ packages: dependency: transitive description: path: "." - ref: cake-update-v2 - resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + ref: cake-update-v4 + resolved-ref: "437dadd0bd9bf73ec6a551299577799341f6486a" url: "https://github.com/cake-tech/blockchain_utils" source: git - version: "3.3.0" + version: "4.3.0" bluez: dependency: transitive description: @@ -131,18 +131,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -573,8 +573,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 - resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -599,10 +599,10 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + resolved-ref: "084fb7bf13ec42d74f26ac08c883ce07c10fca7e" url: "https://github.com/cake-tech/on_chain.git" source: git - version: "3.7.0" + version: "6.2.0" package_config: dependency: transitive description: @@ -779,11 +779,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -860,10 +870,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -978,4 +997,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.4" + flutter: ">=3.24.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 90d0b85ab..a11e85b6c 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -2,11 +2,10 @@ name: cw_monero description: A new flutter plugin project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.19.0 <3.0.0" + sdk: ^3.5.0 flutter: ">=1.20.0" dependencies: @@ -27,7 +26,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 @@ -35,8 +34,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 mockito: ^5.4.5 hive_generator: ^2.0.1 diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index b63c634ee..c852a8c0d 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -3,11 +3,11 @@ import 'dart:convert'; import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_nano/nano_block_info_response.dart'; import 'package:cw_core/n2_node.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; -import 'package:http/http.dart' as http; import 'package:cw_core/node.dart'; import 'package:nanoutil/nanoutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -66,8 +66,8 @@ class NanoClient { } Future getBalance(String address) async { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -76,7 +76,8 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body) as Map; if (response.statusCode != 200 || data["error"] != null || data["balance"] == null || @@ -93,8 +94,8 @@ class NanoClient { Future getAccountInfo(String address) async { try { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -104,8 +105,9 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); - return AccountInfoResponse.fromJson(data as Map); + + final data = jsonDecode(response.body) as Map; + return AccountInfoResponse.fromJson(data); } catch (e) { printV("error while getting account info $e"); return null; @@ -114,8 +116,8 @@ class NanoClient { Future getBlockContents(String block) async { try { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -125,7 +127,8 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body) as Map; return BlockContentsResponse.fromJson(data["contents"] as Map); } catch (e) { printV("error while getting block info $e"); @@ -181,8 +184,8 @@ class NanoClient { } Future requestWork(String hash) async { - final response = await http.post( - _powNode!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _powNode!.uri, headers: getHeaders(_powNode!.uri.host), body: json.encode( { @@ -191,8 +194,9 @@ class NanoClient { }, ), ); + if (response.statusCode == 200) { - final Map decoded = json.decode(response.body) as Map; + final decoded = jsonDecode(response.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -224,13 +228,13 @@ class NanoClient { "block": block, }); - final processResponse = await http.post( - _node!.uri, + final processResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: processBody, ); - final Map decoded = json.decode(processResponse.body) as Map; + final Map decoded = jsonDecode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -423,12 +427,11 @@ class NanoClient { "subtype": "receive", "block": receiveBlock, }); - final processResponse = await http.post( - _node!.uri, + final processResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: processBody, ); - final Map decoded = json.decode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); @@ -440,16 +443,17 @@ class NanoClient { required String destinationAddress, required String privateKey, }) async { - final receivableResponse = await http.post(_node!.uri, - headers: getHeaders(_node!.uri.host), - body: jsonEncode({ - "action": "receivable", - "account": destinationAddress, - "count": "-1", - "source": true, - })); - - final receivableData = await jsonDecode(receivableResponse.body); + final receivableResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, + headers: getHeaders(_node!.uri.host), + body: jsonEncode({ + "action": "receivable", + "account": destinationAddress, + "count": "-1", + "source": true, + }), + ); + final receivableData = jsonDecode(receivableResponse.body) as Map; if (receivableData["blocks"] == "" || receivableData["blocks"] == null) { return 0; } @@ -492,15 +496,18 @@ class NanoClient { Future> fetchTransactions(String address) async { try { - final response = await http.post(_node!.uri, - headers: getHeaders(_node!.uri.host), - body: jsonEncode({ - "action": "account_history", - "account": address, - "count": "100", - // "raw": true, - })); - final data = await jsonDecode(response.body); + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, + headers: getHeaders(_node!.uri.host), + body: jsonEncode({ + "action": "account_history", + "account": address, + "count": "100", + // "raw": true, + }), + ); + + final data = jsonDecode(response.body) as Map; final transactions = data["history"] is List ? data["history"] as List : []; // Map the transactions list to NanoTransactionModel using the factory @@ -516,13 +523,14 @@ class NanoClient { Future> getN2Reps() async { final uri = Uri.parse(N2_REPS_ENDPOINT); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: getHeaders(uri.host), body: jsonEncode({"action": "reps"}), ); try { - final List nodes = (json.decode(response.body) as List) + + final List nodes = (jsonDecode(response.body) as List) .map((dynamic e) => N2Node.fromJson(e as Map)) .toList(); return nodes; @@ -533,8 +541,8 @@ class NanoClient { Future getRepScore(String rep) async { final uri = Uri.parse(N2_REPS_ENDPOINT); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: getHeaders(uri.host), body: jsonEncode({ "action": "rep_info", @@ -542,7 +550,8 @@ class NanoClient { }), ); try { - final N2Node node = N2Node.fromJson(json.decode(response.body) as Map); + + final N2Node node = N2Node.fromJson(jsonDecode(response.body) as Map); return node.score ?? 100; } catch (error) { return 100; diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 1eb90852d..aa85d6dde 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -86,50 +91,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: - dependency: "direct overridden" + dependency: transitive description: name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.7+1" + version: "8.0.0" built_collection: dependency: transitive description: @@ -142,10 +147,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: transitive description: @@ -238,10 +243,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -373,18 +378,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -397,10 +402,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: transitive description: @@ -473,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -517,10 +530,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" nanodart: dependency: transitive description: @@ -551,7 +564,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + resolved-ref: "01cbbacbb05d2113aafa8b7c4a2bb766f749d8d8" url: "https://github.com/cake-tech/on_chain.git" source: git version: "3.7.0" @@ -559,10 +572,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -583,10 +596,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -663,26 +676,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" rational: dependency: transitive description: @@ -695,18 +708,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -751,18 +764,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -771,27 +784,37 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -852,10 +875,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -916,18 +948,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -945,5 +977,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index 3ddd9769e..a85a4aee5 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -31,13 +31,12 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 - build_runner_core: 7.2.7+1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_polygon/lib/default_polygon_erc20_tokens.dart b/cw_polygon/lib/default_polygon_erc20_tokens.dart index deff285c0..208a63345 100644 --- a/cw_polygon/lib/default_polygon_erc20_tokens.dart +++ b/cw_polygon/lib/default_polygon_erc20_tokens.dart @@ -31,6 +31,13 @@ class DefaultPolygonErc20Tokens { decimal: 6, enabled: true, ), + Erc20Token( + name: "Decentralized Euro", + symbol: "DEURO", + contractAddress: "0xC2ff25dD99e467d2589b2c26EDd270F220F14E47", + decimal: 18, + enabled: true, + ), Erc20Token( name: "Avalanche Token", symbol: "AVAX", @@ -77,6 +84,6 @@ class DefaultPolygonErc20Tokens { .iconPath; } catch (_) {} - return Erc20Token.copyWith(token, iconPath, 'POLY'); + return Erc20Token.copyWith(token, iconPath, 'POL'); }).toList(); } diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index cb8331977..62dcec50e 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -18,13 +18,20 @@ class PolygonClient extends EVMChainClient { EtherAmount? gasPrice, EtherAmount? maxFeePerGas, }) { + EtherAmount? finalGasPrice = gasPrice; + + if (gasPrice == null && maxFeePerGas != null) { + // If we have EIP-1559 parameters but no legacy gasPrice, then use maxFeePerGas as gasPrice + finalGasPrice = maxFeePerGas; + } + return Transaction( from: from, to: to, value: amount, - // data: data, + data: data, maxGas: maxGas, - // gasPrice: gasPrice, + gasPrice: finalGasPrice, // maxFeePerGas: maxFeePerGas, // maxPriorityFeePerGas: maxPriorityFeePerGas, ); @@ -40,12 +47,13 @@ class PolygonClient extends EVMChainClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", "module": "account", "action": contractAddress != null ? "tokentx" : "txlist", if (contractAddress != null) "contractaddress": contractAddress, "address": address, - "apikey": secrets.polygonScanApiKey, + "apikey": secrets.etherScanApiKey, })); final jsonResponse = json.decode(response.body) as Map; @@ -67,11 +75,12 @@ class PolygonClient extends EVMChainClient { @override Future> fetchInternalTransactions(String address) async { try { - final response = await httpClient.get(Uri.https("api.polygonscan.io", "/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", "module": "account", "action": "txlistinternal", "address": address, - "apikey": secrets.polygonScanApiKey, + "apikey": secrets.etherScanApiKey, })); final jsonResponse = json.decode(response.body) as Map; diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index b2bf064b1..991552068 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -41,11 +41,19 @@ class PolygonWallet extends EVMChainWallet { } @override - void addInitialTokens() { + void addInitialTokens([bool isMigration = false]) { final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; - for (var token in initialErc20Tokens) { - evmChainErc20TokensBox.put(token.contractAddress, token); + for (final token in initialErc20Tokens) { + if (evmChainErc20TokensBox.containsKey(token.contractAddress)) { + final existingToken = evmChainErc20TokensBox.get(token.contractAddress); + if (existingToken?.tag != token.tag) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } else { + if (isMigration) token.enabled = false; + evmChainErc20TokensBox.put(token.contractAddress, token); + } } } diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 994912e8d..05af311e7 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -55,6 +55,7 @@ class PolygonWalletService extends EVMChainWalletService { ); await wallet.init(); + wallet.addInitialTokens(true); await wallet.save(); saveBackup(name); return wallet; diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml index 8421562b4..bcbd80443 100644 --- a/cw_polygon/pubspec.yaml +++ b/cw_polygon/pubspec.yaml @@ -34,7 +34,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.4.7 + build_runner: ^2.4.15 # For information on the generic Dart part of this file, see the diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 05b0cec82..d57da5af0 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_solana/pending_solana_transaction.dart'; @@ -11,14 +13,17 @@ import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:cw_solana/solana_transaction_model.dart'; import 'package:cw_solana/spl_token.dart'; -import 'package:http/http.dart' as http; import 'package:on_chain/solana/solana.dart'; +import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart'; import 'package:on_chain/solana/src/models/pda/pda.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta.dart'; import '.secrets.g.dart' as secrets; class SolanaWalletClient { - final httpClient = http.Client(); + // Minimum amount in SOL to consider a transaction valid (to filter spam) + static const double minValidAmount = 0.00000003; + final httpClient = ProxyWrapper().getHttpClient(); SolanaRPC? _provider; bool connect(Node node) { @@ -82,7 +87,8 @@ class SolanaWalletClient { } } - Future getSplTokenBalance(String mintAddress, String walletAddress) async { + Future getSplTokenBalance( + String mintAddress, String walletAddress) async { // Fetch the token accounts (a token can have multiple accounts for various uses) final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); @@ -118,14 +124,16 @@ class SolanaWalletClient { ), ); - final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; + final fee = + (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; return fee; } catch (_) { return 0.0; } } - Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { + Future getEstimatedFee( + SolanaPublicKey publicKey, Commitment commitment) async { final message = await _getMessageForNativeTransaction( publicKey: publicKey, destinationAddress: publicKey.toAddress().address, @@ -155,170 +163,88 @@ class SolanaWalletClient { if (meta == null || transaction == null) return null; final int fee = meta.fee; + final feeInSol = fee / SolanaUtils.lamportsPerSol; final message = transaction.message; final instructions = message.compiledInstructions; - String sender = ""; - String receiver = ""; - String signature = (txResponse.transaction?.signatures.isEmpty ?? true) ? "" : Base58Encoder.encode(txResponse.transaction!.signatures.first); + for (final instruction in instructions) { final programId = message.accountKeys[instruction.programIdIndex]; - if (programId == SystemProgramConst.programId) { + if (programId == SystemProgramConst.programId || + programId == ComputeBudgetConst.programId) { // For native solana transactions + if (instruction.accounts.length < 2) continue; - if (txResponse.version == TransactionType.legacy) { - // For legacy transfers, the fee payer (index 0) is the sender. - sender = message.accountKeys[0].address; + // Get the fee payer index based on transaction type + // For legacy transfers, the first account is usually the fee payer + // For versioned, the first account in instruction is usually the fee payer + final feePayerIndex = + txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0]; - final senderPreBalance = meta.preBalances[0]; - final senderPostBalance = meta.postBalances[0]; - final feeForTx = fee / SolanaUtils.lamportsPerSol; + final transactionModel = await _parseNativeTransaction( + message: message, + meta: meta, + fee: fee, + feeInSol: feeInSol, + feePayerIndex: feePayerIndex, + walletAddress: walletAddress, + signature: signature, + blockTime: blockTime, + ); - // The loss on the sender's account would include both the transfer amount and the fee. - // So we would subtract the fee to calculate the actual amount that was transferred (in lamports). - final transferLamports = (senderPreBalance - senderPostBalance) - BigInt.from(fee); - - // Next, we attempt to find the receiver by comparing the balance changes. - // (The index 0 is for the sender so we skip it.) - bool foundReceiver = false; - for (int i = 1; i < meta.preBalances.length; i++) { - // The increase in balance on the receiver account should correspond to the transfer amount we calculated earlieer. - final pre = meta.preBalances[i]; - final post = meta.postBalances[i]; - if ((post - pre) == transferLamports) { - receiver = message.accountKeys[i].address; - foundReceiver = true; - break; - } - } - - if (!foundReceiver) { - // Optionally (and rarely), if no account shows the exact expected change, - // we set the receiver address to unknown. - receiver = "unknown"; - } - - final amount = transferLamports / BigInt.from(1e9); - - return SolanaTransactionModel( - isOutgoingTx: sender == walletAddress, - from: sender, - to: receiver, - id: signature, - amount: amount.abs(), - programId: SystemProgramConst.programId.address, - tokenSymbol: 'SOL', - blockTimeInInt: blockTime?.toInt() ?? 0, - fee: feeForTx, - ); - } else { - if (instruction.accounts.length < 2) continue; - final senderIndex = instruction.accounts[0]; - final receiverIndex = instruction.accounts[1]; - - sender = message.accountKeys[senderIndex].address; - receiver = message.accountKeys[receiverIndex].address; - - final feeForTx = fee / SolanaUtils.lamportsPerSol; - - final preBalances = meta.preBalances; - final postBalances = meta.postBalances; - - final amountInString = - (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9)) - .toDouble() - - feeForTx) - .toStringAsFixed(6); - - final amount = double.parse(amountInString); - - return SolanaTransactionModel( - isOutgoingTx: sender == walletAddress, - from: sender, - to: receiver, - id: signature, - amount: amount.abs(), - programId: SystemProgramConst.programId.address, - tokenSymbol: 'SOL', - blockTimeInInt: blockTime?.toInt() ?? 0, - fee: feeForTx, - ); + if (transactionModel != null) { + return transactionModel; } } else if (programId == SPLTokenProgramConst.tokenProgramId) { // For SPL Token transactions if (instruction.accounts.length < 2) continue; - final preBalances = meta.preTokenBalances; - final postBalances = meta.postTokenBalances; - - double amount = 0.0; - bool isOutgoing = false; - String? mintAddress; - - double userPreAmount = 0.0; - if (preBalances != null && preBalances.isNotEmpty) { - for (final preBal in preBalances) { - if (preBal.owner?.address == walletAddress) { - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; - - mintAddress = preBal.mint.address; - break; - } - } - } - - double userPostAmount = 0.0; - if (postBalances != null && postBalances.isNotEmpty) { - for (final postBal in postBalances) { - if (postBal.owner?.address == walletAddress) { - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; - - mintAddress ??= postBal.mint.address; - break; - } - } - } - - final diff = userPreAmount - userPostAmount; - final rawAmount = diff.abs(); - - final amountInString = rawAmount.toStringAsFixed(6); - amount = double.parse(amountInString); - - isOutgoing = diff > 0; - - if (mintAddress == null && instruction.accounts.length >= 4) { - final mintIndex = instruction.accounts[3]; - mintAddress = message.accountKeys[mintIndex].address; - } - - final sender = message.accountKeys[instruction.accounts[0]].address; - final receiver = message.accountKeys[instruction.accounts[1]].address; - - String? tokenSymbol = splTokenSymbol; - - if (tokenSymbol == null && mintAddress != null) { - final token = await getTokenInfo(mintAddress); - tokenSymbol = token?.symbol; - } - - return SolanaTransactionModel( - isOutgoingTx: isOutgoing, - from: sender, - to: receiver, - id: signature, - amount: amount, - programId: SPLTokenProgramConst.tokenProgramId.address, - blockTimeInInt: blockTime?.toInt() ?? 0, - tokenSymbol: tokenSymbol ?? '', - fee: fee / SolanaUtils.lamportsPerSol, + final transactionModel = await _parseSPLTokenTransaction( + message: message, + meta: meta, + fee: fee, + feeInSol: feeInSol, + instruction: instruction, + walletAddress: walletAddress, + signature: signature, + blockTime: blockTime, + splTokenSymbol: splTokenSymbol, ); + + if (transactionModel != null) { + return transactionModel; + } + } else if (programId == AssociatedTokenAccountProgramConst.associatedTokenProgramId) { + // For ATA program, we need to check if this is a create account transaction + // or if it's part of a normal token transfer + + // We skip this transaction if this is the only instruction (this means that it's a create account transaction) + if (instructions.length == 1) { + return null; + } + + // We look for a token transfer instruction in the same transaction + bool hasTokenTransfer = false; + for (final otherInstruction in instructions) { + final otherProgramId = message.accountKeys[otherInstruction.programIdIndex]; + if (otherProgramId == SPLTokenProgramConst.tokenProgramId) { + hasTokenTransfer = true; + break; + } + } + + // If there's no token transfer instruction, it means this is just an ATA creation transaction + if (!hasTokenTransfer) { + return null; + } + + continue; } else { return null; } @@ -330,6 +256,144 @@ class SolanaWalletClient { return null; } + Future _parseNativeTransaction({ + required VersionedMessage message, + required ConfirmedTransactionMeta meta, + required int fee, + required double feeInSol, + required int feePayerIndex, + required String walletAddress, + required String signature, + required BigInt? blockTime, + }) async { + // Calculate total balance changes across all accounts + BigInt totalBalanceChange = BigInt.zero; + String? sender; + String? receiver; + + for (int i = 0; i < meta.preBalances.length; i++) { + final preBalance = meta.preBalances[i]; + final postBalance = meta.postBalances[i]; + final balanceChange = preBalance - postBalance; + + if (balanceChange > BigInt.zero) { + // This account sent funds + sender = message.accountKeys[i].address; + totalBalanceChange += balanceChange; + } else if (balanceChange < BigInt.zero) { + // This account received funds + receiver = message.accountKeys[i].address; + } + } + + // We subtract the fee from total balance change if the fee payer is the sender + if (sender == message.accountKeys[feePayerIndex].address) { + totalBalanceChange -= BigInt.from(fee); + } + + if (sender == null || receiver == null) { + return null; + } + + final amount = totalBalanceChange / BigInt.from(1e9); + final amountInSol = amount.abs().toDouble(); + + // Skip transactions with very small amounts (likely spam) + if (amountInSol < minValidAmount) { + return null; + } + + return SolanaTransactionModel( + isOutgoingTx: sender == walletAddress, + from: sender, + to: receiver, + id: signature, + amount: amountInSol, + programId: SystemProgramConst.programId.address, + tokenSymbol: 'SOL', + blockTimeInInt: blockTime?.toInt() ?? 0, + fee: feeInSol, + ); + } + + Future _parseSPLTokenTransaction({ + required VersionedMessage message, + required ConfirmedTransactionMeta meta, + required int fee, + required double feeInSol, + required CompiledInstruction instruction, + required String walletAddress, + required String signature, + required BigInt? blockTime, + String? splTokenSymbol, + }) async { + final preBalances = meta.preTokenBalances; + final postBalances = meta.postTokenBalances; + + double amount = 0.0; + bool isOutgoing = false; + String? mintAddress; + + double userPreAmount = 0.0; + if (preBalances != null && preBalances.isNotEmpty) { + for (final preBal in preBalances) { + if (preBal.owner?.address == walletAddress) { + userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress = preBal.mint.address; + break; + } + } + } + + double userPostAmount = 0.0; + if (postBalances != null && postBalances.isNotEmpty) { + for (final postBal in postBalances) { + if (postBal.owner?.address == walletAddress) { + userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress ??= postBal.mint.address; + break; + } + } + } + + final diff = userPreAmount - userPostAmount; + final rawAmount = diff.abs(); + + final amountInString = rawAmount.toStringAsFixed(6); + amount = double.parse(amountInString); + + isOutgoing = diff > 0; + + if (mintAddress == null && instruction.accounts.length >= 4) { + final mintIndex = instruction.accounts[3]; + mintAddress = message.accountKeys[mintIndex].address; + } + + final sender = message.accountKeys[instruction.accounts[0]].address; + final receiver = message.accountKeys[instruction.accounts[1]].address; + + String? tokenSymbol = splTokenSymbol; + + if (tokenSymbol == null && mintAddress != null) { + final token = await getTokenInfo(mintAddress); + tokenSymbol = token?.symbol; + } + + return SolanaTransactionModel( + isOutgoingTx: isOutgoing, + from: sender, + to: receiver, + id: signature, + amount: amount, + programId: SPLTokenProgramConst.tokenProgramId.address, + blockTimeInInt: blockTime?.toInt() ?? 0, + tokenSymbol: tokenSymbol ?? '', + fee: feeInSol, + ); + } + /// Load the Address's transactions into the account Future> fetchTransactions( SolAddress address, { @@ -369,23 +433,28 @@ class SolanaWalletClient { } })); - final versionedBatchResponses = batchResponses.whereType(); + final versionedBatchResponses = + batchResponses.whereType(); - final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction( - txResponse: tx, - splTokenSymbol: splTokenSymbol, - walletAddress: walletAddress?.address ?? address.address, - )); + final parsedTransactionsFutures = + versionedBatchResponses.map((tx) => parseTransaction( + txResponse: tx, + splTokenSymbol: splTokenSymbol, + walletAddress: walletAddress?.address ?? address.address, + )); final parsedTransactions = await Future.wait(parsedTransactionsFutures); - transactions.addAll(parsedTransactions.whereType().toList()); + transactions.addAll( + parsedTransactions.whereType().toList()); - // Calling the callback after each batch is processed, therefore passing the current list of transactions. - onUpdate(List.from(transactions)); + // Only update UI if we have new valid transactions + if (parsedTransactions.isNotEmpty) { + onUpdate(List.from(transactions)); + } if (i + batchSize < signatures.length) { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 300)); } } @@ -446,8 +515,8 @@ class SolanaWalletClient { } Future fetchSPLTokenInfo(String mintAddress) async { - final programAddress = - MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress)); + final programAddress = MetaplexTokenMetaDataProgramUtils.findMetadataPda( + mint: SolAddress(mintAddress)); final token = await _provider!.request( SolanaRPCGetMetadataAccount( @@ -468,8 +537,9 @@ class SolanaWalletClient { // iconPath = await _client.getIconImageFromTokenUri(metadata.uri); // } catch (_) {} - String filteredTokenSymbol = - metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + String filteredTokenSymbol = metadata.symbol + .replaceFirst(RegExp('^\\\$'), '') + .replaceAll('\u0000', ''); return SPLToken.fromMetadata( name: metadata.name, @@ -585,7 +655,8 @@ class SolanaWalletClient { return message; } - Future _getFeeFromCompiledMessage(Message message, Commitment commitment) async { + Future _getFeeFromCompiledMessage( + Message message, Commitment commitment) async { final base64Message = base64Encode(message.serialize()); final fee = await getFeeForMessage(base64Message, commitment); @@ -724,7 +795,8 @@ class SolanaWalletClient { required SolAddress mintAddress, required bool shouldCreateATA, }) async { - final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + final associatedTokenAccount = + AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerAddress, ); @@ -732,19 +804,24 @@ class SolanaWalletClient { SolanaAccountInfo? accountInfo; try { accountInfo = await _provider!.request( - SolanaRPCGetAccountInfo(account: associatedTokenAccount.address), + SolanaRPCGetAccountInfo( + account: associatedTokenAccount.address, + commitment: Commitment.confirmed, + ), ); } catch (e) { accountInfo = null; } - // If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain. + // If account exists, we return the associated token account if (accountInfo != null) return associatedTokenAccount; if (!shouldCreateATA) return null; + final payerAddress = payerPrivateKey.publicKey().toAddress(); + final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: payerPrivateKey.publicKey().toAddress(), + payer: payerAddress, associatedToken: associatedTokenAccount.address, owner: ownerAddress, mint: mintAddress, @@ -753,19 +830,23 @@ class SolanaWalletClient { final blockhash = await _getLatestBlockhash(Commitment.confirmed); final transaction = SolanaTransaction( - payerKey: payerPrivateKey.publicKey().toAddress(), + payerKey: payerAddress, instructions: [createAssociatedTokenAccount], recentBlockhash: blockhash, + type: TransactionType.v0, ); - transaction.sign([payerPrivateKey]); + final serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: payerPrivateKey, + transaction: transaction, + ); await sendTransaction( - serializedTransaction: transaction.serializeString(), + serializedTransaction: serializedTransaction, commitment: Commitment.confirmed, ); - // Delay for propagation on the blockchain for newly created associated token addresses + // Wait for confirmation await Future.delayed(const Duration(seconds: 2)); return associatedTokenAccount; @@ -786,7 +867,8 @@ class SolanaWalletClient { final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); ProgramDerivedAddress? associatedSenderAccount; try { - associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + associatedSenderAccount = + AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerPrivateKey.publicKey().toAddress(), ); @@ -890,7 +972,7 @@ class SolanaWalletClient { }) async { /// Sign the transaction with the owner's private key. final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); - + transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); /// Serialize the transaction. @@ -921,7 +1003,8 @@ class SolanaWalletClient { if (uri.isEmpty || uri == '…') return null; try { - final response = await httpClient.get(Uri.parse(uri)); + final client = ProxyWrapper().getHttpIOClient(); + final response = await client.get(Uri.parse(uri)); final jsonResponse = json.decode(response.body) as Map; diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 82b5a2bc0..c91ca6efc 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -33,9 +33,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index ee93fbd53..cceec5327 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_tron/pending_tron_transaction.dart'; import 'package:cw_tron/tron_abi.dart'; import 'package:cw_tron/tron_balance.dart'; @@ -13,12 +14,12 @@ import 'package:cw_tron/tron_token.dart'; import 'package:cw_tron/tron_transaction_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:http/http.dart'; import '.secrets.g.dart' as secrets; import 'package:on_chain/on_chain.dart'; class TronClient { - final httpClient = Client(); + late final client = ProxyWrapper().getHttpIOClient(); + TronProvider? _provider; // This is an internal tracker, so we don't have to "refetch". int _nativeTxEstimatedFee = 0; @@ -28,7 +29,7 @@ class TronClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get( + final response = await client.get( Uri.https( "api.trongrid.io", "/v1/accounts/$address/transactions", @@ -61,7 +62,7 @@ class TronClient { Future> fetchTrc20ExcludedTransactions(String address) async { try { - final response = await httpClient.get( + final response = await client.get( Uri.https( "api.trongrid.io", "/v1/accounts/$address/transactions/trc20", diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart index 8a3301f87..420ff85b0 100644 --- a/cw_tron/lib/tron_http_provider.dart +++ b/cw_tron/lib/tron_http_provider.dart @@ -1,22 +1,22 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:on_chain/tron/tron.dart'; import '.secrets.g.dart' as secrets; class TronHTTPProvider implements TronServiceProvider { TronHTTPProvider( {required this.url, - http.Client? client, - this.defaultRequestTimeout = const Duration(seconds: 30)}) - : client = client ?? http.Client(); + this.defaultRequestTimeout = const Duration(seconds: 30)}); + @override final String url; - final http.Client client; + late final client = ProxyWrapper().getHttpIOClient(); final Duration defaultRequestTimeout; @override - Future> get(TronRequestDetails params, [Duration? timeout]) async { + Future> get(TronRequestDetails params, + [Duration? timeout]) async { final response = await client.get(Uri.parse(params.url(url)), headers: { 'Content-Type': 'application/json', if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, @@ -27,13 +27,16 @@ class TronHTTPProvider implements TronServiceProvider { } @override - Future> post(TronRequestDetails params, [Duration? timeout]) async { + Future> post(TronRequestDetails params, + [Duration? timeout]) async { final response = await client .post(Uri.parse(params.url(url)), headers: { 'Content-Type': 'application/json', - if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, - if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, + if (url.contains("trongrid")) + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) + 'api-key': secrets.tronNowNodesApiKey, }, body: params.toRequestBody()) .timeout(timeout ?? defaultRequestTimeout); diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 80ea7ee51..57c99286c 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -31,9 +31,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.3.3 + build_runner: ^2.4.15 mobx_codegen: ^2.1.1 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 20783490d..a4ba8cbb2 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -110,7 +110,16 @@ int getUnlockedBalance({int accountIndex = 0}) => int getCurrentHeight() => wownero.Wallet_blockChainHeight(wptr!); -int getNodeHeightSync() => wownero.Wallet_daemonBlockChainHeight(wptr!); +int cachedNodeHeight = 0; +int getNodeHeightSync() { + (() async { + final wptrAddress = wptr!.address; + cachedNodeHeight = await Isolate.run(() async { + return wownero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress)); + }); + })(); + return cachedNodeHeight; +} bool isConnectedSync() => wownero.Wallet_connected(wptr!) != 0; @@ -154,7 +163,7 @@ Future setupNodeSync( } void startRefreshSync() { - wownero.Wallet_refreshAsync(wptr!); + // wownero.Wallet_refreshAsync(wptr!); wownero.Wallet_startRefresh(wptr!); } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index e26672277..2befaa20d 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; @@ -183,6 +184,14 @@ abstract class WowneroWalletBase @override Future connectToNode({required Node node}) async { + String socksProxy = node.socksProxyAddress ?? ''; + printV("bootstrapped: ${CakeTor.instance.bootstrapped}"); + printV(" enabled: ${CakeTor.instance.enabled}"); + printV(" port: ${CakeTor.instance.port}"); + printV(" started: ${CakeTor.instance.started}"); + if (CakeTor.instance.enabled) { + socksProxy = "127.0.0.1:${CakeTor.instance.port}"; + } try { syncStatus = ConnectingSyncStatus(); await wownero_wallet.setupNode( @@ -192,7 +201,7 @@ abstract class WowneroWalletBase useSSL: node.isSSL, isLightWallet: false, // FIXME: hardcoded value - socksProxyAddress: node.socksProxyAddress); + socksProxyAddress: socksProxy); wownero_wallet.setTrustedDaemon(node.trusted); syncStatus = ConnectedSyncStatus(); diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index c2d5eeea7..0fa39123b 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -86,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -214,10 +219,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: transitive description: @@ -336,10 +341,10 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: @@ -428,6 +433,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -472,16 +485,16 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" monero: dependency: "direct main" description: path: "impls/monero.dart" - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 - resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -670,27 +683,37 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -751,10 +774,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -844,5 +876,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 7b9ec4c41..4fda054b5 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -2,7 +2,6 @@ name: cw_wownero description: A new flutter plugin project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: @@ -25,17 +24,17 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash path: impls/monero.dart mutex: ^3.1.0 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index ec99b272a..41edf154e 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -86,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -214,10 +219,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -333,10 +338,10 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: @@ -433,6 +438,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -477,16 +490,16 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" monero: dependency: "direct main" description: path: "impls/monero.dart" - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 - resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 + resolved-ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" @@ -667,27 +680,37 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -752,6 +775,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: @@ -841,5 +873,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index ff2c1e9a6..529fe04eb 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -2,7 +2,6 @@ name: cw_zano description: A new flutter plugin project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: @@ -26,15 +25,15 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash + ref: a27fbcb24d91143715ed930a05aaa4d853fba1f2 # monero_c hash path: impls/monero.dart dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.1.1 - build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 + build_resolvers: ^2.4.4 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md index 226883679..d7bc584ad 100644 --- a/docs/builds/ANDROID.md +++ b/docs/builds/ANDROID.md @@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts/android source ./app_env.sh cakewallet diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index 44abaa805..1d23b917c 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.4 +Flutter 3.27.0 ``` NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. @@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following: ### 3. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ``` diff --git a/docs/builds/LINUX.md b/docs/builds/LINUX.md index a97a269a5..0f438a1d6 100644 --- a/docs/builds/LINUX.md +++ b/docs/builds/LINUX.md @@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts ./gen_android_manifest.sh diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index 46a9842a4..7e0f39aab 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.4 +Flutter 3.27.0 ``` ### 1. Installing dependencies @@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch ### 3. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) ... [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ... diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md index 4fec78dc0..7a86dac49 100644 --- a/docs/builds/WINDOWS.md +++ b/docs/builds/WINDOWS.md @@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows ```txt Windows 10 or later (64-bit), x86-64 based -Flutter 3.27.4 +Flutter 3.27.0 ``` ### 1. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). In order for Flutter to function, you'll also need to enable Developer Mode: Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 2. Install Development Tools diff --git a/integration_test/robots/new_wallet_type_page_robot.dart b/integration_test/robots/new_wallet_type_page_robot.dart index 89fc8d390..eb1a1900a 100644 --- a/integration_test/robots/new_wallet_type_page_robot.dart +++ b/integration_test/robots/new_wallet_type_page_robot.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart index 189929737..38a605b9d 100644 --- a/integration_test/robots/wallet_keys_robot.dart +++ b/integration_test/robots/wallet_keys_robot.dart @@ -70,7 +70,10 @@ class WalletKeysAndSeedPageRobot { if (walletType == WalletType.bitcoin || walletType == WalletType.litecoin || walletType == WalletType.bitcoinCash) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } @@ -78,10 +81,14 @@ class WalletKeysAndSeedPageRobot { walletType == WalletType.solana || walletType == WalletType.tron) { if (hasSeed) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasPrivateKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.privateKey!); tester.printToConsole('$walletName wallet has private key properly displayed'); } @@ -89,14 +96,19 @@ class WalletKeysAndSeedPageRobot { if (walletType == WalletType.nano || walletType == WalletType.banano) { if (hasSeed) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasHexSeed) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.hexSeed!); tester.printToConsole('$walletName wallet has hexSeed properly displayed'); } if (hasPrivateKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.privateKey!); tester.printToConsole('$walletName wallet has private key properly displayed'); } @@ -129,35 +141,39 @@ class WalletKeysAndSeedPageRobot { final hasSeedLegacy = Polyseed.isValidSeed(seed); if (hasPublicSpendKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.publicSpendKey); tester.printToConsole('$walletName wallet has public spend key properly displayed'); } if (hasPrivateSpendKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.privateSpendKey); tester.printToConsole('$walletName wallet has private spend key properly displayed'); } if (hasPublicViewKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.publicViewKey); tester.printToConsole('$walletName wallet has public view key properly displayed'); } if (hasPrivateViewKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.privateViewKey); tester.printToConsole('$walletName wallet has private view key properly displayed'); } if (hasSeeds) { - await commonTestCases.dragUntilVisible( - '${walletName}_wallet_seed_item_key', - 'wallet_keys_page_credentials_list_view_key', - ); - commonTestCases.hasText(seed); + await commonTestCases.tapItemByKey('wallet_keys_page_seed'); + final seedWords = seed.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasSeedLegacy) { - await commonTestCases.dragUntilVisible( - '${walletName}_wallet_seed_legacy_item_key', - 'wallet_keys_page_credentials_list_view_key', - ); - commonTestCases.hasText(legacySeed); + await commonTestCases.tapItemByKey('wallet_keys_page_seed_legacy'); + final seedWords = legacySeed.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has legacy seeds properly displayed'); } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3d39d589e..abbd40673 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -76,6 +76,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - payjoin_flutter (0.23.0) - permission_handler_apple (9.3.0): - Flutter - reown_yttrium (0.0.1): @@ -126,6 +127,7 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - payjoin_flutter (from `.symlinks/plugins/payjoin_flutter/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - reown_yttrium (from `.symlinks/plugins/reown_yttrium/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) @@ -186,6 +188,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + payjoin_flutter: + :path: ".symlinks/plugins/payjoin_flutter/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" reown_yttrium: @@ -208,40 +212,43 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6 - cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85 - device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 - devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8 + cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a + cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 + device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f - flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 - in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f + in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 - sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5 + sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 - universal_ble: ff19787898040d721109c6324472e5dd4bc86adc - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index f27ef8d4f..40868aa9f 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -327,5 +327,7 @@ UIViewControllerBasedStatusBarAppearance + FlutterDeepLinkingEnabled + diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index acab662d1..6f401ae3f 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -6,8 +6,9 @@ import 'package:cake_wallet/anonpay/anonpay_status_response.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:http/http.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -20,17 +21,21 @@ class AnonPayApi { final WalletBase wallet; static const anonpayRef = secrets.anonPayReferralCode; - static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion'; static const clearNetAuthority = 'trocador.app'; + // static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion'; + static const onionApiAuthority = clearNetAuthority; static const markup = secrets.trocadorExchangeMarkup; static const anonPayPath = '/anonpay'; static const anonPayStatus = '/anonpay/status'; static const coinPath = 'api/coin'; - static const apiKey = secrets.trocadorApiKey; + static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey; Future paymentStatus(String id) async { - final authority = await _getAuthority(); - final response = await get(Uri.https(authority, "$anonPayStatus/$id")); + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, "$anonPayStatus/$id"), + onionUri: Uri.https(onionApiAuthority, "$anonPayStatus/$id"), + ); + final responseJSON = json.decode(response.body) as Map; final status = responseJSON['Status'] as String; final fiatAmount = responseJSON['Fiat_Amount'] as double?; @@ -69,10 +74,11 @@ class AnonPayApi { if (request.fiatEquivalent != null) { body['fiat_equiv'] = request.fiatEquivalent; } - final authority = await _getAuthority(); - - final response = await get(Uri.https(authority, anonPayPath, body)); - + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, anonPayPath, body), + onionUri: Uri.https(onionApiAuthority, anonPayPath, body), + ); + final responseJSON = json.decode(response.body) as Map; final id = responseJSON['ID'] as String; final url = responseJSON['url'] as String; @@ -146,17 +152,16 @@ class AnonPayApi { 'name': cryptoCurrency.name, }; - final String apiAuthority = await _getAuthority(); - final uri = Uri.https(apiAuthority, coinPath, params); - - final response = await get(uri); - + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, coinPath, params), + onionUri: Uri.https(onionApiAuthority, coinPath, params), + ); + + final responseJSON = json.decode(response.body) as List; if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } - final responseJSON = json.decode(response.body) as List; - if (responseJSON.isEmpty) { throw Exception('No data'); } @@ -197,17 +202,4 @@ class AnonPayApi { return tag.toLowerCase(); } } - - Future _getAuthority() async { - try { - if (useTorOnly) { - return onionApiAuthority; - } - final uri = Uri.https(onionApiAuthority, '/anonpay'); - await get(uri); - return onionApiAuthority; - } catch (e) { - return clearNetAuthority; - } - } } diff --git a/lib/anypay/anypay_api.dart b/lib/anypay/anypay_api.dart index 0b81d24c2..20187484b 100644 --- a/lib/anypay/anypay_api.dart +++ b/lib/anypay/anypay_api.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/anypay/any_pay_payment.dart'; import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; @@ -53,8 +53,12 @@ class AnyPayApi { final body = { 'chain': chainByScheme(scheme), 'currency': currencyByScheme(scheme).title}; - final response = await post(url, headers: headers, body: utf8.encode(json.encode(body))); - + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) { await ExceptionHandler.onError(FlutterErrorDetails(exception: response)); throw Exception('Unexpected response http code: ${response.statusCode}'); @@ -79,7 +83,12 @@ class AnyPayApi { 'chain': chain, 'currency': currency, 'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()}; - final response = await post(Uri.parse(uri), headers: headers, body: utf8.encode(json.encode(body))); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(uri), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final decodedBody = json.decode(response.body) as Map; throw Exception(decodedBody['message'] as String? ?? 'Unexpected response\nError code: 400'); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index cbdb343fe..131bc3a02 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -747,5 +747,6 @@ class CWBitcoin extends Bitcoin { final _wallet = wallet as ElectrumWallet; (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions(); (_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null; + (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint = null; } } diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 55ef5e6c3..f57cc5472 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -64,6 +64,7 @@ abstract class BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async => null; } diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index 1805b7e1a..da854faaf 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -50,6 +50,7 @@ class Quote extends SelectableOption { this.rampName, this.rampIconPath, this.limits, + this.customPaymentMethodType, }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title); final double rate; @@ -68,6 +69,7 @@ class Quote extends SelectableOption { bool _isBestRate = false; bool isBuyAction; Limits? limits; + String? customPaymentMethodType; late FiatCurrency _fiatCurrency; late CryptoCurrency _cryptoCurrency; @@ -130,7 +132,7 @@ class Quote extends SelectableOption { set setLimits(Limits limits) => this.limits = limits; factory Quote.fromOnramperJson(Map json, bool isBuyAction, - Map metaData, PaymentType paymentType) { + Map metaData, PaymentType paymentType, String? customPaymentMethodType) { final rate = _toDouble(json['rate']) ?? 0.0; final networkFee = _toDouble(json['networkFee']) ?? 0.0; final transactionFee = _toDouble(json['transactionFee']) ?? 0.0; @@ -183,6 +185,7 @@ class Quote extends SelectableOption { rampName: rampName, rampIconPath: rampIconPath, paymentType: paymentType, + customPaymentMethodType: customPaymentMethodType, quoteId: json['quoteId'] as String? ?? '', recommendations: enumRecommendations, provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!, diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 267406893..eed527cf1 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -17,7 +18,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class DFXBuyProvider extends BuyProvider { @@ -100,11 +100,12 @@ class DFXBuyProvider extends BuyProvider { }); final uri = Uri.https(_baseUrl, _authPath); - var response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: {'Content-Type': 'application/json'}, body: requestBody, ); + if (response.statusCode == 201) { final responseBody = jsonDecode(response.body); @@ -137,8 +138,10 @@ class DFXBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, '/v1/fiat'); try { - final response = await http.get(url, headers: {'accept': 'application/json'}); - + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; for (final item in data) { @@ -160,8 +163,8 @@ class DFXBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain}); try { - final response = await http.get(url, headers: {'accept': 'application/json'}); - + final response = await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final responseData = jsonDecode(response.body); @@ -231,6 +234,7 @@ class DFXBuyProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async { /// if buying with any currency other than eur or chf then DFX is not supported @@ -270,7 +274,12 @@ class DFXBuyProvider extends BuyProvider { }); try { - final response = await http.put(url, headers: headers, body: body); + final response = await ProxyWrapper().put( + clearnetUri: url, + headers: headers, + body: body, + ); + final responseData = jsonDecode(response.body); if (response.statusCode == 200) { @@ -373,7 +382,7 @@ class DFXBuyProvider extends BuyProvider { case 'Instant': return PaymentType.sepa; default: - return PaymentType.all; + return PaymentType.unknown; } } diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart index 6e00a7e07..1d2d10f58 100644 --- a/lib/buy/kryptonim/kryptonim.dart +++ b/lib/buy/kryptonim/kryptonim.dart @@ -9,10 +9,10 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'dart:developer'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class KryptonimBuyProvider extends BuyProvider { @@ -74,9 +74,14 @@ class KryptonimBuyProvider extends BuyProvider { }); try { - final response = await http.post(url, headers: headers, body: body); + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: body, + ); if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) { + return jsonDecode(response.body) as Map; } else { return {}; @@ -113,6 +118,7 @@ class KryptonimBuyProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode, }) async { log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); @@ -149,7 +155,7 @@ class KryptonimBuyProvider extends BuyProvider { final selectedPaymentType = PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?); - final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType); + final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType ?? PaymentType.unknown); quote.setFiatCurrency = fiatCurrency; quote.setCryptoCurrency = cryptoCurrency; diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart index e96a3575c..002ab0465 100644 --- a/lib/buy/meld/meld_buy_provider.dart +++ b/lib/buy/meld/meld_buy_provider.dart @@ -8,13 +8,13 @@ import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'dart:developer'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class MeldBuyProvider extends BuyProvider { @@ -71,8 +71,8 @@ class MeldBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, path, params); try { - final response = await http.get( - url, + final response = await ProxyWrapper().get( + clearnetUri: url, headers: { 'Authorization': _isProduction ? '' : _testApiKey, 'Meld-Version': '2023-12-19', @@ -80,6 +80,7 @@ class MeldBuyProvider extends BuyProvider { 'content-type': 'application/json', }, ); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; @@ -104,6 +105,7 @@ class MeldBuyProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async { String? paymentMethod; if (paymentType != null && paymentType != PaymentType.all) { @@ -129,7 +131,12 @@ class MeldBuyProvider extends BuyProvider { }); try { - final response = await http.post(url, headers: headers, body: body); + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: body, + ); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 6c568886f..1f9ab1bb1 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -14,24 +14,24 @@ import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/themes/core/material_base_theme.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart'; import 'package:url_launcher/url_launcher.dart'; class MoonPayProvider extends BuyProvider { MoonPayProvider({ - required SettingsStore settingsStore, + required AppStore appStore, required WalletBase wallet, bool isTestEnvironment = false, }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl, baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl, - this._settingsStore = settingsStore, + this._appStore = appStore, super( wallet: wallet, isTestEnvironment: isTestEnvironment, @@ -41,7 +41,7 @@ class MoonPayProvider extends BuyProvider { supportedFiatList: supportedFiatToCryptoPairs( notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto)); - final SettingsStore _settingsStore; + final AppStore _appStore; static const _baseSellTestUrl = 'sell-sandbox.moonpay.com'; static const _baseSellProductUrl = 'sell.moonpay.com'; @@ -86,24 +86,24 @@ class MoonPayProvider extends BuyProvider { static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; - static String themeToMoonPayTheme(ThemeBase theme) { + static String themeToMoonPayTheme(MaterialThemeBase theme) { switch (theme.type) { - case ThemeType.bright: case ThemeType.light: return 'light'; case ThemeType.dark: return 'dark'; - case ThemeType.oled: - return 'dark'; } } Future getMoonpaySignature(String query) async { final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); - final response = await post(uri, - headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, - body: json.encode({'query': query})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, + body: json.encode({'query': query}), + ); + if (response.statusCode == 200) { return (jsonDecode(response.body) as Map)['signature'] as String; @@ -123,7 +123,11 @@ class MoonPayProvider extends BuyProvider { final url = Uri.https(_baseUrl, path, params); try { - final response = await get(url, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'accept': 'application/json'}, + ); + if (response.statusCode == 200) { return jsonDecode(response.body) as Map; } else { @@ -162,6 +166,7 @@ class MoonPayProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async { String? paymentMethod; @@ -193,8 +198,8 @@ class MoonPayProvider extends BuyProvider { final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath'; final url = Uri.https(_baseUrl, path, params); try { - final response = await get(url); - + final response = await ProxyWrapper().get(clearnetUri: url); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; @@ -236,9 +241,9 @@ class MoonPayProvider extends BuyProvider { required String cryptoCurrencyAddress, String? countryCode}) async { final Map params = { - 'theme': themeToMoonPayTheme(_settingsStore.currentTheme), - 'language': _settingsStore.languageCode, - 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark + 'theme': themeToMoonPayTheme(_appStore.themeStore.currentTheme), + 'language': _appStore.settingsStore.languageCode, + 'colorCode': _appStore.themeStore.currentTheme.isDark ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name, @@ -259,7 +264,6 @@ class MoonPayProvider extends BuyProvider { try { final uri = await requestMoonPayUrl( walletAddress: cryptoCurrencyAddress, - settingsStore: _settingsStore, isBuyAction: isBuyAction, amount: amount.toString(), params: params); @@ -288,7 +292,6 @@ class MoonPayProvider extends BuyProvider { Future requestMoonPayUrl({ required String walletAddress, - required SettingsStore settingsStore, required bool isBuyAction, required Map params, String? amount, @@ -310,7 +313,8 @@ class MoonPayProvider extends BuyProvider { Future findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Transaction $id is not found!'); @@ -410,7 +414,7 @@ class MoonPayProvider extends BuyProvider { case 'yellow_card_bank_transfer': return PaymentType.yellowCardBankTransfer; default: - return PaymentType.all; + return PaymentType.unknown; } } } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index dc9812d1d..80c9a6326 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -8,17 +8,17 @@ import 'package:cake_wallet/buy/pairs_utils.dart'; import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { - OnRamperBuyProvider(this._settingsStore, + OnRamperBuyProvider(this._themeStore, {required WalletBase wallet, bool isTestEnvironment = false}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, @@ -33,12 +33,15 @@ class OnRamperBuyProvider extends BuyProvider { static const quotes = '/quotes'; static const paymentTypes = '/payment-types'; static const supported = '/supported'; + static const defaultsAll = '/defaults/all'; static const List _notSupportedCrypto = []; static const List _notSupportedFiat = []; static Map _onrampMetadata = {}; - final SettingsStore _settingsStore; + final ThemeStore _themeStore; + + String? recommendedPaymentType; String get _apiKey => secrets.onramperApiKey; @@ -57,6 +60,36 @@ class OnRamperBuyProvider extends BuyProvider { @override bool get isAggregator => true; + Future getRecommendedPaymentType(bool isBuyAction) async { + + final params = {'type': isBuyAction ? 'buy' : 'sell'}; + + final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params); + + try { + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'Authorization': _apiKey, 'accept': 'application/json'}, + ); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body) as Map; + final recommended = data['message']['recommended'] as Map; + + final recommendedPaymentType = recommended['paymentMethod'] as String?; + + return recommendedPaymentType ; + } else { + final responseBody = + jsonDecode(response.body) as Map; + printV('Failed to fetch available payment types: ${responseBody['message']}'); + } + } catch (e) { + printV('Failed to fetch available payment types: $e'); + } + return null; + } + Future> getAvailablePaymentTypes( String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { @@ -72,14 +105,19 @@ class OnRamperBuyProvider extends BuyProvider { try { final response = - await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); - + await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + if (response.statusCode == 200) { final Map data = jsonDecode(response.body) as Map; final List message = data['message'] as List; - return message + + final allAvailablePaymentMethods = message .map((item) => PaymentMethod.fromOnramperJson(item as Map)) .toList(); + + recommendedPaymentType = await getRecommendedPaymentType(isBuyAction); + + return allAvailablePaymentMethods; } else { final responseBody = jsonDecode(response.body) as Map; @@ -97,7 +135,8 @@ class OnRamperBuyProvider extends BuyProvider { try { final response = - await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + if (response.statusCode == 200) { final Map data = jsonDecode(response.body) as Map; @@ -131,13 +170,13 @@ class OnRamperBuyProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async { String? paymentMethod; - if (paymentType != null && paymentType != PaymentType.all) { - paymentMethod = normalizePaymentMethod(paymentType); - if (paymentMethod == null) paymentMethod = paymentType.name; - } + if (paymentType == PaymentType.all && recommendedPaymentType != null) paymentMethod = recommendedPaymentType!; + else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType; + else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType); final actionType = isBuyAction ? 'buy' : 'sell'; @@ -160,8 +199,8 @@ class OnRamperBuyProvider extends BuyProvider { final headers = {'Authorization': _apiKey, 'accept': 'application/json'}; try { - final response = await http.get(url, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; if (data.isEmpty) return null; @@ -182,7 +221,7 @@ class OnRamperBuyProvider extends BuyProvider { if (rampMetaData == null) continue; final quote = Quote.fromOnramperJson( - item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod)); + item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType); quote.setFiatCurrency = fiatCurrency; quote.setCryptoCurrency = cryptoCurrency; validQuotes.add(quote); @@ -210,22 +249,18 @@ class OnRamperBuyProvider extends BuyProvider { String? countryCode}) async { final actionType = isBuyAction ? 'buy' : 'sell'; - final primaryColor = getColorStr(Theme.of(context).primaryColor); - final secondaryColor = getColorStr(Theme.of(context).colorScheme.background); - final primaryTextColor = getColorStr(Theme.of(context).extension()!.titleColor); + final primaryColor = getColorStr(Theme.of(context).colorScheme.primary,); + final secondaryColor = getColorStr(Theme.of(context).colorScheme.surface); + final primaryTextColor = getColorStr(Theme.of(context).colorScheme.onSurface); final secondaryTextColor = - getColorStr(Theme.of(context).extension()!.secondaryTextColor); - final containerColor = getColorStr(Theme.of(context).colorScheme.background); - var cardColor = getColorStr(Theme.of(context).cardColor); - - if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { - cardColor = getColorStr(Colors.white); - } + getColorStr(Theme.of(context).colorScheme.onSurfaceVariant); + final containerColor = getColorStr(Theme.of(context).colorScheme.surface); + var cardColor = getColorStr(Theme.of(context).colorScheme.surfaceContainer); final defaultCrypto = quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase(); - final paymentMethod = normalizePaymentMethod(quote.paymentType); + final paymentMethod = quote.paymentType == PaymentType.unknown ? quote.customPaymentMethodType : normalizePaymentMethod(quote.paymentType); final uri = Uri.https(_baseUrl, '', { 'apiKey': _apiKey, @@ -330,6 +365,8 @@ class OnRamperBuyProvider extends BuyProvider { return 'dana'; case PaymentType.ideal: return 'ideal'; + case PaymentType.pixPay: + return 'pix'; default: return null; } @@ -379,8 +416,10 @@ class OnRamperBuyProvider extends BuyProvider { return PaymentType.dana; case 'ideal': return PaymentType.ideal; + case 'pix': + return PaymentType.pixPay; default: - return PaymentType.all; + return PaymentType.unknown; } } diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index 14b119aa0..322ca68ba 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -34,6 +34,8 @@ enum PaymentType { yellowCardBankTransfer, fiatBalance, bancontact, + pixPay, + unknown, } extension PaymentTypeTitle on PaymentType { @@ -101,6 +103,8 @@ extension PaymentTypeTitle on PaymentType { return 'Fiat Balance'; case PaymentType.bancontact: return 'Bancontact'; + case PaymentType.pixPay: + return 'PIX Pay'; default: return null; } @@ -158,12 +162,14 @@ class PaymentMethod extends SelectableOption { required this.customTitle, required this.customIconPath, this.customDescription, + this.customPaymentMethodType, }) : super(title: paymentMethodType.title ?? customTitle); final PaymentType paymentMethodType; final String customTitle; final String customIconPath; final String? customDescription; + final String? customPaymentMethodType; bool isSelected = false; @override @@ -188,7 +194,8 @@ class PaymentMethod extends SelectableOption { factory PaymentMethod.fromOnramperJson(Map json) { final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?); return PaymentMethod( - paymentMethodType: type, + paymentMethodType: type ?? PaymentType.unknown, + customPaymentMethodType: json['paymentTypeId'] as String?, customTitle: json['name'] as String? ?? 'Unknown', customIconPath: json['icon'] as String? ?? 'assets/images/card.png', customDescription: json['description'] as String?); @@ -212,7 +219,7 @@ class PaymentMethod extends SelectableOption { final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); final logos = json['logos'] as Map; return PaymentMethod( - paymentMethodType: type, + paymentMethodType: type ?? PaymentType.unknown, customTitle: json['name'] as String? ?? 'Unknown', customIconPath: logos['dark'] as String? ?? 'assets/images/card.png', customDescription: json['description'] as String?); @@ -221,13 +228,13 @@ class PaymentMethod extends SelectableOption { factory PaymentMethod.fromKryptonimJson(Map json) { final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?); return PaymentMethod( - paymentMethodType: type, + paymentMethodType: type ?? PaymentType.unknown, customTitle: json['payment_method'] as String? ?? 'Unknown', customIconPath: 'assets/images/card.png', ); } - static PaymentType getPaymentTypeId(String? type) { + static PaymentType? getPaymentTypeId(String? type) { switch (type?.toLowerCase()) { case 'banktransfer': case 'bank': @@ -289,8 +296,10 @@ class PaymentMethod extends SelectableOption { return PaymentType.sepaOpenBankingPayment; case 'bancontact': return PaymentType.bancontact; + case 'pix': + return PaymentType.pixPay; default: - return PaymentType.all; + return null; } } } diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 93efd5642..4b9c1aa70 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -18,7 +19,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class RobinhoodBuyProvider extends BuyProvider { @@ -69,7 +69,8 @@ class RobinhoodBuyProvider extends BuyProvider { final uri = Uri.https(_apiBaseUrl, '$_assetsPath', {'applicationId': _applicationId}); try { - final response = await http.get(uri, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final responseData = jsonDecode(response.body) as Map; @@ -122,12 +123,14 @@ class RobinhoodBuyProvider extends BuyProvider { final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); - var response = await http.post(uri, - headers: {'Content-Type': 'application/json'}, - body: json - .encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature})); + var response = await ProxyWrapper().post( + clearnetUri: uri, + headers: {'Content-Type': 'application/json'}, + body: json.encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature}), + ); if (response.statusCode == 200) { + return (jsonDecode(response.body) as Map)['connectId'] as String; } else { throw Exception('Provider currently unavailable. Status: ${response.statusCode}'); @@ -192,6 +195,7 @@ class RobinhoodBuyProvider extends BuyProvider { required bool isBuyAction, required String walletAddress, PaymentType? paymentType, + String? customPaymentMethodType, String? countryCode}) async { String? paymentMethod; @@ -218,7 +222,8 @@ class RobinhoodBuyProvider extends BuyProvider { Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams); try { - final response = await http.get(uri, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); + final responseData = jsonDecode(response.body) as Map; if (response.statusCode == 200) { @@ -267,7 +272,7 @@ class RobinhoodBuyProvider extends BuyProvider { case 'bank_transfer': return PaymentType.bankTransfer; default: - return PaymentType.all; + return PaymentType.unknown; } } } diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index 7fe6f4be3..3f9844df6 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -3,7 +3,7 @@ import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:cake_wallet/buy/pairs_utils.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; @@ -73,18 +73,21 @@ class WyreBuyProvider extends BuyProvider { 'referrerAccountId': _accountId, 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] }; - final response = await post(uri, - headers: { - 'Authorization': 'Bearer $_secretKey', - 'Content-Type': 'application/json', - 'cache-control': 'no-cache' - }, - body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body), + ); if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Url $url is not found!'); } + final responseJSON = json.decode(response.body) as Map; final urlFromResponse = responseJSON['url'] as String; return urlFromResponse; @@ -101,18 +104,21 @@ class WyreBuyProvider extends BuyProvider { 'country': _countryCode }; final uri = Uri.parse(quoteUrl); - final response = await post(uri, - headers: { - 'Authorization': 'Bearer $_secretKey', - 'Content-Type': 'application/json', - 'cache-control': 'no-cache' - }, - body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body), + ); if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Quote is not found!'); } + final responseJSON = json.decode(response.body) as Map; final sourceAmount = responseJSON['sourceAmount'] as double; final destAmount = responseJSON['destAmount'] as double; @@ -125,8 +131,7 @@ class WyreBuyProvider extends BuyProvider { Future findOrderById(String id) async { final orderUrl = baseApiUrl + _ordersSuffix + '/$id'; final orderUri = Uri.parse(orderUrl); - final orderResponse = await get(orderUri); - + final orderResponse = await ProxyWrapper().get(clearnetUri: orderUri); if (orderResponse.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Order $id is not found!'); } @@ -142,8 +147,7 @@ class WyreBuyProvider extends BuyProvider { final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; final transferUri = Uri.parse(transferUrl); - final transferResponse = await get(transferUri); - + final transferResponse = await ProxyWrapper().get(clearnetUri: transferUri); if (transferResponse.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Transfer $transferId is not found!'); } diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart index 5f1a350c0..a3c7f48d0 100644 --- a/lib/cake_pay/cake_pay_api.dart +++ b/lib/cake_pay/cake_pay_api.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/entities/country.dart'; -import 'package:http/http.dart' as http; class CakePayApi { static const testBaseUri = false; @@ -32,12 +32,17 @@ class CakePayApi { 'Content-Type': 'application/json', 'Authorization': 'Api-Key $apiKey', }; - final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode({'email': email}), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } + final bodyJson = json.decode(response.body) as Map; if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) { @@ -64,12 +69,17 @@ class CakePayApi { }; final query = {'email': email, 'otp': code}; - final response = await http.post(uri, headers: headers, body: json.encode(query)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(query), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } + final bodyJson = json.decode(response.body) as Map; if (bodyJson.containsKey('error')) { @@ -116,9 +126,14 @@ class CakePayApi { }; try { - final response = await http.post(uri, headers: headers, body: json.encode(query)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(query), + ); if (response.statusCode != 201) { + final responseBody = json.decode(response.body); if (responseBody is List) { throw '${responseBody[0]}'; @@ -127,6 +142,7 @@ class CakePayApi { } } + final bodyJson = json.decode(response.body) as Map; return CakePayOrder.fromMap(bodyJson); } catch (e) { @@ -145,7 +161,8 @@ class CakePayApi { 'X-CSRFToken': CSRFToken, }; - final response = await http.get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + printV('Response: ${response.statusCode}'); @@ -168,7 +185,11 @@ class CakePayApi { }; try { - final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode({'email': email}), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); @@ -187,8 +208,8 @@ class CakePayApi { 'Authorization': 'Api-Key $apiKey', }; - final response = await http.get(uri, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } @@ -234,14 +255,15 @@ class CakePayApi { 'Authorization': 'Api-Key $apiKey', }; - var response = await http.get(uri, headers: headers); + var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode != 200) { throw Exception( 'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}'); } - final bodyJson = json.decode(utf8.decode(response.bodyBytes)); + final bodyJson = json.decode(response.body); if (bodyJson is List && bodyJson.isEmpty) { return []; diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 911e939d1..c79ac0980 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -38,9 +38,9 @@ class AddressValidator extends TextValidator { '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{25,39}}').pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - pattern = '^${RegExp(r'ltc1q[ac-hj-np-z02-9]{25,39}').pattern}\$|^${MwebAddress.regex.pattern}\$'; + pattern = '${P2wpkhAddress.regex.pattern}|${MwebAddress.regex.pattern}'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: @@ -335,10 +335,6 @@ class AddressValidator extends TextValidator { } } - if (pattern != null) { - return "$BEFORE_REGEX($pattern)$AFTER_REGEX"; - } - - return null; + return pattern != null ? "($pattern)" : null; } } diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart index 12eb81f99..a503f5c1b 100644 --- a/lib/core/background_sync.dart +++ b/lib/core/background_sync.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/tor.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_core/sync_status.dart'; @@ -15,6 +16,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/foundation.dart'; class BackgroundSync { final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -90,6 +92,11 @@ class BackgroundSync { } Future sync() async { + final settingsStore = getIt.get(); + if (settingsStore.currentBuiltinTor) { + printV("Starting Tor"); + await ensureTorStarted(context: null); + } printV("Background sync started"); await _syncWallets(); printV("Background sync completed"); @@ -100,7 +107,6 @@ class BackgroundSync { final walletListViewModel = getIt.get(); final settingsStore = getIt.get(); - final List moneroWallets = walletListViewModel.wallets .where((element) => !element.isHardware) .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type)) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index cb36072fe..87bb71ce9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/get_encryption_key.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/themes/utils/theme_list.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -83,7 +83,7 @@ class $BackupService { outer: for (var file in zip.files) { final filename = file.name; - for (var ignore in ignoreFiles) { + for (var ignore in ignoreFiles) { if (filename.endsWith(ignore) && !filename.contains("wallets/")) { printV("ignoring backup file: $filename"); continue outer; @@ -110,7 +110,7 @@ class $BackupService { } Future verifyWallets() async { - final walletInfoSource = await _reloadHiveWalletInfoBox(); + final walletInfoSource = await reloadHiveWalletInfoBox(); correctWallets = walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); @@ -119,7 +119,7 @@ class $BackupService { } } - Future> _reloadHiveWalletInfoBox() async { + Future> reloadHiveWalletInfoBox() async { final appDir = await getAppDir(); await CakeHive.close(); CakeHive.init(appDir.path); @@ -145,7 +145,7 @@ class $BackupService { MapEntry(key, TransactionDescription.fromJson(value as Map))); var box = transactionDescriptionBox; if (!box.isOpen) { - final transactionDescriptionsBoxKey = + final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: _secureStorage, forKey: TransactionDescription.boxKey); box = await CakeHive.openBox( TransactionDescription.boxName, @@ -251,19 +251,22 @@ class $BackupService { await importWalletKeychainInfo(info); }); - for (var key in (keychainJSON['_all'] as Map).keys) { - try { - if (!key.startsWith('MONERO_WALLET_')) continue; - final decodedPassword = decodeWalletPassword(password: keychainJSON['_all'][key].toString()); - final walletName = key.split('_WALLET_')[1]; - final walletType = key.split('_WALLET_')[0].toLowerCase(); - await importWalletKeychainInfo({ - 'name': walletName, - 'type': "WalletType.$walletType", - 'password': decodedPassword, - }); - } catch (e) { - printV('Error importing wallet ($key) password: $e'); + if (keychainJSON['_all'] is Map) { + for (var key in (keychainJSON['_all'] as Map).keys) { + try { + if (!key.startsWith('MONERO_WALLET_')) continue; + final decodedPassword = decodeWalletPassword( + password: keychainJSON['_all'][key].toString()); + final walletName = key.split('_WALLET_')[1]; + final walletType = key.split('_WALLET_')[0].toLowerCase(); + await importWalletKeychainInfo({ + 'name': walletName, + 'type': "WalletType.$walletType", + 'password': decodedPassword, + }); + } catch (e) { + printV('Error importing wallet ($key) password: $e'); + } } } @@ -285,13 +288,15 @@ class $BackupService { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': await keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await keyService.getWalletPassword(walletName: walletInfo.name), + 'hardwareWalletType': walletInfo.hardwareWalletType?.index, }; } catch (e) { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': '' + 'password': '', + 'hardwareWalletType': walletInfo.hardwareWalletType?.index, }; } })); diff --git a/lib/core/backup_service_v3.dart b/lib/core/backup_service_v3.dart index 76798aa64..a0640dfd3 100644 --- a/lib/core/backup_service_v3.dart +++ b/lib/core/backup_service_v3.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/utils/package_info.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; enum BackupVersion { @@ -177,7 +178,7 @@ class BackupServiceV3 extends $BackupService { final archive = ZipDecoder().decodeStream(inputStream); final metadataFile = archive.findFile('metadata.json'); if (metadataFile == null) { - throw Exception('Invalid v3 backup: missing metadata.json'); + return BackupVersion.unknown; } final metadataBytes = metadataFile.rawContent!.readBytes(); final metadataString = utf8.decode(metadataBytes); @@ -188,7 +189,7 @@ class BackupServiceV3 extends $BackupService { } } - throw Exception('Invalid backup file: unknown version'); + return BackupVersion.unknown; } finally { raf.closeSync(); } @@ -305,6 +306,7 @@ class BackupServiceV3 extends $BackupService { // Continue importing the backup the old way await super.verifyWallets(); + await verifyHardwareWallets(password); await super.importKeychainDumpV2(password); await super.importPreferencesDump(); await super.importTransactionDescriptionDump(); @@ -313,6 +315,39 @@ class BackupServiceV3 extends $BackupService { decryptedData.deleteSync(); } + Future verifyHardwareWallets(String password, + {String keychainSalt = secrets.backupKeychainSalt}) async { + final walletInfoSource = await reloadHiveWalletInfoBox(); + final appDir = await getAppDir(); + final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); + final decryptedKeychainDumpFileData = await decryptV2( + keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) + as Map; + final keychainWalletsInfo = keychainJSON['wallets'] as List; + + final expectedHardwareWallets = keychainWalletsInfo + .where((e) => + (e as Map).containsKey("hardwareWalletType") && + e["hardwareWalletType"] != null) + .toList(); + + for (final expectedHardwareWallet in expectedHardwareWallets) { + final info = expectedHardwareWallet as Map; + final actualWalletInfo = walletInfoSource.values + .where((e) => + e.name == info['name'] && e.type.toString() == info['type']) + .firstOrNull; + if (actualWalletInfo != null && + info["hardwareWalletType"] != + actualWalletInfo.hardwareWalletType?.index) { + actualWalletInfo.hardwareWalletType = + HardwareWalletType.values[info["hardwareWalletType"] as int]; + await actualWalletInfo.save(); + } + } + } + Future exportBackupFileV3(String password, {String nonce = secrets.backupSalt}) async { final metadata = BackupMetadata( version: BackupVersion.v3, @@ -467,4 +502,4 @@ This backup was created on ${DateTime.now().toIso8601String()} file.writeAsBytesSync(data); return file; } -} \ No newline at end of file +} diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 8a37175b4..669e38128 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -1,37 +1,35 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; -const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; +const _fiatApiOnionAuthority = 'kfkyguqtz5vcnbvar5pjgddkaeawbo4j3r4fj3e22k3tzqageplosiid.onion'; const _fiatApiPath = '/v2/rates'; -Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as String; - final fiat = args['fiat'] as String; - final torOnly = args['torOnly'] as bool; +Future _fetchPrice(String crypto, String fiat, bool torOnly) async { final Map queryParams = { 'interval_count': '1', 'base': crypto.split(".").first, 'quote': fiat, - 'key': secrets.fiatApiKey, }; num price = 0.0; try { - late final Uri uri; - if (torOnly) { - uri = Uri.http(_fiatApiOnionAuthority, _fiatApiPath, queryParams); - } else { - uri = Uri.https(_fiatApiClearNetAuthority, _fiatApiPath, queryParams); - } + final onionUri = Uri.http(_fiatApiOnionAuthority, _fiatApiPath, queryParams); + final clearnetUri = Uri.https(_fiatApiClearNetAuthority, _fiatApiPath, queryParams); - final response = await get(uri); + final response = await ProxyWrapper().get( + onionUri: onionUri, + clearnetUri: torOnly ? onionUri : clearnetUri, + headers: { + "x-api-key": secrets.fiatApiKey, + } + ); + if (response.statusCode != 200) { return 0.0; @@ -50,18 +48,11 @@ Future _fetchPrice(Map args) async { } } -Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, { - 'fiat': fiat.toString(), - 'crypto': crypto.toString(), - 'torOnly': torOnly, - }); - class FiatConversionService { static Future fetchPrice({ required CryptoCurrency crypto, required FiatCurrency fiat, required bool torOnly, }) async => - await _fetchPriceAsync(crypto, fiat, torOnly); + await _fetchPrice(crypto.toString(), fiat.toString(), torOnly); } diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index 6449bbf90..4b317d9c3 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -5,15 +5,13 @@ import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; class OpenCryptoPayService { static bool isOpenCryptoPayQR(String value) => value.toLowerCase().contains("lightning=lnurl") || value.toLowerCase().startsWith("lnurl"); - final Client _httpClient = Client(); - Future commitOpenCryptoPayRequest( String txHex, { required String txId, @@ -31,7 +29,8 @@ class OpenCryptoPayService { queryParams['tx'] = txId; final response = - await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams)); + await ProxyWrapper().get(clearnetUri: Uri.https(uri.authority, uri.path, queryParams)); + if (response.statusCode == 200) { final body = jsonDecode(response.body) as Map; @@ -40,13 +39,13 @@ class OpenCryptoPayService { throw OpenCryptoPayException(body.toString()); } throw OpenCryptoPayException( - "Unexpected status code ${response.statusCode} ${response.body}"); + "Unexpected status code ${response.statusCode} ${response}"); } Future cancelOpenCryptoPayRequest(OpenCryptoPayRequest request) async { final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/cancel/")); - await _httpClient.delete(uri); + await ProxyWrapper().delete(clearnetUri: uri); } Future getOpenCryptoPayInvoice(String lnUrl) async { @@ -73,7 +72,8 @@ class OpenCryptoPayService { Future<(_OpenCryptoPayQuote, Map>)> _getOpenCryptoPayParams(Uri uri) async { - final response = await _httpClient.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 200) { final responseBody = jsonDecode(response.body) as Map; @@ -119,8 +119,8 @@ class OpenCryptoPayService { queryParams['asset'] = asset.title; queryParams['method'] = _getMethod(asset); - final response = - await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams)); + final response = await ProxyWrapper().get(clearnetUri: Uri.https(uri.authority, uri.path, queryParams)); + if (response.statusCode == 200) { final responseBody = jsonDecode(response.body) as Map; diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart new file mode 100644 index 000000000..3b696b88a --- /dev/null +++ b/lib/core/trade_monitor.dart @@ -0,0 +1,247 @@ +import 'dart:async'; +import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class TradeMonitor { + static const int _tradeCheckIntervalMinutes = 5; + static const int _maxTradeAgeHours = 24; + + TradeMonitor({ + required this.tradesStore, + required this.trades, + required this.appStore, + required this.preferences, + }); + + final TradesStore tradesStore; + final Box trades; + final AppStore appStore; + final Map _tradeTimers = {}; + final SharedPreferences preferences; + + ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) { + switch (description) { + case ExchangeProviderDescription.changeNow: + return ChangeNowExchangeProvider(settingsStore: appStore.settingsStore); + case ExchangeProviderDescription.sideShift: + return SideShiftExchangeProvider(); + case ExchangeProviderDescription.simpleSwap: + return SimpleSwapExchangeProvider(); + case ExchangeProviderDescription.trocador: + return TrocadorExchangeProvider(); + case ExchangeProviderDescription.exolix: + return ExolixExchangeProvider(); + case ExchangeProviderDescription.thorChain: + return ThorChainExchangeProvider(tradesStore: trades); + case ExchangeProviderDescription.swapTrade: + return SwapTradeExchangeProvider(); + case ExchangeProviderDescription.letsExchange: + return LetsExchangeExchangeProvider(); + case ExchangeProviderDescription.stealthEx: + return StealthExExchangeProvider(); + case ExchangeProviderDescription.chainflip: + return ChainflipExchangeProvider(tradesStore: trades); + case ExchangeProviderDescription.xoSwap: + return XOSwapExchangeProvider(); + } + return null; + } + + void monitorActiveTrades(String walletId) { + // Checks if the trade monitoring is permitted + // i.e the user has not disabled the exchange api mode or the status updates + final isTradeMonitoringPermitted = _isTradeMonitoringPermitted(); + if (!isTradeMonitoringPermitted) { + return; + } + + final trades = tradesStore.trades; + final tradesToCancel = []; + + for (final item in trades) { + final trade = item.trade; + + final provider = _getProviderByDescription(trade.provider); + + // Multiple checks to see if to skip the trade, if yes, we cancel the timer if it exists + if (_shouldSkipTrade(trade, walletId, provider)) { + tradesToCancel.add(trade.id); + continue; + } + + if (_tradeTimers.containsKey(trade.id)) { + printV('Trade ${trade.id} is already being monitored'); + continue; + } else { + _startTradeMonitoring(trade, provider!); + } + } + + // After going through the list of available trades, we cancel the timers in the tradesToCancel list + _cancelMultipleTradeTimers(tradesToCancel); + } + + bool _isTradeMonitoringPermitted() { + final disableAutomaticExchangeStatusUpdates = + appStore.settingsStore.disableAutomaticExchangeStatusUpdates; + if (disableAutomaticExchangeStatusUpdates) { + printV('Automatic exchange status updates are disabled'); + return false; + } + + final exchangeApiMode = appStore.settingsStore.exchangeStatus; + if (exchangeApiMode == ExchangeApiMode.disabled) { + printV('Exchange API mode is disabled'); + return false; + } + + return true; + } + + bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) { + if (trade.walletId != walletId) { + printV('Skipping trade ${trade.id} because it\'s not for this wallet'); + return true; + } + + final createdAt = trade.createdAt; + if (createdAt == null) { + printV('Skipping trade ${trade.id} because it has no createdAt'); + return true; + } + + if (DateTime.now().difference(createdAt).inHours > _maxTradeAgeHours) { + printV('Skipping trade ${trade.id} because it\'s older than ${_maxTradeAgeHours} hours'); + return true; + } + + if (_isFinalState(trade.state)) { + printV('Skipping trade ${trade.id} because it\'s in a final state'); + return true; + } + + if (provider == null) { + printV('Skipping trade ${trade.id} because the provider is not supported'); + return true; + } + + if (appStore.settingsStore.exchangeStatus == ExchangeApiMode.torOnly && + !provider.supportsOnionAddress) { + printV('Skipping ${provider.description}, no TOR support'); + return true; + } + + return false; + } + + void _startTradeMonitoring(Trade trade, ExchangeProvider provider) { + final timer = Timer.periodic( + Duration(minutes: _tradeCheckIntervalMinutes), + (_) => _checkTradeStatus(trade, provider), + ); + + _checkTradeStatus(trade, provider); + + _tradeTimers[trade.id] = timer; + } + + Future _checkTradeStatus(Trade trade, ExchangeProvider provider) async { + final lastUpdatedAtFromPrefs = preferences.getString('trade_${trade.id}_updated_at'); + + if (lastUpdatedAtFromPrefs != null) { + final lastUpdatedAtDateTime = DateTime.parse(lastUpdatedAtFromPrefs); + final timeSinceLastUpdate = DateTime.now().difference(lastUpdatedAtDateTime).inMinutes; + + if (timeSinceLastUpdate < _tradeCheckIntervalMinutes) { + printV( + 'Skipping trade ${trade.id} status update check because it was updated less than ${_tradeCheckIntervalMinutes} minutes ago ($timeSinceLastUpdate minutes ago)', + ); + return; + } + } + + try { + final updated = await provider.findTradeById(id: trade.id); + trade + ..stateRaw = updated.state.raw + ..receiveAmount = updated.receiveAmount ?? trade.receiveAmount + ..outputTransaction = updated.outputTransaction ?? trade.outputTransaction; + printV('Trade ${trade.id} updated: ${trade.state}'); + await trade.save(); + + await preferences.setString('trade_${trade.id}_updated_at', DateTime.now().toIso8601String()); + printV('Trade ${trade.id} updated at: ${DateTime.now().toIso8601String()}'); + + // If the updated trade is in a final state, we cancel the timer + if (_isFinalState(updated.state)) { + printV('Trade ${trade.id} is in final state'); + _cancelSingleTradeTimer(trade.id); + } + } catch (e) { + printV('Error fetching status for ${trade.id}: $e'); + } + } + + bool _isFinalState(TradeState state) { + return { + TradeState.completed.raw, + TradeState.success.raw, + TradeState.confirmed.raw, + TradeState.settled.raw, + TradeState.finished.raw, + TradeState.expired.raw, + TradeState.failed.raw, + TradeState.notFound.raw, + }.contains(state.raw); + } + + void _cancelSingleTradeTimer(String tradeId) { + if (_tradeTimers.containsKey(tradeId)) { + _tradeTimers[tradeId]?.cancel(); + _tradeTimers.remove(tradeId); + printV('Trade timer for ${tradeId} cancelled'); + } + } + + void _cancelMultipleTradeTimers(List tradeIds) { + for (final tradeId in tradeIds) { + _cancelSingleTradeTimer(tradeId); + } + } + + /// This is called when the app is brought back to foreground. + void resumeTradeMonitoring() { + if (appStore.wallet != null) { + monitorActiveTrades(appStore.wallet!.id); + } + } + + /// There's no need to run the trade checks when the app is in background. + /// We only want to update the trade status when the app is in foreground. + /// This helps to reduce the battery usage, network usage and enhance overall privacy. + /// + /// This is called when the app is sent to background or when the app is closed. + void stopTradeMonitoring() { + printV('Stopping trade monitoring'); + _cancelMultipleTradeTimers(_tradeTimers.keys.toList()); + } +} diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 382b1d6c2..f16bf7e14 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -73,8 +73,11 @@ class WalletLoadingService { return wallet; } catch (error, stack) { await ExceptionHandler.resetLastPopupDate(); + final isLedgerError = await ExceptionHandler.isLedgerError(error); + if (isLedgerError) rethrow; await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + // try fetching the seeds of the corrupted wallet to show it to the user String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):"; try { diff --git a/lib/core/yat_service.dart b/lib/core/yat_service.dart index 92e81e5ef..84aac1626 100644 --- a/lib/core/yat_service.dart +++ b/lib/core/yat_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:cake_wallet/entities/yat_record.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; class YatService { static bool isDevMode = false; @@ -33,7 +33,8 @@ class YatService { final yatRecords = []; try { - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final resBody = json.decode(response.body) as Map; final results = resBody["result"] as Map; // Favour a subaddress over a standard address. @@ -42,7 +43,7 @@ class YatService { results[MONERO_STD_ADDRESS] ?? results[tag]) as Map; - if (yatRecord != null) { + if (yatRecord.isNotEmpty) { yatRecords.add(YatRecord.fromJson(yatRecord)); } diff --git a/lib/di.dart b/lib/di.dart index c8b002008..5a7be0f1f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -33,18 +33,24 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/haven/cw_haven.dart'; import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; +import 'package:cake_wallet/src/screens/dev/network_requests.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; +import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; +import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; import 'package:cake_wallet/view_model/dev/secure_preferences.dart'; import 'package:cake_wallet/view_model/dev/shared_preferences.dart'; +import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cake_wallet/view_model/start_tor_view_model.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; @@ -135,7 +141,6 @@ import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; -import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -152,11 +157,9 @@ import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; @@ -276,6 +279,7 @@ import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart'; import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; final getIt = GetIt.instance; @@ -325,6 +329,11 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); getIt.registerSingleton(secureStorage); + getIt.registerSingletonAsync(() async { + final store = ThemeStore(); + await store.loadThemePreferences(); + return store; + }); } final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && @@ -335,10 +344,6 @@ Future setup({ nodeSource: _nodeSource, powNodeSource: _powNodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, - // Enforce darkTheme on platforms other than mobile till the design for other themes is completed - initialTheme: responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile - ? null - : ThemeList.darkTheme, ); if (_isSetupFinished) { @@ -356,7 +361,8 @@ Future setup({ authenticationStore: getIt.get(), walletList: getIt.get(), settingsStore: getIt.get(), - nodeListStore: getIt.get())); + nodeListStore: getIt.get(), + themeStore: getIt.get())); getIt.registerSingleton( TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get())); getIt.registerSingleton( @@ -506,19 +512,42 @@ Future setup({ settingsStore: getIt.get(), fiatConvertationStore: getIt.get())); - getIt.registerFactory(() => DashboardViewModel( - balanceViewModel: getIt.get(), - appStore: getIt.get(), + getIt.registerFactory( + () => ExchangeViewModel( + getIt.get(), + _tradesSource, + getIt.get(), + getIt.get(), + getIt.get().settingsStore, + getIt.get(), + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerSingleton( + TradeMonitor( tradesStore: getIt.get(), - tradeFilterStore: getIt.get(), - transactionFilterStore: getIt.get(), - settingsStore: settingsStore, - yatStore: getIt.get(), - ordersStore: getIt.get(), - anonpayTransactionsStore: getIt.get(), - payjoinTransactionsStore: getIt.get(), - sharedPreferences: getIt.get(), - keyService: getIt.get())); + trades: _tradesSource, + appStore: getIt.get(), + preferences: getIt.get(), + ), + ); + + getIt.registerFactory(() => DashboardViewModel( + tradeMonitor: getIt.get(), + balanceViewModel: getIt.get(), + appStore: getIt.get(), + tradesStore: getIt.get(), + tradeFilterStore: getIt.get(), + transactionFilterStore: getIt.get(), + settingsStore: settingsStore, + yatStore: getIt.get(), + ordersStore: getIt.get(), + anonpayTransactionsStore: getIt.get(), + payjoinTransactionsStore: getIt.get(), + sharedPreferences: getIt.get(), + keyService: getIt.get())); getIt.registerFactory( () => AuthService( @@ -663,7 +692,6 @@ Future setup({ return walletKitService; }); - getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => BalancePage( nftViewModel: getIt.get(), dashboardViewModel: getIt.get(), @@ -901,7 +929,7 @@ Future setup({ getIt.get(param1: account))); getIt.registerFactory(() => - DisplaySettingsViewModel(getIt.get())); + DisplaySettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactory(() => SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); @@ -1030,13 +1058,13 @@ Future setup({ getIt.get().wallet!.isHardwareWallet ? getIt.get() : null)); getIt.registerFactory(() => MoonPayProvider( - settingsStore: getIt.get().settingsStore, + appStore: getIt.get(), wallet: getIt.get().wallet!, isTestEnvironment: kDebugMode, )); getIt.registerFactory(() => OnRamperBuyProvider( - getIt.get().settingsStore, + getIt.get(), wallet: getIt.get().wallet!, )); @@ -1050,19 +1078,6 @@ Future setup({ getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); - getIt.registerFactory( - () => ExchangeViewModel( - getIt.get(), - _tradesSource, - getIt.get(), - getIt.get(), - getIt.get().settingsStore, - getIt.get(), - getIt.get(), - getIt.get(), - ), - ); - getIt.registerFactory( () => FeesViewModel( getIt.get(), @@ -1236,7 +1251,7 @@ Future setup({ TradeDetailsViewModel( tradeForDetails: trade, trades: _tradesSource, - settingsStore: getIt.get())); + appStore: getIt.get())); getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); @@ -1431,7 +1446,7 @@ Future setup({ (AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsViewModel( anonPayApi: getIt.get(), anonpayInvoiceInfo: anonpayInvoiceInfo, - settingsStore: getIt.get(), + themeStore: getIt.get(), )); getIt.registerFactoryParam( @@ -1440,7 +1455,7 @@ Future setup({ sessionId, transactionInfo, payjoinSessionSource: _payjoinSessionSource, - settingsStore: getIt.get(), + themeStore: getIt.get(), )); getIt.registerFactoryParam( @@ -1481,7 +1496,7 @@ Future setup({ () => WalletConnectConnectionsView(walletKitService: getIt.get()), ); - getIt.registerFactory(() => TorPage(getIt.get())); + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => SignViewModel(getIt.get().wallet!)); @@ -1499,5 +1514,13 @@ Future setup({ getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); + getIt.registerFactory(() => DevNetworkRequests()); + + getIt.registerFactory(() => StartTorPage(StartTorViewModel(),)); + + getIt.registerFactory(() => DEuroViewModel(getIt())); + + getIt.registerFactory(() => DEuroSavingsPage(getIt())); + _isSetupFinished = true; } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 45234d5ec..a5dfec97b 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -37,7 +37,7 @@ const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum-rpc.publicnode.com'; const polygonDefaultNodeUri = 'polygon-bor-rpc.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; -const nanoDefaultNodeUri = 'nano.nownodes.io'; +const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com'; const tronDefaultNodeUri = 'api.trongrid.io'; diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index 512244c1b..78b0f4178 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,14 +1,16 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:ens_dart/ens_dart.dart'; -import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; class EnsRecord { + static Future fetchEnsAddress(String name, {WalletBase? wallet}) async { + Web3Client? _client; if (wallet != null && wallet.type == WalletType.ethereum) { @@ -20,7 +22,9 @@ class EnsRecord { } if (_client == null) { - _client = Web3Client("https://ethereum-rpc.publicnode.com", Client()); + late final client = ProxyWrapper().getHttpIOClient(); + + _client = Web3Client("https://ethereum-rpc.publicnode.com", client); } try { diff --git a/lib/entities/fio_address_provider.dart b/lib/entities/fio_address_provider.dart index a88804c97..dcfd79c96 100644 --- a/lib/entities/fio_address_provider.dart +++ b/lib/entities/fio_address_provider.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class FioAddressProvider { static const apiAuthority = 'fio.blockpane.com'; @@ -13,13 +13,17 @@ class FioAddressProvider { final body = {"fio_name": fioAddress}; final uri = Uri.https(apiAuthority, availCheck); - final response = - await http.post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); if (response.statusCode != 200) { return isFioRegistered; } + final responseJSON = json.decode(response.body) as Map; isFioRegistered = responseJSON['is_registered'] as int == 1; @@ -35,9 +39,13 @@ class FioAddressProvider { }; final uri = Uri.https(apiAuthority, getAddress); - final response = - await http.post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] as String; diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 0cb2e4058..9fd1481a5 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -29,7 +29,7 @@ class MainActions { static MainActions showWalletsAction = MainActions._( name: (context) => S.of(context).wallets, - image: 'assets/images/wallet_new.png', + image: 'assets/images/wallet_icon.png', onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed( context, @@ -42,7 +42,7 @@ class MainActions { static MainActions receiveAction = MainActions._( name: (context) => S.of(context).receive, - image: 'assets/images/received.png', + image: 'assets/images/receive.png', onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed(context, Routes.addressPage); }, @@ -50,7 +50,7 @@ class MainActions { static MainActions swapAction = MainActions._( name: (context) => S.of(context).swap, - image: 'assets/images/transfer.png', + image: 'assets/images/swap.png', isEnabled: (viewModel) => viewModel.isEnabledSwapAction, canShow: (viewModel) => viewModel.hasSwapAction, onTap: (BuildContext context, DashboardViewModel viewModel) async { @@ -62,7 +62,7 @@ class MainActions { static MainActions sendAction = MainActions._( name: (context) => S.of(context).send, - image: 'assets/images/upload.png', + image: 'assets/images/send2.png', isEnabled: (viewModel) => viewModel.canSend, onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed(context, Routes.send); @@ -70,8 +70,8 @@ class MainActions { ); static MainActions tradeAction = MainActions._( - name: (context) => S.of(context).exchange, - image: 'assets/images/buy_sell.png', + name: (context) => S.of(context).buy, + image: 'assets/images/buy.png', isEnabled: (viewModel) => viewModel.isEnabledTradeAction, canShow: (viewModel) => viewModel.hasTradeAction, onTap: (BuildContext context, DashboardViewModel viewModel) async { diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 3fbbe0709..01e6322cc 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -165,13 +165,19 @@ class AddressResolver { "zone" ]; - static String? extractAddressByType({required String raw, required CryptoCurrency type}) { - final addressPattern = AddressValidator.getAddressFromStringPattern(type); + static String? extractAddressByType( + {required String raw, + required CryptoCurrency type, + bool requireSurroundingWhitespaces = true}) { + var addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern == null) { throw Exception('Unexpected token: $type for getAddressFromStringPattern'); } + if (requireSurroundingWhitespaces) + addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX"; + final match = RegExp(addressPattern, multiLine: true).firstMatch(raw); return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (Match match) { diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 494888a86..0f3c1fe48 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -24,6 +24,7 @@ class PreferencesKey { static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableTradeOption = 'disable_buy'; + static const disableAutomaticExchangeStatusUpdates = 'disable_automatic_exchange_status_updates'; static const disableBulletinKey = 'disable_bulletin'; static const walletListOrder = 'wallet_list_order'; static const contactListOrder = 'contact_list_order'; @@ -34,6 +35,7 @@ class PreferencesKey { static const disableExchangeKey = 'disable_exchange'; static const exchangeStatusKey = 'exchange_status'; static const currentTheme = 'current_theme'; + static const themeMode = 'theme_mode'; static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; @@ -64,6 +66,7 @@ class PreferencesKey { static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const syncModeKey = 'sync_mode'; static const syncAllKey = 'sync_all'; + static const builtinTorKey = 'builtin_tor'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; static const sortBalanceBy = 'sort_balance_by'; @@ -83,6 +86,7 @@ class PreferencesKey { static const lookupsENS = 'looks_up_ens'; static const lookupsWellKnown = 'looks_up_well_known'; static const usePayjoin = 'use_payjoin'; + static const showPayjoinCard = 'show_payjoin_card'; static const showCameraConsent = 'show_camera_consent'; static const showDecredInfoCard = 'show_decred_info_card'; diff --git a/lib/entities/qr_scanner.dart b/lib/entities/qr_scanner.dart index c4d37ad6c..31353a8f8 100644 --- a/lib/entities/qr_scanner.dart +++ b/lib/entities/qr_scanner.dart @@ -15,7 +15,7 @@ Future presentQRScanner(BuildContext context) async { try { final result = await Navigator.of(context).push( MaterialPageRoute( - builder:(context) { + builder: (context) { return BarcodeScannerSimple(); }, ), @@ -67,7 +67,7 @@ class _BarcodeScannerSimpleState extends State { void _handleBarcodeInternal(BarcodeCapture barcodes) { for (final barcode in barcodes.barcodes) { // don't handle unknown QR codes - if (barcode.rawValue?.trim().isEmpty??false == false) continue; + if (barcode.rawValue?.trim().isEmpty ?? false == false) continue; if (barcode.rawValue!.startsWith("ur:")) { if (urCodes.contains(barcode.rawValue)) continue; setState(() { @@ -81,7 +81,8 @@ class _BarcodeScannerSimpleState extends State { SchedulerBinding.instance.addPostFrameCallback((_) { Navigator.of(context).pop(ur.inputs.join("\n")); }); - }; + } + ; } } if (urCodes.isNotEmpty) return; @@ -110,18 +111,21 @@ class _BarcodeScannerSimpleState extends State { ToggleFlashlightButton(controller: ctrl), ], ), - backgroundColor: Colors.black, + backgroundColor: Theme.of(context).colorScheme.surface, body: Stack( children: [ MobileScanner( onDetect: _handleBarcode, controller: ctrl, ), - if (ur.inputs.length != 0) - Center(child: - Text( + if (ur.inputs.length != 0) + Center( + child: Text( "${ur.inputs.length}/${ur.count}", - style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white) + style: Theme.of(context) + .textTheme + .displayLarge + ?.copyWith(color: Theme.of(context).colorScheme.onSurface), ), ), SizedBox( @@ -158,7 +162,6 @@ class _BarcodeScannerSimpleState extends State { } } - class ToggleFlashlightButton extends StatelessWidget { const ToggleFlashlightButton({required this.controller, super.key}); @@ -199,9 +202,9 @@ class ToggleFlashlightButton extends StatelessWidget { }, ); case TorchState.unavailable: - return const Icon( + return Icon( Icons.no_flash, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ); } }, @@ -326,16 +329,14 @@ class ProgressPainter extends CustomPainter { const fullAngle = 360.0; var startAngle = 0.0; for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) { - var sweepAngle = - (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0; - drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect, - startAngle, sweepAngle); + var sweepAngle = (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0; + drawSector( + canvas, urQrProgress.receivedPartIndexes.contains(i), rect, startAngle, sweepAngle); startAngle += sweepAngle; } } - void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, - double sweepAngle) { + void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, double sweepAngle) { final paint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 8 diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index 0c5632875..22f0be045 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -2,16 +2,19 @@ import 'package:cw_core/enumerable_item.dart'; import 'package:cw_core/wallet_info.dart'; class MoneroSeedType extends EnumerableItem with Serializable { - const MoneroSeedType({required String title, required int raw}) : super(title: title, raw: raw); + const MoneroSeedType({required String title, required int raw, this.shortTitle}) + : super(title: title, raw: raw); + + final String? shortTitle; static const all = [legacy, polyseed, bip39]; static const defaultSeedType = polyseed; - static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)'); - static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)'); + static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)', shortTitle: "Legacy"); + static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)', shortTitle: "Polyseed"); static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero'); - static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39 (12 words)'); + static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39 (12 words)', shortTitle: "BIP39"); static MoneroSeedType deserialize({required int raw}) { switch (raw) { diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 2ac573652..05f64820e 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -5,7 +5,7 @@ part 'transaction_description.g.dart'; @HiveType(typeId: TransactionDescription.typeId) class TransactionDescription extends HiveObject { - TransactionDescription({required this.id, this.recipientAddress, this.transactionNote}); + TransactionDescription({required this.id, this.recipientAddress, this.transactionNote, this.transactionKey}); static const typeId = TRANSACTION_TYPE_ID; static const boxName = 'TransactionDescriptions'; @@ -20,12 +20,16 @@ class TransactionDescription extends HiveObject { @HiveField(2) String? transactionNote; + @HiveField(3) + String? transactionKey; + String get note => transactionNote ?? ''; Map toJson() => { 'id': id, 'recipientAddress': recipientAddress, 'transactionNote': transactionNote, + 'transactionKey': transactionKey, }; factory TransactionDescription.fromJson(Map json) { @@ -33,6 +37,7 @@ class TransactionDescription extends HiveObject { id: json['id'] as String, recipientAddress: json['recipientAddress'] as String?, transactionNote: json['transactionNote'] as String?, + transactionKey: json['transactionKey'] as String?, ); } } diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index a047c85d9..cf6eb8945 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,15 +1,16 @@ import 'dart:convert'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; Future fetchUnstoppableDomainAddress(String domain, String ticker) async { var address = ''; try { final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records"); - final jsonString = await http.read(uri); - final jsonParsed = json.decode(jsonString) as Map; + final response = await ProxyWrapper().get(clearnetUri: uri); + + final jsonParsed = json.decode(response.body) as Map; if (jsonParsed["records"] == null) { throw Exception(".records response from $uri is empty"); }; diff --git a/lib/entities/wellknown_record.dart b/lib/entities/wellknown_record.dart index dbe808281..6dd94440b 100644 --- a/lib/entities/wellknown_record.dart +++ b/lib/entities/wellknown_record.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class WellKnownRecord { WellKnownRecord({ @@ -40,14 +40,15 @@ class WellKnownRecord { } // lookup domain/.well-known/nano-currency.json and check if it has a nano address: - final http.Response response = await http.get( - Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"), + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"), headers: {"Accept": "application/json"}, ); if (response.statusCode != 200) { return null; } + final Map decoded = json.decode(response.body) as Map; // Access the first element in the names array and retrieve its address diff --git a/lib/entities/zano_alias.dart b/lib/entities/zano_alias.dart index 1ddf95178..ec966e225 100644 --- a/lib/entities/zano_alias.dart +++ b/lib/entities/zano_alias.dart @@ -1,14 +1,14 @@ import 'dart:convert'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class ZanoAlias { static Future fetchZanoAliasAddress(String alias) async { try { final uri = Uri.parse("http://zano.cakewallet.com:11211/json_rpc"); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, body: json.encode({ "id": 0, "jsonrpc": "2.0", @@ -16,6 +16,7 @@ class ZanoAlias { "params": {"alias": alias} }), ); + final jsonParsed = json.decode(response.body) as Map; return jsonParsed['result']['alias_details']['address'] as String?; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 40c7a0f77..a6a6a7205 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -67,8 +67,7 @@ class CWEthereum extends Ethereum { @override String getPublicKey(WalletBase wallet) { final privateKeyInUnitInt = (wallet as EthereumWallet).evmChainPrivateKey; - final publicKey = privateKeyInUnitInt.address.hex; - return publicKey; + return privateKeyInUnitInt.address.hex; } @override @@ -138,29 +137,24 @@ class CWEthereum extends Ethereum { } @override - List getERC20Currencies(WalletBase wallet) { - final ethereumWallet = wallet as EthereumWallet; - return ethereumWallet.erc20Currencies; - } + List getERC20Currencies(WalletBase wallet) => + (wallet as EthereumWallet).erc20Currencies; @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) async { - await (wallet as EthereumWallet).addErc20Token(token as Erc20Token); - } + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).addErc20Token(token as Erc20Token); @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => - await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => - await (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); @override - Future getErc20Token(WalletBase wallet, String contractAddress) async { - final ethereumWallet = wallet as EthereumWallet; - return await ethereumWallet.getErc20Token(contractAddress, 'eth'); - } + Future getErc20Token(WalletBase wallet, String contractAddress) => + (wallet as EthereumWallet).getErc20Token(contractAddress, 'eth'); @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { @@ -177,23 +171,19 @@ class CWEthereum extends Ethereum { } @override - void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { - (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); - } + void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) => + (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); @override - Web3Client? getWeb3Client(WalletBase wallet) { - return (wallet as EthereumWallet).getWeb3Client(); - } + Web3Client? getWeb3Client(WalletBase wallet) => (wallet as EthereumWallet).getWeb3Client(); + @override String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedgerConnection( - WalletBase wallet, ledger.LedgerConnection connection) { + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) - .setLedgerConnection( - connection, wallet.walletInfo.derivationInfo?.derivationPath); + .setLedgerConnection(connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override @@ -212,4 +202,49 @@ class CWEthereum extends Ethereum { List getDefaultTokenContractAddresses() { return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); } + + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final ethereumWallet = wallet as EthereumWallet; + return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + } + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, + CryptoCurrency token, TransactionPriority priority) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, spender, token, priority as EVMChainTransactionPriority); + + // Integrations + @override + Future getDEuroSavingsBalance(WalletBase wallet) => + DEuro(wallet as EthereumWallet).savingsBalance; + + @override + Future getDEuroAccruedInterest(WalletBase wallet) => + DEuro(wallet as EthereumWallet).accruedInterest; + + @override + Future getDEuroInterestRate(WalletBase wallet) => + DEuro(wallet as EthereumWallet).interestRate; + + @override + Future getDEuroSavingsApproved(WalletBase wallet) => + DEuro(wallet as EthereumWallet).approvedBalance; + + @override + Future addDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) => + DEuro(wallet as EthereumWallet) + .depositSavings(amount, priority as EVMChainTransactionPriority); + + @override + Future removeDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) => + DEuro(wallet as EthereumWallet) + .withdrawSavings(amount, priority as EVMChainTransactionPriority); + + @override + Future enableDEuroSaving(WalletBase wallet, TransactionPriority priority) => + DEuro(wallet as EthereumWallet).enableSavings(priority as EVMChainTransactionPriority); } diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart index a2c27b745..c2e2f385c 100644 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class ChainflipExchangeProvider extends ExchangeProvider { ChainflipExchangeProvider({required this.tradesStore}) @@ -275,7 +275,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { Future> _getRequest(String path, Map params) async { final uri = Uri.https(_baseURL, path, params); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if ((response.statusCode != 200) || (response.body.contains('error'))) { throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); @@ -287,7 +288,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { Future?> _getStatus(Map params) async { final uri = Uri.https(_baseURL, _txInfoPath, params); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) return null; diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 79f8d70d4..dbc4cc3ab 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -11,12 +11,11 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/distribution_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class ChangeNowExchangeProvider extends ExchangeProvider { ChangeNowExchangeProvider({required SettingsStore settingsStore}) @@ -30,7 +29,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { ]; static final apiKey = - DeviceInfo.instance.isMobile ? secrets.changeNowApiKey : secrets.changeNowApiKeyDesktop; + isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey; static const apiAuthority = 'api.changenow.io'; static const createTradePath = '/v2/exchange'; static const findTradeByIdPath = '/v2/exchange/by-id'; @@ -73,7 +72,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { 'flow': _getFlow(isFixedRateMode) }; final uri = Uri.https(apiAuthority, rangePath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -118,7 +117,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { params['fromAmount'] = amount.toString(); final uri = Uri.https(apiAuthority, estimatedAmountPath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['fromAmount'].toString()); final toAmount = double.parse(responseJSON['toAmount'].toString()); @@ -177,7 +177,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider { } final uri = Uri.https(apiAuthority, createTradePath); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -220,7 +224,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final headers = {apiHeaderKey: apiKey}; final params = {'id': id}; final uri = Uri.https(apiAuthority, findTradeByIdPath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 43f63b8ca..0b7491bf5 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -9,14 +9,15 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class ExolixExchangeProvider extends ExchangeProvider { ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - static final apiKey = secrets.exolixApiKey; + static final apiKey = isMoneroOnly ? secrets.exolixMoneroApiKey : secrets.exolixCakeWalletApiKey; static const apiBaseUrl = 'exolix.com'; static const transactionsPath = '/api/v2/transactions'; static const ratePath = '/api/v2/rate'; @@ -86,8 +87,9 @@ class ExolixExchangeProvider extends ExchangeProvider { // Maximum of 2 attempts to fetch limits for (int i = 0; i < 2; i++) { final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + + if (response.statusCode == 200) { final responseJSON = json.decode(response.body) as Map; final minAmount = responseJSON['minAmount']; @@ -133,7 +135,8 @@ class ExolixExchangeProvider extends ExchangeProvider { params['amount'] = amount.toString(); final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; if (response.statusCode != 200) { @@ -172,7 +175,12 @@ class ExolixExchangeProvider extends ExchangeProvider { body['amount'] = request.fromAmount; final uri = Uri.https(apiBaseUrl, transactionsPath); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -214,8 +222,8 @@ class ExolixExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final findTradeByIdPath = '$transactionsPath/$id'; final uri = Uri.https(apiBaseUrl, findTradeByIdPath); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); if (response.statusCode == 400) { diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index d07297fcf..1fe1e9e35 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -10,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; class LetsExchangeExchangeProvider extends ExchangeProvider { LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -152,7 +152,11 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { final uri = Uri.https(_baseUrl, isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams); - final response = await http.post(uri, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + ); + if (response.statusCode != 200) { throw Exception('LetsExchange create trade failed: ${response.body}'); @@ -218,7 +222,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { }; final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); - final response = await http.get(url, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); + if (response.statusCode != 200) { throw Exception('LetsExchange fetch trade failed: ${response.body}'); @@ -266,7 +271,11 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { try { final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params); - final response = await http.post(uri, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + ); + if (response.statusCode != 200) { throw Exception('LetsExchange fetch info failed: ${response.body}'); } diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 12ec59100..3c9b2d951 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; @@ -11,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class SideShiftExchangeProvider extends ExchangeProvider { SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -60,8 +59,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future checkIsAvailable() async { const url = apiBaseUrl + permissionPath; final uri = Uri.parse(url); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error']['message'] as String; @@ -90,7 +89,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { "$apiBaseUrl$rangePath/${fromCurrency.title.toLowerCase()}-$fromNetwork/${toCurrency.title.toLowerCase()}-$toNetwork"; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; @@ -137,7 +137,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount"; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; if (response.statusCode == 500) { @@ -186,7 +187,12 @@ class SideShiftExchangeProvider extends ExchangeProvider { final headers = {'Content-Type': 'application/json'}; final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { if (response.statusCode == 400) { @@ -227,8 +233,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final url = apiBaseUrl + orderPath + '/' + id; final uri = Uri.parse(url); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) { throw TradeNotFoundException(id, provider: description); } @@ -281,7 +287,12 @@ class SideShiftExchangeProvider extends ExchangeProvider { 'depositNetwork': _networkFor(request.fromCurrency), }; final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { if (response.statusCode == 400) { diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index 5391d5f89..cc02ac799 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -11,8 +11,8 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; class SimpleSwapExchangeProvider extends ExchangeProvider { SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -48,7 +48,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { @override Future checkIsAvailable() async { final uri = Uri.https(apiAuthority, getEstimatePath, {'api_key': apiKey}); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); return !(response.statusCode == 403); } @@ -66,7 +66,8 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, rangePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; @@ -104,10 +105,10 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { 'fixed': isFixedRateMode.toString() }; final uri = Uri.https(apiAuthority, getEstimatePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.body == "null") return 0.00; - final data = json.decode(response.body) as String; return double.parse(data) / amount; @@ -134,7 +135,12 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, createExchangePath, params); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200 && response.statusCode != 201) { if (response.statusCode == 400) { @@ -176,8 +182,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final params = {'api_key': apiKey, 'id': id}; final uri = Uri.https(apiAuthority, getExchangePath, params); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + + if (response.statusCode == 404) { throw TradeNotFoundException(id, provider: description); } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 11b8d768a..8c7efae65 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -10,8 +10,8 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart' as http; class StealthExExchangeProvider extends ExchangeProvider { StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -63,8 +63,12 @@ class StealthExExchangeProvider extends ExchangeProvider { }; try { - final response = await http.post(Uri.parse(_baseUrl + _rangePath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _rangePath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) { throw Exception('StealthEx fetch limits failed: ${response.body}'); } @@ -134,8 +138,12 @@ class StealthExExchangeProvider extends ExchangeProvider { 'additional_fee_percent': _additionalFeePercent, }; - final response = await http.post(Uri.parse(_baseUrl + _exchangesPath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _exchangesPath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { throw Exception('StealthEx create trade failed: ${response.body}'); @@ -202,8 +210,9 @@ class StealthExExchangeProvider extends ExchangeProvider { final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); - final response = await http.get(uri, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + + if (response.statusCode != 200) { throw Exception('StealthEx fetch trade failed: ${response.body}'); } @@ -260,8 +269,12 @@ class StealthExExchangeProvider extends ExchangeProvider { }; try { - final response = await http.post(Uri.parse(_baseUrl + _amountPath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _amountPath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) return {}; final responseJSON = json.decode(response.body) as Map; final rate = responseJSON['rate'] as Map?; diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index b86482335..f6c8332db 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -10,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class SwapTradeExchangeProvider extends ExchangeProvider { SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -47,10 +47,10 @@ class SwapTradeExchangeProvider extends ExchangeProvider { String get title => 'SwapTrade'; @override - bool get isAvailable => false; + bool get isAvailable => true; @override - bool get isEnabled => false; + bool get isEnabled => true; @override bool get supportsFixedRate => false; @@ -59,7 +59,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade; @override - Future checkIsAvailable() async => false; + Future checkIsAvailable() async => true; @override Future fetchLimits({ @@ -69,7 +69,8 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }) async { try { final uri = Uri.https(apiAuthority, getCoins); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; @@ -116,7 +117,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, getRate, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode != 200) @@ -153,7 +159,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, createOrder, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode == 400 || responseBody["success"] == false) { @@ -196,7 +207,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, order, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode == 400 || responseBody["success"] == false) { diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index aa7ab2d27..16246abac 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -7,10 +7,10 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; class ThorChainExchangeProvider extends ExchangeProvider { ThorChainExchangeProvider({required this.tradesStore}) @@ -23,6 +23,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { // CryptoCurrency.eth, CryptoCurrency.ltc, CryptoCurrency.bch, + CryptoCurrency.usdtbsc, // CryptoCurrency.aave, // CryptoCurrency.dai, // CryptoCurrency.gusd, @@ -164,7 +165,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { if (id.isEmpty) throw Exception('Trade id is empty'); final formattedId = id.startsWith('0x') ? id.substring(2) : id; final uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId'); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) { throw Exception('Trade not found for id: $formattedId'); @@ -217,8 +219,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { static Future?>? lookupAddressByName(String name) async { final uri = Uri.https(_baseURL, '$_nameLookUpPath$name'); - final response = await http.get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { return null; } @@ -244,8 +246,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { Future> _getSwapQuote(Map params) async { Uri uri = Uri.https(_baseNodeURL, _quotePath, params); - final response = await http.get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw Exception('Unexpected HTTP status: ${response.statusCode}'); } @@ -258,7 +260,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { } String _normalizeCurrency(CryptoCurrency currency) { - final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title; + final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.tag ?? currency.title; return '$networkTitle.${currency.title}'; } diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 26a9b2e35..cc8d8fa60 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -8,9 +8,10 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class TrocadorExchangeProvider extends ExchangeProvider { TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}}) @@ -51,9 +52,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { CryptoCurrency.zaddr, ]; - static const apiKey = secrets.trocadorApiKey; - static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; + static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey; static const clearNetAuthority = 'api.trocador.app'; + static const onionApiAuthority = clearNetAuthority; + // static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; static const markup = secrets.trocadorExchangeMarkup; static const newRatePath = '/new_rate'; static const createTradePath = '/new_trade'; @@ -97,7 +99,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(coinPath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -138,12 +141,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(newRatePath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + final responseJSON = json.decode(response.body) as Map; - - if (responseJSON['error'] != null) throw Exception(responseJSON['error']); - final fromAmount = double.parse(responseJSON['amount_from'].toString()); final toAmount = double.parse(responseJSON['amount_to'].toString()); final rateId = responseJSON['trade_id'] as String? ?? ''; @@ -206,8 +207,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { params['provider'] = _provider.first as String; final uri = await _getUri(createTradePath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] as String; @@ -230,6 +232,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { final providerName = responseJSON['provider'] as String; final amount = responseJSON['amount_from']?.toString(); final receiveAmount = responseJSON['amount_to']?.toString(); + final addressProviderMemo = responseJSON['address_provider_memo'] as String?; return Trade( id: id, @@ -247,15 +250,17 @@ class TrocadorExchangeProvider extends ExchangeProvider { receiveAmount: receiveAmount ?? request.toAmount, payoutAddress: payoutAddress, isSendAll: isSendAll, + extraId: addressProviderMemo, ); } @override Future findTradeById({required String id}) async { final uri = await _getUri(tradePath, {'id': id}); - return get(uri, headers: {'API-Key': apiKey}).then((response) { + return ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}).then((response) async { if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); + final responseListJson = json.decode(response.body) as List; final responseJSON = responseListJson.first; @@ -290,7 +295,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { Future> fetchProviders() async { final uri = await _getUri(providersListPath, {'api_key': apiKey}); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -342,6 +348,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { return 'TRC20'; case 'LN': return 'Lightning'; + case 'BSC': + return 'BEP20'; default: return tag.toLowerCase(); } @@ -353,7 +361,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { if (useTorOnly) return uri; try { - await get(uri); + await ProxyWrapper().get(clearnetUri: uri); return uri; } catch (e) { diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index 5611d6855..e70eab568 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -10,8 +10,7 @@ import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; - +import 'package:cw_core/utils/proxy_wrapper.dart'; class XOSwapExchangeProvider extends ExchangeProvider { XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -72,7 +71,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { final uri = Uri.https(_apiAuthority, _apiPath + _assets, {'networks': normalizedNetwork, 'query': currency.title}); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}'); } @@ -102,7 +102,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { if (curFrom == null || curTo == null) return []; final pairId = curFrom + '_' + curTo; final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath'); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) return []; return json.decode(response.body) as List; } catch (e) { @@ -205,7 +206,12 @@ class XOSwapExchangeProvider extends ExchangeProvider { 'pairId': pairId, }; - final response = await http.post(uri, headers: _headers, body: json.encode(payload)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: _headers, + body: json.encode(payload), + ); + if (response.statusCode != 201) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] ?? 'Unknown error'; @@ -254,7 +260,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { try { final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id'); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { final responseJSON = json.decode(response.body) as Map; if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') { diff --git a/lib/main.dart b/lib/main.dart index 43b5951df..709304939 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,9 +25,12 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/core/material_base_theme.dart'; +import 'package:cake_wallet/themes/utils/theme_provider.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; @@ -38,6 +41,8 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; @@ -50,6 +55,7 @@ import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; import 'package:logging/logging.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -86,6 +92,9 @@ Future runAppWithZone({Key? topLevelKey}) async { ledgerFile.writeAsStringSync("$content\n${event.message}"); }); } + if (FeatureFlag.hasDevOptions) { + ProxyWrapper.logger = MemoryProxyLogger(); + } runApp(App(key: topLevelKey)); isAppRunning = true; @@ -193,8 +202,8 @@ Future initializeAppConfigs({bool loadWallet = true}) async { final powNodes = await CakeHive.openBox(Node.boxName + "pow"); // must be different from Node.boxName final transactionDescriptions = await CakeHive.openBox( - TransactionDescription.boxName, - encryptionKey: transactionDescriptionsBoxKey); + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey); final trades = await CakeHive.openBox(Trade.boxName, encryptionKey: tradesBoxKey); final orders = await CakeHive.openBox(Order.boxName, encryptionKey: ordersBoxKey); final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); @@ -206,8 +215,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { final havenSeedStoreBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey); - final havenSeedStore = await CakeHive.openBox( - HavenSeedStore.boxName, + final havenSeedStore = await CakeHive.openBox(HavenSeedStore.boxName, encryptionKey: havenSeedStoreBoxKey); await initialSetup( @@ -232,25 +240,26 @@ Future initializeAppConfigs({bool loadWallet = true}) async { ); } -Future initialSetup( - {required bool loadWallet, - required SharedPreferences sharedPreferences, - required Box nodes, - required Box powNodes, - required Box walletInfoSource, - required Box contactSource, - required Box tradesSource, - required Box ordersSource, - // required FiatConvertationService fiatConvertationService, - required Box