Merge remote-tracking branch 'origin/main' into electrum-sp-refactors

This commit is contained in:
Rafael Saes 2024-12-16 10:48:27 -03:00
commit 6cbb4c60d6
142 changed files with 3179 additions and 671 deletions

View file

@ -0,0 +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

View file

@ -62,6 +62,7 @@ jobs:
/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: |
@ -73,8 +74,8 @@ jobs:
id: cache-keystore
uses: actions/cache@v3
with:
path: /opt/android/cake_wallet/android/app/key.jks
key: $STORE_PASS
path: /opt/android/cake_wallet/android/app
key: keystore
- if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
name: Generate KeyStore

View file

@ -61,14 +61,32 @@ jobs:
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
- name: Clone Repo
run: |
sudo mkdir -p /opt/android
sudo chown $USER /opt/android
cd /opt/android
git clone https://github.com/cake-tech/cake_wallet.git --branch ${{ env.BRANCH_NAME }}
# - name: Cache Keystore
# id: cache-keystore
# uses: actions/cache@v3
# with:
# path: /opt/android/cake_wallet/android/app
# key: keystore
#
# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
- 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: Execute Build and Setup Commands
run: |
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
@ -115,19 +133,6 @@ jobs:
cd /opt/android/cake_wallet/scripts/android/
./build_mwebd.sh --dont-install
# - name: Cache Keystore
# id: cache-keystore
# uses: actions/cache@v3
# with:
# path: /opt/android/cake_wallet/android/app/key.jks
# key: $STORE_PASS
#
# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
- 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
@ -183,6 +188,7 @@ jobs:
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 nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
@ -201,7 +207,7 @@ jobs:
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.dar
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

View file

@ -165,6 +165,7 @@ jobs:
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 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 testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart

View file

@ -99,4 +99,4 @@ configurations {
implementation.exclude module:'proto-google-common-protos'
implementation.exclude module:'protolite-well-known-types'
implementation.exclude module:'protobuf-javalite'
}
}

View file

@ -6,5 +6,7 @@
uri: rpc.flashbots.net
-
uri: eth-mainnet.public.blastapi.io
-
uri: eth.nownodes.io
-
uri: ethereum.publicnode.com

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -3,4 +3,6 @@
-
uri: polygon-bor.publicnode.com
-
uri: polygon.llamarpc.com
uri: polygon.llamarpc.com
-
uri: matic.nownodes.io

View file

@ -227,21 +227,21 @@ class ElectrumClient {
return [];
});
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) =>
call(method: 'blockchain.scripthash.listunspent', params: [scriptHash])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, dynamic>) {
return val;
}
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) async {
final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]);
return <String, dynamic>{};
}).toList();
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, dynamic>) {
return val;
}
return [];
});
return <String, dynamic>{};
}).toList();
}
return [];
}
Future<List<Map<String, dynamic>>> getMempool(String scriptHash) =>
call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash])

View file

