From 6499aa2d6f8d86f547490845950ece0d2b1a70a0 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Mon, 16 Dec 2024 18:08:56 -0500 Subject: [PATCH 01/30] generic-enhancements (#1886) * Finishing updating docs and community links * Add missing images --- .github/ISSUE_TEMPLATE/config.yml | 2 +- assets/images/discord.png | Bin 0 -> 15968 bytes assets/images/discourse.png | Bin 0 -> 58169 bytes .../screens/dashboard/pages/balance_page.dart | 2 +- lib/src/screens/welcome/welcome_page.dart | 2 +- lib/view_model/support_view_model.dart | 192 +++++++++--------- 6 files changed, 97 insertions(+), 101 deletions(-) create mode 100644 assets/images/discord.png create mode 100644 assets/images/discourse.png diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9a5308e22..f8cc8f9ca 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,7 +4,7 @@ contact_links: url: https://github.com/cake-tech/cake_wallet/discussions/new?category=feature-requests about: Suggest an idea for Cake Wallet - name: Not sure where to start? - url: https://guides.cakewallet.com + url: https://docs.cakewallet.com about: Start by reading checking out the guides! - name: Need help? url: https://cakewallet.com/#contact diff --git a/assets/images/discord.png b/assets/images/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..23fe37a36221a7e49699f0210b45dd05fe876db7 GIT binary patch literal 15968 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 literal 0 HcmV?d00001 diff --git a/assets/images/discourse.png b/assets/images/discourse.png new file mode 100644 index 0000000000000000000000000000000000000000..b8bab2c5dcfe374de7d9454faf79629a827d4d67 GIT binary patch literal 58169 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 NSJnQ#SlRmS{{d^IB5eQw literal 0 HcmV?d00001 diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index a0886ab9b..a71a6288b 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -384,7 +384,7 @@ class CryptoBalanceWidget extends StatelessWidget { behavior: HitTestBehavior.opaque, onTap: () => launchUrl( Uri.parse( - "https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"), + "https://docs.cakewallet.com/cryptos/litecoin/#mweb"), mode: LaunchMode.externalApplication, ), child: Text( diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index f76c7723a..b45322996 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -25,7 +25,7 @@ class WelcomePage extends BasePage { @override Widget trailing(BuildContext context) { - final Uri _url = Uri.parse('https://guides.cakewallet.com/docs/basic-features/basic-features/'); + final Uri _url = Uri.parse('https://docs.cakewallet.com/get-started/setup/create-first-wallet/'); return IconButton( icon: Icon(Icons.info_outline), onPressed: () async { diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index f6f1cba0f..69659916f 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -11,102 +11,99 @@ class SupportViewModel = SupportViewModelBase with _$SupportViewModel; abstract class SupportViewModelBase with Store { SupportViewModelBase() - : items = [ - LinkListItem( - title: 'Email', - icon: 'assets/images/support_icon.png', - linkTitle: 'support@cakewallet.com', - link: 'mailto:support@cakewallet.com'), - if (!isMoneroOnly) - LinkListItem( - title: 'Website', - icon: 'assets/images/global.png', - linkTitle: 'cakewallet.com', - link: 'https://cakewallet.com'), - if (!isMoneroOnly) - LinkListItem( - title: 'GitHub', - icon: 'assets/images/github.png', - hasIconColor: true, - linkTitle: S.current.apk_update, - link: 'https://github.com/cake-tech/cake_wallet/releases'), - LinkListItem( - title: 'Discord', - icon: 'assets/images/discord.png', - linkTitle: 'discord.gg/pwmWa6aFpX', - link: 'https://discord.gg/pwmWa6aFpX'), - LinkListItem( - title: 'Telegram', - icon: 'assets/images/Telegram.png', - linkTitle: 't.me/cakewallet', - link: 'https://t.me/cakewalletannouncements'), - LinkListItem( - title: 'Telegram Support Bot', - icon: 'assets/images/Telegram.png', - linkTitle: '@cakewallet_bot', - link: 'https://t.me/cakewallet_bot'), - LinkListItem( - title: 'ChangeNow', - icon: 'assets/images/change_now.png', - linkTitle: 'support@changenow.io', - link: 'mailto:support@changenow.io'), - LinkListItem( - title: 'SideShift', - icon: 'assets/images/sideshift.png', - linkTitle: 'help.sideshift.ai', - link: 'https://help.sideshift.ai/en/'), - LinkListItem( - title: 'SimpleSwap', - icon: 'assets/images/simpleSwap.png', - linkTitle: 'support@simpleswap.io', - link: 'mailto:support@simpleswap.io'), - LinkListItem( - title: 'Exolix', - icon: 'assets/images/exolix.png', - linkTitle: 'support@exolix.com', - link: 'mailto:support@exolix.com'), - LinkListItem( - title: 'Quantex', - icon: 'assets/images/quantex.png', - linkTitle: 'help.myquantex.com', - link: 'mailto:support@exolix.com'), - LinkListItem( - title: 'Trocador', - icon: 'assets/images/trocador.png', - linkTitle: 'mail@trocador.app', - link: 'mailto:mail@trocador.app'), - LinkListItem( - title: 'Onramper', - icon: 'assets/images/onramper_dark.png', - lightIcon: 'assets/images/onramper_light.png', - linkTitle: 'View exchanges', - link: 'https://docs.cakewallet.com/support/buy/#onramper'), - LinkListItem( - title: 'DFX', - icon: 'assets/images/dfx_dark.png', - lightIcon: 'assets/images/dfx_light.png', - linkTitle: 'support@dfx.swiss', - link: 'mailto:support@dfx.swiss'), - if (!isMoneroOnly) ... [ - LinkListItem( - title: 'MoonPay', - icon: 'assets/images/moonpay.png', - linkTitle: S.current.submit_request, - link: 'https://support.moonpay.com/hc/en-gb/requests/new'), - LinkListItem( - title: 'Robinhood Connect', - icon: 'assets/images/robinhood_dark.png', - lightIcon: 'assets/images/robinhood_light.png', - linkTitle: S.current.submit_request, - link: 'https://robinhood.com/contact') - ] - //LinkListItem( - // title: 'Yat', - // icon: 'assets/images/yat_mini_logo.png', - // hasIconColor: true, - // linkTitle: 'support@y.at', - // link: 'mailto:support@y.at') - ]; + : items = [ + LinkListItem( + title: 'Email', + icon: 'assets/images/support_icon.png', + linkTitle: 'support@cakewallet.com', + link: 'mailto:support@cakewallet.com'), + LinkListItem( + title: 'Website', + icon: 'assets/images/global.png', + linkTitle: 'cakewallet.com', + link: 'https://cakewallet.com'), + LinkListItem( + title: 'Forum', + icon: 'assets/images/discourse.png', + linkTitle: 'forum.cakewallet.com', + link: 'https://forum.cakewallet.com'), + LinkListItem( + title: 'GitHub', + icon: 'assets/images/github.png', + hasIconColor: true, + linkTitle: S.current.apk_update, + link: 'https://github.com/cake-tech/cake_wallet/releases'), + LinkListItem( + title: 'Discord', + icon: 'assets/images/discord.png', + linkTitle: 'discord.gg/pwmWa6aFpX', + link: 'https://discord.gg/pwmWa6aFpX'), + LinkListItem( + title: 'Telegram', + icon: 'assets/images/Telegram.png', + linkTitle: 't.me/cakewallet', + link: 'https://t.me/cakewalletannouncements'), + LinkListItem( + title: 'Telegram Support Bot', + icon: 'assets/images/Telegram.png', + linkTitle: '@cakewallet_bot', + link: 'https://t.me/cakewallet_bot'), + LinkListItem( + title: 'ChangeNow', + icon: 'assets/images/change_now.png', + linkTitle: 'support@changenow.io', + link: 'mailto:support@changenow.io'), + LinkListItem( + title: 'SideShift', + icon: 'assets/images/sideshift.png', + linkTitle: 'help.sideshift.ai', + link: 'https://help.sideshift.ai/en/'), + LinkListItem( + title: 'SimpleSwap', + icon: 'assets/images/simpleSwap.png', + linkTitle: 'support@simpleswap.io', + link: 'mailto:support@simpleswap.io'), + LinkListItem( + title: 'Exolix', + icon: 'assets/images/exolix.png', + linkTitle: 'support@exolix.com', + link: 'mailto:support@exolix.com'), + LinkListItem( + title: 'Quantex', + icon: 'assets/images/quantex.png', + linkTitle: 'help.myquantex.com', + link: 'mailto:support@exolix.com'), + LinkListItem( + title: 'Trocador', + icon: 'assets/images/trocador.png', + linkTitle: 'mail@trocador.app', + link: 'mailto:mail@trocador.app'), + LinkListItem( + title: 'Onramper', + icon: 'assets/images/onramper_dark.png', + lightIcon: 'assets/images/onramper_light.png', + linkTitle: 'View exchanges', + link: 'https://docs.cakewallet.com/support/buy/#onramper'), + LinkListItem( + title: 'DFX', + icon: 'assets/images/dfx_dark.png', + lightIcon: 'assets/images/dfx_light.png', + linkTitle: 'support@dfx.swiss', + link: 'mailto:support@dfx.swiss'), + if (!isMoneroOnly) ...[ + LinkListItem( + title: 'MoonPay', + icon: 'assets/images/moonpay.png', + linkTitle: S.current.submit_request, + link: 'https://support.moonpay.com/hc/en-gb/requests/new'), + LinkListItem( + title: 'Robinhood Connect', + icon: 'assets/images/robinhood_dark.png', + lightIcon: 'assets/images/robinhood_light.png', + linkTitle: S.current.submit_request, + link: 'https://robinhood.com/contact') + ] + ]; final docsUrl = 'https://docs.cakewallet.com'; @@ -114,8 +111,7 @@ abstract class SupportViewModelBase with Store { var supportUrl = "https://app.chatwoot.com/widget?website_token=${secrets.chatwootWebsiteToken}&locale=${locale}"; - if (authToken.isNotEmpty) - supportUrl += "&cw_conversation=$authToken"; + if (authToken.isNotEmpty) supportUrl += "&cw_conversation=$authToken"; return supportUrl; } From e4deb8455adb9d9cd24ccd0485b7b0cdbfb6305a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 17 Dec 2024 01:10:50 +0200 Subject: [PATCH 02/30] Update automated_integration_test.yml --- .../workflows/automated_integration_test.yml | 596 +++++++++--------- 1 file changed, 298 insertions(+), 298 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 588bc1821..51bc83ce0 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -1,298 +1,298 @@ -#name: Automated Integration Tests -# -#on: -# pull_request: -# branches: [main, CW-659-Transaction-History-Automated-Tests] -# workflow_dispatch: -# inputs: -# branch: -# description: "Branch name to build" -# required: true -# default: "main" -# -#jobs: -# Automated_integration_test: -# runs-on: ubuntu-20.04 -# strategy: -# fail-fast: false -# matrix: -# api-level: [29] -# # arch: [x86, x86_64] -# env: -# STORE_PASS: test@cake_wallet -# KEY_PASS: test@cake_wallet -# PR_NUMBER: ${{ github.event.number }} -# -# steps: -# - name: is pr -# if: github.event_name == 'pull_request' -# run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV -# -# - name: is not pr -# if: github.event_name != 'pull_request' -# run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV -# -# - name: Free Disk Space (Ubuntu) -# uses: insightsengineering/disk-space-reclaimer@v1 -# with: -# tools-cache: true -# android: false -# dotnet: true -# haskell: true -# large-packages: true -# swap-storage: true -# docker-images: true -# -# - uses: actions/checkout@v2 -# - uses: actions/setup-java@v2 -# with: -# distribution: "temurin" -# java-version: "17" -# - name: Configure placeholder git details -# run: | -# git config --global user.email "CI@cakewallet.com" -# git config --global user.name "Cake Github Actions" -# - name: Flutter action -# uses: subosito/flutter-action@v1 -# with: -# flutter-version: "3.24.0" -# channel: stable -# -# - name: Install package dependencies -# run: | -# sudo apt update -# sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang -# -# - name: Execute Build and Setup Commands -# run: | -# sudo mkdir -p /opt/android -# sudo chown $USER /opt/android -# cd /opt/android -# -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -# cargo install cargo-ndk -# git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }} -# cd cake_wallet/scripts/android/ -# ./install_ndk.sh -# source ./app_env.sh cakewallet -# chmod +x pubspec_gen.sh -# ./app_config.sh -# -# - name: Cache Externals -# id: cache-externals -# uses: actions/cache@v3 -# with: -# path: | -# /opt/android/cake_wallet/cw_haven/android/.cxx -# /opt/android/cake_wallet/scripts/monero_c/release -# key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} -# -# - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} -# name: Generate Externals -# run: | -# cd /opt/android/cake_wallet/scripts/android/ -# source ./app_env.sh cakewallet -# ./build_monero_all.sh -# -# - name: Install Flutter dependencies -# run: | -# cd /opt/android/cake_wallet -# flutter pub get -# -# -# - name: Install go and gomobile -# run: | -# # install go > 1.23: -# wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz -# sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz -# export PATH=$PATH:/usr/local/go/bin -# export PATH=$PATH:~/go/bin -# go install golang.org/x/mobile/cmd/gomobile@latest -# gomobile init -# -# - name: Build mwebd -# run: | -# # paths are reset after each step, so we need to set them again: -# export PATH=$PATH:/usr/local/go/bin -# export PATH=$PATH:~/go/bin -# cd /opt/android/cake_wallet/scripts/android/ -# ./build_mwebd.sh --dont-install -# -# - name: Generate KeyStore -# run: | -# cd /opt/android/cake_wallet/android/app -# keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS -# -# - name: Generate key properties -# run: | -# cd /opt/android/cake_wallet -# flutter packages pub run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS -# -# - name: Generate localization -# run: | -# cd /opt/android/cake_wallet -# flutter packages pub run tool/generate_localization.dart -# -# - name: Build generated code -# run: | -# cd /opt/android/cake_wallet -# ./model_generator.sh -# -# - name: Add secrets -# run: | -# cd /opt/android/cake_wallet -# touch lib/.secrets.g.dart -# touch cw_evm/lib/.secrets.g.dart -# touch cw_solana/lib/.secrets.g.dart -# touch cw_core/lib/.secrets.g.dart -# touch cw_nano/lib/.secrets.g.dart -# touch cw_tron/lib/.secrets.g.dart -# echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart -# echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart -# echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart -# echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart -# 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 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 -# echo "const moonPayApiKey = '${{ secrets.MOON_PAY_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const moonPaySecretKey = '${{ secrets.MOON_PAY_SECRET_KEY }}';" >> lib/.secrets.g.dart -# echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart -# echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart -# echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart -# 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 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 -# echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart -# 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 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 -# echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart -# echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart -# echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart -# echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart -# echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart -# echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart -# echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart -# echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart -# echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart -# echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart -# echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dart -# echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart -# echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart -# echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart -# echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart -# echo "const moneroTestWalletSeeds ='${{ secrets.MONERO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const moneroLegacyTestWalletSeeds = '${{ secrets.MONERO_LEGACY_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const bitcoinTestWalletSeeds = '${{ secrets.BITCOIN_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const ethereumTestWalletSeeds = '${{ secrets.ETHEREUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const litecoinTestWalletSeeds = '${{ secrets.LITECOIN_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const wowneroTestWalletSeeds = '${{ secrets.WOWNERO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart -# echo "const moneroTestWalletReceiveAddress = '${{ secrets.MONERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const bitcoinTestWalletReceiveAddress = '${{ secrets.BITCOIN_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const ethereumTestWalletReceiveAddress = '${{ secrets.ETHEREUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const litecoinTestWalletReceiveAddress = '${{ secrets.LITECOIN_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart -# echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart -# -# - name: Rename app -# run: | -# echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties -# -# - name: Build -# run: | -# cd /opt/android/cake_wallet -# flutter build apk --release --split-per-abi -# -# # - name: Rename apk file -# # run: | -# # cd /opt/android/cake_wallet/build/app/outputs/flutter-apk -# # mkdir test-apk -# # cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk -# # cp app-x86_64-release.apk test-apk/${{env.BRANCH_NAME}}_x86.apk -# -# # - name: Upload Artifact -# # uses: kittaakos/upload-artifact-as-is@v0 -# # with: -# # path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ -# -# # - name: Send Test APK -# # continue-on-error: true -# # uses: adrey/slack-file-upload-action@1.0.5 -# # with: -# # token: ${{ secrets.SLACK_APP_TOKEN }} -# # path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/${{env.BRANCH_NAME}}.apk -# # channel: ${{ secrets.SLACK_APK_CHANNEL }} -# # title: "${{ env.BRANCH_NAME }}.apk" -# # filename: ${{ env.BRANCH_NAME }}.apk -# # initial_comment: ${{ github.event.head_commit.message }} -# -# - name: 🦾 Enable KVM -# run: | -# echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules -# sudo udevadm control --reload-rules -# sudo udevadm trigger --name-match=kvm -# -# - name: 🦾 Cache gradle -# uses: gradle/actions/setup-gradle@v3 -# -# - name: 🦾 Cache AVD -# uses: actions/cache@v4 -# id: avd-cache -# with: -# path: | -# ~/.android/avd/* -# ~/.android/adb* -# key: avd-${{ matrix.api-level }} -# -# - name: 🦾 Create AVD and generate snapshot for caching -# if: steps.avd-cache.outputs.cache-hit != 'true' -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# api-level: ${{ matrix.api-level }} -# force-avd-creation: false -# # arch: ${{ matrix.arch }} -# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# working-directory: /opt/android/cake_wallet -# disable-animations: false -# script: echo "Generated AVD snapshot for caching." -# -# - name: 🚀 Integration tests on Android Emulator -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# api-level: ${{ matrix.api-level }} -# force-avd-creation: false -# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -# disable-animations: true -# working-directory: /opt/android/cake_wallet -# script: | -# chmod a+rx integration_test_runner.sh -# ./integration_test_runner.sh \ No newline at end of file +name: Automated Integration Tests + +on: + # pull_request: + # branches: [main, CW-659-Transaction-History-Automated-Tests] + workflow_dispatch: + inputs: + branch: + description: "Branch name to build" + required: true + default: "main" + +jobs: + Automated_integration_test: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + api-level: [29] + # arch: [x86, x86_64] + env: + STORE_PASS: test@cake_wallet + KEY_PASS: test@cake_wallet + PR_NUMBER: ${{ github.event.number }} + + steps: + - name: is pr + if: github.event_name == 'pull_request' + run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV + + - name: is not pr + if: github.event_name != 'pull_request' + run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV + + - name: Free Disk Space (Ubuntu) + uses: insightsengineering/disk-space-reclaimer@v1 + with: + tools-cache: true + android: false + dotnet: true + haskell: true + large-packages: true + swap-storage: true + docker-images: true + + - uses: actions/checkout@v2 + - uses: actions/setup-java@v2 + with: + distribution: "temurin" + java-version: "17" + - name: Configure placeholder git details + run: | + git config --global user.email "CI@cakewallet.com" + git config --global user.name "Cake Github Actions" + - name: Flutter action + uses: subosito/flutter-action@v1 + with: + flutter-version: "3.24.0" + channel: stable + + - name: Install package dependencies + run: | + sudo apt update + sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang + + - name: Execute Build and Setup Commands + run: | + sudo mkdir -p /opt/android + sudo chown $USER /opt/android + cd /opt/android + -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + cargo install cargo-ndk + git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }} + cd cake_wallet/scripts/android/ + ./install_ndk.sh + source ./app_env.sh cakewallet + chmod +x pubspec_gen.sh + ./app_config.sh + + - name: Cache Externals + id: cache-externals + uses: actions/cache@v3 + with: + path: | + /opt/android/cake_wallet/cw_haven/android/.cxx + /opt/android/cake_wallet/scripts/monero_c/release + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }} + + - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} + name: Generate Externals + run: | + cd /opt/android/cake_wallet/scripts/android/ + source ./app_env.sh cakewallet + ./build_monero_all.sh + + - name: Install Flutter dependencies + run: | + cd /opt/android/cake_wallet + flutter pub get + + + - name: Install go and gomobile + run: | + # install go > 1.23: + wget https://go.dev/dl/go1.23.1.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.1.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + + - name: Build mwebd + run: | + # paths are reset after each step, so we need to set them again: + export PATH=$PATH:/usr/local/go/bin + export PATH=$PATH:~/go/bin + cd /opt/android/cake_wallet/scripts/android/ + ./build_mwebd.sh --dont-install + + - name: Generate KeyStore + run: | + cd /opt/android/cake_wallet/android/app + keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS + + - name: Generate key properties + run: | + cd /opt/android/cake_wallet + flutter packages pub run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS + + - name: Generate localization + run: | + cd /opt/android/cake_wallet + flutter packages pub run tool/generate_localization.dart + + - name: Build generated code + run: | + cd /opt/android/cake_wallet + ./model_generator.sh + + - name: Add secrets + run: | + cd /opt/android/cake_wallet + touch lib/.secrets.g.dart + touch cw_evm/lib/.secrets.g.dart + touch cw_solana/lib/.secrets.g.dart + touch cw_core/lib/.secrets.g.dart + touch cw_nano/lib/.secrets.g.dart + touch cw_tron/lib/.secrets.g.dart + echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart + echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart + echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart + 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 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 + echo "const moonPayApiKey = '${{ secrets.MOON_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const moonPaySecretKey = '${{ secrets.MOON_PAY_SECRET_KEY }}';" >> lib/.secrets.g.dart + echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart + echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart + echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart + 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 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 + echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + 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 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 + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart + echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart + echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart + echo "const moneroTestWalletSeeds ='${{ secrets.MONERO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const moneroLegacyTestWalletSeeds = '${{ secrets.MONERO_LEGACY_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const bitcoinTestWalletSeeds = '${{ secrets.BITCOIN_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const ethereumTestWalletSeeds = '${{ secrets.ETHEREUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const litecoinTestWalletSeeds = '${{ secrets.LITECOIN_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const wowneroTestWalletSeeds = '${{ secrets.WOWNERO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const moneroTestWalletReceiveAddress = '${{ secrets.MONERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const bitcoinTestWalletReceiveAddress = '${{ secrets.BITCOIN_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const ethereumTestWalletReceiveAddress = '${{ secrets.ETHEREUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const litecoinTestWalletReceiveAddress = '${{ secrets.LITECOIN_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart + + - name: Rename app + run: | + echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + + - name: Build + run: | + cd /opt/android/cake_wallet + flutter build apk --release --split-per-abi + + # - name: Rename apk file + # run: | + # cd /opt/android/cake_wallet/build/app/outputs/flutter-apk + # mkdir test-apk + # cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk + # cp app-x86_64-release.apk test-apk/${{env.BRANCH_NAME}}_x86.apk + + # - name: Upload Artifact + # uses: kittaakos/upload-artifact-as-is@v0 + # with: + # path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ + + # - name: Send Test APK + # continue-on-error: true + # uses: adrey/slack-file-upload-action@1.0.5 + # with: + # token: ${{ secrets.SLACK_APP_TOKEN }} + # path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/${{env.BRANCH_NAME}}.apk + # channel: ${{ secrets.SLACK_APK_CHANNEL }} + # title: "${{ env.BRANCH_NAME }}.apk" + # filename: ${{ env.BRANCH_NAME }}.apk + # initial_comment: ${{ github.event.head_commit.message }} + + - name: 🦾 Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: 🦾 Cache gradle + uses: gradle/actions/setup-gradle@v3 + + - name: 🦾 Cache AVD + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + + - name: 🦾 Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + # arch: ${{ matrix.arch }} + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + working-directory: /opt/android/cake_wallet + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: 🚀 Integration tests on Android Emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + working-directory: /opt/android/cake_wallet + script: | + chmod a+rx integration_test_runner.sh + ./integration_test_runner.sh From b1751f1fd68743d8369ca7c20c073484b0d31267 Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 17 Dec 2024 15:19:13 +0200 Subject: [PATCH 03/30] fix pending LtC transaction (#1887) --- cw_bitcoin/lib/litecoin_wallet.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index f7cc20bcd..79dcbf415 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -758,6 +758,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) return false; if (!tx.isPending) return false; + final isMwebTx = (tx.inputAddresses?.any((addr) => addr.contains("mweb")) ?? false) || + (tx.outputAddresses?.any((addr) => addr.contains("mweb")) ?? false); + + if (!isMwebTx) { + return false; + } + final outputId = [], target = {}; final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; From 77c4eaaf4f748abcbfe3821f4c3d0ed6626829f0 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 17 Dec 2024 19:57:57 +0100 Subject: [PATCH 04/30] More Ledger Monero Fixes (#1888) * More Ledger Monero Fixes * Minor fixes --- .../connect_device/connect_device_page.dart | 20 +++++-- .../hardware_wallet/ledger_view_model.dart | 59 +++++++++++-------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 5e94c78a4..5e52b887c 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -92,6 +92,7 @@ class ConnectDevicePageBodyState extends State { late StreamSubscription? _bleRefresh = null; bool longWait = false; + Timer? _longWaitTimer; @override void initState() { @@ -108,7 +109,7 @@ class ConnectDevicePageBodyState extends State { Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); } - Future.delayed(Duration(seconds: 10), () { + _longWaitTimer = Timer(Duration(seconds: 10), () { if (widget.ledgerVM.bleIsEnabled && bleDevices.isEmpty) setState(() => longWait = true); }); @@ -121,6 +122,7 @@ class ConnectDevicePageBodyState extends State { _bleStateTimer?.cancel(); _usbRefreshTimer?.cancel(); _bleRefresh?.cancel(); + _longWaitTimer?.cancel(); widget.ledgerVM.stopScanning(); super.dispose(); @@ -206,7 +208,8 @@ class ConnectDevicePageBodyState extends State { offstage: !longWait, child: Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text(S.of(context).if_you_dont_see_your_device, + child: Text( + S.of(context).if_you_dont_see_your_device, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -235,7 +238,6 @@ class ConnectDevicePageBodyState extends State { ), ), ), - if (bleDevices.length > 0) ...[ Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), @@ -277,7 +279,9 @@ class ConnectDevicePageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.titleColor, + color: Theme.of(context) + .extension()! + .titleColor, ), ), ), @@ -299,8 +303,12 @@ class ConnectDevicePageBodyState extends State { if (widget.allowChangeWallet) ...[ PrimaryButton( text: S.of(context).wallets, - color: Theme.of(context).extension()!.createNewWalletButtonBackgroundColor, - textColor: Theme.of(context).extension()!.restoreWalletButtonTextColor, + color: Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor, + textColor: Theme.of(context) + .extension()! + .restoreWalletButtonTextColor, onPressed: _onChangeWallet, ) ], diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index b48f641a2..4c084c778 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -99,47 +99,54 @@ abstract class LedgerViewModelBase with Store { } Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + _isConnecting = true; + _connectingWalletType = type; if (isConnected) { try { - await _connectionChangeListener?.cancel(); - _connectionChangeListener = null; await _connection!.disconnect().catchError((_) {}); } catch (_) {} } + final ledger = device.connectionType == sdk.ConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB; - - if (_connectionChangeListener == null) { - _connectionChangeListener = ledger.deviceStateChanges.listen((event) { - printV('Ledger Device State Changed: $event'); - if (event == sdk.BleConnectionState.disconnected) { - _connection = null; - if (type == WalletType.monero) { - monero!.resetLedgerConnection(); - - Navigator.of( navigatorKey.currentContext!).pushNamed( - Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: WalletType.monero, - allowChangeWallet: true, - isReconnect: true, - onConnectDevice: (context, ledgerVM) async { - Navigator.of(context).pop(); - }, - ), - ); - } - } - }); + if (_connectionChangeSubscription == null) { + _connectionChangeSubscription = ledger.deviceStateChanges + .listen(_connectionChangeListener); } _connection = await ledger.connect(device); + _isConnecting = false; } - StreamSubscription? _connectionChangeListener; + StreamSubscription? _connectionChangeSubscription; sdk.LedgerConnection? _connection; + bool _isConnecting = true; + WalletType? _connectingWalletType; + + void _connectionChangeListener( + sdk.BleConnectionState event, ) { + printV('Ledger Device State Changed: $event'); + if (event == sdk.BleConnectionState.disconnected && !_isConnecting) { + _connection = null; + if (_connectingWalletType == WalletType.monero) { + monero!.resetLedgerConnection(); + + Navigator.of(navigatorKey.currentContext!).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: WalletType.monero, + allowChangeWallet: true, + isReconnect: true, + onConnectDevice: (context, ledgerVM) async { + Navigator.of(context).pop(); + }, + ), + ); + } + } + } bool get isConnected => _connection != null && !(_connection!.isDisconnected); From 502a7eaafa56b951fb4328ef5fefc8d3d82ac873 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 18 Dec 2024 16:46:36 +0200 Subject: [PATCH 05/30] v4.22.0 release candidate (#1879) * update app versions * change default solana node * update unspents more frequently to avoid unupdated state * temporary fix for polygon sending issue * change tron default node --- assets/solana_node_list.yml | 4 +-- assets/text/Monerocom_Release_Notes.txt | 5 ++-- assets/text/Release_Notes.txt | 7 +++-- assets/tron_node_list.yml | 3 +-- cw_bitcoin/lib/electrum_wallet.dart | 3 +++ cw_haven/pubspec.lock | 4 +-- cw_monero/pubspec.lock | 4 +-- cw_nano/pubspec.lock | 4 +-- cw_polygon/lib/polygon_client.dart | 8 +++--- cw_wownero/pubspec.lock | 4 +-- lib/entities/default_settings_migration.dart | 28 +++++++++++++++++--- lib/view_model/send/send_view_model.dart | 3 ++- scripts/android/app_env.sh | 8 +++--- scripts/ios/app_env.sh | 8 +++--- scripts/linux/app_env.sh | 4 +-- scripts/macos/app_env.sh | 8 +++--- scripts/windows/build_exe_installer.iss | 2 +- 17 files changed, 67 insertions(+), 40 deletions(-) diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index e5641d3f8..c96b370a8 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -1,10 +1,10 @@ - uri: rpc.ankr.com - is_default: true useSSL: true - uri: api.mainnet-beta.solana.com:443 useSSL: true - uri: solana-rpc.publicnode.com:443 - useSSL: true \ No newline at end of file + useSSL: true + is_default: true \ No newline at end of file diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 556010062..3a6706a26 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,2 +1,3 @@ -UI/UX enhancements -Bug fixes and app improvements \ No newline at end of file +Support Monero Ledger +Bug fixes +New designs and better user experience \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 556010062..f7d5e4d2c 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,2 +1,5 @@ -UI/UX enhancements -Bug fixes and app improvements \ No newline at end of file +Support Monero Ledger +Prepare for Haven removal +Improve Ethereum and Polygon sending process +Bug fixes +New designs and better user experience \ No newline at end of file diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index f9fd91179..1e34de712 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -4,9 +4,8 @@ useSSL: true - uri: api.trongrid.io - is_default: false + is_default: true useSSL: true - uri: trx.nownodes.io - is_default: true useSSL: true \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index d9041cba4..64eafb021 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1099,6 +1099,7 @@ abstract class ElectrumWalletBase )..addListener((transaction) async { transactionHistory.addOne(transaction); await updateBalance(); + await updateAllUnspents(); }); } @@ -1191,6 +1192,7 @@ abstract class ElectrumWalletBase .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); await updateBalance(); + await updateAllUnspents(); }); } catch (e) { throw e; @@ -1796,6 +1798,7 @@ abstract class ElectrumWalletBase }); transactionHistory.addOne(transaction); await updateBalance(); + await updateAllUnspents(); }); } catch (e) { throw e; diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index cb5d3e2c3..b6cae9f39 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -716,10 +716,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 9aa076a56..24be1c0dd 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -829,10 +829,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index f4d5c00f8..f426d96dc 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -874,10 +874,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index d9f96d1c9..cb8331977 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -22,11 +22,11 @@ class PolygonClient extends EVMChainClient { from: from, to: to, value: amount, - data: data, + // data: data, maxGas: maxGas, - gasPrice: gasPrice, - maxFeePerGas: maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas, + // gasPrice: gasPrice, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, ); } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 532bb236b..1e16fa089 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -757,10 +757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 92cb752cd..64370503f 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,13 +1,11 @@ import 'dart:convert'; import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; import 'package:cake_wallet/haven/haven.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/root_dir.dart'; @@ -42,8 +40,8 @@ const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'nano.nownodes.io'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; -const solanaDefaultNodeUri = 'rpc.ankr.com'; -const tronDefaultNodeUri = 'trx.nownodes.io'; +const solanaDefaultNodeUri = 'solana-rpc.publicnode.com:443'; +const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const moneroWorldNodeUri = '.moneroworld.com'; @@ -311,6 +309,27 @@ Future defaultSettingsMigration( type: WalletType.ethereum, useSSL: true, ); + _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.tron, + newDefaultUri: tronDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + useSSL: true, + oldUri: [ + 'tron-rpc.publicnode.com:443', + 'trx.nownodes.io', + ], + ); + _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.solana, + newDefaultUri: solanaDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentSolanaNodeIdKey, + useSSL: true, + oldUri: ['rpc.ankr.com'], + ); default: break; } @@ -332,6 +351,7 @@ Future _backupHavenSeeds(Box havenSeedStore) async { } return; } + /// generic function for changing any wallet default node /// instead of making a new function for each change Future _changeDefaultNode({ diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 40f877fef..daca4380f 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -26,7 +26,6 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -100,6 +99,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor outputs .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); + + unspentCoinsListViewModel.initialSetup(); } @observable diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 24f1f5a51..385414f24 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.18.2" -MONERO_COM_BUILD_NUMBER=108 +MONERO_COM_VERSION="1.19.0" +MONERO_COM_BUILD_NUMBER=109 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.21.2" -CAKEWALLET_BUILD_NUMBER=239 +CAKEWALLET_VERSION="4.22.0" +CAKEWALLET_BUILD_NUMBER=240 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 816ddd29a..580adad8e 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.18.2" -MONERO_COM_BUILD_NUMBER=105 +MONERO_COM_VERSION="1.19.0" +MONERO_COM_BUILD_NUMBER=106 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.21.2" -CAKEWALLET_BUILD_NUMBER=284 +CAKEWALLET_VERSION="4.22.0" +CAKEWALLET_BUILD_NUMBER=287 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 12f4cf8be..6d8557d6c 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.11.2" -CAKEWALLET_BUILD_NUMBER=40 +CAKEWALLET_VERSION="1.12.0" +CAKEWALLET_BUILD_NUMBER=41 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index bed3eb326..8970a35f7 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.8.1" -MONERO_COM_BUILD_NUMBER=37 +MONERO_COM_VERSION="1.9.0" +MONERO_COM_BUILD_NUMBER=38 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.14.2" -CAKEWALLET_BUILD_NUMBER=98 +CAKEWALLET_VERSION="1.15.0" +CAKEWALLET_BUILD_NUMBER=99 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 2cdd8c47c..155e65005 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "0.2.1" +#define MyAppVersion "0.3.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" From 3473cfe0a0540c49959c42cd58dbee5a96ad47ae Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 18 Dec 2024 17:25:11 +0200 Subject: [PATCH 06/30] fix currency text --- lib/view_model/send/send_view_model.dart | 6 +++--- scripts/macos/app_env.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index daca4380f..c1e0953a0 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -718,9 +718,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return '''${S.current.insufficient_funds_for_tx} \n\n''' - '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ETH (${parsedErrorMessageResult.balanceUsd} USD)\n\n''' - '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ETH (${parsedErrorMessageResult.txCostUsd} USD)\n\n''' - '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ETH (${parsedErrorMessageResult.overshotUsd} USD)'''; + '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' + '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n''' + '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})'''; } return errorMessage; diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 8970a35f7..37e7890c4 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -17,7 +17,7 @@ fi MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.9.0" -MONERO_COM_BUILD_NUMBER=38 +MONERO_COM_BUILD_NUMBER=39 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" From 301cb3b7e0f9f92c4df7d6a9e32d5b18f589cb6c Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 18 Dec 2024 19:55:48 +0200 Subject: [PATCH 07/30] minor: add examples for address resolver scheme [skip ci] --- lib/entities/parse_address_from_domain.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 54fa4e75a..5c5075737 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -134,6 +134,7 @@ class AddressResolver { Future resolve(BuildContext context, String text, CryptoCurrency currency) async { final ticker = currency.title; try { + // twitter handle example: @username if (text.startsWith('@') && !text.substring(1).contains('@')) { if (settingsStore.lookupsTwitter) { final formattedName = text.substring(1); @@ -165,6 +166,7 @@ class AddressResolver { } } + // Mastodon example: @username@hostname.xxx if (text.startsWith('@') && text.contains('@', 1) && text.contains('.', 1)) { if (settingsStore.lookupsMastodon) { final subText = text.substring(1); From a6f61c595f98858a58b47cf61f798c309c0ecc7a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:41:59 +0100 Subject: [PATCH 08/30] fix: Bug when building Monero.Com resulting from solana exceptions situated in send viewmodel from cw_solana package (#1893) --- cw_core/lib/exceptions.dart | 10 +++++++++- cw_solana/lib/solana_exceptions.dart | 16 +++++++++------- lib/view_model/send/send_view_model.dart | 9 ++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index 80bdd2886..cfd44f18f 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -27,7 +27,7 @@ class TransactionCommitFailed implements Exception { @override String toString() { - return errorMessage??"unknown error"; + return errorMessage ?? "unknown error"; } } @@ -44,3 +44,11 @@ class TransactionCommitFailedBIP68Final implements Exception {} class TransactionCommitFailedLessThanMin implements Exception {} class TransactionInputNotSupported implements Exception {} + +class SignNativeTokenTransactionRentException implements Exception {} + +class CreateAssociatedTokenAccountException implements Exception {} + +class SignSPLTokenTransactionRentException implements Exception {} + +class NoAssociatedTokenAccountException implements Exception {} diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart index 888c95068..697521c29 100644 --- a/cw_solana/lib/solana_exceptions.dart +++ b/cw_solana/lib/solana_exceptions.dart @@ -1,4 +1,5 @@ import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/exceptions.dart'; class SolanaTransactionCreationException implements Exception { final String exceptionMessage; @@ -20,18 +21,19 @@ class SolanaTransactionWrongBalanceException implements Exception { String toString() => exceptionMessage; } -class SolanaSignNativeTokenTransactionRentException implements Exception {} - -class SolanaCreateAssociatedTokenAccountException implements Exception { - final String exceptionMessage; +class SolanaSignNativeTokenTransactionRentException + extends SignNativeTokenTransactionRentException {} +class SolanaCreateAssociatedTokenAccountException extends CreateAssociatedTokenAccountException { SolanaCreateAssociatedTokenAccountException(this.exceptionMessage); + + final String exceptionMessage; } -class SolanaSignSPLTokenTransactionRentException implements Exception {} +class SolanaSignSPLTokenTransactionRentException extends SignSPLTokenTransactionRentException {} -class SolanaNoAssociatedTokenAccountException implements Exception { - const SolanaNoAssociatedTokenAccountException(this.account, this.mint); +class SolanaNoAssociatedTokenAccountException extends NoAssociatedTokenAccountException { + SolanaNoAssociatedTokenAccountException(this.account, this.mint); final String account; final String mint; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index c1e0953a0..f8599513c 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -26,7 +26,6 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; -import 'package:cw_solana/solana_exceptions.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -676,19 +675,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - if (error is SolanaSignNativeTokenTransactionRentException) { + if (error is SignNativeTokenTransactionRentException) { return S.current.solana_sign_native_transaction_rent_exception; } - if (error is SolanaCreateAssociatedTokenAccountException) { + if (error is CreateAssociatedTokenAccountException) { return S.current.solana_create_associated_token_account_exception; } - if (error is SolanaSignSPLTokenTransactionRentException) { + if (error is SignSPLTokenTransactionRentException) { return S.current.solana_sign_spl_token_transaction_rent_exception; } - if (error is SolanaNoAssociatedTokenAccountException) { + if (error is NoAssociatedTokenAccountException) { return S.current.solana_no_associated_token_account_exception; } From ac1c1989408f98a91b46cf0e58f183ff05b1ec6c Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 19 Dec 2024 22:48:52 +0200 Subject: [PATCH 09/30] minor --- .../seed/seed_verification/seed_verification_step_view.dart | 2 +- lib/utils/exception_handler.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart index c7b4b31e6..b8c1500dc 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart @@ -71,7 +71,7 @@ class SeedVerificationStepView extends StatelessWidget { return GestureDetector( onTap: () async { final isCorrectWord = walletSeedViewModel.isChosenWordCorrect(option); - final isSecondWrongEntry = walletSeedViewModel.wrongEntries == 2; + final isSecondWrongEntry = walletSeedViewModel.wrongEntries >= 2; if (!isCorrectWord) { await showBar( context, diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index d79dfe314..357a69fa6 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -221,7 +221,7 @@ class ExceptionHandler { // just ignoring until we find a solution to this issue or migrate from flutter secure storage "core/auth_service.dart:63", "core/key_service.dart:14", - "core/wallet_loading_service.dart:132", + "core/wallet_loading_service.dart:133", ]; static Future _addDeviceInfo(File file) async { From 20d30013d06d605e51d8283eb59b1c753af07c25 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 25 Dec 2024 21:26:15 +0200 Subject: [PATCH 10/30] support Adding query params to node url (#1901) * support Adding query params to node url * minor ui fix [skip ci] --- cw_core/lib/node.dart | 17 +++++------------ .../seed_verification_step_view.dart | 4 +++- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 18d2ffc44..aa7d27254 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -22,8 +22,8 @@ class Node extends HiveObject with Keyable { this.useSSL, this.trusted = false, this.socksProxyAddress, + this.path = '', String? uri, - String? path, WalletType? type, }) { if (uri != null) { @@ -32,9 +32,6 @@ class Node extends HiveObject with Keyable { if (type != null) { this.type = type; } - if (path != null) { - this.path = path; - } } Node.fromMap(Map map) @@ -95,19 +92,15 @@ class Node extends HiveObject with Keyable { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return createUriFromElectrumAddress(uriRaw, path ?? ''); + return createUriFromElectrumAddress(uriRaw, path!); case WalletType.nano: case WalletType.banano: - if (isSSL) { - return Uri.https(uriRaw, path ?? ''); - } else { - return Uri.http(uriRaw, path ?? ''); - } case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: case WalletType.tron: - return Uri.https(uriRaw, path ?? ''); + return Uri.parse( + "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") ? path : "/$path"}"); case WalletType.none: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -247,7 +240,7 @@ class Node extends HiveObject with Keyable { if (proxy == null) { return false; } - final proxyAddress = proxy!.split(':')[0]; + final proxyAddress = proxy.split(':')[0]; final proxyPort = int.parse(proxy.split(':')[1]); try { final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5)); diff --git a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart index b8c1500dc..8eb5e2cb7 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart @@ -81,7 +81,9 @@ class SeedVerificationStepView extends StatelessWidget { ); if (isSecondWrongEntry) { - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } } } }, From b79ef988c80a6e7000b2d906b49791a69e6733f7 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 25 Dec 2024 21:27:28 +0200 Subject: [PATCH 11/30] Cw 868 fix false synchronised status when the socket connection fails due to network issues (#1892) * prevent setting Synced status when the connection is lost * fallback for UTXO fetch failures * minor fix --- cw_bitcoin/lib/electrum.dart | 4 +- cw_bitcoin/lib/electrum_wallet.dart | 71 +++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 4fc4c1ad8..1f5c369e3 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -235,7 +235,7 @@ class ElectrumClient { return []; }); - Future>> getListUnspent(String scriptHash) async { + Future>?> getListUnspent(String scriptHash) async { final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]); if (result is List) { @@ -248,7 +248,7 @@ class ElectrumClient { }).toList(); } - return []; + return null; } Future>> getMempool(String scriptHash) => diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 64eafb021..eae830db1 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -478,6 +478,7 @@ abstract class ElectrumWalletBase if (alwaysScan == true) { _setListeners(walletInfo.restoreHeight); } else { + if (syncStatus is LostConnectionSyncStatus) return; syncStatus = SyncedSyncStatus(); } } catch (e, stacktrace) { @@ -1361,6 +1362,10 @@ abstract class ElectrumWalletBase Future updateAllUnspents() async { List updatedUnspentCoins = []; + final previousUnspentCoins = List.from(unspentCoins.where((utxo) => + utxo.bitcoinAddressRecord.type != SegwitAddresType.mweb && + utxo.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)); + if (hasSilentPaymentsScanning) { // Update unspents stored from scanned silent payment transactions transactionHistory.transactions.values.forEach((tx) { @@ -1377,13 +1382,27 @@ abstract class ElectrumWalletBase if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); - await Future.wait(walletAddresses.allAddresses + final addressFutures = walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); + .map((address) => fetchUnspent(address)) + .toList(); - unspentCoins = updatedUnspentCoins; + final results = await Future.wait(addressFutures); + final failedCount = results.where((result) => result == null).length; + + if (failedCount == 0) { + for (final result in results) { + updatedUnspentCoins.addAll(result!); + } + unspentCoins = updatedUnspentCoins; + } else { + unspentCoins = handleFailedUtxoFetch( + failedCount: failedCount, + previousUnspentCoins: previousUnspentCoins, + updatedUnspentCoins: updatedUnspentCoins, + results: results, + ); + } final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id); @@ -1396,6 +1415,38 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + List handleFailedUtxoFetch({ + required int failedCount, + required List previousUnspentCoins, + required List updatedUnspentCoins, + required List?> results, + }) { + + if (failedCount == results.length) { + printV("All UTXOs failed to fetch, falling back to previous UTXOs"); + return previousUnspentCoins; + } + + final successfulUtxos = []; + for (final result in results) { + if (result != null) { + successfulUtxos.addAll(result); + } + } + + if (failedCount > 0 && successfulUtxos.isEmpty) { + printV("Some UTXOs failed, but no successful UTXOs, falling back to previous UTXOs"); + return previousUnspentCoins; + } + + if (failedCount > 0) { + printV("Some UTXOs failed, updating with successful UTXOs"); + updatedUnspentCoins.addAll(successfulUtxos); + } + + return updatedUnspentCoins; + } + Future updateCoins(List newUnspentCoins) async { if (newUnspentCoins.isEmpty) { return; @@ -1427,15 +1478,17 @@ abstract class ElectrumWalletBase @action Future updateUnspentsForAddress(BitcoinAddressRecord address) async { final newUnspentCoins = await fetchUnspent(address); - await updateCoins(newUnspentCoins); + await updateCoins(newUnspentCoins ?? []); } @action - Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; + Future?> fetchUnspent(BitcoinAddressRecord address) async { List updatedUnspentCoins = []; - unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + + // Failed to fetch unspents + if (unspents == null) return null; await Future.wait(unspents.map((unspent) async { try { From c6b9f054cc761a0fbb21f983724805e8374d71ab Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 25 Dec 2024 21:27:46 +0200 Subject: [PATCH 12/30] Switch to SSL for Cake's Electrum and Monero nodes (#1899) * Force SSL for Electrum and Monero nodes Some Cleanup * minor [skip ci] * potential fix for transactions not cleared correctly [skip ci] * minor fix [skip ci] --- lib/core/wallet_loading_service.dart | 2 - lib/entities/default_settings_migration.dart | 69 ++++++++----------- .../evm_transaction_error_fees_handler.dart | 14 ++-- lib/main.dart | 2 +- .../wallet_connect/utils/string_parsing.dart | 5 ++ lib/utils/exception_handler.dart | 4 +- .../dashboard/dashboard_view_model.dart | 8 +-- 7 files changed, 49 insertions(+), 55 deletions(-) diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 6b1553443..f1996bae8 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -2,12 +2,10 @@ import 'dart:async'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 64370503f..9e06d25da 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -167,7 +167,11 @@ Future defaultSettingsMigration( break; case 18: - await addOnionNode(nodes); + await updateWalletTypeNodesWithNewNode( + nodes: nodes, + newNodeUri: "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081", + type: WalletType.monero, + ); break; case 19: @@ -261,15 +265,15 @@ Future defaultSettingsMigration( await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); break; case 42: - updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences); + _fixNodesUseSSLFlag(nodes); break; case 43: - await _updateCakeXmrNode(nodes); + _fixNodesUseSSLFlag(nodes); _deselectExchangeProvider(sharedPreferences, "THORChain"); _deselectExchangeProvider(sharedPreferences, "SimpleSwap"); break; case 44: - await _updateCakeXmrNode(nodes); + _fixNodesUseSSLFlag(nodes); await _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -297,14 +301,12 @@ Future defaultSettingsMigration( updateWalletTypeNodesWithNewNode( newNodeUri: 'matic.nownodes.io', - sharedPreferences: sharedPreferences, nodes: nodes, type: WalletType.polygon, useSSL: true, ); updateWalletTypeNodesWithNewNode( newNodeUri: 'eth.nownodes.io', - sharedPreferences: sharedPreferences, nodes: nodes, type: WalletType.ethereum, useSSL: true, @@ -330,6 +332,22 @@ Future defaultSettingsMigration( useSSL: true, oldUri: ['rpc.ankr.com'], ); + break; + case 46: + _fixNodesUseSSLFlag(nodes); + updateWalletTypeNodesWithNewNode( + newNodeUri: 'litecoin.stackwallet.com:20063', + nodes: nodes, + type: WalletType.litecoin, + useSSL: true, + ); + updateWalletTypeNodesWithNewNode( + newNodeUri: 'electrum-ltc.bysh.me:50002', + nodes: nodes, + type: WalletType.litecoin, + useSSL: true, + ); + break; default: break; } @@ -361,7 +379,8 @@ Future _changeDefaultNode({ required String newDefaultUri, required String currentNodePreferenceKey, required bool useSSL, - required List oldUri, // leave empty if you want to force replace the node regardless of the user's current node + required List + oldUri, // leave empty if you want to force replace the node regardless of the user's current node }) async { final currentNodeId = sharedPreferences.getInt(currentNodePreferenceKey); final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); @@ -389,11 +408,10 @@ Future _changeDefaultNode({ /// Generic function for adding a new Node for a Wallet Type. Future updateWalletTypeNodesWithNewNode({ - required SharedPreferences sharedPreferences, required Box nodes, required WalletType type, required String newNodeUri, - required bool useSSL, + bool? useSSL, }) async { // If it already exists in the box of nodes, no need to add it annymore. if (nodes.values.any((node) => node.uriRaw == newNodeUri)) return; @@ -407,26 +425,6 @@ Future updateWalletTypeNodesWithNewNode({ ); } -Future _updateCakeXmrNode(Box nodes) async { - final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri); - - if (node != null) { - node.trusted = true; - node.useSSL = true; - await node.save(); - } -} - -void updateBtcElectrumNodeToUseSSL(Box nodes, SharedPreferences sharedPreferences) { - final btcElectrumNode = - nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri); - - if (btcElectrumNode != null) { - btcElectrumNode.useSSL = true; - btcElectrumNode.save(); - } -} - void _deselectExchangeProvider(SharedPreferences sharedPreferences, String providerName) { final Map exchangeProvidersSelection = json.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") @@ -445,8 +443,10 @@ void _fixNodesUseSSLFlag(Box nodes) { switch (node.uriRaw) { case cakeWalletLitecoinElectrumUri: case cakeWalletBitcoinElectrumUri: + case newCakeWalletBitcoinUri: + case newCakeWalletMoneroUri: node.useSSL = true; - break; + node.trusted = true; } } } @@ -580,15 +580,6 @@ Future validateBitcoinSavedTransactionPriority(SharedPreferences sharedPre } } -Future addOnionNode(Box nodes) async { - final onionNodeUri = "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081"; - - // check if the user has this node before (added it manually) - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == onionNodeUri) == null) { - await nodes.add(Node(uri: onionNodeUri, type: WalletType.monero)); - } -} - Future replaceNodesMigration({required Box nodes}) async { final replaceNodes = { 'eu-node.cakewallet.io:18081': diff --git a/lib/entities/evm_transaction_error_fees_handler.dart b/lib/entities/evm_transaction_error_fees_handler.dart index 63f6e164d..b802f9883 100644 --- a/lib/entities/evm_transaction_error_fees_handler.dart +++ b/lib/entities/evm_transaction_error_fees_handler.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; + class EVMTransactionErrorFeesHandler { EVMTransactionErrorFeesHandler({ this.balanceWei, @@ -64,14 +66,14 @@ class EVMTransactionErrorFeesHandler { return EVMTransactionErrorFeesHandler( balanceWei: balanceWei.toString(), - balanceEth: balanceEth.toString().substring(0, 12), - balanceUsd: balanceUsd.toString().substring(0, 4), + balanceEth: balanceEth.toString().safeSubString(0, 12), + balanceUsd: balanceUsd.toString().safeSubString(0, 4), txCostWei: txCostWei.toString(), - txCostEth: txCostEth.toString().substring(0, 12), - txCostUsd: txCostUsd.toString().substring(0, 4), + txCostEth: txCostEth.toString().safeSubString(0, 12), + txCostUsd: txCostUsd.toString().safeSubString(0, 4), overshotWei: overshotWei.toString(), - overshotEth: overshotEth.toString().substring(0, 12), - overshotUsd: overshotUsd.toString().substring(0, 4), + overshotEth: overshotEth.toString().safeSubString(0, 12), + overshotUsd: overshotUsd.toString().safeSubString(0, 4), ); } else { // If any value is missing, return an error message diff --git a/lib/main.dart b/lib/main.dart index 510705105..fd25a1e9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,7 +215,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 45, + initialMigrationVersion: 46, ); } diff --git a/lib/src/screens/wallet_connect/utils/string_parsing.dart b/lib/src/screens/wallet_connect/utils/string_parsing.dart index b9fdca7b2..0aed1b9e9 100644 --- a/lib/src/screens/wallet_connect/utils/string_parsing.dart +++ b/lib/src/screens/wallet_connect/utils/string_parsing.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:convert/convert.dart'; @@ -13,4 +14,8 @@ extension StringParsing on String { return this; } + + String safeSubString(int start, int end) { + return this.substring(0, min(this.toString().length, 12)); + } } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 357a69fa6..66cbc61a0 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -219,9 +219,9 @@ class ExceptionHandler { // probably when the device was locked and then opened on Cake // this is solved by a restart of the app // just ignoring until we find a solution to this issue or migrate from flutter secure storage - "core/auth_service.dart:63", + "core/auth_service.dart:64", "core/key_service.dart:14", - "core/wallet_loading_service.dart:133", + "core/wallet_loading_service.dart:131", ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 808657f66..387c66511 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,13 +4,11 @@ import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -643,7 +641,7 @@ abstract class DashboardViewModelBase with Store { transactions.clear(); - transactions.addAll( + transactions = ObservableList.of( wallet.transactionHistory.transactions.values.map( (transaction) => TransactionListItem( transaction: transaction, @@ -705,7 +703,7 @@ abstract class DashboardViewModelBase with Store { monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll( + transactions = ObservableList.of( _accountTransactions.map( (transaction) => TransactionListItem( transaction: transaction, @@ -725,7 +723,7 @@ abstract class DashboardViewModelBase with Store { wow.wownero!.getCurrentAccount(wallet).id) .toList(); - transactions.addAll( + transactions = ObservableList.of( _accountTransactions.map( (transaction) => TransactionListItem( transaction: transaction, From 3e93a5ecb883b7be4289f88a60a6343da1ea567a Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 25 Dec 2024 20:28:05 +0100 Subject: [PATCH 13/30] fix: Generic fixes (#1897) --- .../seed/seed_verification/seed_verification_step_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart index 8eb5e2cb7..9fd70be05 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_step_view.dart @@ -70,6 +70,8 @@ class SeedVerificationStepView extends StatelessWidget { (option) { return GestureDetector( onTap: () async { + if (walletSeedViewModel.wrongEntries > 2) return; + final isCorrectWord = walletSeedViewModel.isChosenWordCorrect(option); final isSecondWrongEntry = walletSeedViewModel.wrongEntries >= 2; if (!isCorrectWord) { From ed12ff6afeae9085aa343a89c5fb44520eea032d Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 27 Dec 2024 00:42:36 +0200 Subject: [PATCH 14/30] change Solana node (#1903) * change Solana node * Fix reaching limit for fetching transactions --- .../workflows/automated_integration_test.yml | 2 + .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/solana_node_list.yml | 3 + cw_core/lib/exceptions.dart | 6 +- cw_solana/lib/solana_client.dart | 79 +++++++++++-------- cw_solana/lib/solana_exceptions.dart | 4 +- cw_solana/pubspec.yaml | 2 +- .../wallet_connect/web3wallet_service.dart | 17 ++-- lib/entities/default_settings_migration.dart | 15 +++- lib/view_model/send/send_view_model.dart | 2 +- pubspec_base.yaml | 2 +- tool/utils/secret_key.dart | 2 + 13 files changed, 90 insertions(+), 48 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 51bc83ce0..9eba75cc0 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -173,6 +173,7 @@ jobs: echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart @@ -185,6 +186,7 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index d98c0b77b..951b20dab 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -184,6 +184,7 @@ jobs: echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart @@ -197,6 +198,7 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index f690e0236..89c4af8f2 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -156,6 +156,7 @@ jobs: echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart 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 @@ -167,6 +168,7 @@ jobs: echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index c96b370a8..3ba74d980 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -7,4 +7,7 @@ - uri: solana-rpc.publicnode.com:443 useSSL: true +- + uri: solana-mainnet.core.chainstack.com + useSSL: true is_default: true \ No newline at end of file diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index cfd44f18f..885f5cb2b 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -47,7 +47,11 @@ class TransactionInputNotSupported implements Exception {} class SignNativeTokenTransactionRentException implements Exception {} -class CreateAssociatedTokenAccountException implements Exception {} +class CreateAssociatedTokenAccountException implements Exception { + final String errorMessage; + + CreateAssociatedTokenAccountException(this.errorMessage); +} class SignSPLTokenTransactionRentException implements Exception {} diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 95376c563..9447aad38 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -21,22 +21,23 @@ class SolanaWalletClient { bool connect(Node node) { try { - Uri? rpcUri; - String webSocketUrl; - bool isModifiedNodeUri = false; + Uri rpcUri = node.uri; + String webSocketUrl = 'wss://${node.uriRaw}'; if (node.uriRaw == 'rpc.ankr.com') { - isModifiedNodeUri = true; String ankrApiKey = secrets.ankrApiKey; rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; - } else { - webSocketUrl = 'wss://${node.uriRaw}'; + } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { + String chainStackApiKey = secrets.chainStackApiKey; + + rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); + webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; } _client = SolanaClient( - rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri, + rpcUrl: rpcUri, websocketUrl: Uri.parse(webSocketUrl), timeout: const Duration(minutes: 2), ); @@ -115,10 +116,14 @@ class SolanaWalletClient { final message = _getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol); - final recentBlockhash = await _getRecentBlockhash(commitment); + final latestBlockhash = await _getLatestBlockhash(commitment); - final estimatedFee = - _getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment); + final estimatedFee = _getFeeFromCompiledMessage( + message, + ownerKeypair.publicKey, + latestBlockhash, + commitment, + ); return estimatedFee; } @@ -131,13 +136,25 @@ class SolanaWalletClient { List transactions = []; try { - final response = await _client!.rpcClient.getTransactionsList( - publicKey, + final signatures = await _client!.rpcClient.getSignaturesForAddress( + publicKey.toBase58(), commitment: Commitment.confirmed, - limit: 1000, ); - for (final tx in response) { + final List transactionDetails = []; + for (int i = 0; i < signatures.length; i += 20) { + final response = await _client!.rpcClient.getMultipleTransactions( + signatures.sublist(i, math.min(i + 20, signatures.length)), + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + transactionDetails.addAll(response); + + // to avoid reaching the node RPS limit + await Future.delayed(Duration(milliseconds: 500)); + } + + for (final tx in transactionDetails) { if (tx.transaction is ParsedTransaction) { final parsedTx = (tx.transaction as ParsedTransaction); final message = parsedTx.message; @@ -310,16 +327,16 @@ class SolanaWalletClient { } } - Future _getRecentBlockhash(Commitment commitment) async { - final latestBlockhash = + Future _getLatestBlockhash(Commitment commitment) async { + final latestBlockHashResult = await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value; - final recentBlockhash = RecentBlockhash( - blockhash: latestBlockhash.blockhash, - feeCalculator: const FeeCalculator(lamportsPerSignature: 500), + final latestBlockhash = LatestBlockhash( + blockhash: latestBlockHashResult.blockhash, + lastValidBlockHeight: latestBlockHashResult.lastValidBlockHeight, ); - return recentBlockhash; + return latestBlockhash; } Message _getMessageForNativeTransaction( @@ -342,11 +359,11 @@ class SolanaWalletClient { Future _getFeeFromCompiledMessage( Message message, Ed25519HDPublicKey feePayer, - RecentBlockhash recentBlockhash, + LatestBlockhash latestBlockhash, Commitment commitment, ) async { final compile = message.compile( - recentBlockhash: recentBlockhash.blockhash, + recentBlockhash: latestBlockhash.blockhash, feePayer: feePayer, ); @@ -391,12 +408,12 @@ class SolanaWalletClient { final signers = [ownerKeypair]; - RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment); + LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, signers.first.publicKey, - recentBlockhash, + latestBlockhash, commitment, ); @@ -422,14 +439,14 @@ class SolanaWalletClient { message: updatedMessage, signers: signers, commitment: commitment, - recentBlockhash: recentBlockhash, + latestBlockhash: latestBlockhash, ); } else { signedTx = await _signTransactionInternal( message: message, signers: signers, commitment: commitment, - recentBlockhash: recentBlockhash, + latestBlockhash: latestBlockhash, ); } @@ -507,12 +524,12 @@ class SolanaWalletClient { final signers = [ownerKeypair]; - RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment); + LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, signers.first.publicKey, - recentBlockhash, + latestBlockhash, commitment, ); @@ -530,7 +547,7 @@ class SolanaWalletClient { message: message, signers: signers, commitment: commitment, - recentBlockhash: recentBlockhash, + latestBlockhash: latestBlockhash, ); sendTx() async => await sendTransaction( @@ -552,9 +569,9 @@ class SolanaWalletClient { required Message message, required List signers, required Commitment commitment, - required RecentBlockhash recentBlockhash, + required LatestBlockhash latestBlockhash, }) async { - final signedTx = await signTransaction(recentBlockhash, message, signers); + final signedTx = await signTransaction(latestBlockhash, message, signers); return signedTx; } diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart index 697521c29..96ba0bb6f 100644 --- a/cw_solana/lib/solana_exceptions.dart +++ b/cw_solana/lib/solana_exceptions.dart @@ -25,9 +25,7 @@ class SolanaSignNativeTokenTransactionRentException extends SignNativeTokenTransactionRentException {} class SolanaCreateAssociatedTokenAccountException extends CreateAssociatedTokenAccountException { - SolanaCreateAssociatedTokenAccountException(this.exceptionMessage); - - final String exceptionMessage; + SolanaCreateAssociatedTokenAccountException(super.errorMessage); } class SolanaSignSPLTokenTransactionRentException extends SignSPLTokenTransactionRentException {} diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 6fd5cd97c..807acdca8 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - solana: ^0.30.4 + solana: ^0.31.0+1 cw_core: path: ../cw_core http: ^1.1.0 diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index ad892a594..3740d3dfe 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -140,25 +140,24 @@ abstract class Web3WalletServiceBase with Store { for (final cId in SolanaChainId.values) { final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); - Uri? rpcUri; - String webSocketUrl; - bool isModifiedNodeUri = false; + Uri rpcUri = node.uri; + String webSocketUrl = 'wss://${node.uriRaw}'; if (node.uriRaw == 'rpc.ankr.com') { - isModifiedNodeUri = true; - - //A better way to handle this instead of adding this to the general secrets? String ankrApiKey = secrets.ankrApiKey; rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; - } else { - webSocketUrl = 'wss://${node.uriRaw}'; + } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { + String chainStackApiKey = secrets.chainStackApiKey; + + rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey'); + webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey'; } SolanaChainServiceImpl( reference: cId, - rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri, + rpcUrl: rpcUri, webSocketUrl: webSocketUrl, wcKeyService: walletKeyService, bottomSheetService: _bottomSheetHandler, diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 9e06d25da..96638621a 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -40,7 +40,7 @@ const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'nano.nownodes.io'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; -const solanaDefaultNodeUri = 'solana-rpc.publicnode.com:443'; +const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com'; const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; @@ -347,6 +347,19 @@ Future defaultSettingsMigration( type: WalletType.litecoin, useSSL: true, ); + _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.solana, + newDefaultUri: solanaDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentSolanaNodeIdKey, + useSSL: true, + oldUri: [ + 'rpc.ankr.com', + 'api.mainnet-beta.solana.com:443', + 'solana-rpc.publicnode.com:443', + ], + ); break; default: break; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index f8599513c..78bc867db 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -680,7 +680,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } if (error is CreateAssociatedTokenAccountException) { - return S.current.solana_create_associated_token_account_exception; + return "${S.current.solana_create_associated_token_account_exception}\n\n${error.errorMessage}"; } if (error is SignSPLTokenTransactionRentException) { diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 221f1d9bf..e87b5a44e 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -106,7 +106,7 @@ dependencies: flutter_svg: ^2.0.9 polyseed: ^0.0.6 nostr_tools: ^1.0.9 - solana: ^0.30.1 + solana: ^0.31.0+1 ledger_flutter_plus: ^1.4.1 hashlib: ^1.19.2 diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index e17a509d7..b7a581ff8 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -38,6 +38,7 @@ class SecretKey { SecretKey('walletConnectProjectId', () => ''), SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), + SecretKey('chainStackApiKey', () => ''), SecretKey('quantexExchangeMarkup', () => ''), SecretKey('seeds', () => ''), SecretKey('testCakePayApiKey', () => ''), @@ -86,6 +87,7 @@ class SecretKey { static final solanaSecrets = [ SecretKey('ankrApiKey', () => ''), + SecretKey('chainStackApiKey', () => ''), ]; static final nanoSecrets = [ From 542920a5128a6a80faa0459dc72bbbe1afe92539 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 27 Dec 2024 03:31:48 +0200 Subject: [PATCH 15/30] Remove payfura (#1904) * change Solana node * Fix reaching limit for fetching transactions * Remove payfura --- .../workflows/automated_integration_test.yml | 1 - .github/workflows/pr_test_build_android.yml | 1 - .github/workflows/pr_test_build_linux.yml | 1 - lib/buy/payfura/payfura_buy_provider.dart | 24 ------------------- lib/di.dart | 6 ----- tool/utils/secret_key.dart | 1 - 6 files changed, 34 deletions(-) delete mode 100644 lib/buy/payfura/payfura_buy_provider.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 9eba75cc0..b299c9340 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -171,7 +171,6 @@ jobs: 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 - echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 951b20dab..cdd0e40b4 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -182,7 +182,6 @@ jobs: 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 - echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 89c4af8f2..891327d1e 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -154,7 +154,6 @@ jobs: 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 - echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart diff --git a/lib/buy/payfura/payfura_buy_provider.dart b/lib/buy/payfura/payfura_buy_provider.dart deleted file mode 100644 index eb9104df0..000000000 --- a/lib/buy/payfura/payfura_buy_provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cw_core/wallet_base.dart'; - -class PayfuraBuyProvider { - PayfuraBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) - : this._settingsStore = settingsStore, - this._wallet = wallet; - - final SettingsStore _settingsStore; - final WalletBase _wallet; - - static const _baseUrl = 'exchange.payfura.com'; - - Uri requestUrl() { - return Uri.https(_baseUrl, '', { - 'apiKey': secrets.payfuraApiKey, - 'to': _wallet.currency.title, - 'from': _settingsStore.fiatCurrency.title, - 'walletAddress': '${_wallet.currency.title}:${_wallet.walletAddresses.address}', - 'mode': 'buy' - }); - } -} diff --git a/lib/di.dart b/lib/di.dart index 358f72a77..91ec692ef 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -11,7 +11,6 @@ import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/order.dart'; -import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/auth_service.dart'; @@ -1022,11 +1021,6 @@ Future setup({ getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); - getIt.registerFactory(() => PayfuraBuyProvider( - settingsStore: getIt.get().settingsStore, - wallet: getIt.get().wallet!, - )); - getIt.registerFactory(() => ExchangeViewModel( getIt.get(), _tradesSource, diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index b7a581ff8..5c316c54b 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -30,7 +30,6 @@ class SecretKey { SecretKey('twitterBearerToken', () => ''), SecretKey('anonPayReferralCode', () => ''), SecretKey('fiatApiKey', () => ''), - SecretKey('payfuraApiKey', () => ''), SecretKey('chatwootWebsiteToken', () => ''), SecretKey('exolixApiKey', () => ''), SecretKey('robinhoodApplicationId', () => ''), From b1c9be637f407d1f131d1ad269f6e0dbf01b59ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?rafael=20x=C9=B1r?= Date: Fri, 27 Dec 2024 00:01:41 -0300 Subject: [PATCH 16/30] fix: wrong card for vendor country (#1905) * fix: wrong card for vendor country * Update lib/cake_pay/cake_pay_vendor.dart --------- Co-authored-by: Omar Hatem --- lib/cake_pay/cake_pay_api.dart | 4 ++-- lib/cake_pay/cake_pay_card.dart | 8 ++++++-- lib/cake_pay/cake_pay_service.dart | 2 +- lib/cake_pay/cake_pay_vendor.dart | 18 +++++++++++------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart index f9aa2f0f1..68aba3f3e 100644 --- a/lib/cake_pay/cake_pay_api.dart +++ b/lib/cake_pay/cake_pay_api.dart @@ -204,8 +204,8 @@ class CakePayApi { /// Get Vendors Future> getVendors({ required String apiKey, + required String country, int? page, - String? country, String? countryCode, String? search, List? vendorIds, @@ -247,7 +247,7 @@ class CakePayApi { } return (bodyJson['results'] as List) - .map((e) => CakePayVendor.fromJson(e as Map)) + .map((e) => CakePayVendor.fromJson(e as Map, country)) .toList(); } } diff --git a/lib/cake_pay/cake_pay_card.dart b/lib/cake_pay/cake_pay_card.dart index 26fa0c50b..d3f07e409 100644 --- a/lib/cake_pay/cake_pay_card.dart +++ b/lib/cake_pay/cake_pay_card.dart @@ -81,7 +81,11 @@ class CakePayCard { } static String fixEncoding(String text) { - final bytes = latin1.encode(text); - return utf8.decode(bytes, allowMalformed: true); + try { + final bytes = latin1.encode(text); + return utf8.decode(bytes, allowMalformed: true); + } catch (_) { + return text; + } } } diff --git a/lib/cake_pay/cake_pay_service.dart b/lib/cake_pay/cake_pay_service.dart index 768588775..9ba65df9a 100644 --- a/lib/cake_pay/cake_pay_service.dart +++ b/lib/cake_pay/cake_pay_service.dart @@ -29,8 +29,8 @@ class CakePayService { /// Get Vendors Future> getVendors({ + required String country, int? page, - String? country, String? countryCode, String? search, List? vendorIds, diff --git a/lib/cake_pay/cake_pay_vendor.dart b/lib/cake_pay/cake_pay_vendor.dart index c947fa882..564896654 100644 --- a/lib/cake_pay/cake_pay_vendor.dart +++ b/lib/cake_pay/cake_pay_vendor.dart @@ -7,7 +7,7 @@ class CakePayVendor { final String name; final bool unavailable; final String? cakeWarnings; - final List countries; + final String country; final CakePayCard? card; CakePayVendor({ @@ -15,19 +15,23 @@ class CakePayVendor { required this.name, required this.unavailable, this.cakeWarnings, - required this.countries, + required this.country, this.card, }); - factory CakePayVendor.fromJson(Map json) { + factory CakePayVendor.fromJson(Map json, String country) { final name = stripHtmlIfNeeded(json['name'] as String); final decodedName = fixEncoding(name); var cardsJson = json['cards'] as List?; - CakePayCard? firstCard; + CakePayCard? cardForVendor; if (cardsJson != null && cardsJson.isNotEmpty) { - firstCard = CakePayCard.fromJson(cardsJson.first as Map); + try { + cardForVendor = CakePayCard.fromJson(cardsJson + .where((element) => element['country'] == country) + .first as Map); + } catch (_) {} } return CakePayVendor( @@ -35,8 +39,8 @@ class CakePayVendor { name: decodedName, unavailable: json['unavailable'] as bool? ?? false, cakeWarnings: json['cake_warnings'] as String?, - countries: List.from(json['countries'] as List? ?? []), - card: firstCard, + country: country, + card: cardForVendor, ); } From c9a6abeea4a075611467e08e7134ccf8665c7f91 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 27 Dec 2024 04:54:47 +0100 Subject: [PATCH 17/30] Full balance (#1457) * fix: Confirm widget is still mounted * feat: Modify balance display to include full balance * fix: Modifying balance * chore: Feature cleanup * fix: Add frozen balance into consideration when taking available balance and add field to make full balance display only on bitcoin and litecoin wallets * fix: Adjust balance card to display correct available and unavailable balance, unavailable balance should only be displayed when there is one WIP * fix: Cleanup balance page and balance page view_model * chore: Revert formatting * fix: Remove full balance * fix: Remove full balance * fix: Remove full balance * chore: Rever formating [skip ci] * feat: Finalize display only available and unavailable balance * fix: Modify the way balance is displayed, activate frozen balance with label, remove unavailable/additional balance for bitcoin wallet type * fix: Issues coming from syncing with main * fix: Modify additional balance label * fix: Monero and Wownero balances display bug * fix: Resolve merge conflicts * feat: Activate CPFP for BTC, LTC and BCH, also fix issues with frozen balance display * - minor fix - remove unused functions * Fix conflicts --------- Co-authored-by: Omar Hatem Co-authored-by: tuxsudo --- cw_bitcoin/lib/electrum_balance.dart | 4 +- cw_bitcoin/lib/electrum_wallet.dart | 26 +- cw_core/lib/monero_balance.dart | 25 +- cw_core/lib/wownero_balance.dart | 24 +- cw_evm/lib/evm_erc20_balance.dart | 9 +- cw_monero/lib/monero_wallet.dart | 17 +- .../robots/dashboard_page_robot.dart | 2 +- lib/di.dart | 2 +- lib/src/screens/dashboard/dashboard_page.dart | 2 +- .../dashboard/desktop_dashboard_page.dart | 2 +- .../dashboard/pages/balance/balance_page.dart | 88 ++ .../pages/balance/balance_row_widget.dart | 654 +++++++++ .../pages/balance/crypto_balance_widget.dart | 424 ++++++ .../screens/dashboard/pages/balance_page.dart | 1164 ----------------- .../dashboard/balance_view_model.dart | 18 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 43 files changed, 1257 insertions(+), 1232 deletions(-) create mode 100644 lib/src/screens/dashboard/pages/balance/balance_page.dart create mode 100644 lib/src/screens/dashboard/pages/balance/balance_row_widget.dart create mode 100644 lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart delete mode 100644 lib/src/screens/dashboard/pages/balance_page.dart diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index ebd2f06ae..37c34058b 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -39,7 +39,7 @@ class ElectrumBalance extends Balance { int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => bitcoinAmountToString(amount: ((confirmed + unconfirmed) - frozen) ); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @@ -58,7 +58,7 @@ class ElectrumBalance extends Balance { @override String get formattedFullAvailableBalance => - bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen); String toJSON() => json.encode({ 'confirmed': confirmed, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index eae830db1..3ab1505c9 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2213,18 +2213,6 @@ abstract class ElectrumWalletBase var totalConfirmed = 0; var totalUnconfirmed = 0; - unspentCoinsInfo.values.forEach((info) { - unspentCoins.forEach((element) { - if (element.hash == info.hash && - element.vout == info.vout && - info.isFrozen && - element.bitcoinAddressRecord.address == info.address && - element.value == info.value) { - totalFrozen += element.value; - } - }); - }); - if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments @@ -2240,6 +2228,20 @@ abstract class ElectrumWalletBase }); } + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) return; + + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + final balances = await Future.wait(balanceFutures); if (balances.isNotEmpty && balances.first['confirmed'] == null) { diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 9a63c407e..42c00b97e 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -3,36 +3,25 @@ import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedFullBalance = moneroAmountToString(amount: frozenBalance + fullBalance), - formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), - formattedLockedBalance = - moneroAmountToString(amount: frozenBalance + fullBalance - unlockedBalance), + : formattedUnconfirmedBalance = moneroAmountToString(amount: fullBalance - unlockedBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); - MoneroBalance.fromString( - {required this.formattedFullBalance, - required this.formattedUnlockedBalance, - this.formattedLockedBalance = '0.0'}) - : fullBalance = moneroParseAmount(amount: formattedFullBalance), - unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), - frozenBalance = moneroParseAmount(amount: formattedLockedBalance), - super(moneroParseAmount(amount: formattedUnlockedBalance), - moneroParseAmount(amount: formattedFullBalance)); - final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedFullBalance; + final String formattedUnconfirmedBalance; final String formattedUnlockedBalance; - final String formattedLockedBalance; + final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => - formattedLockedBalance == '0.0' ? '' : formattedLockedBalance; + formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; @override String get formattedAvailableBalance => formattedUnlockedBalance; @override - String get formattedAdditionalBalance => formattedFullBalance; + String get formattedAdditionalBalance => formattedUnconfirmedBalance; } diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart index 2820659f2..b04560a79 100644 --- a/cw_core/lib/wownero_balance.dart +++ b/cw_core/lib/wownero_balance.dart @@ -3,36 +3,26 @@ import 'package:cw_core/wownero_amount_format.dart'; class WowneroBalance extends Balance { WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedFullBalance = wowneroAmountToString(amount: fullBalance), + : formattedUnconfirmedBalance = wowneroAmountToString(amount: fullBalance - unlockedBalance), formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance - frozenBalance), - formattedLockedBalance = - wowneroAmountToString(amount: frozenBalance + fullBalance - unlockedBalance), + formattedFrozenBalance = + wowneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); - WowneroBalance.fromString( - {required this.formattedFullBalance, - required this.formattedUnlockedBalance, - this.formattedLockedBalance = '0.0'}) - : fullBalance = wowneroParseAmount(amount: formattedFullBalance), - unlockedBalance = wowneroParseAmount(amount: formattedUnlockedBalance), - frozenBalance = wowneroParseAmount(amount: formattedLockedBalance), - super(wowneroParseAmount(amount: formattedUnlockedBalance), - wowneroParseAmount(amount: formattedFullBalance)); - final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedFullBalance; + final String formattedUnconfirmedBalance; final String formattedUnlockedBalance; - final String formattedLockedBalance; + final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => - formattedLockedBalance == '0.0' ? '' : formattedLockedBalance; + formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; @override String get formattedAvailableBalance => formattedUnlockedBalance; @override - String get formattedAdditionalBalance => formattedFullBalance; + String get formattedAdditionalBalance => formattedUnconfirmedBalance; } \ No newline at end of file diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index 1727d7962..8962f7053 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -11,13 +11,12 @@ class EVMChainERC20Balance extends Balance { final int exponent; @override - String get formattedAdditionalBalance { - final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); - return formattedBalance.substring(0, min(12, formattedBalance.length)); - } + String get formattedAdditionalBalance => _balance(); @override - String get formattedAvailableBalance { + String get formattedAvailableBalance => _balance(); + + String _balance() { final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); return formattedBalance.substring(0, min(12, formattedBalance.length)); } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 21d5b6d4b..4d2f95e47 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -751,11 +751,18 @@ abstract class MoneroWalletBase extends WalletBase - element.walletId == id && - element.accountIndex == walletAddresses.account!.id)) { - if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value; - } + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.value == info.value && info.walletId == id && + info.accountIndex == walletAddresses.account!.id) { + if (element.isFrozen && !element.isSending) frozenBalance+= element.value; + } + }); + }); + return frozenBalance; } diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart index bc5f411ad..8e058d9b2 100644 --- a/integration_test/robots/dashboard_page_robot.dart +++ b/integration_test/robots/dashboard_page_robot.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/lib/di.dart b/lib/di.dart index 91ec692ef..4458f8ebd 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -79,7 +79,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 8c236404d..b1934f4a3 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -24,7 +24,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; diff --git a/lib/src/screens/dashboard/desktop_dashboard_page.dart b/lib/src/screens/dashboard/desktop_dashboard_page.dart index b25d0774b..c7cd67dfa 100644 --- a/lib/src/screens/dashboard/desktop_dashboard_page.dart +++ b/lib/src/screens/dashboard/desktop_dashboard_page.dart @@ -8,7 +8,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/version_comparator.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/router.dart' as Router; diff --git a/lib/src/screens/dashboard/pages/balance/balance_page.dart b/lib/src/screens/dashboard/pages/balance/balance_page.dart new file mode 100644 index 000000000..b53d2d56b --- /dev/null +++ b/lib/src/screens/dashboard/pages/balance/balance_page.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class BalancePage extends StatelessWidget { + BalancePage({ + required this.dashboardViewModel, + required this.settingsStore, + required this.nftViewModel, + }); + + final DashboardViewModel dashboardViewModel; + final NFTViewModel nftViewModel; + final SettingsStore settingsStore; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (context) { + final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); + return DefaultTabController( + length: isEVMCompatible ? 2 : 1, + child: Column( + children: [ + if (isEVMCompatible) + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: TabBar( + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + physics: NeverScrollableScrollPhysics(), + labelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: + Theme.of(context).extension()!.pageTitleTextColor, + height: 1, + ), + unselectedLabelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: + Theme.of(context).extension()!.pageTitleTextColor, + height: 1, + ), + labelColor: + Theme.of(context).extension()!.pageTitleTextColor, + dividerColor: Colors.transparent, + indicatorColor: + Theme.of(context).extension()!.pageTitleTextColor, + unselectedLabelColor: Theme.of(context) + .extension()! + .pageTitleTextColor + .withOpacity(0.5), + tabAlignment: TabAlignment.start, + tabs: [ + Tab(text: 'My Crypto'), + Tab(text: 'My NFTs'), + ], + ), + ), + ), + Expanded( + child: TabBarView( + physics: NeverScrollableScrollPhysics(), + children: [ + CryptoBalanceWidget(dashboardViewModel: dashboardViewModel), + if (isEVMCompatible) NFTListingPage(nftViewModel: nftViewModel) + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart new file mode 100644 index 000000000..e3cff4760 --- /dev/null +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -0,0 +1,654 @@ +import 'dart:math'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BalanceRowWidget extends StatelessWidget { + BalanceRowWidget({ + required this.availableBalanceLabel, + required this.availableBalance, + required this.availableFiatBalance, + required this.additionalBalanceLabel, + required this.additionalBalance, + required this.additionalFiatBalance, + required this.secondAvailableBalanceLabel, + required this.secondAvailableBalance, + required this.secondAvailableFiatBalance, + required this.secondAdditionalBalanceLabel, + required this.secondAdditionalBalance, + required this.secondAdditionalFiatBalance, + required this.frozenBalance, + required this.frozenFiatBalance, + required this.currency, + required this.hasAdditionalBalance, + required this.hasSecondAvailableBalance, + required this.hasSecondAdditionalBalance, + required this.isTestnet, + required this.dashboardViewModel, + super.key, + }); + + final String availableBalanceLabel; + final String availableBalance; + final String availableFiatBalance; + final String additionalBalanceLabel; + final String additionalBalance; + final String additionalFiatBalance; + final String secondAvailableBalanceLabel; + final String secondAvailableBalance; + final String secondAvailableFiatBalance; + final String secondAdditionalBalanceLabel; + final String secondAdditionalBalance; + final String secondAdditionalFiatBalance; + final String frozenBalance; + final String frozenFiatBalance; + final CryptoCurrency currency; + final bool hasAdditionalBalance; + final bool hasSecondAvailableBalance; + final bool hasSecondAdditionalBalance; + final bool isTestnet; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: hasAdditionalBalance + ? () => _showBalanceDescription( + context, S.of(context).available_balance_description) + : null, + child: Row( + children: [ + Semantics( + hint: 'Double tap to see more information', + container: true, + child: Text('${availableBalanceLabel}', + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1)), + ), + if (hasAdditionalBalance) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), + ], + ), + ), + SizedBox(height: 6), + AutoSizeText(availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .balanceAmountColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.start), + SizedBox(height: 6), + if (isTestnet) + Text(S.of(context).testnet_coins_no_value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.textColor, + height: 1)), + if (!isTestnet) + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: + Theme.of(context).extension()!.textColor, + height: 1)), + ], + ), + SizedBox( + width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 10), + Text( + currency.title, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ), + ], + ), + ), + if (frozenBalance.isNotEmpty) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: hasAdditionalBalance + ? () => _showBalanceDescription( + context, S.of(context).unavailable_balance_description) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 26), + Row( + children: [ + Text( + S.of(context).unavailable_balance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ), + ], + ), + SizedBox(height: 8), + AutoSizeText( + frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.balanceAmountColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + frozenFiatBalance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ), + if (hasAdditionalBalance) + GestureDetector( + onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + additionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${additionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ + SizedBox(height: 10), + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: Theme.of(context).extension()!.cardBorderColor, + width: 1, + ), + color: Theme.of(context).extension()!.syncedBackgroundColor, + ), + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (currency == CryptoCurrency.ltc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: EdgeInsets.only(right: 16, top: 0), + child: Column( + children: [ + Container( + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Theme.of(context) + .extension()! + .assetTitleColor, + size: 40, + ), + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ], + ), + if (hasSecondAvailableBalance) + GestureDetector( + onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://docs.cakewallet.com/cryptos/litecoin.html#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + '${secondAvailableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAvailableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 6), + if (!isTestnet) + Text( + '${secondAvailableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + Container( + margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), + child: Stack( + children: [ + if (hasSecondAdditionalBalance) + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${secondAdditionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + secondAdditionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .assetTitleColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + if (!isTestnet) + Text( + '${secondAdditionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .textColor, + height: 1, + ), + ), + ], + ), + ], + ), + ], + ), + ), + IntrinsicHeight( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegin, + child: OutlinedButton( + onPressed: () { + final mwebAddress = + bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((mwebAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri( + Uri.parse("litecoin:${mwebAddress}")); + } + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegin, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 24), + Expanded( + child: Semantics( + label: S.of(context).litecoin_mweb_pegout, + child: OutlinedButton( + onPressed: () { + final litecoinAddress = + bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); + PaymentRequest? paymentRequest = null; + if ((litecoinAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri( + Uri.parse("litecoin:${litecoinAddress}")); + } + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.mweb, + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/upload.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).litecoin_mweb_pegout, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16), + ], + ), + ), + ), + ], + ], + ); + } + + void _showBalanceDescription(BuildContext context, String content) { + showPopUp(context: context, builder: (_) => InformationPage(information: content)); + } +} diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart new file mode 100644 index 000000000..0bdf388d3 --- /dev/null +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -0,0 +1,424 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_row_widget.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:cake_wallet/src/widgets/introducing_card.dart'; +import 'package:cake_wallet/src/widgets/standard_switch.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CryptoBalanceWidget extends StatelessWidget { + const CryptoBalanceWidget({ + super.key, + required this.dashboardViewModel, + }); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Observer( + builder: (_) { + if (dashboardViewModel.getMoneroError != null) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DashBoardRoundedCardWidget( + title: "Invalid monero bindings", + subTitle: dashboardViewModel.getMoneroError.toString(), + onTap: () {}, + ), + ); + } + return Container(); + }, + ), + Observer( + builder: (_) { + if (dashboardViewModel.getWowneroError != null) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DashBoardRoundedCardWidget( + title: "Invalid wownero bindings", + subTitle: dashboardViewModel.getWowneroError.toString(), + onTap: () {}, + )); + } + return Container(); + }, + ), + Observer( + builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts + ? HomeScreenAccountWidget( + walletName: dashboardViewModel.name, accountName: dashboardViewModel.subname) + : Column( + children: [ + SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(left: 24, bottom: 16), + child: Observer( + builder: (_) { + return Row( + children: [ + Text( + dashboardViewModel.balanceViewModel.asset, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .pageTitleTextColor, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + if (dashboardViewModel.wallet.isHardwareWallet) + Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'assets/images/hardware_wallet/ledger_nano_x.png', + width: 24, + color: Theme.of(context) + .extension()! + .pageTitleTextColor, + ), + ), + if (dashboardViewModel + .balanceViewModel.isHomeScreenSettingsEnabled) + InkWell( + onTap: () => Navigator.pushNamed(context, Routes.homeSettings, + arguments: dashboardViewModel.balanceViewModel), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'assets/images/home_screen_settings_icon.png', + color: Theme.of(context) + .extension()! + .pageTitleTextColor, + ), + ), + ), + ], + ); + }, + ), + ), + ], + )), + Observer( + builder: (_) { + if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) { + return IntroducingCard( + title: S.of(context).introducing_cake_pay, + subTitle: S.of(context).cake_pay_learn_more, + borderColor: Theme.of(context).extension()!.cardBorderColor, + closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); + } + return Container(); + }, + ), + Observer(builder: (_) { + if (!dashboardViewModel.showRepWarning) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + title: S.of(context).rep_warning, + subTitle: S.of(context).rep_warning_sub, + onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), + onClose: () { + dashboardViewModel.settingsStore.shouldShowRepWarning = false; + }, + ), + ); + }), + Observer( + builder: (_) { + return ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), + itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, + itemBuilder: (__, index) { + final balance = + dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); + return Observer(builder: (_) { + return BalanceRowWidget( + dashboardViewModel: dashboardViewModel, + availableBalanceLabel: + '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', + availableBalance: balance.availableBalance, + availableFiatBalance: balance.fiatAvailableBalance, + additionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', + additionalBalance: balance.additionalBalance, + additionalFiatBalance: balance.fiatAdditionalBalance, + frozenBalance: balance.frozenBalance, + frozenFiatBalance: balance.fiatFrozenBalance, + currency: balance.asset, + hasAdditionalBalance: + dashboardViewModel.balanceViewModel.hasAdditionalBalance, + hasSecondAdditionalBalance: + dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, + hasSecondAvailableBalance: + dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, + secondAdditionalBalance: balance.secondAdditionalBalance, + secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, + secondAvailableBalance: balance.secondAvailableBalance, + secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, + secondAdditionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', + secondAvailableBalanceLabel: + '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', + isTestnet: dashboardViewModel.isTestnet, + ); + }); + }, + ); + }, + ), + Observer(builder: (context) { + return Column( + children: [ + if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: "This wallet has encountered an issue", + subTitle: "Here are the things that you should note:\n - " + + dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + + "\n\nPlease restart your wallet and if it doesn't help contact our support.", + onTap: () {}, + )) + ], + if (dashboardViewModel.showSilentPaymentsCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.of(context).silent_payments, + subTitle: S.of(context).enable_silent_payments_scanning, + hint: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://docs.cakewallet.com/cryptos/bitcoin#silent-payments"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + S.of(context).what_is_silent_payments, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + softWrap: true, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], + ), + ), + Observer( + builder: (_) => StandardSwitch( + value: dashboardViewModel.silentPaymentsScanningActive, + onTaped: () => _toggleSilentPaymentsScanning(context), + ), + ) + ], + ), + ], + ), + onTap: () => _toggleSilentPaymentsScanning(context), + icon: Icon( + Icons.lock, + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 50, + ), + ), + ), + ], + if (dashboardViewModel.showMwebCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.of(context).litecoin_mweb, + subTitle: S.of(context).litecoin_mweb_description, + hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, + ), + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: + Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + softWrap: true, + ), + ), + SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => _dismissMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + S.of(context).litecoin_mweb_dismiss, + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () => _enableMweb(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + S.of(context).enable, + maxLines: 1, + ), + ), + ), + ], + ), + ], + ), + onTap: () => {}, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + color: Color.fromARGB(255, 11, 70, 129), + size: 40, + ), + ), + ), + ), + ], + ], + ); + }), + ], + ), + ); + } + + Future _toggleSilentPaymentsScanning(BuildContext context) async { + final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; + final newValue = !isSilentPaymentsScanningActive; + + dashboardViewModel.silentPaymentsScanningActive = newValue; + + final needsToSwitch = !isSilentPaymentsScanningActive && + await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false; + + if (needsToSwitch) { + return showPopUp( + context: context, + builder: (BuildContext context) => AlertWithTwoActions( + alertTitle: S.of(context).change_current_node_title, + alertContent: S.of(context).confirm_silent_payments_switch_node, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + dashboardViewModel.setSilentPaymentsScanning(newValue); + Navigator.of(context).pop(); + }, + actionLeftButton: () { + dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; + Navigator.of(context).pop(); + }, + )); + } + + return dashboardViewModel.setSilentPaymentsScanning(newValue); + } + + Future _enableMweb(BuildContext context) async { + if (!dashboardViewModel.hasEnabledMwebBefore) { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_warning, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + } + dashboardViewModel.setMwebEnabled(); + } + + Future _dismissMweb(BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) => AlertWithOneAction( + alertTitle: S.of(context).alert_notice, + alertContent: S.of(context).litecoin_mweb_enable_later, + buttonText: S.of(context).understand, + buttonAction: () { + Navigator.of(context).pop(); + }, + )); + dashboardViewModel.dismissMweb(); + } +} diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart deleted file mode 100644 index a71a6288b..000000000 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ /dev/null @@ -1,1164 +0,0 @@ -import 'dart:math'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; -import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; -import 'package:cake_wallet/src/widgets/introducing_card.dart'; -import 'package:cake_wallet/src/widgets/standard_switch.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; -import 'package:cake_wallet/utils/feature_flag.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BalancePage extends StatelessWidget { - BalancePage({ - required this.dashboardViewModel, - required this.settingsStore, - required this.nftViewModel, - }); - - final DashboardViewModel dashboardViewModel; - final NFTViewModel nftViewModel; - final SettingsStore settingsStore; - - @override - Widget build(BuildContext context) { - return Observer( - builder: (context) { - final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); - return DefaultTabController( - length: isEVMCompatible ? 2 : 1, - child: Column( - children: [ - if (isEVMCompatible) - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 8), - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - physics: NeverScrollableScrollPhysics(), - labelStyle: TextStyle( - fontSize: 18, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: - Theme.of(context).extension()!.pageTitleTextColor, - height: 1, - ), - unselectedLabelStyle: TextStyle( - fontSize: 18, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: - Theme.of(context).extension()!.pageTitleTextColor, - height: 1, - ), - labelColor: - Theme.of(context).extension()!.pageTitleTextColor, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).extension()!.pageTitleTextColor, - unselectedLabelColor: Theme.of(context) - .extension()! - .pageTitleTextColor - .withOpacity(0.5), - tabAlignment: TabAlignment.start, - tabs: [ - Tab(text: 'My Crypto'), - Tab(text: 'My NFTs'), - ], - ), - ), - ), - Expanded( - child: TabBarView( - physics: NeverScrollableScrollPhysics(), - children: [ - CryptoBalanceWidget(dashboardViewModel: dashboardViewModel), - if (isEVMCompatible) NFTListingPage(nftViewModel: nftViewModel) - ], - ), - ), - ], - ), - ); - }, - ); - } -} - -class CryptoBalanceWidget extends StatelessWidget { - const CryptoBalanceWidget({ - super.key, - required this.dashboardViewModel, - }); - - final DashboardViewModel dashboardViewModel; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Observer( - builder: (_) { - if (dashboardViewModel.getMoneroError != null) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DashBoardRoundedCardWidget( - title: "Invalid monero bindings", - subTitle: dashboardViewModel.getMoneroError.toString(), - onTap: () {}, - ), - ); - } - return Container(); - }, - ), - Observer( - builder: (_) { - if (dashboardViewModel.getWowneroError != null) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DashBoardRoundedCardWidget( - title: "Invalid wownero bindings", - subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, - )); - } - return Container(); - }, - ), - Observer( - builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts - ? HomeScreenAccountWidget( - walletName: dashboardViewModel.name, - accountName: dashboardViewModel.subname) - : Column( - children: [ - SizedBox(height: 16), - Container( - margin: const EdgeInsets.only(left: 24, bottom: 16), - child: Observer( - builder: (_) { - return Row( - children: [ - Text( - dashboardViewModel.balanceViewModel.asset, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - if (dashboardViewModel.wallet.isHardwareWallet) - Padding( - padding: const EdgeInsets.all(8.0), - child: Image.asset( - 'assets/images/hardware_wallet/ledger_nano_x.png', - width: 24, - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - ), - ), - if (dashboardViewModel - .balanceViewModel.isHomeScreenSettingsEnabled) - InkWell( - onTap: () => Navigator.pushNamed( - context, Routes.homeSettings, - arguments: dashboardViewModel.balanceViewModel), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Image.asset( - 'assets/images/home_screen_settings_icon.png', - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - ), - ), - ), - ], - ); - }, - ), - ), - ], - )), - Observer( - builder: (_) { - if (dashboardViewModel.balanceViewModel.isShowCard && - FeatureFlag.isCakePayEnabled) { - return IntroducingCard( - title: S.of(context).introducing_cake_pay, - subTitle: S.of(context).cake_pay_learn_more, - borderColor: Theme.of(context).extension()!.cardBorderColor, - closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); - } - return Container(); - }, - ), - Observer(builder: (_) { - if (!dashboardViewModel.showRepWarning) { - return const SizedBox(); - } - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - title: S.of(context).rep_warning, - subTitle: S.of(context).rep_warning_sub, - onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), - onClose: () { - dashboardViewModel.settingsStore.shouldShowRepWarning = false; - }, - ), - ); - }), - Observer( - builder: (_) { - return ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), - itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, - itemBuilder: (__, index) { - final balance = - dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); - return Observer(builder: (_) { - return BalanceRowWidget( - dashboardViewModel: dashboardViewModel, - availableBalanceLabel: - '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', - availableBalance: balance.availableBalance, - availableFiatBalance: balance.fiatAvailableBalance, - additionalBalanceLabel: - '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', - additionalBalance: balance.additionalBalance, - additionalFiatBalance: balance.fiatAdditionalBalance, - frozenBalance: balance.frozenBalance, - frozenFiatBalance: balance.fiatFrozenBalance, - currency: balance.asset, - hasAdditionalBalance: - dashboardViewModel.balanceViewModel.hasAdditionalBalance, - hasSecondAdditionalBalance: - dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, - hasSecondAvailableBalance: - dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, - secondAdditionalBalance: balance.secondAdditionalBalance, - secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, - secondAvailableBalance: balance.secondAvailableBalance, - secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, - secondAdditionalBalanceLabel: - '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', - secondAvailableBalanceLabel: - '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', - isTestnet: dashboardViewModel.isTestnet, - ); - }); - }, - ); - }, - ), - Observer(builder: (context) { - return Column( - children: [ - if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: "This wallet has encountered an issue", - subTitle: "Here are the things that you should note:\n - " + - dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + - "\n\nPlease restart your wallet and if it doesn't help contact our support.", - onTap: () {}, - )) - ], - if (dashboardViewModel.showSilentPaymentsCard) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: S.of(context).silent_payments, - subTitle: S.of(context).enable_silent_payments_scanning, - hint: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/bitcoin#silent-payments"), - mode: LaunchMode.externalApplication, - ), - child: Row( - children: [ - Text( - S.of(context).what_is_silent_payments, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], - ), - ), - Observer( - builder: (_) => StandardSwitch( - value: dashboardViewModel.silentPaymentsScanningActive, - onTaped: () => _toggleSilentPaymentsScanning(context), - ), - ) - ], - ), - ], - ), - onTap: () => _toggleSilentPaymentsScanning(context), - icon: Icon( - Icons.lock, - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 50, - ), - ), - ), - ], - if (dashboardViewModel.showMwebCard) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: S.of(context).litecoin_mweb, - subTitle: S.of(context).litecoin_mweb_description, - hint: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - ), - SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () => _enableMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - child: Text( - S.of(context).enable, - maxLines: 1, - ), - ), - ), - ], - ), - ], - ), - onTap: () => {}, - icon: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, - ), - ), - ), - ), - ], - ], - ); - }), - ], - ), - ); - } - - Future _toggleSilentPaymentsScanning(BuildContext context) async { - final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; - final newValue = !isSilentPaymentsScanningActive; - - dashboardViewModel.silentPaymentsScanningActive = newValue; - - final needsToSwitch = !isSilentPaymentsScanningActive && - await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false; - - if (needsToSwitch) { - return showPopUp( - context: context, - builder: (BuildContext context) => AlertWithTwoActions( - alertTitle: S.of(context).change_current_node_title, - alertContent: S.of(context).confirm_silent_payments_switch_node, - rightButtonText: S.of(context).confirm, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - dashboardViewModel.setSilentPaymentsScanning(newValue); - Navigator.of(context).pop(); - }, - actionLeftButton: () { - dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; - Navigator.of(context).pop(); - }, - )); - } - - return dashboardViewModel.setSilentPaymentsScanning(newValue); - } - - Future _enableMweb(BuildContext context) async { - if (!dashboardViewModel.hasEnabledMwebBefore) { - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).litecoin_mweb_warning, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - )); - } - dashboardViewModel.setMwebEnabled(); - } - - Future _dismissMweb(BuildContext context) async { - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).litecoin_mweb_enable_later, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - )); - dashboardViewModel.dismissMweb(); - } -} - -class BalanceRowWidget extends StatelessWidget { - BalanceRowWidget({ - required this.availableBalanceLabel, - required this.availableBalance, - required this.availableFiatBalance, - required this.additionalBalanceLabel, - required this.additionalBalance, - required this.additionalFiatBalance, - required this.secondAvailableBalanceLabel, - required this.secondAvailableBalance, - required this.secondAvailableFiatBalance, - required this.secondAdditionalBalanceLabel, - required this.secondAdditionalBalance, - required this.secondAdditionalFiatBalance, - required this.frozenBalance, - required this.frozenFiatBalance, - required this.currency, - required this.hasAdditionalBalance, - required this.hasSecondAvailableBalance, - required this.hasSecondAdditionalBalance, - required this.isTestnet, - required this.dashboardViewModel, - super.key, - }); - - final String availableBalanceLabel; - final String availableBalance; - final String availableFiatBalance; - final String additionalBalanceLabel; - final String additionalBalance; - final String additionalFiatBalance; - final String secondAvailableBalanceLabel; - final String secondAvailableBalance; - final String secondAvailableFiatBalance; - final String secondAdditionalBalanceLabel; - final String secondAdditionalBalance; - final String secondAdditionalFiatBalance; - final String frozenBalance; - final String frozenFiatBalance; - final CryptoCurrency currency; - final bool hasAdditionalBalance; - final bool hasSecondAvailableBalance; - final bool hasSecondAdditionalBalance; - final bool isTestnet; - final DashboardViewModel dashboardViewModel; - - // void _showBalanceDescription(BuildContext context) { - // showPopUp( - // context: context, - // builder: (_) => - // InformationPage(information: S.of(context).available_balance_description), - // ); - // } - - @override - Widget build(BuildContext context) { - return Column(children: [ - Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, - ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => _showBalanceDescription( - context, S.of(context).available_balance_description) - : null, - child: Row( - children: [ - Semantics( - hint: 'Double tap to see more information', - container: true, - child: Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), - ), - if (hasAdditionalBalance) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ), - ], - ), - ), - SizedBox(height: 6), - AutoSizeText(availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .balanceAmountColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.start), - SizedBox(height: 6), - if (isTestnet) - Text(S.of(context).testnet_coins_no_value, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1)), - if (!isTestnet) - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), - - ], - ), - - SizedBox( - width: min(MediaQuery.of(context).size.width * 0.2, 100), - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), - ), - ), - const SizedBox(height: 10), - Text( - currency.title, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: - Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - ), - if (frozenBalance.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => _showBalanceDescription( - context, S.of(context).unavailable_balance_description) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Row( - children: [ - Text( - S.of(context).unavailable_balance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ), - ], - ), - SizedBox(height: 8), - AutoSizeText( - frozenBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.balanceAmountColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - frozenFiatBalance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ), - if (hasAdditionalBalance) - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ), - ], - ), - ), - ), - if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ - SizedBox(height: 10), - Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, - ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Stack( - children: [ - if (currency == CryptoCurrency.ltc) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: EdgeInsets.only(right: 16, top: 0), - child: Column( - children: [ - Container( - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Theme.of(context) - .extension()! - .assetTitleColor, - size: 40, - ), - ), - const SizedBox(height: 10), - Text( - 'MWEB', - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ], - ), - if (hasSecondAvailableBalance) - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin.html#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Row( - children: [ - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 6), - if (!isTestnet) - Text( - '${secondAvailableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .textColor, - height: 1, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), - child: Stack( - children: [ - if (hasSecondAdditionalBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .textColor, - height: 1, - ), - ), - ], - ), - ], - ), - ], - ), - ), - IntrinsicHeight( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Semantics( - label: S.of(context).litecoin_mweb_pegin, - child: OutlinedButton( - onPressed: () { - final mwebAddress = - bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((mwebAddress?.isNotEmpty ?? false)) { - paymentRequest = - PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, - }, - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400 - .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400 - .withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - height: 30, - width: 30, - 'assets/images/received.png', - color: Theme.of(context) - .extension()! - .balanceAmountColor, - ), - const SizedBox(width: 8), - Text( - S.of(context).litecoin_mweb_pegin, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - SizedBox(width: 24), - Expanded( - child: Semantics( - label: S.of(context).litecoin_mweb_pegout, - child: OutlinedButton( - onPressed: () { - final litecoinAddress = - bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((litecoinAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${litecoinAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.mweb, - }, - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400 - .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400 - .withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - height: 30, - width: 30, - 'assets/images/upload.png', - color: Theme.of(context) - .extension()! - .balanceAmountColor, - ), - const SizedBox(width: 8), - Text( - S.of(context).litecoin_mweb_pegout, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ), - SizedBox(height: 16), - ], - ), - ), - ), - ], - ]); - } - - void _showBalanceDescription(BuildContext context, String content) { - showPopUp(context: context, builder: (_) => InformationPage(information: content)); - } -} diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 075cf6b75..0c4407e60 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -158,17 +158,17 @@ abstract class BalanceViewModelBase with Store { case WalletType.banano: case WalletType.solana: case WalletType.tron: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.none: return S.current.xmr_available_balance; - default: - return S.current.confirmed; } } @computed String get additionalBalanceLabel { switch (wallet.type) { - case WalletType.monero: - case WalletType.wownero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: @@ -357,7 +357,12 @@ abstract class BalanceViewModelBase with Store { bool mwebEnabled = false; @computed - bool get hasAdditionalBalance => _hasAdditionalBalanceForWalletType(wallet.type); + bool get hasAdditionalBalance { + bool isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); + bool isNotZeroAmount = additionalBalance != "0.0"; + + return isWalletTypeActivated && isNotZeroAmount; + } @computed bool get hasSecondAdditionalBalance => @@ -373,6 +378,9 @@ abstract class BalanceViewModelBase with Store { case WalletType.polygon: case WalletType.solana: case WalletType.tron: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.litecoin: return false; default: return true; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 3a91d2cb0..28b35c35e 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -330,6 +330,7 @@ "freeze": "تجميد", "frequently_asked_questions": "الأسئلة الشائعة", "frozen": "مجمدة", + "frozen_balance": "التوازن المجمد", "full_balance": "الرصيد الكامل", "gas_exceeds_allowance": "الغاز المطلوب بالمعاملة يتجاوز البدل.", "generate_name": "توليد الاسم", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 9d86f74e4..64cd7c61f 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -330,6 +330,7 @@ "freeze": "Замразяване", "frequently_asked_questions": "Често задавани въпроси", "frozen": "Замразени", + "frozen_balance": "Замразен баланс", "full_balance": "Пълен баланс", "gas_exceeds_allowance": "Газът, изискван от транзакцията, надвишава надбавката.", "generate_name": "Генериране на име", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 25a4845d9..7d458e5af 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -330,6 +330,7 @@ "freeze": "Zmrazit", "frequently_asked_questions": "Často kladené otázky", "frozen": "Zmraženo", + "frozen_balance": "Zmrazená rovnováha", "full_balance": "Celkový zůstatek", "gas_exceeds_allowance": "Plyn vyžadovaný transakcí přesahuje příspěvek.", "generate_name": "Generovat jméno", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index cf44ab05d..0b2d22f62 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -330,6 +330,7 @@ "freeze": "Einfrieren", "frequently_asked_questions": "Häufig gestellte Fragen", "frozen": "Gefroren", + "frozen_balance": "Gefrorenes Gleichgewicht", "full_balance": "Gesamtguthaben", "gas_exceeds_allowance": "Die durch Transaktion erforderliche Gas übertrifft die Zulage.", "generate_name": "Namen generieren", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 69ba98f34..7da5e0fa1 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -330,6 +330,7 @@ "freeze": "Freeze", "frequently_asked_questions": "Frequently asked questions", "frozen": "Frozen", + "frozen_balance": "Frozen Balance", "full_balance": "Full Balance", "gas_exceeds_allowance": "Gas required by transaction exceeds allowance.", "generate_name": "Generate Name", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3ab98c5f5..724f691c9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -330,6 +330,7 @@ "freeze": "Congelar", "frequently_asked_questions": "Preguntas frecuentes", "frozen": "Congelada", + "frozen_balance": "Equilibrio congelado", "full_balance": "Balance completo", "gas_exceeds_allowance": "El gas requerido por la transacción excede la asignación.", "generate_name": "Generar nombre", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 50fb6c3db..85c6e2646 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -330,6 +330,7 @@ "freeze": "Geler", "frequently_asked_questions": "Foire aux questions", "frozen": "Gelées", + "frozen_balance": "Équilibre gelé", "full_balance": "Solde Complet", "gas_exceeds_allowance": "Le gaz requis par la transaction dépasse l'allocation.", "generate_name": "Générer un nom", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index a532a54b7..cb457f07a 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -330,6 +330,7 @@ "freeze": "Daskare", "frequently_asked_questions": "Tambayoyin da ake yawan yi", "frozen": "Daskararre", + "frozen_balance": "Daidaituwa mai sanyi", "full_balance": "DUKAN KUDI", "gas_exceeds_allowance": "Gas da ake buƙata ta hanyar ma'amala ya wuce izini.", "generate_name": "Ƙirƙirar Suna", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index b20291df7..2eaa53e87 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -330,6 +330,7 @@ "freeze": "फ्रीज", "frequently_asked_questions": "अक्सर पूछे जाने वाले प्रश्न", "frozen": "जमा हुआ", + "frozen_balance": "जमे हुए संतुलन", "full_balance": "पूर्ण संतुलन", "gas_exceeds_allowance": "लेनदेन द्वारा आवश्यक गैस भत्ता से अधिक है।", "generate_name": "नाम जनरेट करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9b228d843..303009403 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -330,6 +330,7 @@ "freeze": "Zamrznuti", "frequently_asked_questions": "Često postavljana pitanja", "frozen": "Smrznuto", + "frozen_balance": "Smrznuta ravnoteža", "full_balance": "Pun iznos", "gas_exceeds_allowance": "Plin potreban transakcijom premašuje dodatak.", "generate_name": "Generiraj ime", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 668c99a14..4b03eb2dd 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -330,6 +330,7 @@ "freeze": "Կասեցնել", "frequently_asked_questions": "Հաճախ տրվող հարցեր", "frozen": "Կասեցված", + "frozen_balance": "Սառեցված հավասարակշռություն", "full_balance": "Լրիվ մնացորդ", "gas_exceeds_allowance": "Գործարքով պահանջվող գազը գերազանցում է նպաստը:", "generate_name": "Գեներացնել անուն", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 3102ea2e0..d142ab41b 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -330,6 +330,7 @@ "freeze": "Freeze", "frequently_asked_questions": "Pertanyaan yang sering diajukan", "frozen": "Dibekukan", + "frozen_balance": "Keseimbangan beku", "full_balance": "Saldo Penuh", "gas_exceeds_allowance": "Gas yang dibutuhkan oleh transaksi melebihi tunjangan.", "generate_name": "Hasilkan Nama", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index cbeaa191d..ae26f2d13 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -331,6 +331,7 @@ "freeze": "Congelare", "frequently_asked_questions": "Domande frequenti", "frozen": "Congelato", + "frozen_balance": "Equilibrio congelato", "full_balance": "Saldo Completo", "gas_exceeds_allowance": "Il gas richiesto dalla transazione supera l'indennità.", "generate_name": "Genera nome", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 95ae7b672..041265697 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -330,6 +330,7 @@ "freeze": "氷結", "frequently_asked_questions": "よくある質問", "frozen": "凍った", + "frozen_balance": "凍結バランス", "full_balance": "フルバランス", "gas_exceeds_allowance": "取引に必要なガスは、手当を超えています。", "generate_name": "名前の生成", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 4e9a7cff7..7fb3bab95 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -330,6 +330,7 @@ "freeze": "얼다", "frequently_asked_questions": "자주 묻는 질문", "frozen": "겨울 왕국", + "frozen_balance": "냉동 균형", "full_balance": "풀 밸런스", "gas_exceeds_allowance": "거래에 필요한 가스는 수당을 초과합니다.", "generate_name": "이름 생성", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index c73db85dd..1498403e0 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -330,6 +330,7 @@ "freeze": "အေးခဲ", "frequently_asked_questions": "မေးလေ့ရှိသောမေးခွန်းများ", "frozen": "ဖြူဖြူ", + "frozen_balance": "လက်ကျန်ငွေ", "full_balance": "Balance အပြည့်", "gas_exceeds_allowance": "ငွေပေးငွေယူမှလိုအပ်သောဓာတ်ငွေ့ထောက်ပံ့ကြေးကျော်လွန်။", "generate_name": "အမည်ဖန်တီးပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 367b8f625..d63b85a3e 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -330,6 +330,7 @@ "freeze": "Bevriezen", "frequently_asked_questions": "Veelgestelde vragen", "frozen": "Bevroren", + "frozen_balance": "Bevroren balans", "full_balance": "Volledig saldo", "gas_exceeds_allowance": "Gas vereist door transactie overschrijdt de vergoeding.", "generate_name": "Naam genereren", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index ad797bb24..0e9c53310 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -330,6 +330,7 @@ "freeze": "Zamróź", "frequently_asked_questions": "Często zadawane pytania", "frozen": "Zamrożone", + "frozen_balance": "Mrożona równowaga", "full_balance": "Pełne saldo", "gas_exceeds_allowance": "Gaz wymagany przez transakcję przekracza dodatek.", "generate_name": "Wygeneruj nazwę", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index d9df7e2ee..2653adde4 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -330,6 +330,7 @@ "freeze": "Congelar", "frequently_asked_questions": "Perguntas frequentes", "frozen": "Congeladas", + "frozen_balance": "Equilíbrio congelado", "full_balance": "Saldo total", "gas_exceeds_allowance": "O gás exigido pela transação excede o subsídio.", "generate_name": "Gerar nome", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index d9468f148..af7759316 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -330,6 +330,7 @@ "freeze": "Заморозить", "frequently_asked_questions": "Часто задаваемые вопросы", "frozen": "Заморожено", + "frozen_balance": "Замороженный баланс", "full_balance": "Весь баланс", "gas_exceeds_allowance": "Газ, требуемый в результате транзакции, превышает пособие.", "generate_name": "Создать имя", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 649d59d59..43ef057ad 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -330,6 +330,7 @@ "freeze": "ดักจับ", "frequently_asked_questions": "คำถามที่พบบ่อย", "frozen": "ถูกดักจับ", + "frozen_balance": "สมดุลแช่แข็ง", "full_balance": "ยอดคงเหลือทั้งหมด", "gas_exceeds_allowance": "ก๊าซที่ต้องการโดยการทำธุรกรรมเกินค่าเผื่อ", "generate_name": "สร้างชื่อ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 33b4d23d6..1e845550d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -330,6 +330,7 @@ "freeze": "I-freeze", "frequently_asked_questions": "Mga madalas itanong", "frozen": "Frozen", + "frozen_balance": "Frozen na balanse", "full_balance": "Buong Balanse", "gas_exceeds_allowance": "Ang gas na kinakailangan ng transaksyon ay lumampas sa allowance.", "generate_name": "Bumuo ng pangalan", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 7b27ded8a..48567f883 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -330,6 +330,7 @@ "freeze": "Dondur", "frequently_asked_questions": "Sıkça sorulan sorular", "frozen": "Dondurulmuş", + "frozen_balance": "Dondurulmuş denge", "full_balance": "Tüm bakiye", "gas_exceeds_allowance": "İşlemin gerektirdiği gaz ödeneği aşar.", "generate_name": "İsim Oluştur", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index cdf169a5a..5d26be28d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -330,6 +330,7 @@ "freeze": "Заморозити", "frequently_asked_questions": "Часті запитання", "frozen": "Заморожено", + "frozen_balance": "Заморожений баланс", "full_balance": "Весь баланс", "gas_exceeds_allowance": "Газ, необхідний транзакціям, перевищує надбавку.", "generate_name": "Згенерувати назву", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 5213cbaeb..f9ca35bff 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -330,6 +330,7 @@ "freeze": "منجمد", "frequently_asked_questions": "اکثر پوچھے گئے سوالات", "frozen": "منجمد", + "frozen_balance": "منجمد توازن", "full_balance": "مکمل بیلنس", "gas_exceeds_allowance": "لین دین کے ذریعہ درکار گیس الاؤنس سے زیادہ ہے۔", "generate_name": "نام پیدا کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 963c3002a..17c7cfc8d 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -329,6 +329,7 @@ "freeze": "Đóng băng", "frequently_asked_questions": "Các câu hỏi thường gặp", "frozen": "Đã đóng băng", + "frozen_balance": "Cân bằng đông lạnh", "full_balance": "Số dư đầy đủ", "gas_exceeds_allowance": "Gas theo yêu cầu của giao dịch vượt quá trợ cấp.", "generate_name": "Tạo tên", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index dd9c16347..8bd6b6a34 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -331,6 +331,7 @@ "freeze": "Tì pa", "frequently_asked_questions": "Àwọn ìbéèrè la máa ń béèrè", "frozen": "Ó l'a tì pa", + "frozen_balance": "Iwontunwonsi ti o tutu", "full_balance": "Ìyókù owó kíkún", "gas_exceeds_allowance": "Gaasi ti a beere nipasẹ idunadura ju lọ.", "generate_name": "Ṣẹda Orukọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 2d70c2325..483b10050 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -330,6 +330,7 @@ "freeze": "凍結", "frequently_asked_questions": "常见问题", "frozen": "凍結的", + "frozen_balance": "冷冻平衡", "full_balance": "全部余额", "gas_exceeds_allowance": "交易要求的气体超出了津贴。", "generate_name": "生成名称", From 4bba9f6ddb4a1c76f74b9bc5f5855e1469795c3a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 28 Dec 2024 00:18:46 +0200 Subject: [PATCH 18/30] Solana enhancements (#1907) * fix: Confirm widget is still mounted * feat: Modify balance display to include full balance * fix: Modifying balance * chore: Feature cleanup * fix: Add frozen balance into consideration when taking available balance and add field to make full balance display only on bitcoin and litecoin wallets * fix: Adjust balance card to display correct available and unavailable balance, unavailable balance should only be displayed when there is one WIP * fix: Cleanup balance page and balance page view_model * chore: Revert formatting * fix: Remove full balance * fix: Remove full balance * fix: Remove full balance * chore: Rever formating [skip ci] * feat: Finalize display only available and unavailable balance * fix: Modify the way balance is displayed, activate frozen balance with label, remove unavailable/additional balance for bitcoin wallet type * fix: Issues coming from syncing with main * fix: Modify additional balance label * fix: Monero and Wownero balances display bug * fix: Resolve merge conflicts * feat: Activate CPFP for BTC, LTC and BCH, also fix issues with frozen balance display * - minor fix - remove unused functions * Fix conflicts * Temporarily remove misused function Ignore creating associated account for receiver (testing) * revert associated recipient account removal * Migrate eth and polygon nodes to new urls and https --------- Co-authored-by: Blazebrain Co-authored-by: tuxsudo --- assets/ethereum_server_list.yml | 4 ++- assets/polygon_node_list.yml | 4 ++- cw_solana/lib/solana_client.dart | 22 +++++++------ lib/entities/default_settings_migration.dart | 34 ++++++++++++++++++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml index 965638471..ed425c3c7 100644 --- a/assets/ethereum_server_list.yml +++ b/assets/ethereum_server_list.yml @@ -1,5 +1,7 @@ - - uri: ethereum.publicnode.com + uri: ethereum-rpc.publicnode.com + useSSL: true + isDefault: true - uri: eth.llamarpc.com - diff --git a/assets/polygon_node_list.yml b/assets/polygon_node_list.yml index 63878bc0c..3b2cdcdc3 100644 --- a/assets/polygon_node_list.yml +++ b/assets/polygon_node_list.yml @@ -1,7 +1,9 @@ - uri: polygon-rpc.com - - uri: polygon-bor.publicnode.com + uri: polygon-bor-rpc.publicnode.com + useSSL: true + isDefault: true - uri: polygon.llamarpc.com - diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 9447aad38..2207822bb 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -379,16 +379,18 @@ class SolanaWalletClient { required double solBalance, required double fee, }) async { - final rent = - await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed); - - final rentInSol = (rent / lamportsPerSol).toDouble(); - - final remnant = solBalance - (inputAmount + fee); - - if (remnant > rentInSol) return true; - - return false; + return true; + // TODO: this is not doing what the name inclines + // final rent = + // await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed); + // + // final rentInSol = (rent / lamportsPerSol).toDouble(); + // + // final remnant = solBalance - (inputAmount + fee); + // + // if (remnant > rentInSol) return true; + // + // return false; } Future _signNativeTokenTransaction({ diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 96638621a..63e70ce4d 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -35,8 +35,8 @@ const publicBitcoinTestnetElectrumUri = '$publicBitcoinTestnetElectrumAddress:$publicBitcoinTestnetElectrumPort'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; -const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; -const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; +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 nanoDefaultPowNodeUri = 'rpc.nano.to'; @@ -360,6 +360,18 @@ Future defaultSettingsMigration( 'solana-rpc.publicnode.com:443', ], ); + _updateNode( + nodes: nodes, + currentUri: "ethereum.publicnode.com", + newUri: "ethereum-rpc.publicnode.com", + useSSL: true, + ); + _updateNode( + nodes: nodes, + currentUri: "polygon-bor.publicnode.com", + newUri: "polygon-bor-rpc.publicnode.com", + useSSL: true, + ); break; default: break; @@ -375,6 +387,24 @@ Future defaultSettingsMigration( await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); } +void _updateNode({ + required Box nodes, + required String currentUri, + String? newUri, + bool? useSSL, +}) { + for (Node node in nodes.values) { + if (node.uriRaw == currentUri) { + if (newUri != null) { + node.uriRaw = newUri; + } + if (useSSL != null) { + node.useSSL = useSSL; + } + } + } +} + Future _backupHavenSeeds(Box havenSeedStore) async { final future = haven?.backupHavenSeeds(havenSeedStore); if (future != null) { From 2fe2f58cb44ed75008d0c104ef6e011dd20d1a0e Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sat, 28 Dec 2024 07:15:59 +0200 Subject: [PATCH 19/30] fix available balance for monero/wownero --- cw_core/lib/monero_balance.dart | 2 +- cw_core/lib/wownero_balance.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 42c00b97e..34f51faf9 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -4,7 +4,7 @@ import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedUnconfirmedBalance = moneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart index b04560a79..916fa90bc 100644 --- a/cw_core/lib/wownero_balance.dart +++ b/cw_core/lib/wownero_balance.dart @@ -4,7 +4,7 @@ import 'package:cw_core/wownero_amount_format.dart'; class WowneroBalance extends Balance { WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedUnconfirmedBalance = wowneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance), formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); From 4cdee649d1802c2316cdac5e1e7c55baa7fdf8c4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 28 Dec 2024 08:06:37 +0200 Subject: [PATCH 20/30] fix cakepay text encoding (#1902) * fix text encoding * fix initial encoding --- lib/cake_pay/cake_pay_api.dart | 3 ++- lib/cake_pay/cake_pay_card.dart | 27 +++++---------------------- lib/cake_pay/cake_pay_vendor.dart | 10 +--------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart index 68aba3f3e..5f1a350c0 100644 --- a/lib/cake_pay/cake_pay_api.dart +++ b/lib/cake_pay/cake_pay_api.dart @@ -230,6 +230,7 @@ class CakePayApi { var headers = { 'accept': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Api-Key $apiKey', }; @@ -240,7 +241,7 @@ class CakePayApi { 'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}'); } - final bodyJson = json.decode(response.body); + final bodyJson = json.decode(utf8.decode(response.bodyBytes)); if (bodyJson is List && bodyJson.isEmpty) { return []; diff --git a/lib/cake_pay/cake_pay_card.dart b/lib/cake_pay/cake_pay_card.dart index d3f07e409..82ba179e6 100644 --- a/lib/cake_pay/cake_pay_card.dart +++ b/lib/cake_pay/cake_pay_card.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:cake_wallet/entities/fiat_currency.dart'; class CakePayCard { @@ -38,17 +36,11 @@ class CakePayCard { }); factory CakePayCard.fromJson(Map json) { + final name = stripHtmlIfNeeded(json['name'] as String? ?? ''); - final decodedName = fixEncoding(name); - final description = stripHtmlIfNeeded(json['description'] as String? ?? ''); - final decodedDescription = fixEncoding(description); - final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? ''); - final decodedTermsAndConditions = fixEncoding(termsAndConditions); - final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? ''); - final decodedHowToUse = fixEncoding(howToUse); final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? ''); @@ -59,10 +51,10 @@ class CakePayCard { return CakePayCard( id: json['id'] as int? ?? 0, - name: decodedName, - description: decodedDescription, - termsAndConditions: decodedTermsAndConditions, - howToUse: decodedHowToUse, + name: name, + description: description, + termsAndConditions: termsAndConditions, + howToUse: howToUse, expiryAndValidity: json['expiry_and_validity'] as String?, cardImageUrl: json['card_image_url'] as String?, country: json['country'] as String?, @@ -79,13 +71,4 @@ class CakePayCard { static String stripHtmlIfNeeded(String text) { return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); } - - static String fixEncoding(String text) { - try { - final bytes = latin1.encode(text); - return utf8.decode(bytes, allowMalformed: true); - } catch (_) { - return text; - } - } } diff --git a/lib/cake_pay/cake_pay_vendor.dart b/lib/cake_pay/cake_pay_vendor.dart index 564896654..8ad305da0 100644 --- a/lib/cake_pay/cake_pay_vendor.dart +++ b/lib/cake_pay/cake_pay_vendor.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'cake_pay_card.dart'; class CakePayVendor { @@ -21,7 +19,6 @@ class CakePayVendor { factory CakePayVendor.fromJson(Map json, String country) { final name = stripHtmlIfNeeded(json['name'] as String); - final decodedName = fixEncoding(name); var cardsJson = json['cards'] as List?; CakePayCard? cardForVendor; @@ -36,7 +33,7 @@ class CakePayVendor { return CakePayVendor( id: json['id'] as int, - name: decodedName, + name: name, unavailable: json['unavailable'] as bool? ?? false, cakeWarnings: json['cake_warnings'] as String?, country: country, @@ -47,9 +44,4 @@ class CakePayVendor { static String stripHtmlIfNeeded(String text) { return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); } - - static String fixEncoding(String text) { - final bytes = latin1.encode(text); - return utf8.decode(bytes, allowMalformed: true); - } } From 214fc7113c4a865029a38942cb0dadca9947dbe7 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 29 Dec 2024 23:48:53 +0200 Subject: [PATCH 21/30] Fix electrum unspent coins error (#1912) * Refresh unspent coins before creating a transaction * disable seed verification in debug mode [skip ci] --- cw_bitcoin/lib/electrum_wallet.dart | 4 +++- .../seed/seed_verification/seed_verification_page.dart | 3 ++- lib/utils/feature_flag.dart | 4 +++- lib/view_model/wallet_seed_view_model.dart | 7 +++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 3ab1505c9..9a7e45d05 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; @@ -991,6 +990,9 @@ abstract class ElectrumWalletBase @override Future createTransaction(Object credentials) async { try { + // start by updating unspent coins + await updateAllUnspents(); + final outputs = []; final transactionCredentials = credentials as BitcoinTransactionCredentials; final hasMultiDestination = transactionCredentials.outputs.length > 1; diff --git a/lib/src/screens/seed/seed_verification/seed_verification_page.dart b/lib/src/screens/seed/seed_verification/seed_verification_page.dart index 755cb2aae..ac03768ca 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_page.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_page.dart @@ -20,7 +20,8 @@ class SeedVerificationPage extends BasePage { builder: (context) { return Padding( padding: const EdgeInsets.all(16.0), - child: walletSeedViewModel.isVerificationComplete + child: walletSeedViewModel.isVerificationComplete || + walletSeedViewModel.verificationIndices.isEmpty ? SeedVerificationSuccessView( imageColor: titleColor(context), ) diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index efde5208d..593e0f216 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -1,7 +1,9 @@ +import 'package:flutter/foundation.dart'; + class FeatureFlag { static const bool isCakePayEnabled = false; static const bool isExolixEnabled = true; static const bool isInAppTorEnabled = false; static const bool isBackgroundSyncEnabled = false; - static const int verificationWordsCount = 2; + static const int verificationWordsCount = kDebugMode ? 0 : 2; } \ No newline at end of file diff --git a/lib/view_model/wallet_seed_view_model.dart b/lib/view_model/wallet_seed_view_model.dart index 5355c856d..53c76ed10 100644 --- a/lib/view_model/wallet_seed_view_model.dart +++ b/lib/view_model/wallet_seed_view_model.dart @@ -29,6 +29,7 @@ abstract class WalletSeedViewModelBase with Store { List get seedSplit => seed.split(RegExp(r'\s+')); int get columnCount => seedSplit.length <= 16 ? 2 : 3; + double get columnAspectRatio => seedSplit.length <= 16 ? 1.8 : 2.8; /// The indices of the seed to be verified. @@ -60,8 +61,10 @@ abstract class WalletSeedViewModelBase with Store { bool isVerificationComplete = false; void setupSeedVerification() { - generateRandomIndices(); - generateOptions(); + if (verificationWordCount != 0) { + generateRandomIndices(); + generateOptions(); + } } /// Generate the indices of the seeds to be verified. From 831a6d9f9a4449c2971b00e3297f9715e962cdd3 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Mon, 30 Dec 2024 11:46:09 -0500 Subject: [PATCH 22/30] Cw 872 nano enhancements (#1909) * fix headers on all api calls * fix duplicate nano nodes on fresh install, api key fixes * fix liveness indicators + false positive responses to queries --- cw_core/lib/node.dart | 52 ++++++++++++++++---- cw_nano/lib/nano_client.dart | 36 +++++++------- lib/entities/default_settings_migration.dart | 2 +- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index aa7d27254..7d0c2411f 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -99,8 +99,8 @@ class Node extends HiveObject with Keyable { case WalletType.polygon: case WalletType.solana: case WalletType.tron: - return Uri.parse( - "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") ? path : "/$path"}"); + return Uri.parse( + "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") ? path : "/$path"}"); case WalletType.none: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -152,6 +152,7 @@ class Node extends HiveObject with Keyable { return requestMoneroNode(); case WalletType.nano: case WalletType.banano: + return requestNanoNode(); case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: @@ -198,14 +199,16 @@ class Node extends HiveObject with Keyable { ); client.close(); - if (( - response.body.contains("400 Bad Request") // Some other generic error - || response.body.contains("plain HTTP request was sent to HTTPS port") // Cloudflare - || response.headers["location"] != null // Generic reverse proxy - || response.body.contains("301 Moved Permanently") // Poorly configured generic reverse proxy - ) && !(useSSL??false) - ) { - + if ((response.body.contains("400 Bad Request") // Some other generic error + || + response.body.contains("plain HTTP request was sent to HTTPS port") // Cloudflare + || + response.headers["location"] != null // Generic reverse proxy + || + response.body + .contains("301 Moved Permanently") // Poorly configured generic reverse proxy + ) && + !(useSSL ?? false)) { final oldUseSSL = useSSL; useSSL = true; try { @@ -271,6 +274,35 @@ class Node extends HiveObject with Keyable { } } + Future requestNanoNode() async { + try { + final response = await http.post( + uri, + headers: { + "Content-Type": "application/json", + "nano-app": "cake-wallet" + }, + body: jsonEncode( + { + "action": "account_balance", + "account": "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579", + }, + ), + ); + final data = await jsonDecode(response.body); + if (response.statusCode != 200 || + data["error"] != null || + data["balance"] == null || + data["receivable"] == null) { + throw Exception( + "Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}"); + } + return true; + } catch (_) { + return false; + } + } + Future requestEthereumServer() async { try { final response = await http.get( diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 8b62273da..b63c634ee 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -54,12 +54,12 @@ class NanoClient { } } - Map getHeaders() { + Map getHeaders(String host) { final headers = Map.from(CAKE_HEADERS); - if (_node!.uri.host == "rpc.nano.to") { + if (host == "rpc.nano.to") { headers["key"] = nano_secrets.nano2ApiKey; } - if (_node!.uri.host == "nano.nownodes.io") { + if (host == "nano.nownodes.io") { headers["api-key"] = nano_secrets.nanoNowNodesApiKey; } return headers; @@ -68,7 +68,7 @@ class NanoClient { Future getBalance(String address) async { final response = await http.post( _node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: jsonEncode( { "action": "account_balance", @@ -95,7 +95,7 @@ class NanoClient { try { final response = await http.post( _node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: jsonEncode( { "action": "account_info", @@ -116,7 +116,7 @@ class NanoClient { try { final response = await http.post( _node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(_node!.uri.host), body: jsonEncode( { "action": "block_info", @@ -183,7 +183,7 @@ class NanoClient { Future requestWork(String hash) async { final response = await http.post( _powNode!.uri, - headers: getHeaders(), + headers: getHeaders(_powNode!.uri.host), body: json.encode( { "action": "work_generate", @@ -226,7 +226,7 @@ class NanoClient { final processResponse = await http.post( _node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: processBody, ); @@ -425,7 +425,7 @@ class NanoClient { }); final processResponse = await http.post( _node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: processBody, ); @@ -441,7 +441,7 @@ class NanoClient { required String privateKey, }) async { final receivableResponse = await http.post(_node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: jsonEncode({ "action": "receivable", "account": destinationAddress, @@ -493,7 +493,7 @@ class NanoClient { Future> fetchTransactions(String address) async { try { final response = await http.post(_node!.uri, - headers: getHeaders(), + headers: getHeaders(_node!.uri.host), body: jsonEncode({ "action": "account_history", "account": address, @@ -509,15 +509,16 @@ class NanoClient { .map((transaction) => NanoTransactionModel.fromJson(transaction)) .toList(); } catch (e) { - printV(e); - return []; + printV("error fetching transactions: $e"); + rethrow; } } Future> getN2Reps() async { + final uri = Uri.parse(N2_REPS_ENDPOINT); final response = await http.post( - Uri.parse(N2_REPS_ENDPOINT), - headers: CAKE_HEADERS, + uri, + headers: getHeaders(uri.host), body: jsonEncode({"action": "reps"}), ); try { @@ -531,9 +532,10 @@ class NanoClient { } Future getRepScore(String rep) async { + final uri = Uri.parse(N2_REPS_ENDPOINT); final response = await http.post( - Uri.parse(N2_REPS_ENDPOINT), - headers: CAKE_HEADERS, + uri, + headers: getHeaders(uri.host), body: jsonEncode({ "action": "rep_info", "account": rep, diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 63e70ce4d..25140f106 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -505,7 +505,7 @@ Future updateNanoNodeList({required Box nodes}) async { ]; // add new nodes: for (final node in nodeList) { - if (listOfNewEndpoints.contains(node.uriRaw)) { + if (listOfNewEndpoints.contains(node.uriRaw) && !nodes.values.contains(node)) { await nodes.add(node); } } From eb8c6a76e232bc2b98895ef1d0dd4b9dc86a1a6b Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 30 Dec 2024 20:58:58 +0200 Subject: [PATCH 23/30] fix openAlias resolver not being reached --- lib/entities/parse_address_from_domain.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 5c5075737..258ebf485 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -244,7 +244,9 @@ class AddressResolver { if (unstoppableDomains.any((domain) => name.trim() == domain)) { if (settingsStore.lookupsUnstoppableDomains) { final address = await fetchUnstoppableDomainAddress(text, ticker); - return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); + if (address.isNotEmpty) { + return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); + } } } @@ -257,12 +259,25 @@ class AddressResolver { } } + print("@@@@@@@@"); + print(formattedName); + print(domainParts); + print(name); + if (formattedName.contains(".")) { if (settingsStore.lookupsOpenAlias) { final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); + + print("@@@@@@@@"); + print(txtRecord); if (txtRecord != null) { final record = await OpenaliasRecord.fetchAddressAndName( formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord); + print("@@@@@@@@"); + print(record); + print(record.name); + print(record.address); + print(record.description); return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); } } From c880dbd83c54cd1e338ecec4c3caebe183915fd7 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 31 Dec 2024 07:33:45 +0200 Subject: [PATCH 24/30] fix creating associated token account --- cw_solana/lib/solana_client.dart | 49 ++++++++++++++++++++++++++------ cw_solana/lib/solana_wallet.dart | 1 - 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 2207822bb..9412b4e9c 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -481,6 +481,9 @@ class SolanaWalletClient { final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); final mint = Ed25519HDPublicKey.fromBase58(tokenMint); + // Input by the user + final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); + ProgramAccount? associatedRecipientAccount; ProgramAccount? associatedSenderAccount; @@ -503,18 +506,46 @@ class SolanaWalletClient { } try { - associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount( - mint: mint, - owner: destinationOwner, - funder: ownerKeypair, - ); + if (associatedRecipientAccount == null) { + final derivedAddress = await findAssociatedTokenAddress( + owner: destinationOwner, + mint: mint, + ); + + final instruction = AssociatedTokenAccountInstruction.createAccount( + mint: mint, + address: derivedAddress, + owner: ownerKeypair.publicKey, + funder: ownerKeypair.publicKey, + ); + + final _signedTx = await _signTransactionInternal( + message: Message.only(instruction), + signers: [ownerKeypair], + commitment: commitment, + latestBlockhash: await _getLatestBlockhash(commitment), + ); + + await sendTransaction( + signedTransaction: _signedTx, + commitment: commitment, + ); + + associatedRecipientAccount = ProgramAccount( + pubkey: derivedAddress.toBase58(), + account: Account( + owner: destinationOwner.toBase58(), + lamports: 0, + executable: false, + rentEpoch: BigInt.zero, + data: null, + ), + ); + } } catch (e) { throw SolanaCreateAssociatedTokenAccountException(e.toString()); } - // Input by the user - final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); - final instruction = TokenInstruction.transfer( source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey), destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey), @@ -587,6 +618,8 @@ class SolanaWalletClient { signedTransaction.encode(), preflightCommitment: commitment, ); + print("#########"); + print(signature); _client!.waitForSignatureStatus(signature, status: commitment); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 33a2e7df4..15c065918 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -33,7 +33,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; -import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; part 'solana_wallet.g.dart'; From 26c1f0b85c2e2de404d9a4597d69650510b3e7c4 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 31 Dec 2024 08:06:56 +0200 Subject: [PATCH 25/30] remove print --- lib/entities/parse_address_from_domain.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 258ebf485..b13dfa9ad 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -259,25 +259,13 @@ class AddressResolver { } } - print("@@@@@@@@"); - print(formattedName); - print(domainParts); - print(name); - if (formattedName.contains(".")) { if (settingsStore.lookupsOpenAlias) { final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); - print("@@@@@@@@"); - print(txtRecord); if (txtRecord != null) { final record = await OpenaliasRecord.fetchAddressAndName( formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord); - print("@@@@@@@@"); - print(record); - print(record.name); - print(record.address); - print(record.description); return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); } } From 84cc0576d56abed644866cbbfe95fd1b6fb7ddad Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 31 Dec 2024 17:03:17 +0200 Subject: [PATCH 26/30] fix creating associated token account (#1918) --- cw_solana/lib/solana_client.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 9412b4e9c..16f8988b1 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -618,8 +618,6 @@ class SolanaWalletClient { signedTransaction.encode(), preflightCommitment: commitment, ); - print("#########"); - print(signature); _client!.waitForSignatureStatus(signature, status: commitment); From a2cb994c09352eb85ff5d8909aa07ad37933bbb7 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 31 Dec 2024 17:03:36 +0200 Subject: [PATCH 27/30] Fix fee check for erc20 transactions (#1915) --- cw_evm/lib/evm_chain_wallet.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index dca16539c..9ccb05e7f 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -29,7 +29,6 @@ import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_ledger_credentials.dart'; -import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -348,7 +347,7 @@ abstract class EVMChainWalletBase final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == _credentials.currency.title); - final erc20Balance = balance[transactionCurrency]!; + final currencyBalance = balance[transactionCurrency]!; BigInt totalAmount = BigInt.zero; BigInt estimatedFeesForTransaction = BigInt.zero; int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; @@ -385,7 +384,7 @@ abstract class EVMChainWalletBase estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits; maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas; - if (erc20Balance.balance < totalAmount) { + if (currencyBalance.balance < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } } else { @@ -398,7 +397,7 @@ abstract class EVMChainWalletBase } if (output.sendAll && transactionCurrency is Erc20Token) { - totalAmount = erc20Balance.balance; + totalAmount = currencyBalance.balance; } final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( @@ -413,14 +412,15 @@ abstract class EVMChainWalletBase maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas; if (output.sendAll && transactionCurrency is! Erc20Token) { - totalAmount = (erc20Balance.balance - estimatedFeesForTransaction); - - if (estimatedFeesForTransaction > erc20Balance.balance) { - throw EVMChainTransactionFeesException(); - } + totalAmount = (currencyBalance.balance - estimatedFeesForTransaction); } - if (erc20Balance.balance < totalAmount) { + // check the fees on the base currency (Eth/Polygon) + if (estimatedFeesForTransaction > balance[currency]!.balance) { + throw EVMChainTransactionFeesException(); + } + + if (currencyBalance.balance < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } } From 9b27121261a3dd91886cde641ab9f01f44b82075 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 31 Dec 2024 17:04:18 +0200 Subject: [PATCH 28/30] Rename linux app (#1911) --- linux/my_application.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/my_application.cc b/linux/my_application.cc index 7375d05ca..49ed75499 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "cake_wallet"); + gtk_header_bar_set_title(header_bar, "Cake Wallet"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "cake_wallet"); + gtk_window_set_title(window, "Cake Wallet"); } gtk_window_set_default_size(window, 1280, 720); From d33a901f669b9a2a3868b4c184be5fe58a2974a6 Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 31 Dec 2024 17:47:17 +0100 Subject: [PATCH 29/30] Fix unavailable balance not refreshing after the app got opened (#1920) --- cw_monero/lib/monero_wallet.dart | 4 ++++ cw_monero/lib/monero_wallet_service.dart | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 4d2f95e47..9f46d32cd 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -503,6 +503,7 @@ abstract class MoneroWalletBase extends WalletBase updateUnspent() async { + await transaction_history.txHistoryMutex.acquire(); try { refreshCoins(walletAddresses.account!.id); @@ -531,6 +532,7 @@ abstract class MoneroWalletBase extends WalletBase _addCoinInfo(coin)); + transaction_history.txHistoryMutex.release(); return; } @@ -555,7 +557,9 @@ abstract class MoneroWalletBase extends WalletBase Date: Thu, 2 Jan 2025 00:14:00 +0200 Subject: [PATCH 30/30] Fix Mobx issue (#1922) --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 6774a5036..13a32c68c 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -349,8 +349,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { type: addressPageType, network: network, ); - _addresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesByMatch()); + Future.delayed(Duration.zero, () { + _addresses.add(address); + updateAddressesByMatch(); + }); return address; }