@ -28,7 +28,10 @@ abstract class ElectrumTransactionHistoryBase
String _password;
int _height;
Future<void> init() async => await _load();
Future<void> init() async {
clear();
await _load();
}
@override
void addOne(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction;

View file

@ -1150,7 +1150,7 @@ abstract class ElectrumWalletBase
}
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
try {
_workerIsolate!.kill(priority: Isolate.immediate);
await _workerSubscription?.cancel();
@ -1254,19 +1254,18 @@ abstract class ElectrumWalletBase
Future<void> refreshUnspentCoinsInfo() async {
try {
final List<dynamic> keys = <dynamic>[];
final List<dynamic> keys = [];
final currentWalletUnspentCoins =
unspentCoinsInfo.values.where((element) => element.walletId.contains(id));
unspentCoinsInfo.values.where((record) => record.walletId == id);
if (currentWalletUnspentCoins.isNotEmpty) {
currentWalletUnspentCoins.forEach((element) {
final existUnspentCoins = unspentCoins
.where((coin) => element.hash.contains(coin.hash) && element.vout == coin.vout);
for (final element in currentWalletUnspentCoins) {
if (RegexUtils.addressTypeFromStr(element.address, network) is MwebAddress) continue;
if (existUnspentCoins.isEmpty) {
keys.add(element.key);
}
});
final existUnspentCoins = unspentCoins.where((coin) => element == coin);
if (existUnspentCoins.isEmpty) {
keys.add(element.key);
}
}
if (keys.isNotEmpty) {

View file

@ -1302,7 +1302,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_utxoStream?.cancel();
_feeRatesTimer?.cancel();
_syncTimer?.cancel();

View file

@ -7,7 +7,7 @@ enum DeviceConnectionType {
static List<DeviceConnectionType> supportedConnectionTypes(WalletType walletType,
[bool isIOS = false]) {
switch (walletType) {
// case WalletType.monero:
case WalletType.monero:
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.ethereum:

View file

@ -18,4 +18,5 @@ const SPL_TOKEN_TYPE_ID = 16;
const DERIVATION_INFO_TYPE_ID = 17;
const TRON_TOKEN_TYPE_ID = 18;
const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
const MWEB_UTXO_TYPE_ID = 20;
const MWEB_UTXO_TYPE_ID = 20;
const HAVEN_SEED_STORE_TYPE_ID = 21;

View file

@ -83,7 +83,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<void> rescan({required int height});
Future<void> close({required bool shouldCleanup});
Future<void> close({bool shouldCleanup = false});
Future<void> changePassword(String password);

View file

@ -36,7 +36,18 @@ abstract class EVMChainClient {
bool connect(Node node) {
try {
_client = Web3Client(node.uri.toString(), httpClient);
Uri? rpcUri;
bool isModifiedNodeUri = false;
if (node.uriRaw == 'eth.nownodes.io' || node.uriRaw == 'matic.nownodes.io') {
isModifiedNodeUri = true;
String nowNodeApiKey = secrets.nowNodesApiKey;
rpcUri = Uri.https(node.uriRaw, '/$nowNodeApiKey');
}
_client =
Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), httpClient);
return true;
} catch (e) {
@ -83,23 +94,20 @@ abstract class EVMChainClient {
}
}
Future<int> getEstimatedGas({
String? contractAddress,
Future<int> getEstimatedGasUnitsForTransaction({
required EthereumAddress toAddress,
required EthereumAddress senderAddress,
required EtherAmount value,
String? contractAddress,
EtherAmount? gasPrice,
// EtherAmount? maxFeePerGas,
// EtherAmount? maxPriorityFeePerGas,
EtherAmount? maxFeePerGas,
}) async {
try {
if (contractAddress == null) {
final estimatedGas = await _client!.estimateGas(
sender: senderAddress,
gasPrice: gasPrice,
to: toAddress,
value: value,
// maxPriorityFeePerGas: maxPriorityFeePerGas,
// maxFeePerGas: maxFeePerGas,
);
@ -133,7 +141,9 @@ abstract class EVMChainClient {
required Credentials privateKey,
required String toAddress,
required BigInt amount,
required BigInt gas,
required BigInt gasFee,
required int estimatedGasUnits,
required int maxFeePerGas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
@ -152,6 +162,8 @@ abstract class EVMChainClient {
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
maxGas: estimatedGasUnits,
maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
);
Uint8List signedTransaction;
@ -180,7 +192,7 @@ abstract class EVMChainClient {
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount.toString(),
fee: gas,
fee: gasFee,
sendTransaction: _sendTransaction,
exponent: exponent,
);
@ -191,7 +203,10 @@ abstract class EVMChainClient {
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
EtherAmount? gasPrice,
EtherAmount? maxFeePerGas,
Uint8List? data,
int? maxGas,
}) {
return Transaction(
from: from,
@ -199,6 +214,9 @@ abstract class EVMChainClient {
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
data: data,
maxGas: maxGas,
gasPrice: gasPrice,
maxFeePerGas: maxFeePerGas,
);
}

View file

@ -34,7 +34,10 @@ abstract class EVMChainTransactionHistoryBase
//! Common methods across all child classes
Future<void> init() async => await _load();
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {

View file

@ -221,7 +221,7 @@ abstract class EVMChainWalletBase
/// - The exact amount the user wants to send,
/// - The addressHex for the receiving wallet,
/// - A contract address which would be essential in determining if to calcualate the estimate for ERC20 or native ETH
Future<int> calculateActualEstimatedFeeForCreateTransaction({
Future<GasParamsHandler> calculateActualEstimatedFeeForCreateTransaction({
required amount,
required String? contractAddress,
required String receivingAddressHex,
@ -240,22 +240,27 @@ abstract class EVMChainWalletBase
maxFeePerGas = gasPrice;
}
final estimatedGas = await _client.getEstimatedGas(
final estimatedGas = await _client.getEstimatedGasUnitsForTransaction(
contractAddress: contractAddress,
senderAddress: _evmChainPrivateKey.address,
value: EtherAmount.fromBigInt(EtherUnit.wei, amount!),
gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
toAddress: EthereumAddress.fromHex(receivingAddressHex),
// maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
// maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
);
final totalGasFee = estimatedGas * maxFeePerGas;
return totalGasFee;
return GasParamsHandler(
estimatedGasUnits: estimatedGas,
estimatedGasFee: totalGasFee,
maxFeePerGas: maxFeePerGas,
gasPrice: gasPrice,
);
}
return 0;
return GasParamsHandler.zero();
} catch (e) {
return 0;
return GasParamsHandler.zero();
}
}
@ -265,7 +270,7 @@ abstract class EVMChainWalletBase
}
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_client.stop();
_transactionsUpdateTimer?.cancel();
_updateFeesTimer?.cancel();
@ -318,7 +323,7 @@ abstract class EVMChainWalletBase
gasPrice = await _client.getGasUnitPrice();
estimatedGasUnits = await _client.getEstimatedGas(
estimatedGasUnits = await _client.getEstimatedGasUnitsForTransaction(
senderAddress: _evmChainPrivateKey.address,
toAddress: _evmChainPrivateKey.address,
gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice),
@ -349,6 +354,8 @@ abstract class EVMChainWalletBase
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent);
String? contractAddress;
int estimatedGasUnitsForTransaction = 0;
int maxFeePerGasForTransaction = 0;
String toAddress = _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address;
@ -367,14 +374,16 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction(
amount: totalAmount,
receivingAddressHex: toAddress,
priority: _credentials.priority!,
contractAddress: contractAddress,
);
estimatedFeesForTransaction = BigInt.from(estimateFees);
estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee);
estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits;
maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas;
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
@ -392,14 +401,16 @@ abstract class EVMChainWalletBase
totalAmount = erc20Balance.balance;
}
final estimateFees = await calculateActualEstimatedFeeForCreateTransaction(
final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction(
amount: totalAmount,
receivingAddressHex: toAddress,
priority: _credentials.priority!,
contractAddress: contractAddress,
);
estimatedFeesForTransaction = BigInt.from(estimateFees);
estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee);
estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits;
maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas;
if (output.sendAll && transactionCurrency is! Erc20Token) {
totalAmount = (erc20Balance.balance - estimatedFeesForTransaction);
@ -420,12 +431,14 @@ abstract class EVMChainWalletBase
}
final pendingEVMChainTransaction = await _client.signTransaction(
estimatedGasUnits: estimatedGasUnitsForTransaction,
privateKey: _evmChainPrivateKey,
toAddress: toAddress,
amount: totalAmount,
gas: estimatedFeesForTransaction,
gasFee: estimatedFeesForTransaction,
priority: _credentials.priority!,
currency: transactionCurrency,
maxFeePerGas: maxFeePerGasForTransaction,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
@ -728,3 +741,25 @@ abstract class EVMChainWalletBase
@override
final String? passphrase;
}
class GasParamsHandler {
final int estimatedGasUnits;
final int estimatedGasFee;
final int maxFeePerGas;
final int gasPrice;
GasParamsHandler(
{required this.estimatedGasUnits,
required this.estimatedGasFee,
required this.maxFeePerGas,
required this.gasPrice});
static GasParamsHandler zero() {
return GasParamsHandler(
estimatedGasUnits: 0,
estimatedGasFee: 0,
maxFeePerGas: 0,
gasPrice: 0,
);
}
}

View file

@ -108,7 +108,7 @@ abstract class HavenWalletBase
Future<void>? updateBalance() => null;
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_autoSaveTimer?.cancel();

View file

@ -200,9 +200,16 @@ String? commitTransactionFromPointerAddress({required int address, required bool
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR);
String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) {
final transactionPointerAddress = transactionPointer.address;
final txCommit = useUR
? monero.PendingTransaction_commitUR(transactionPointer, 120)
: monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
? monero.PendingTransaction_commitUR(transactionPointer, 120)
: Isolate.run(() {
monero.PendingTransaction_commit(
Pointer.fromAddress(transactionPointerAddress),
filename: '',
overwrite: false,
);
});
String? error = (() {
final status = monero.PendingTransaction_status(transactionPointer.cast());
@ -221,7 +228,7 @@ String? commitTransaction({required monero.PendingTransaction transactionPointer
})();
}
if (error != null) {
if (error != null && error != "no tx keys found for this txid") {
throw CreationTransactionException(message: error);
}
if (useUR) {

View file

@ -121,7 +121,6 @@ Future<bool> setupNodeSync(
daemonUsername: login ?? '',
daemonPassword: password ?? '');
});
// monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true, logPath: '');
final status = monero.Wallet_status(wptr!);

View file

@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:ffi/ffi.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart';
import 'package:monero/monero.dart' as monero;
// import 'package:polyseed/polyseed.dart';
LedgerConnection? gLedger;
@ -28,9 +29,16 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) {
ptr, emptyPointer.cast<UnsignedChar>(), 0);
malloc.free(emptyPointer);
// printV("> ${ledgerRequest.toHexString()}");
_logLedgerCommand(ledgerRequest, false);
final response = await exchange(connection, ledgerRequest);
// printV("< ${response.toHexString()}");
_logLedgerCommand(response, true);
if (ListEquality().equals(response, [0x55, 0x15])) {
await connection.disconnect();
// // TODO: Show POPUP pls unlock your device
// await Future.delayed(Duration(seconds: 15));
// response = await exchange(connection, ledgerRequest);
}
final Pointer<Uint8> result = malloc<Uint8>(response.length);
for (var i = 0; i < response.length; i++) {
@ -82,3 +90,59 @@ class ExchangeOperation extends LedgerRawOperation<Uint8List> {
@override
Future<List<Uint8List>> write(ByteDataWriter writer) async => [inputData];
}
const _ledgerMoneroCommands = {
0x00: "INS_NONE",
0x02: "INS_RESET",
0x20: "INS_GET_KEY",
0x21: "INS_DISPLAY_ADDRESS",
0x22: "INS_PUT_KEY",
0x24: "INS_GET_CHACHA8_PREKEY",
0x26: "INS_VERIFY_KEY",
0x28: "INS_MANAGE_SEEDWORDS",
0x30: "INS_SECRET_KEY_TO_PUBLIC_KEY",
0x32: "INS_GEN_KEY_DERIVATION",
0x34: "INS_DERIVATION_TO_SCALAR",
0x36: "INS_DERIVE_PUBLIC_KEY",
0x38: "INS_DERIVE_SECRET_KEY",
0x3A: "INS_GEN_KEY_IMAGE",
0x3B: "INS_DERIVE_VIEW_TAG",
0x3C: "INS_SECRET_KEY_ADD",
0x3E: "INS_SECRET_KEY_SUB",
0x40: "INS_GENERATE_KEYPAIR",
0x42: "INS_SECRET_SCAL_MUL_KEY",
0x44: "INS_SECRET_SCAL_MUL_BASE",
0x46: "INS_DERIVE_SUBADDRESS_PUBLIC_KEY",
0x48: "INS_GET_SUBADDRESS",
0x4A: "INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY",
0x4C: "INS_GET_SUBADDRESS_SECRET_KEY",
0x70: "INS_OPEN_TX",
0x72: "INS_SET_SIGNATURE_MODE",
0x74: "INS_GET_ADDITIONAL_KEY",
0x76: "INS_STEALTH",
0x77: "INS_GEN_COMMITMENT_MASK",
0x78: "INS_BLIND",
0x7A: "INS_UNBLIND",
0x7B: "INS_GEN_TXOUT_KEYS",
0x7D: "INS_PREFIX_HASH",
0x7C: "INS_VALIDATE",
0x7E: "INS_MLSAG",
0x7F: "INS_CLSAG",
0x80: "INS_CLOSE_TX",
0xA0: "INS_GET_TX_PROOF",
0xC0: "INS_GET_RESPONSE"
};
void _logLedgerCommand(Uint8List command, [bool isResponse = true]) {
String toHexString(Uint8List data) =>
data.map((e) => e.toRadixString(16).padLeft(2, '0')).join();
if (isResponse) {
printV("< ${toHexString(command)}");
} else {
printV(
"> ${_ledgerMoneroCommands[command[1]]} ${toHexString(command.sublist(2))}");
}
}

View file

@ -175,7 +175,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void>? updateBalance() => null;
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_onTxHistoryChangeReaction?.reaction.dispose();

View file

@ -1,5 +1,7 @@
import 'dart:ffi';
import 'dart:io';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/monero_wallet_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/unspent_coins_info.dart';
@ -9,16 +11,16 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:cw_monero/ledger.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:polyseed/polyseed.dart';
import 'package:monero/monero.dart' as monero;
import 'package:polyseed/polyseed.dart';
class MoneroNewWalletCredentials extends WalletCredentials {
MoneroNewWalletCredentials(
@ -133,14 +135,12 @@ class MoneroWalletService extends WalletService<
try {
final path = await pathForWallet(name: name, type: getType());
if (walletFilesExist(path)) {
await repairOldAndroidWallet(name);
}
if (walletFilesExist(path)) await repairOldAndroidWallet(name);
await monero_wallet_manager
.openWalletAsync({'path': path, 'password': password});
final walletInfo = walletInfoSource.values.firstWhere(
(info) => info.id == WalletBase.idFor(name, getType()));
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = MoneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
@ -204,7 +204,7 @@ class MoneroWalletService extends WalletService<
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values.firstWhere(
(info) => info.id == WalletBase.idFor(currentName, getType()));
(info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = MoneroWallet(
walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
@ -255,14 +255,14 @@ class MoneroWalletService extends WalletService<
final password = credentials.password;
final height = credentials.height;
if (wptr == null ) monero_wallet_manager.createWalletPointer();
if (wptr == null) monero_wallet_manager.createWalletPointer();
enableLedgerExchange(wptr!, credentials.ledgerConnection);
await monero_wallet_manager.restoreWalletFromHardwareWallet(
path: path,
password: password!,
restoreHeight: height!,
deviceName: 'Ledger');
path: path,
password: password!,
restoreHeight: height!,
deviceName: 'Ledger');
final wallet = MoneroWallet(
walletInfo: credentials.walletInfo!,
@ -279,7 +279,8 @@ class MoneroWalletService extends WalletService<
}
@override
Future<MoneroWallet> restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials,
Future<MoneroWallet> restoreFromSeed(
MoneroRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
// Restore from Polyseed
if (Polyseed.isValidSeed(credentials.mnemonic)) {
@ -313,7 +314,8 @@ class MoneroWalletService extends WalletService<
final path = await pathForWallet(name: credentials.name, type: getType());
final polyseedCoin = PolyseedCoin.POLYSEED_MONERO;
final lang = PolyseedLang.getByPhrase(credentials.mnemonic);
final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
final polyseed =
Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang);
@ -355,24 +357,18 @@ class MoneroWalletService extends WalletService<
Future<void> repairOldAndroidWallet(String name) async {
try {
if (!Platform.isAndroid) {
return;
}
if (!Platform.isAndroid) return;
final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name);
final dir = Directory(oldAndroidWalletDirPath);
if (!dir.existsSync()) {
return;
}
if (!dir.existsSync()) return;
final newWalletDirPath = await pathForWalletDir(name: name, type: getType());
dir.listSync().forEach((f) {
final file = File(f.path);
final name = f.path
.split('/')
.last;
final name = f.path.split('/').last;
final newPath = newWalletDirPath + '/$name';
final newFile = File(newPath);
@ -391,9 +387,7 @@ class MoneroWalletService extends WalletService<
try {
final path = await pathForWallet(name: name, type: getType());
if (walletFilesExist(path)) {
await repairOldAndroidWallet(name);
}
if (walletFilesExist(path)) await repairOldAndroidWallet(name);
await monero_wallet_manager.openWalletAsync({'path': path, 'password': password});
final walletInfo = walletInfoSource.values
@ -412,8 +406,10 @@ class MoneroWalletService extends WalletService<
@override
bool requireHardwareWalletConnection(String name) {
final walletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
return walletInfo.isHardwareWallet;
return walletInfoSource.values
.firstWhereOrNull(
(info) => info.id == WalletBase.idFor(name, getType()))
?.isHardwareWallet ??
false;
}
}

View file

@ -503,8 +503,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: c41c4dad9aa5003a914cfb2c528c76386f952665
resolved-ref: c41c4dad9aa5003a914cfb2c528c76386f952665
ref: af5277f96073917185864d3596e82b67bee54e78
resolved-ref: af5277f96073917185864d3596e82b67bee54e78
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"

View file

@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: c41c4dad9aa5003a914cfb2c528c76386f952665
ref: af5277f96073917185864d3596e82b67bee54e78
# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

View file

@ -28,7 +28,10 @@ abstract class NanoTransactionHistoryBase extends TransactionHistoryBase<NanoTra
final EncryptionFileUtils encryptionFileUtils;
String _password;
Future<void> init() async => await _load();
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {

View file

@ -150,7 +150,7 @@ abstract class NanoWalletBase
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_client.stop();
_receiveTimer?.cancel();
}

View file

@ -14,11 +14,19 @@ class PolygonClient extends EVMChainClient {
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
int? maxGas,
EtherAmount? gasPrice,
EtherAmount? maxFeePerGas,
}) {
return Transaction(
from: from,
to: to,
value: amount,
data: data,
maxGas: maxGas,
gasPrice: gasPrice,
maxFeePerGas: maxFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
);
}

View file

@ -1,12 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_solana/pending_solana_transaction.dart';
import 'package:cw_solana/solana_balance.dart';
import 'package:cw_solana/solana_exceptions.dart';
import 'package:cw_solana/solana_transaction_model.dart';
import 'package:http/http.dart' as http;
import 'package:solana/dto.dart';
@ -180,7 +181,7 @@ class SolanaWalletClient {
bool isOutgoingTx = transfer.source == publicKey.toBase58();
double amount = (double.tryParse(transfer.amount) ?? 0.0) /
pow(10, splTokenDecimal ?? 9);
math.pow(10, splTokenDecimal ?? 9);
transactions.add(
SolanaTransactionModel(
@ -276,6 +277,7 @@ class SolanaWalletClient {
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
required double solBalance,
String? tokenMint,
List<String> references = const [],
}) async {
@ -290,6 +292,7 @@ class SolanaWalletClient {
ownerKeypair: ownerKeypair,
commitment: commitment,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingNativeTokenTransaction;
} else {
@ -301,6 +304,7 @@ class SolanaWalletClient {
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
commitment: commitment,
solBalance: solBalance,
);
return pendingSPLTokenTransaction;
}
@ -353,6 +357,23 @@ class SolanaWalletClient {
return fee;
}
Future<bool> hasSufficientFundsLeftForRent({
required double inputAmount,
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;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
@ -361,6 +382,7 @@ class SolanaWalletClient {
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
required double solBalance,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
@ -378,6 +400,16 @@ class SolanaWalletClient {
commitment,
);
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: inputAmount,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignNativeTokenTransactionRentException();
}
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
@ -425,6 +457,7 @@ class SolanaWalletClient {
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required double solBalance,
}) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
@ -447,7 +480,7 @@ class SolanaWalletClient {
// Throw an appropriate exception if the sender has no associated
// token account
if (associatedSenderAccount == null) {
throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
throw SolanaNoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
}
try {
@ -457,11 +490,11 @@ class SolanaWalletClient {
funder: ownerKeypair,
);
} catch (e) {
throw Exception('Insufficient SOL balance to complete this transaction: ${e.toString()}');
throw SolanaCreateAssociatedTokenAccountException(e.toString());
}
// Input by the user
final amount = (inputAmount * pow(10, tokenDecimals)).toInt();
final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt();
final instruction = TokenInstruction.transfer(
source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
@ -483,6 +516,16 @@ class SolanaWalletClient {
commitment,
);
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: inputAmount,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignSPLTokenTransactionRentException();
}
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,

View file

@ -19,3 +19,20 @@ class SolanaTransactionWrongBalanceException implements Exception {
@override
String toString() => exceptionMessage;
}
class SolanaSignNativeTokenTransactionRentException implements Exception {}
class SolanaCreateAssociatedTokenAccountException implements Exception {
final String exceptionMessage;
SolanaCreateAssociatedTokenAccountException(this.exceptionMessage);
}
class SolanaSignSPLTokenTransactionRentException implements Exception {}
class SolanaNoAssociatedTokenAccountException implements Exception {
const SolanaNoAssociatedTokenAccountException(this.account, this.mint);
final String account;
final String mint;
}

View file

@ -26,7 +26,10 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<Solan
final EncryptionFileUtils encryptionFileUtils;
String _password;
Future<void> init() async => await _load();
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {

View file

@ -180,7 +180,7 @@ abstract class SolanaWalletBase
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@ -228,6 +228,8 @@ abstract class SolanaWalletBase
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
final solBalance = balance[CryptoCurrency.sol]!.balance;
double totalAmount = 0.0;
bool isSendAll = false;
@ -279,6 +281,7 @@ abstract class SolanaWalletBase
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingSolanaTransaction;

View file

@ -25,7 +25,10 @@ abstract class TronTransactionHistoryBase extends TransactionHistoryBase<TronTra
final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
Future<void> init() async => await _load();
Future<void> init() async {
clear();
await _load();
}
@override
Future<void> save() async {

View file

@ -217,7 +217,7 @@ abstract class TronWalletBase
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
Future<void> close({required bool shouldCleanup}) async => _transactionsUpdateTimer?.cancel();
Future<void> close({bool shouldCleanup = false}) async => _transactionsUpdateTimer?.cancel();
@action
@override

View file

@ -181,13 +181,23 @@ void commitTransaction({required wownero.PendingTransaction transactionPointer})
final txCommit = wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
final String? error = (() {
String? error = (() {
final status = wownero.PendingTransaction_status(transactionPointer.cast());
if (status == 0) {
return null;
}
return wownero.Wallet_errorString(wptr!);
return wownero.PendingTransaction_errorString(transactionPointer.cast());
})();
if (error == null) {
error = (() {
final status = wownero.Wallet_status(wptr!);
if (status == 0) {
return null;
}
return wownero.Wallet_errorString(wptr!);
})();
}
if (error != null) {
throw CreationTransactionException(message: error);

View file

@ -162,7 +162,7 @@ abstract class WowneroWalletBase
Future<void>? updateBalance() => null;
@override
Future<void> close({required bool shouldCleanup}) async {
Future<void> close({bool shouldCleanup = false}) async {
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_onTxHistoryChangeReaction?.reaction.dispose();

View file

@ -463,8 +463,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: c41c4dad9aa5003a914cfb2c528c76386f952665
resolved-ref: c41c4dad9aa5003a914cfb2c528c76386f952665
ref: af5277f96073917185864d3596e82b67bee54e78
resolved-ref: af5277f96073917185864d3596e82b67bee54e78
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -757,10 +757,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.4"
version: "14.2.5"
watcher:
dependency: "direct overridden"
description:

View file

@ -25,7 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: c41c4dad9aa5003a914cfb2c528c76386f952665
ref: af5277f96073917185864d3596e82b67bee54e78
# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

View file

@ -4,10 +4,10 @@ import 'package:cw_core/wallet_type.dart';
class CommonTestConstants {
static final pin = [0, 8, 0, 1];
static final String sendTestAmount = '0.00008';
static final String exchangeTestAmount = '8';
static final String exchangeTestAmount = '0.01';
static final WalletType testWalletType = WalletType.solana;
static final String testWalletName = 'Integrated Testing Wallet';
static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol;
static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol;
static final CryptoCurrency testReceiveCurrency = CryptoCurrency.usdtSol;
static final CryptoCurrency testDepositCurrency = CryptoCurrency.sol;
static final String testWalletAddress = '5v9gTW1yWPffhnbNKuvtL2frevAf4HpBMw8oYnfqUjhm';
}

View file

@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:cake_wallet/main.dart' as app;
import '../robots/create_pin_welcome_page_robot.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/disclaimer_page_robot.dart';
import '../robots/new_wallet_page_robot.dart';
@ -37,6 +38,7 @@ class CommonTestFlows {
_walletListPageRobot = WalletListPageRobot(_tester),
_newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
_restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
_createPinWelcomePageRobot = CreatePinWelcomePageRobot(_tester),
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester),
_walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester);
@ -53,6 +55,7 @@ class CommonTestFlows {
final WalletListPageRobot _walletListPageRobot;
final NewWalletTypePageRobot _newWalletTypePageRobot;
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
@ -190,10 +193,12 @@ class CommonTestFlows {
WalletType walletTypeToCreate,
List<int> pin,
) async {
await _welcomePageRobot.navigateToCreateNewWalletPage();
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCodeForWallet(pin);
await _welcomePageRobot.navigateToCreateNewWalletPage();
await _selectWalletTypeForWallet(walletTypeToCreate);
}
@ -201,12 +206,14 @@ class CommonTestFlows {
WalletType walletTypeToRestore,
List<int> pin,
) async {
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCodeForWallet(pin);
await _welcomePageRobot.navigateToRestoreWalletPage();
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
await setupPinCodeForWallet(pin);
await _selectWalletTypeForWallet(walletTypeToRestore);
}

View file

@ -0,0 +1,53 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/welcome/create_pin_welcome_page.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class CreatePinWelcomePageRobot {
CreatePinWelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isCreatePinWelcomePage() async {
await commonTestCases.isSpecificPage<CreatePinWelcomePage>();
}
void hasTitle() {
String title;
if (isMoneroOnly) {
title = S.current.monero_com;
}
if (isHaven) {
title = S.current.haven_app;
}
title = S.current.cake_wallet;
commonTestCases.hasText(title);
}
void hasDescription() {
String description;
if (isMoneroOnly) {
description = S.current.monero_com_wallet_text;
}
if (isHaven) {
description = S.current.haven_app_wallet_text;
}
description = S.current.new_first_wallet_text;
commonTestCases.hasText(description);
}
Future<void> tapSetAPinButton() async {
await commonTestCases.tapItemByKey('create_pin_welcome_page_create_a_pin_button_key');
await commonTestCases.defaultSleepTime();
}
}

View file

@ -123,10 +123,8 @@ class ExchangePageRobot {
return;
}
await commonTestCases.dragUntilVisible(
'picker_items_index_${depositCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.enterText(depositCurrency.name, 'search_bar_widget_key');
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
@ -149,10 +147,8 @@ class ExchangePageRobot {
return;
}
await commonTestCases.dragUntilVisible(
'picker_items_index_${receiveCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.enterText(receiveCurrency.name, 'search_bar_widget_key');
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
@ -318,7 +314,7 @@ class ExchangePageRobot {
Future<void> handleErrors(String initialAmount) async {
await tester.pumpAndSettle();
await _handleMinLimitError(initialAmount);
await _handleMaxLimitError(initialAmount);

View file

@ -84,13 +84,11 @@ class SendPageRobot {
return;
}
await commonTestCases.dragUntilVisible(
'picker_items_index_${receiveCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.enterText(receiveCurrency.title, 'search_bar_widget_key');
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.fullName}_button_key');
}
Future<void> enterReceiveAddress(String receiveAddress) async {
@ -210,6 +208,7 @@ class SendPageRobot {
_handleAuthPage();
}
}
await tester.pump();
}
Future<void> handleSendResult() async {
@ -366,4 +365,4 @@ class SendPageRobot {
Future<void> _onIgnoreButtonOnSentDialogPressed() async {
await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key');
}
}
}

View file

@ -42,11 +42,11 @@ class WalletKeysAndSeedPageRobot {
bool hasPrivateKey = appStore.wallet!.privateKey != null;
if (walletType == WalletType.monero) {
final moneroWallet = appStore.wallet as MoneroWallet;
final moneroWallet = appStore.wallet as MoneroWalletBase;
final lang = PolyseedLang.getByPhrase(moneroWallet.seed);
final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish);
_confirmMoneroWalletCredentials(
await _confirmMoneroWalletCredentials(
appStore,
walletName,
moneroWallet.seed,
@ -59,7 +59,7 @@ class WalletKeysAndSeedPageRobot {
final lang = PolyseedLang.getByPhrase(wowneroWallet.seed);
final legacySeed = wowneroWallet.seedLegacy(lang.nameEnglish);
_confirmMoneroWalletCredentials(
await _confirmMoneroWalletCredentials(
appStore,
walletName,
wowneroWallet.seed,
@ -105,12 +105,12 @@ class WalletKeysAndSeedPageRobot {
await commonTestCases.defaultSleepTime(seconds: 5);
}
void _confirmMoneroWalletCredentials(
Future<void> _confirmMoneroWalletCredentials(
AppStore appStore,
String walletName,
String seed,
String legacySeed,
) {
) async {
final keys = appStore.wallet!.keys as MoneroWalletKeys;
final hasPublicSpendKey = commonTestCases.isKeyPresent(
@ -145,10 +145,18 @@ class WalletKeysAndSeedPageRobot {
tester.printToConsole('$walletName wallet has private view key properly displayed');
}
if (hasSeeds) {
await commonTestCases.dragUntilVisible(
'${walletName}_wallet_seed_item_key',
'wallet_keys_page_credentials_list_view_key',
);
commonTestCases.hasText(seed);
tester.printToConsole('$walletName wallet has seeds properly displayed');
}
if (hasSeedLegacy) {
await commonTestCases.dragUntilVisible(
'${walletName}_wallet_seed_legacy_item_key',
'wallet_keys_page_credentials_list_view_key',
);
commonTestCases.hasText(legacySeed);
tester.printToConsole('$walletName wallet has legacy seeds properly displayed');
}

View file

@ -101,7 +101,7 @@ Future<void> _confirmSeedsFlowForWalletType(
walletKeysAndSeedPageRobot.hasTitle();
walletKeysAndSeedPageRobot.hasShareWarning();
walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType);
await walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType);
await walletKeysAndSeedPageRobot.backToDashboard();
}

45
integration_test_runner.sh Executable file
View file

@ -0,0 +1,45 @@
#!/bin/bash
declare -a targets
declare -a passed_tests
declare -a failed_tests
# Collect all Dart test files in the integration_test directory
while IFS= read -r -d $'\0' file; do
targets+=("$file")
done < <(find integration_test/test_suites -name "*.dart" -type f -print0)
# Run each test and collect results
for target in "${targets[@]}"
do
echo "Running test: $target"
if flutter drive \
--driver=test_driver/integration_test.dart \
--target="$target"; then
echo "✅ Test passed: $target"
passed_tests+=("$target")
else
echo "❌ Test failed: $target"
failed_tests+=("$target")
fi
done
# Provide a summary of test results
echo -e "\n===== Test Summary ====="
if [ ${#passed_tests[@]} -gt 0 ]; then
echo "✅ Passed Tests:"
for test in "${passed_tests[@]}"; do
echo " - $test"
done
fi
if [ ${#failed_tests[@]} -gt 0 ]; then
echo -e "\n❌ Failed Tests:"
for test in "${failed_tests[@]}"; do
echo " - $test"
done
# Exit with a non-zero status to indicate failure
exit 1
else
echo -e "\n🎉 All tests passed successfully!"
fi

View file

@ -93,6 +93,9 @@ class CakePayApi {
required int quantity,
required String userEmail,
required String token,
required bool confirmsNoVpn,
required bool confirmsVoidedRefund,
required bool confirmsTermsAgreed,
}) async {
final uri = Uri.https(baseCakePayUri, createOrderPath);
final headers = {
@ -106,7 +109,10 @@ class CakePayApi {
'quantity': quantity,
'user_email': userEmail,
'token': token,
'send_email': true
'send_email': true,
'confirms_no_vpn': confirmsNoVpn,
'confirms_voided_refund': confirmsVoidedRefund,
'confirms_terms_agreed': confirmsTermsAgreed,
};
try {

View file

@ -90,17 +90,27 @@ class CakePayService {
}
/// Purchase Gift Card
Future<CakePayOrder> createOrder(
{required int cardId, required String price, required int quantity}) async {
Future<CakePayOrder> createOrder({
required int cardId,
required String price,
required int quantity,
required bool confirmsNoVpn,
required bool confirmsVoidedRefund,
required bool confirmsTermsAgreed,
}) async {
final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!;
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.createOrder(
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail);
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
);
}
///Simulate Purchase Gift Card

View file

@ -80,7 +80,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.shib:
pattern = '0x[0-9a-zA-Z]+';
case CryptoCurrency.xrp:
pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}';
pattern = '[0-9a-zA-Z]{34}|[0-9a-zA-Z]{33}|X[0-9a-zA-Z]{46}';
case CryptoCurrency.xhv:
pattern = 'hvx|hvi|hvs[0-9a-zA-Z]+';
case CryptoCurrency.xag:
@ -106,9 +106,8 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.wow:
pattern = '[0-9a-zA-Z]+';
case CryptoCurrency.bch:
pattern = '^(bitcoincash:)?(q|p)[0-9a-zA-Z]{41,42}';
case CryptoCurrency.bnb:
pattern = '[0-9a-zA-Z]+';
pattern = '(?:bitcoincash:)?(q|p)[0-9a-zA-Z]{41}'
'|[13][a-km-zA-HJ-NP-Z1-9]{25,34}';
case CryptoCurrency.hbar:
pattern = '[0-9a-zA-Z.]+';
case CryptoCurrency.zaddr:
@ -203,7 +202,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.avaxc:
return [42];
case CryptoCurrency.bch:
return [42, 43, 44, 54, 55];
return [42, 54, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35];
case CryptoCurrency.bnb:
return [42];
case CryptoCurrency.nano:
@ -282,10 +281,14 @@ class AddressValidator extends TextValidator {
switch (type) {
case CryptoCurrency.xmr:
case CryptoCurrency.wow:
pattern = '(4[0-9a-zA-Z]{94})'
'|(8[0-9a-zA-Z]{94})'
'|([0-9a-zA-Z]{106})';
case CryptoCurrency.wow:
pattern = '(W[0-9a-zA-Z]{94})'
'|(W[0-9a-zA-Z]{94})'
'|(W[0-9a-zA-Z]{96})'
'|([0-9a-zA-Z]{106})';
case CryptoCurrency.btc:
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';

View file

@ -39,6 +39,8 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
@ -1188,6 +1190,9 @@ Future<void> setup({
getIt.registerFactoryParam<PreSeedPage, int, void>(
(seedPhraseLength, _) => PreSeedPage(seedPhraseLength));
getIt.registerFactoryParam<TransactionSuccessPage, String, void>(
(content, _) => TransactionSuccessPage(content: content));
getIt.registerFactoryParam<TradeDetailsViewModel, Trade, void>((trade, _) =>
TradeDetailsViewModel(
tradeForDetails: trade,
@ -1423,5 +1428,7 @@ Future<void> setup({
getIt.registerFactory(() => SignViewModel(getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
_isSetupFinished = true;
}

View file

@ -1,9 +1,13 @@
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';
@ -52,7 +56,8 @@ Future<void> defaultSettingsMigration(
required Box<Node> powNodes,
required Box<WalletInfo> walletInfoSource,
required Box<Trade> tradeSource,
required Box<Contact> contactSource}) async {
required Box<Contact> contactSource,
required Box<HavenSeedStore> havenSeedStore}) async {
if (Platform.isIOS) {
await ios_migrate_v1(walletInfoSource, tradeSource, contactSource);
}
@ -289,7 +294,23 @@ Future<void> defaultSettingsMigration(
],
);
break;
case 45:
await _backupHavenSeeds(havenSeedStore);
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,
);
default:
break;
}
@ -304,6 +325,13 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
}
Future<void> _backupHavenSeeds(Box<HavenSeedStore> havenSeedStore) async {
final future = haven?.backupHavenSeeds(havenSeedStore);
if (future != null) {
await future;
}
return;
}
/// generic function for changing any wallet default node
/// instead of making a new function for each change
Future<void> _changeDefaultNode({
@ -339,6 +367,26 @@ Future<void> _changeDefaultNode({
}
}
/// Generic function for adding a new Node for a Wallet Type.
Future<void> updateWalletTypeNodesWithNewNode({
required SharedPreferences sharedPreferences,
required Box<Node> nodes,
required WalletType type,
required String newNodeUri,
required 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;
await nodes.add(
Node(
uri: newNodeUri,
type: type,
useSSL: useSSL,
),
);
}
Future<void> _updateCakeXmrNode(Box<Node> nodes) async {
final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri);

View file

@ -0,0 +1,81 @@
class EVMTransactionErrorFeesHandler {
EVMTransactionErrorFeesHandler({
this.balanceWei,
this.balanceEth,
this.balanceUsd,
this.txCostWei,
this.txCostEth,
this.txCostUsd,
this.overshotWei,
this.overshotEth,
this.overshotUsd,
this.error,
});
String? balanceWei;
String? balanceEth;
String? balanceUsd;
String? txCostWei;
String? txCostEth;
String? txCostUsd;
String? overshotWei;
String? overshotEth;
String? overshotUsd;
String? error;
factory EVMTransactionErrorFeesHandler.parseEthereumFeesErrorMessage(
String errorMessage,
double assetPriceUsd,
) {
// Define Regular Expressions to extract the numerical values
RegExp balanceRegExp = RegExp(r'balance (\d+)');
RegExp txCostRegExp = RegExp(r'tx cost (\d+)');
RegExp overshotRegExp = RegExp(r'overshot (\d+)');
// Match the patterns in the error message
Match? balanceMatch = balanceRegExp.firstMatch(errorMessage);
Match? txCostMatch = txCostRegExp.firstMatch(errorMessage);
Match? overshotMatch = overshotRegExp.firstMatch(errorMessage);
// Check if all required values are found
if (balanceMatch != null && txCostMatch != null && overshotMatch != null) {
// Extract the numerical strings
String balanceStr = balanceMatch.group(1)!;
String txCostStr = txCostMatch.group(1)!;
String overshotStr = overshotMatch.group(1)!;
// Parse the numerical strings to BigInt
BigInt balanceWei = BigInt.parse(balanceStr);
BigInt txCostWei = BigInt.parse(txCostStr);
BigInt overshotWei = BigInt.parse(overshotStr);
// Convert wei to ETH (1 ETH = 1e18 wei)
double balanceEth = balanceWei.toDouble() / 1e18;
double txCostEth = txCostWei.toDouble() / 1e18;
double overshotEth = overshotWei.toDouble() / 1e18;
// Calculate the USD values
double balanceUsd = balanceEth * assetPriceUsd;
double txCostUsd = txCostEth * assetPriceUsd;
double overshotUsd = overshotEth * assetPriceUsd;
return EVMTransactionErrorFeesHandler(
balanceWei: balanceWei.toString(),
balanceEth: balanceEth.toString().substring(0, 12),
balanceUsd: balanceUsd.toString().substring(0, 4),
txCostWei: txCostWei.toString(),
txCostEth: txCostEth.toString().substring(0, 12),
txCostUsd: txCostUsd.toString().substring(0, 4),
overshotWei: overshotWei.toString(),
overshotEth: overshotEth.toString().substring(0, 12),
overshotUsd: overshotUsd.toString().substring(0, 4),
);
} else {
// If any value is missing, return an error message
return EVMTransactionErrorFeesHandler(error: 'Could not parse the error message.');
}
}
}

View file

@ -0,0 +1,19 @@
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'haven_seed_store.g.dart';
@HiveType(typeId: HavenSeedStore.typeId)
class HavenSeedStore extends HiveObject {
HavenSeedStore({required this.id, this.seed});
static const typeId = HAVEN_SEED_STORE_TYPE_ID;
static const boxName = 'HavenSeedStore';
static const boxKey = 'havenSeedStoreKey';
@HiveField(0, defaultValue: '')
String id;
@HiveField(2)
String? seed;
}

View file

@ -92,6 +92,7 @@ class PreferencesKey {
static const donationLinkWalletName = 'donation_link_wallet_name';
static const lastSeenAppVersion = 'last_seen_app_version';
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const showAddressBookPopupEnabled = 'show_address_book_popup_enabled';
static const isNewInstall = 'is_new_install';
static const serviceStatusShaKey = 'service_status_sha_key';
static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';

View file

@ -60,15 +60,17 @@ class ExolixExchangeProvider extends ExchangeProvider {
Future<bool> checkIsAvailable() async => true;
@override
Future<Limits> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
Future<Limits> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
}) async {
final params = <String, String>{
'rateType': _getRateType(isFixedRateMode),
'amount': '1',
'apiToken': apiKey,
};
if (isFixedRateMode) {
params['coinFrom'] = _normalizeCurrency(to);
params['coinTo'] = _normalizeCurrency(from);
@ -80,14 +82,30 @@ class ExolixExchangeProvider extends ExchangeProvider {
params['networkFrom'] = _networkFor(from);
params['networkTo'] = _networkFor(to);
}
final uri = Uri.https(apiBaseUrl, ratePath, params);
final response = await get(uri);
if (response.statusCode != 200)
throw Exception('Unexpected http status: ${response.statusCode}');
// Maximum of 2 attempts to fetch limits
for (int i = 0; i < 2; i++) {
final uri = Uri.https(apiBaseUrl, ratePath, params);
final response = await get(uri);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
return Limits(min: responseJSON['minAmount'] as double?);
if (response.statusCode == 200) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final minAmount = responseJSON['minAmount'];
final maxAmount = responseJSON['maxAmount'];
return Limits(min: _toDouble(minAmount), max: _toDouble(maxAmount));
} else if (response.statusCode == 422) {
final errorResponse = json.decode(response.body) as Map<String, dynamic>;
if (errorResponse.containsKey('minAmount')) {
params['amount'] = errorResponse['minAmount'].toString();
continue;
}
throw Exception('Error 422: ${errorResponse['message'] ?? 'Unknown error'}');
} else {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
}
throw Exception('Failed to fetch limits after retrying.');
}
@override
@ -279,4 +297,15 @@ class ExolixExchangeProvider extends ExchangeProvider {
String _normalizeAddress(String address) =>
address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
static double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else if (value is String) {
return double.tryParse(value);
}
return null;
}
}

View file

@ -168,6 +168,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final expiredAtTimestamp = responseJSON['expired_at'] as int;
final extraId = responseJSON['deposit_extra_id'] as String?;
final createdAt = DateTime.parse(createdAtString);
final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
@ -199,6 +200,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
expiredAt: expiredAt,
extraId: extraId,
);
} catch (e) {
log(e.toString());
@ -231,6 +233,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final expiredAtTimestamp = responseJSON['expired_at'] as int;
final extraId = responseJSON['deposit_extra_id'] as String?;
final createdAt = DateTime.parse(createdAtString);
final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
@ -249,6 +252,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
createdAt: createdAt,
expiredAt: expiredAt,
isRefund: status == 'refund',
extraId: extraId,
);
}

View file

@ -203,6 +203,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final inputAddress = responseJSON['depositAddress'] as String;
final settleAddress = responseJSON['settleAddress'] as String;
final depositAmount = responseJSON['depositAmount'] as String?;
final depositMemo = responseJSON['depositMemo'] as String?;
return Trade(
id: id,
@ -217,6 +218,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
payoutAddress: settleAddress,
createdAt: DateTime.now(),
isSendAll: isSendAll,
extraId: depositMemo
);
}
@ -251,6 +253,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
final isVariable = (responseJSON['type'] as String) == 'variable';
final expiredAtRaw = responseJSON['expiresAt'] as String;
final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal();
final depositMemo = responseJSON['depositMemo'] as String?;
return Trade(
id: id,
@ -261,7 +264,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
amount: expectedSendAmount ?? '',
state: TradeState.deserialize(raw: status ?? 'created'),
expiredAt: expiredAt,
payoutAddress: settleAddress);
payoutAddress: settleAddress,
extraId: depositMemo);
}
Future<String> _createQuote(TradeRequest request) async {

View file

@ -154,6 +154,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
final receiveAmount = toDouble(withdrawal['amount']);
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final extraId = deposit['extra_id'] as String?;
final createdAt = DateTime.parse(createdAtString);
final expiredAt = validUntil != null
@ -188,6 +189,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
expiredAt: expiredAt,
extraId: extraId,
);
} catch (e) {
log(e.toString());
@ -220,6 +222,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['created_at'] as String;
final createdAt = DateTime.parse(createdAtString);
final extraId = deposit['extra_id'] as String?;
return Trade(
id: respId,
@ -234,6 +237,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
isRefund: status == 'refunded',
extraId: extraId,
);
}

View file

@ -171,7 +171,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
if (!isFixedRateMode) 'amount_from': request.fromAmount,
if (isFixedRateMode) 'amount_to': request.toAmount,
'address': request.toAddress,
'refund': request.refundAddress
'refund': request.refundAddress,
'refund_memo' : '0',
};
if (isFixedRateMode) {
@ -262,6 +263,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
final password = responseJSON['password'] as String;
final providerId = responseJSON['id_provider'] as String;
final providerName = responseJSON['provider'] as String;
final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
return Trade(
id: id,
@ -277,6 +279,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
password: password,
providerId: providerId,
providerName: providerName,
extraId: addressProviderMemo,
);
});
}

View file

@ -143,6 +143,7 @@ class Trade extends HiveObject {
isRefund: map['isRefund'] as bool?,
isSendAll: map['isSendAll'] as bool?,
router: map['router'] as String?,
extraId: map['extra_id'] as String?,
);
}
@ -162,6 +163,7 @@ class Trade extends HiveObject {
'isRefund': isRefund,
'isSendAll': isSendAll,
'router': router,
'extra_id': extraId,
};
}

View file

@ -307,6 +307,23 @@ class CWHaven extends Haven {
return havenTransactionInfo.accountIndex;
}
@override
Future<void> backupHavenSeeds(Box<HavenSeedStore> havenSeedStore) async {
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
final wallets = walletInfoSource.values
.where((element) => element.type == WalletType.haven);
for (var w in wallets) {
final walletService = HavenWalletService(walletInfoSource);
final flutterSecureStorage = secureStorageShared;
final keyService = KeyService(flutterSecureStorage);
final password = await keyService.getWalletPassword(walletName: w.name);
final wallet = await walletService.openWallet(w.name, password);
await havenSeedStore.add(HavenSeedStore(id: wallet.id, seed: wallet.seed));
wallet.close();
}
await havenSeedStore.flush();
}
@override
WalletService createHavenWalletService(Box<WalletInfo> walletInfoSource) {
return HavenWalletService(walletInfoSource);

View file

@ -9,6 +9,7 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cake_wallet/entities/get_encryption_key.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
@ -164,6 +165,10 @@ Future<void> initializeAppConfigs() async {
CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(HavenSeedStore.typeId)) {
CakeHive.registerAdapter(HavenSeedStoreAdapter());
}
if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
@ -188,6 +193,12 @@ Future<void> initializeAppConfigs() async {
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
final havenSeedStoreBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
final havenSeedStore = await CakeHive.openBox<HavenSeedStore>(
HavenSeedStore.boxName,
encryptionKey: havenSeedStoreBoxKey);
await initialSetup(
sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes,
@ -203,7 +214,8 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 44,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 45,
);
}
@ -222,7 +234,8 @@ Future<void> initialSetup(
required SecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
int initialMigrationVersion = 15}) async {
required Box<HavenSeedStore> havenSeedStore,
int initialMigrationVersion = 15, }) async {
LanguageService.loadLocaleList();
await defaultSettingsMigration(
secureStorage: secureStorage,
@ -232,7 +245,8 @@ Future<void> initialSetup(
contactSource: contactSource,
tradeSource: tradesSource,
nodes: nodes,
powNodes: powNodes);
powNodes: powNodes,
havenSeedStore: havenSeedStore);
await setup(
walletInfoSource: walletInfoSource,
nodeSource: nodes,

View file

@ -44,13 +44,16 @@ void startAuthenticationStateChange(
} catch (error, stack) {
loginError = error;
await ExceptionHandler.resetLastPopupDate();
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
await ExceptionHandler.onError(
FlutterErrorDetails(exception: error, stack: stack));
}
return;
}
if (state == AuthenticationState.allowed) {
if (requireHardwareWalletConnection()) {
if ([AuthenticationState.allowed, AuthenticationState.allowedCreate]
.contains(state)) {
if (state == AuthenticationState.allowed &&
requireHardwareWalletConnection()) {
await navigatorKey.currentState!.pushNamedAndRemoveUntil(
Routes.connectDevices,
(route) => false,
@ -58,14 +61,14 @@ void startAuthenticationStateChange(
walletType: WalletType.monero,
onConnectDevice: (context, ledgerVM) async {
monero!.setGlobalLedgerConnection(ledgerVM.connection);
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
buttonAction: () => Navigator.of(context).pop()),
);
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
buttonAction: () => Navigator.of(context).pop()),
);
await loadCurrentWallet();
getIt.get<BottomSheetService>().resetCurrentSheet();
await navigatorKey.currentState!

View file

@ -66,9 +66,11 @@ import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart';
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
import 'package:cake_wallet/src/screens/send/send_page.dart';
import 'package:cake_wallet/src/screens/send/send_template_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart';
import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
@ -597,6 +599,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (_) => getIt.get<PreSeedPage>(param1: settings.arguments as int));
case Routes.transactionSuccessPage:
return MaterialPageRoute<void>(
builder: (_) => getIt.get<TransactionSuccessPage>(param1: settings.arguments as String));
case Routes.backup:
return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<BackupPage>());
@ -807,6 +813,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
),
);
case Routes.walletSeedVerificationPage:
return MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_) => getIt.get<SeedVerificationPage>(),
);
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(

View file

@ -53,6 +53,7 @@ class Routes {
static const restoreWalletType = '/restore_wallet_type';
static const restoreWallet = '/restore_wallet';
static const preSeedPage = '/pre_seed_page';
static const transactionSuccessPage = '/transaction_success_info_page';
static const backup = '/backup';
static const editBackupPassword = '/edit_backup_passowrd';
static const restoreFromBackup = '/restore_from_backup';
@ -115,4 +116,5 @@ class Routes {
static const urqrAnimatedPage = '/urqr/animated_page';
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
static const walletGroupDescription = '/wallet_group_description';
static const walletSeedVerificationPage = '/wallet_seed_verification_page';
}

View file

@ -116,7 +116,7 @@ class CakePayCardsPage extends BasePage {
},
child: Container(
width: 32,
padding: EdgeInsets.all(8),
padding: EdgeInsets.only(top: 7, bottom: 7),
decoration: BoxDecoration(
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
border: Border.all(
@ -125,7 +125,7 @@ class CakePayCardsPage extends BasePage {
borderRadius: BorderRadius.circular(10),
),
child: Image.asset(
'assets/images/filter.png',
'assets/images/filter_icon.png',
color: Theme.of(context).extension<FilterTheme>()!.iconColor,
))),
);
@ -141,14 +141,14 @@ class CakePayCardsPage extends BasePage {
}
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 6),
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
border: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
child: Container(
margin: EdgeInsets.symmetric(vertical: 2),
margin: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Image.asset(
@ -363,13 +363,10 @@ class _SearchWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final searchIcon = ExcludeSemantics(
child: Padding(
padding: EdgeInsets.all(8),
child: Image.asset(
'assets/images/mini_search_icon.png',
child: Icon( Icons.search,
color: Theme.of(context).extension<FilterTheme>()!.iconColor,
//size: 24
),
),
);
return TextField(
@ -379,8 +376,8 @@ class _SearchWidget extends StatelessWidget {
decoration: InputDecoration(
filled: true,
contentPadding: EdgeInsets.only(
top: 10,
left: 10,
top: 8,
left: 8,
),
fillColor: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
hintText: S.of(context).search,

View file

@ -10,8 +10,10 @@ import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.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/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
@ -23,6 +25,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class CakePayBuyCardDetailPage extends BasePage {
CakePayBuyCardDetailPage(this.cakePayPurchaseViewModel);
@ -207,8 +210,10 @@ class CakePayBuyCardDetailPage extends BasePage {
padding: EdgeInsets.only(bottom: 12),
child: Observer(builder: (_) {
return LoadingPrimaryButton(
isLoading: cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState,
onPressed: () => purchaseCard(context),
isDisabled: cakePayPurchaseViewModel.isPurchasing,
isLoading: cakePayPurchaseViewModel.isPurchasing ||
cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState,
onPressed: () => confirmPurchaseFirst(context),
text: S.of(context).purchase_gift_card,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
@ -253,6 +258,48 @@ class CakePayBuyCardDetailPage extends BasePage {
});
}
Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
if (!cakePayPurchaseViewModel.confirmsNoVpn ||
!cakePayPurchaseViewModel.confirmsVoidedRefund ||
!cakePayPurchaseViewModel.confirmsTermsAgreed) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => ThreeCheckboxAlert(
alertTitle: S.of(context).cakepay_confirm_purchase,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).confirm,
actionLeftButton: () {
cakePayPurchaseViewModel.isPurchasing = false;
Navigator.of(context).pop();
},
actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
cakePayPurchaseViewModel.confirmsNoVpn = confirmsNoVpn;
cakePayPurchaseViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
cakePayPurchaseViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
Navigator.of(context).pop();
},
),
);
}
if (cakePayPurchaseViewModel.confirmsNoVpn &&
cakePayPurchaseViewModel.confirmsVoidedRefund &&
cakePayPurchaseViewModel.confirmsTermsAgreed) {
await purchaseCard(context);
}
}
Future<void> confirmPurchaseFirst(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
cakePayPurchaseViewModel.isPurchasing = true;
await _showconfirmPurchaseFirstAlert(context);
}
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
@ -263,7 +310,9 @@ class CakePayBuyCardDetailPage extends BasePage {
} catch (_) {
await cakePayPurchaseViewModel.cakePayService.logout();
}
}
}
cakePayPurchaseViewModel.isPurchasing = false;
}
void _showHowToUseCard(
@ -428,3 +477,201 @@ class CakePayBuyCardDetailPage extends BasePage {
}
}
}
class ThreeCheckboxAlert extends BaseAlertDialog {
ThreeCheckboxAlert({
required this.alertTitle,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
required this.actionRightButton,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final Function(bool, bool, bool) actionRightButton;
final bool alertBarrierDismissible;
bool checkbox1 = false;
void toggleCheckbox1() => checkbox1 = !checkbox1;
bool checkbox2 = false;
void toggleCheckbox2() => checkbox2 = !checkbox2;
bool checkbox3 = false;
void toggleCheckbox3() => checkbox3 = !checkbox3;
bool showValidationMessage = true;
@override
String get titleText => alertTitle;
@override
bool get isDividerExists => true;
@override
String get leftActionButtonText => leftButtonText;
@override
String get rightActionButtonText => rightButtonText;
@override
VoidCallback get actionLeft => actionLeftButton;
@override
VoidCallback get actionRight => () {
actionRightButton(checkbox1, checkbox2, checkbox3);
};
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
Widget content(BuildContext context) {
return ThreeCheckboxAlertContent(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
}
}
class ThreeCheckboxAlertContent extends StatefulWidget {
ThreeCheckboxAlertContent({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
Key? key,
}) : super(key: key);
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
@override
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
}
}
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
_ThreeCheckboxAlertContentState({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
});
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
bool showValidationMessage = true;
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StandardCheckbox(
value: checkbox1,
caption: S.of(context).cakepay_confirm_no_vpn,
onChanged: (bool? value) {
setState(() {
checkbox1 = value ?? false;
toggleCheckbox1();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox2,
caption: S.of(context).cakepay_confirm_voided_refund,
onChanged: (bool? value) {
setState(() {
checkbox2 = value ?? false;
toggleCheckbox2();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox3,
caption: S.of(context).cakepay_confirm_terms_agreed,
onChanged: (bool? value) {
setState(() {
checkbox3 = value ?? false;
toggleCheckbox3();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse("https://cakepay.com/cakepay-web-terms.txt"),
mode: LaunchMode.externalApplication,
),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).settings_terms_and_conditions,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).primaryColor,
decoration: TextDecoration.none,
height: 1,
),
softWrap: true,
),
),
),
if (showValidationMessage)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Please confirm all checkboxes',
style: TextStyle(
color: Colors.red,
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
}

View file

@ -22,11 +22,13 @@ class ConnectDevicePageParams {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
ConnectDevicePageParams({
required this.walletType,
required this.onConnectDevice,
this.allowChangeWallet = false,
this.isReconnect = false,
});
}
@ -34,19 +36,33 @@ class ConnectDevicePage extends BasePage {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final LedgerViewModel ledgerVM;
ConnectDevicePage(ConnectDevicePageParams params, this.ledgerVM)
: walletType = params.walletType,
onConnectDevice = params.onConnectDevice,
allowChangeWallet = params.allowChangeWallet;
allowChangeWallet = params.allowChangeWallet,
isReconnect = params.isReconnect;
@override
String get title => S.current.restore_title_from_hardware_wallet;
String get title => isReconnect
? S.current.reconnect_your_hardware_wallet
: S.current.restore_title_from_hardware_wallet;
@override
Widget body(BuildContext context) => ConnectDevicePageBody(
walletType, onConnectDevice, allowChangeWallet, ledgerVM);
Widget? leading(BuildContext context) =>
!isReconnect ? super.leading(context) : null;
@override
Widget body(BuildContext context) => PopScope(
canPop: !isReconnect,
child: ConnectDevicePageBody(
walletType,
onConnectDevice,
allowChangeWallet,
ledgerVM,
));
}
class ConnectDevicePageBody extends StatefulWidget {
@ -75,6 +91,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
late Timer? _bleStateTimer = null;
late StreamSubscription<LedgerDevice>? _bleRefresh = null;
bool longWait = false;
@override
void initState() {
super.initState();
@ -89,6 +107,11 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
_usbRefreshTimer =
Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
}
Future.delayed(Duration(seconds: 10), () {
if (widget.ledgerVM.bleIsEnabled && bleDevices.isEmpty)
setState(() => longWait = true);
});
});
}
@ -98,6 +121,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
_bleStateTimer?.cancel();
_usbRefreshTimer?.cancel();
_bleRefresh?.cancel();
widget.ledgerVM.stopScanning();
super.dispose();
}
@ -118,12 +143,14 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Future<void> _refreshBleDevices() async {
try {
if (widget.ledgerVM.bleIsEnabled) {
_bleRefresh = widget.ledgerVM
.scanForBleDevices()
.listen((device) => setState(() => bleDevices.add(device)))
..onError((e) {
throw e.toString();
});
_bleRefresh =
widget.ledgerVM.scanForBleDevices().listen((device) => setState(() {
bleDevices.add(device);
if (longWait) longWait = false;
}))
..onError((e) {
throw e.toString();
});
_bleRefreshTimer?.cancel();
_bleRefreshTimer = null;
}
@ -175,15 +202,21 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
textAlign: TextAlign.center,
),
),
// DeviceTile(
// onPressed: () => Navigator.of(context).push(
// MaterialPageRoute<void>(
// builder: (BuildContext context) => DebugDevicePage(),
// ),
// ),
// title: "Debug Ledger",
// leading: imageLedger,
// ),
Offstage(
offstage: !longWait,
child: Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(S.of(context).if_you_dont_see_your_device,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.titleColor),
textAlign: TextAlign.center,
),
),
),
Observer(
builder: (_) => Offstage(
offstage: widget.ledgerVM.bleIsEnabled,

View file

@ -322,6 +322,7 @@ class _ContactListBodyState extends State<ContactListBody> {
? widget.contactListViewModel.contacts
: widget.contactListViewModel.contactsToShow;
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Container(
child: FilteredList(
list: contacts,

View file

@ -119,12 +119,7 @@ class CryptoBalanceWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
child: SingleChildScrollView(
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -326,7 +321,7 @@ class CryptoBalanceWidget extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse(
"https://guides.cakewallet.com/docs/cryptos/bitcoin/#silent-payments"),
"https://docs.cakewallet.com/cryptos/bitcoin#silent-payments"),
mode: LaunchMode.externalApplication,
),
child: Row(
@ -556,7 +551,6 @@ class CryptoBalanceWidget extends StatelessWidget {
}),
],
),
),
);
}
@ -705,44 +699,47 @@ class BalanceRowWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
GestureDetector(
onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: hasAdditionalBalance
? () => _showBalanceDescription(
context, S.of(context).available_balance_description)
: null,
child: Column(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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<BalancePageTheme>()!
.labelTextColor,
height: 1)),
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<BalancePageTheme>()!
.labelTextColor,
height: 1)),
),
if (hasAdditionalBalance)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor),
),
],
),
if (hasAdditionalBalance)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor),
),
],
),
SizedBox(height: 6),
AutoSizeText(availableBalance,
@ -775,9 +772,10 @@ class BalanceRowWidget extends StatelessWidget {
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<BalancePageTheme>()!.textColor,
height: 1)),
],
),
),
SizedBox(
width: min(MediaQuery.of(context).size.width * 0.2, 100),
child: Center(
@ -820,6 +818,7 @@ class BalanceRowWidget extends StatelessWidget {
),
],
),
),
if (frozenBalance.isNotEmpty)
GestureDetector(
behavior: HitTestBehavior.opaque,
@ -886,7 +885,9 @@ class BalanceRowWidget extends StatelessWidget {
),
),
if (hasAdditionalBalance)
Column(
GestureDetector(
onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
@ -929,12 +930,13 @@ class BalanceRowWidget extends StatelessWidget {
),
],
),
),
],
),
),
),
if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[
SizedBox(height: 16),
SizedBox(height: 10),
Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
@ -989,7 +991,9 @@ class BalanceRowWidget extends StatelessWidget {
],
),
if (hasSecondAvailableBalance)
Row(
GestureDetector(
onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -998,7 +1002,7 @@ class BalanceRowWidget extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse(
"https://guides.cakewallet.com/docs/cryptos/litecoin/#mweb"),
"https://docs.cakewallet.com/cryptos/litecoin.html#mweb"),
mode: LaunchMode.externalApplication,
),
child: Row(
@ -1061,6 +1065,7 @@ class BalanceRowWidget extends StatelessWidget {
),
],
),
),
],
),
),

View file

@ -53,7 +53,7 @@ class TransactionsPage extends StatelessWidget {
onTap: () {
try {
final uri = Uri.parse(
"https://guides.cakewallet.com/docs/FAQ/why_are_my_funds_not_appearing/");
"https://docs.cakewallet.com/faq/funds-not-appearing");
launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {}
},

View file

@ -347,7 +347,7 @@ class _WalletNameFormState extends State<WalletNameForm> {
key: ValueKey('new_wallet_page_confirm_button_key'),
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Colors.green,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: _walletNewVM.state is IsExecutingState,
isDisabled: _walletNewVM.name.isEmpty,

View file

@ -40,7 +40,7 @@ class SelectButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor);
final backgroundColor = color ?? (isSelected ? Theme.of(context).primaryColor : Theme.of(context).cardColor);
final effectiveTextColor = textColor ??
(isSelected
? Theme.of(context).extension<WalletListTheme>()!.restoreWalletButtonTextColor

View file

@ -31,7 +31,7 @@ class _HeaderTileState extends State<HeaderTile> {
@override
Widget build(BuildContext context) {
final searchIcon = Image.asset("assets/images/search_icon.png",
final searchIcon = Icon( Icons.search,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor);
return Container(

View file

@ -56,9 +56,8 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
}
if (isMoneroOnly) {
// return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS)
// .isNotEmpty;
return false;
return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS)
.isNotEmpty;
}
return true;

View file

@ -25,5 +25,5 @@ class PreSeedPage extends InfoPage {
@override
void Function(BuildContext) get onPressed =>
(BuildContext context) => Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true);
(BuildContext context) => Navigator.of(context).pushNamed(Routes.seed, arguments: true);
}

View file

@ -0,0 +1,35 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_step_view.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_success_view.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class SeedVerificationPage extends BasePage {
final WalletSeedViewModel walletSeedViewModel;
SeedVerificationPage(this.walletSeedViewModel);
@override
String? get title => S.current.verify_seed;
@override
Widget body(BuildContext context) {
return Observer(
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: walletSeedViewModel.isVerificationComplete
? SeedVerificationSuccessView(
imageColor: titleColor(context),
)
: SeedVerificationStepView(
walletSeedViewModel: walletSeedViewModel,
questionTextColor: titleColor(context),
),
);
},
);
}
}

View file

@ -0,0 +1,135 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class SeedVerificationStepView extends StatelessWidget {
const SeedVerificationStepView({
required this.walletSeedViewModel,
required this.questionTextColor,
super.key,
});
final WalletSeedViewModel walletSeedViewModel;
final Color questionTextColor;
@override
Widget build(BuildContext context) {
return Observer(
builder: (context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 48),
Align(
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${S.current.seed_position_question_one} ',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: questionTextColor,
),
),
TextSpan(
text: '${getOrdinal(walletSeedViewModel.currentWordIndex + 1)} ',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: questionTextColor,
),
),
TextSpan(
text: S.current.seed_position_question_two,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: questionTextColor,
),
),
],
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
Align(
alignment: Alignment.center,
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: walletSeedViewModel.currentOptions.map(
(option) {
return GestureDetector(
onTap: () async {
final isCorrectWord = walletSeedViewModel.isChosenWordCorrect(option);
final isSecondWrongEntry = walletSeedViewModel.wrongEntries == 2;
if (!isCorrectWord) {
await showBar<void>(
context,
isSecondWrongEntry
? S.current.incorrect_seed_option_back
: S.current.incorrect_seed_option,
);
if (isSecondWrongEntry) {
Navigator.pop(context);
}
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).cardColor,
),
child: Text(
option,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
),
),
);
},
).toList(),
),
),
],
),
);
},
);
}
String getOrdinal(int number) {
// Handle special cases for 11th, 12th, 13th
final lastTwoDigits = number % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 13) {
return '${number}th';
}
// Check the last digit for st, nd, rd, or default th
final lastDigit = number % 10;
switch (lastDigit) {
case 1:
return '${number}st';
case 2:
return '${number}nd';
case 3:
return '${number}rd';
default:
return '${number}th';
}
}
}

View file

@ -0,0 +1,74 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
class SeedVerificationSuccessView extends StatelessWidget {
const SeedVerificationSuccessView({required this.imageColor, super.key});
final Color imageColor;
@override
Widget build(BuildContext context) {
final image = Image.asset('assets/images/seed_verified.png', color: imageColor);
return Center(
child: Column(
children: [
ConstrainedBox(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3),
child: AspectRatio(
aspectRatio: 1.8,
child: image,
),
),
SizedBox(height: 16),
Text(
S.current.seed_verified,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
SizedBox(height: 48),
RichText(
text: TextSpan(
children: [
TextSpan(
text: '${S.current.seed_verified_subtext} ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
TextSpan(
text: S.current.seed_display_path,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
],
),
textAlign: TextAlign.center,
),
Spacer(),
PrimaryButton(
key: ValueKey('wallet_seed_page_open_wallet_button_key'),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
text: S.current.open_wallet,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
SizedBox(height: 16),
],
),
);
}
}

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/pin_code_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
@ -15,7 +15,8 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import '../../../themes/extensions/send_page_theme.dart';
class WalletSeedPage extends BasePage {
WalletSeedPage(this.walletSeedViewModel, {required this.isNewWalletCreated});
@ -29,62 +30,34 @@ class WalletSeedPage extends BasePage {
final bool isNewWalletCreated;
final WalletSeedViewModel walletSeedViewModel;
@override
void onClose(BuildContext context) async {
if (isNewWalletCreated) {
final confirmed = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertDialogKey: ValueKey('wallet_seed_page_seed_alert_dialog_key'),
alertRightActionButtonKey:
ValueKey('wallet_seed_page_seed_alert_confirm_button_key'),
alertLeftActionButtonKey: ValueKey('wallet_seed_page_seed_alert_back_button_key'),
alertTitle: S.of(context).seed_alert_title,
alertContent: S.of(context).seed_alert_content,
leftButtonText: S.of(context).seed_alert_back,
rightButtonText: S.of(context).seed_alert_yes,
actionLeftButton: () => Navigator.of(context).pop(false),
actionRightButton: () => Navigator.of(context).pop(true),
);
},
) ??
false;
if (confirmed) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
return;
}
Navigator.of(context).pop();
}
@override
Widget? leading(BuildContext context) => isNewWalletCreated ? null : super.leading(context);
@override
Widget trailing(BuildContext context) {
final copyImage = Image.asset(
'assets/images/copy_address.png',
color: Theme.of(context)
.extension<CakeTextTheme>()!
.buttonTextColor
);
return isNewWalletCreated
? GestureDetector(
key: ValueKey('wallet_seed_page_next_button_key'),
onTap: () => onClose(context),
key: ValueKey('wallet_seed_page_copy_seeds_button_key'),
onTap: () {
ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: walletSeedViewModel.seed),
);
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Container(
width: 100,
height: 32,
padding: EdgeInsets.all(8),
width: 40,
alignment: Alignment.center,
margin: EdgeInsets.only(left: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(16)),
color: Theme.of(context).cardColor),
child: Text(
S.of(context).seed_language_next,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor),
borderRadius: BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).cardColor,
),
child: copyImage,
),
)
: Offstage();
@ -92,118 +65,181 @@ class WalletSeedPage extends BasePage {
@override
Widget body(BuildContext context) {
final image = currentTheme.type == ThemeType.dark ? imageDark : imageLight;
return WillPopScope(
onWillPop: () async => false,
child: Container(
padding: EdgeInsets.all(24),
alignment: Alignment.center,
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3),
child: AspectRatio(aspectRatio: 1, child: image),
),
Observer(builder: (_) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
key: ValueKey('wallet_seed_page_wallet_name_text_key'),
walletSeedViewModel.name,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
),
Padding(
padding: EdgeInsets.only(top: 20, left: 16, right: 16),
child: Text(
key: ValueKey('wallet_seed_page_wallet_seed_text_key'),
walletSeedViewModel.seed,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color:
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor),
),
)
],
);
}),
Column(
children: <Widget>[
isNewWalletCreated
? Padding(
padding: EdgeInsets.only(bottom: 43, left: 43, right: 43),
child: Text(
key: ValueKey(
'wallet_seed_page_wallet_seed_reminder_text_key',
),
S.of(context).seed_reminder,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: Theme.of(context)
.extension<TransactionTradeTheme>()!
.detailsTitlesColor,
),
),
)
: Offstage(),
Row(
mainAxisSize: MainAxisSize.max,
onWillPop: () async => false,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8),
alignment: Alignment.center,
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Observer(
builder: (_) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 8.0),
child: PrimaryButton(
key: ValueKey('wallet_seed_page_save_seeds_button_key'),
onPressed: () {
ShareUtil.share(
text: walletSeedViewModel.seed,
context: context,
);
},
text: S.of(context).save,
color: Colors.green,
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: currentTheme.type == ThemeType.dark
? Color.fromRGBO(132, 110, 64, 1)
: Color.fromRGBO(194, 165, 94, 1),
borderRadius: BorderRadius.all(Radius.circular(12)),
border: Border.all(
color: currentTheme.type == ThemeType.dark
? Color.fromRGBO(177, 147, 41, 1)
: Color.fromRGBO(125, 122, 15, 1),
width: 2.0,
)),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 64,
color: Colors.white.withOpacity(0.75),
),
SizedBox(width: 6),
Expanded(
child: Text(
S.current.cake_seeds_save_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: currentTheme.type == ThemeType.dark
? Colors.white.withOpacity(0.75)
: Colors.white.withOpacity(0.85),
),
),
),
],
),
),
SizedBox(height: 20),
Text(
key: ValueKey('wallet_seed_page_wallet_name_text_key'),
walletSeedViewModel.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
SizedBox(height: 20),
Expanded(
child: GridView.builder(
itemCount: walletSeedViewModel.seedSplit.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.8,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemBuilder: (context, index) {
final item = walletSeedViewModel.seedSplit[index];
final numberCount = index + 1;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).cardColor,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
child: Text(
//maxLines: 1,
numberCount.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
height: 1,
fontWeight: FontWeight.w800,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.buttonTextColor
.withOpacity(0.5)),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
'${item[0].toLowerCase()}${item.substring(1)}',
style: TextStyle(
fontSize: 14,
height: 0.8,
fontWeight: FontWeight.w700,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.buttonTextColor),
),
),
],
),
);
},
),
),
],
),
);
},
),
Column(
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Flexible(
child: Container(
padding: EdgeInsets.only(right: 8.0, top: 8.0),
child: PrimaryButton(
key: ValueKey('wallet_seed_page_save_seeds_button_key'),
onPressed: () {
ShareUtil.share(
text: walletSeedViewModel.seed,
context: context,
);
},
text: S.of(context).save,
color: Theme.of(context).cardColor,
textColor: currentTheme.type == ThemeType.dark
? Theme.of(context).extension<DashboardPageTheme>()!.textColor
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
),
),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 8.0, top: 8.0),
child: Builder(
builder: (context) => PrimaryButton(
key: ValueKey('wallet_seed_page_verify_seed_button_key'),
onPressed: () =>
Navigator.pushNamed(context, Routes.walletSeedVerificationPage),
text: S.current.verify_seed,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
),
),
Flexible(
child: Container(
padding: EdgeInsets.only(left: 8.0),
child: Builder(
builder: (context) => PrimaryButton(
key: ValueKey('wallet_seed_page_copy_seeds_button_key'),
onPressed: () {
ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: walletSeedViewModel.seed),
);
showBar<void>(context, S.of(context).copied_to_clipboard);
},
text: S.of(context).copy,
color: Theme.of(context).extension<PinCodeTheme>()!.indicatorsColor,
textColor: Colors.white,
),
),
),
)
],
)
],
)
],
),
)
],
),
SizedBox(height: 12),
],
)
],
),
));
),
),
);
}
}

View file

@ -500,95 +500,6 @@ class SendPage extends BasePage {
actionRightButton: () async {
Navigator.of(_dialogContext).pop();
sendViewModel.commitTransaction(context);
await showPopUp<void>(
context: context,
builder: (BuildContext _dialogContext) {
return Observer(builder: (_) {
final state = sendViewModel.state;
if (state is FailureState) {
Navigator.of(_dialogContext).pop();
}
if (state is TransactionCommitted) {
newContactAddress =
newContactAddress ?? sendViewModel.newContactAddress();
if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) {
newContactAddress = null;
}
final successMessage = S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}'
: '';
final newContactMessage = newContactAddress != null
? '\n${S.of(_dialogContext).add_contact_to_address_book}'
: '';
String alertContent =
"$successMessage$waitMessage$newContactMessage";
if (newContactAddress != null) {
return AlertWithTwoActions(
alertDialogKey: ValueKey('send_page_sent_dialog_key'),
alertTitle: '',
alertContent: alertContent,
rightButtonText: S.of(_dialogContext).add_contact,
leftButtonText: S.of(_dialogContext).ignor,
alertLeftActionButtonKey:
ValueKey('send_page_sent_dialog_ignore_button_key'),
alertRightActionButtonKey: ValueKey(
'send_page_sent_dialog_add_contact_button_key'),
actionRightButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
Navigator.of(context).pushNamed(
Routes.addressBookAddContact,
arguments: newContactAddress);
newContactAddress = null;
},
actionLeftButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
newContactAddress = null;
});
} else {
if (initialPaymentRequest?.callbackMessage?.isNotEmpty ??
false) {
alertContent = initialPaymentRequest!.callbackMessage!;
}
return AlertWithOneAction(
alertTitle: '',
alertContent: alertContent,
buttonText: S.of(_dialogContext).ok,
buttonAction: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
});
}
}
return Offstage();
});
});
if (state is TransactionCommitted) {
if (initialPaymentRequest?.callbackUrl?.isNotEmpty ?? false) {
// wait a second so it's not as jarring:
await Future.delayed(Duration(seconds: 1));
try {
launchUrl(
Uri.parse(initialPaymentRequest!.callbackUrl!),
mode: LaunchMode.externalApplication,
);
} catch (e) {
printV(e);
}
}
}
},
actionLeftButton: () => Navigator.of(_dialogContext).pop());
});
@ -597,7 +508,64 @@ class SendPage extends BasePage {
}
if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final successMessage = S.of(context).send_success(
sendViewModel.selectedCryptoCurrency.toString());
final waitMessage = sendViewModel.walletType == WalletType.solana
? '. ${S.of(context).waitFewSecondForTxUpdate}'
: '';
String alertContent = "$successMessage$waitMessage";
await Navigator.of(context).pushNamed(
Routes.transactionSuccessPage,
arguments: alertContent
);
newContactAddress = newContactAddress ?? sendViewModel.newContactAddress();
if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) newContactAddress = null;
if (newContactAddress != null && sendViewModel.showAddressBookPopup) {
await showPopUp<void>(
context: context,
builder: (BuildContext _dialogContext) => AlertWithTwoActions(
alertDialogKey: ValueKey('send_page_sent_dialog_key'),
alertTitle: '',
alertContent: S.of(_dialogContext).add_contact_to_address_book,
rightButtonText: S.of(_dialogContext).add_contact,
leftButtonText: S.of(_dialogContext).ignor,
alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'),
alertRightActionButtonKey:
ValueKey('send_page_sent_dialog_add_contact_button_key'),
actionRightButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
Navigator.of(context)
.pushNamed(Routes.addressBookAddContact, arguments: newContactAddress);
newContactAddress = null;
},
actionLeftButton: () {
Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview();
newContactAddress = null;
}));
}
if (initialPaymentRequest?.callbackUrl?.isNotEmpty ?? false) {
// wait a second so it's not as jarring:
await Future.delayed(Duration(seconds: 1));
try {
launchUrl(
Uri.parse(initialPaymentRequest!.callbackUrl!),
mode: LaunchMode.externalApplication,
);
} catch (e) {
printV(e);
}
}
sendViewModel.clearOutputs();
});
}
@ -612,7 +580,10 @@ class SendPage extends BasePage {
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
buttonAction: () => Navigator.of(context).pop());
buttonAction: () {
sendViewModel.state = InitialExecutionState();
Navigator.of(context).pop();
});
});
});
}

View file

@ -0,0 +1,32 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart';
class TransactionSuccessPage extends InfoPage {
TransactionSuccessPage({required this.content})
: super(
imageLightPath: 'assets/images/birthday_cake.png',
imageDarkPath: 'assets/images/birthday_cake.png',
);
final String content;
@override
bool get onWillPop => false;
@override
String get pageTitle => 'Transaction Sent Successfully';
@override
String get pageDescription => content;
@override
String get buttonText => S.current.ok;
@override
Key? get buttonKey => ValueKey('transaction_success_info_page_button_key');
@override
void Function(BuildContext) get onPressed =>
(BuildContext context) => Navigator.of(context).pop();
}

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/setting_priority_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cw_core/wallet_type.dart';
@ -62,6 +63,13 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.readDisclaimer),
),
SettingsSwitcherCell(
title: S.of(context).show_address_book_popup,
value: _otherSettingsViewModel.showAddressBookPopup,
onValueChange: (_, bool value) {
_otherSettingsViewModel.setShowAddressBookPopup(value);
},
),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),

View file

@ -20,7 +20,7 @@ class Setup2FAPage extends BasePage {
Widget body(BuildContext context) {
final cake2FAGuideTitle = 'Cake 2FA Guide';
final cake2FAGuideUri =
Uri.parse('https://guides.cakewallet.com/docs/advanced-features/authentication');
Uri.parse('https://docs.cakewallet.com/features/advanced/authentication/');
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [

View file

@ -198,8 +198,9 @@ class TOTPEnterCode extends BasePage {
},
);
if (isForSetup && result) {
Navigator.pushReplacementNamed(
context, Routes.modify2FAPage);
if (context.mounted) {
Navigator.pushReplacementNamed(context, Routes.modify2FAPage);
}
}
},

View file

@ -6,7 +6,6 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
@ -30,7 +29,7 @@ class Setup2FAQRPage extends BasePage {
width: 16,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor);
final cake2FAHowToUseUrl = Uri.parse(
'https://guides.cakewallet.com/docs/advanced-features/authentication/#enabling-cake-2fa');
'https://docs.cakewallet.com/features/advanced/authentication/#enabling-cake-2fa');
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(

View file

@ -70,7 +70,7 @@ class SupportPage extends BasePage {
),
title: S.of(context).support_title_guides,
description: S.of(context).support_description_guides,
onPressed: () => _launchUrl(supportViewModel.guidesUrl),
onPressed: () => _launchUrl(supportViewModel.docsUrl),
),
),
Padding(

View file

@ -8,7 +8,8 @@ class TradeDetailsListCardItem extends StandartListItem {
{required this.id,
required this.createdAt,
required this.pair,
required this.onTap})
required this.onTap,
this.extraId})
: super(title: '', value: '');
factory TradeDetailsListCardItem.tradeDetails(
@ -16,9 +17,19 @@ class TradeDetailsListCardItem extends StandartListItem {
required String createdAt,
required CryptoCurrency from,
required CryptoCurrency to,
required void Function(BuildContext) onTap}) {
required void Function(BuildContext) onTap,
String? extraId}) {
final extraIdTitle = from == CryptoCurrency.xrp
? S.current.destination_tag
: from == CryptoCurrency.xlm
? S.current.memo
: S.current.extra_id;
return TradeDetailsListCardItem(
id: '${S.current.trade_details_id} ${formatAsText(id)}',
extraId: extraId != null ? '$extraIdTitle $extraId' : null,
createdAt: formatAsText(createdAt),
pair: '${formatAsText(from)}${formatAsText(to)}',
onTap: onTap);
@ -27,6 +38,7 @@ class TradeDetailsListCardItem extends StandartListItem {
final String id;
final String createdAt;
final String pair;
final String? extraId;
final void Function(BuildContext) onTap;
static String formatAsText<T>(T value) => value?.toString() ?? '';

View file

@ -69,6 +69,7 @@ class TradeDetailsPageBodyState extends State<TradeDetailsPageBody> {
if (item is TradeDetailsListCardItem)
return TradeDetailsStandardListCard(
id: item.id,
extraId: item.extraId,
create: item.createdAt,
pair: item.pair,
currentTheme: tradeDetailsViewModel.settingsStore.currentTheme.type,

View file

@ -154,7 +154,7 @@ class UnspentCoinsListFormState extends State<UnspentCoinsListForm> {
SizedBox(height: 15),
Expanded(
child: unspentCoinsListViewModel.items.isEmpty
? Center(child: Text('No unspent coins available\ntry to reconnect',textAlign: TextAlign.center))
? Center(child: Text('No unspent coins available',textAlign: TextAlign.center))
: ListView.separated(
itemCount: unspentCoinsListViewModel.items.length,
separatorBuilder: (_, __) => SizedBox(height: 15),

View file

@ -84,6 +84,7 @@ class WalletKeysPage extends BasePage {
child: Observer(
builder: (_) {
return ListView.separated(
key: ValueKey('wallet_keys_page_credentials_list_view_key'),
separatorBuilder: (context, index) => Container(
height: 1,
padding: EdgeInsets.only(left: 24),

View file

@ -334,6 +334,26 @@ class WalletListBodyState extends State<WalletListBody> {
padding: const EdgeInsets.all(24),
child: Column(
children: <Widget>[
PrimaryImageButton(
key: ValueKey('wallet_list_page_restore_wallet_button_key'),
onPressed: () {
if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
widget.authService.authenticateAction(
context,
route: Routes.restoreOptions,
arguments: false,
conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
}
},
image: restoreWalletImage,
text: S.of(context).wallet_list_restore_wallet,
color: Theme.of(context).cardColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
SizedBox(height: 10.0),
PrimaryImageButton(
key: ValueKey('wallet_list_page_create_new_wallet_button_key'),
onPressed: () {
@ -373,26 +393,6 @@ class WalletListBodyState extends State<WalletListBody> {
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
SizedBox(height: 10.0),
PrimaryImageButton(
key: ValueKey('wallet_list_page_restore_wallet_button_key'),
onPressed: () {
if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) {
widget.authService.authenticateAction(
context,
route: Routes.restoreOptions,
arguments: false,
conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
}
},
image: restoreWalletImage,
text: S.of(context).wallet_list_restore_wallet,
color: Theme.of(context).cardColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
)
],
),
),
@ -422,8 +422,9 @@ class WalletListBodyState extends State<WalletListBody> {
if (!isAuthenticatedSuccessfully) return;
try {
if (widget.walletListViewModel
.requireHardwareWalletConnection(wallet)) {
final requireHardwareWalletConnection = widget.walletListViewModel
.requireHardwareWalletConnection(wallet);
if (requireHardwareWalletConnection) {
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
@ -445,8 +446,6 @@ class WalletListBodyState extends State<WalletListBody> {
);
}
changeProcessText(
S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
@ -456,6 +455,9 @@ class WalletListBodyState extends State<WalletListBody> {
if (responsiveLayoutUtil.shouldRenderMobileUI) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (this.mounted) {
if (requireHardwareWalletConnection) {
Navigator.of(context).pop();
}
widget.onWalletLoaded.call(context);
}
});

View file

@ -17,13 +17,14 @@ class SearchBarWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextFormField(
key: ValueKey('search_bar_widget_key'),
controller: searchController,
style: TextStyle(color: Theme.of(context).extension<PickerTheme>()!.searchHintColor),
decoration: InputDecoration(
hintText: hintText ?? S.of(context).search,
hintStyle: TextStyle(color: Theme.of(context).extension<PickerTheme>()!.searchHintColor),
prefixIcon: Image.asset("assets/images/search_icon.png",
color: Theme.of(context).extension<PickerTheme>()!.searchIconColor),
prefixIcon: Icon( Icons.search,
color: Theme.of(context).primaryColor),
filled: true,
fillColor: Theme.of(context).extension<PickerTheme>()!.searchBackgroundFillColor,
alignLabelWithHint: false,

View file

@ -26,7 +26,9 @@ class StandardCheckbox extends StatelessWidget {
], begin: Alignment.centerLeft, end: Alignment.centerRight);
final boxBorder = Border.all(
color: borderColor ?? Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor, width: 1.0);
color: borderColor ?? Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
width: 1.0,
);
final checkedBoxDecoration = BoxDecoration(
gradient: gradientBackground ? baseGradient : null,
@ -41,6 +43,7 @@ class StandardCheckbox extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
height: 24.0,
@ -55,13 +58,22 @@ class StandardCheckbox extends StatelessWidget {
: Offstage(),
),
if (caption.isNotEmpty)
Padding(
Flexible(
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
caption,
softWrap: true,
style: TextStyle(
fontSize: 16.0, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
))
fontSize: 16.0,
fontFamily: 'Lato',
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
),
)
],
),
);

View file

@ -6,12 +6,14 @@ import 'package:cake_wallet/themes/theme_base.dart';
class TradeDetailsStandardListCard extends StatelessWidget {
TradeDetailsStandardListCard(
{required this.id,
this.extraId,
required this.create,
required this.pair,
required this.onTap,
required this.currentTheme});
final String id;
final String? extraId;
final String create;
final String pair;
final ThemeType currentTheme;
@ -57,6 +59,16 @@ class TradeDetailsStandardListCard extends StatelessWidget {
SizedBox(
height: 8,
),
if (extraId != null && extraId!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(extraId!,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: textColor)),
),
Text(create,
style: TextStyle(
fontSize: 12,

View file

@ -4,7 +4,7 @@ part 'authentication_store.g.dart';
class AuthenticationStore = AuthenticationStoreBase with _$AuthenticationStore;
enum AuthenticationState { uninitialized, installed, allowed, _reset }
enum AuthenticationState { uninitialized, installed, allowed, allowedCreate, _reset }
abstract class AuthenticationStoreBase with Store {
AuthenticationStoreBase() : state = AuthenticationState.uninitialized;
@ -23,4 +23,10 @@ abstract class AuthenticationStoreBase with Store {
state = AuthenticationState._reset;
state = AuthenticationState.allowed;
}
@action
void allowedCreate() {
state = AuthenticationState._reset;
state = AuthenticationState.allowedCreate;
}
}

View file

@ -54,6 +54,7 @@ abstract class SettingsStoreBase with Store {
required BackgroundTasks backgroundTasks,
required SharedPreferences sharedPreferences,
required bool initialShouldShowMarketPlaceInDashboard,
required bool initialShowAddressBookPopupEnabled,
required FiatCurrency initialFiatCurrency,
required BalanceDisplayMode initialBalanceDisplayMode,
required bool initialSaveRecipientAddress,
@ -158,6 +159,7 @@ abstract class SettingsStoreBase with Store {
walletListAscending = initialWalletListAscending,
contactListAscending = initialContactListAscending,
shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
showAddressBookPopupEnabled = initialShowAddressBookPopupEnabled,
exchangeStatus = initialExchangeStatus,
currentTheme = initialTheme,
pinCodeLength = initialPinLength,
@ -355,6 +357,11 @@ abstract class SettingsStoreBase with Store {
(bool value) =>
sharedPreferences.setBool(PreferencesKey.shouldShowMarketPlaceInDashboard, value));
reaction(
(_) => showAddressBookPopupEnabled,
(bool value) =>
sharedPreferences.setBool(PreferencesKey.showAddressBookPopupEnabled, value));
reaction((_) => pinCodeLength,
(int pinLength) => sharedPreferences.setInt(PreferencesKey.currentPinLength, pinLength));
@ -612,6 +619,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool shouldShowMarketPlaceInDashboard;
@observable
bool showAddressBookPopupEnabled;
@observable
ObservableList<ActionListDisplayMode> actionlistDisplayMode;
@ -926,6 +936,8 @@ abstract class SettingsStoreBase with Store {
final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0;
final shouldShowMarketPlaceInDashboard =
sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true;
final showAddressBookPopupEnabled =
sharedPreferences.getBool(PreferencesKey.showAddressBookPopupEnabled) ?? true;
final exchangeStatus = ExchangeApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ??
ExchangeApiMode.enabled.raw);
@ -1196,6 +1208,7 @@ abstract class SettingsStoreBase with Store {
secureStorage: secureStorage,
sharedPreferences: sharedPreferences,
initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard,
initialShowAddressBookPopupEnabled: showAddressBookPopupEnabled,
nodes: nodes,
powNodes: powNodes,
appVersion: packageInfo.version,
@ -1373,6 +1386,9 @@ abstract class SettingsStoreBase with Store {
shouldShowMarketPlaceInDashboard =
sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
shouldShowMarketPlaceInDashboard;
showAddressBookPopupEnabled =
sharedPreferences.getBool(PreferencesKey.showAddressBookPopupEnabled) ??
showAddressBookPopupEnabled;
exchangeStatus = ExchangeApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ??
ExchangeApiMode.enabled.raw);

View file

@ -1,9 +1,11 @@
import 'dart:io';
import 'package:cake_wallet/di.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/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/root_dir.dart';
@ -29,9 +31,21 @@ class ExceptionHandler {
_file = File('${appDocDir.path}/error.txt');
}
String? walletType;
CustomTrace? programInfo;
try {
walletType = getIt.get<AppStore>().wallet?.type.name;
programInfo = CustomTrace(stackTrace ?? StackTrace.current);
} catch (_) {}
final exception = {
"${DateTime.now()}": {
"Error": "$error\n\n",
"WalletType": "$walletType\n\n",
"VerboseLog":
"${programInfo?.fileName}#${programInfo?.lineNumber}:${programInfo?.columnNumber} ${programInfo?.callerFunctionName}\n\n",
"Library": "$library\n\n",
"StackTrace": stackTrace.toString(),
}

View file

@ -3,4 +3,5 @@ class FeatureFlag {
static const bool isExolixEnabled = true;
static const bool isInAppTorEnabled = false;
static const bool isBackgroundSyncEnabled = false;
static const int verificationWordsCount = 2;
}

Some files were not shown because too many files have changed in this diff Show more