diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9a5308e22..f8cc8f9ca 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,7 +4,7 @@ contact_links: url: https://github.com/cake-tech/cake_wallet/discussions/new?category=feature-requests about: Suggest an idea for Cake Wallet - name: Not sure where to start? - url: https://guides.cakewallet.com + url: https://docs.cakewallet.com about: Start by reading checking out the guides! - name: Need help? url: https://cakewallet.com/#contact diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 18ad16e4b..272f7bbee 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,3 +11,4 @@ Please include a summary of the changes and which issue is fixed / feature is ad - [ ] Format code - [ ] Look for code duplication - [ ] Clear naming for variables and methods +- [ ] Manual tests in accessibility mode (TalkBack on Android) passed diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml new file mode 100644 index 000000000..47b08c44d --- /dev/null +++ b/.github/workflows/automated_integration_test.yml @@ -0,0 +1,305 @@ +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-24.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.27.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 changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart + echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart + echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart + 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 trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart + echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + 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 exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart + echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart + echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart + echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart + echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_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 + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> 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 diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml deleted file mode 100644 index c0042bf5c..000000000 --- a/.github/workflows/cache_dependencies.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Cache Dependencies - -on: - workflow_dispatch: - push: - branches: [ main ] - -jobs: - test: - - runs-on: ubuntu-20.04 - - steps: - - 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.19.6" - channel: stable - - - name: Install package dependencies - run: 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 main - 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 diff --git a/.github/workflows/no_http_imports.yaml b/.github/workflows/no_http_imports.yaml new file mode 100644 index 000000000..dad6821ac --- /dev/null +++ b/.github/workflows/no_http_imports.yaml @@ -0,0 +1,21 @@ +name: No http imports + +on: [pull_request] + +jobs: + PR_test_build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for http package usage + if: github.event_name == 'pull_request' + run: | + GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) || true)" + [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 + echo "$GIT_GREP_OUT" + echo "There are .dart files which use http imports" + echo "Using http package breaks proxy integration" + echo "Please use ProxyWrapper.getHttpClient() from package:cw_core/utils/proxy_wrapper.dart" + exit 1 + \ No newline at end of file diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml new file mode 100644 index 000000000..507793bd8 --- /dev/null +++ b/.github/workflows/no_print_in_dart.yaml @@ -0,0 +1,19 @@ +name: No print statements in dart files + +on: [pull_request] + +jobs: + PR_test_build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for print() statements in dart code (use printV() instead) + if: github.event_name == 'pull_request' + run: | + GIT_GREP_OUT="$(git grep ' print(' | (grep .dart: || test $? = 1) | (grep -v print_verbose.dart || test $? = 1) | (grep -v print_verbose_dummy.dart || test $? = 1) || true)" + [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 + echo "$GIT_GREP_OUT" + echo "There are .dart files which use print() statements" + echo "Please use printV from package:cw_core/utils/print_verbose.dart" + exit 1 diff --git a/.github/workflows/no_restricted_imports.yaml b/.github/workflows/no_restricted_imports.yaml new file mode 100644 index 000000000..03c3de018 --- /dev/null +++ b/.github/workflows/no_restricted_imports.yaml @@ -0,0 +1,47 @@ +name: No restricted imports in lib directory + +on: [pull_request] + +jobs: + check_restricted_imports: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for restricted imports in lib directory + if: github.event_name == 'pull_request' + run: | + RESTRICTED_PACKAGES=( + "cw_bitcoin" + "cw_bitcoin_cash" + "cw_ethereum" + "cw_evm" + "cw_haven" + "cw_mweb" + "cw_nano" + "cw_polygon" + "cw_solana" + "cw_tron" + "cw_wownero" + "cw_zano" + ) + + FOUND_RESTRICTED=false + + for package in "${RESTRICTED_PACKAGES[@]}"; do + GREP_RESULT=$(find lib -type f -name "*.dart" -exec grep -l "import.*package:$package" {} \; || true) + + if [ -n "$GREP_RESULT" ]; then + echo "Found restricted import of '$package' in the following files:" + echo "$GREP_RESULT" + FOUND_RESTRICTED=true + fi + done + + if [ "$FOUND_RESTRICTED" = true ]; then + echo "Error: Restricted package imports found in lib/ directory" + echo "Please remove these imports as they are not allowed in the lib/ directory" + exit 1 + else + echo "No restricted imports found. All good!" + fi \ No newline at end of file diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 3cc67e53a..f7c226ce4 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -1,155 +1,105 @@ -name: PR Test Build +name: Cake Wallet Android -on: - pull_request: - branches: [main] - workflow_dispatch: - inputs: - branch: - description: "Branch name to build" - required: true - default: "main" +on: [pull_request] +defaults: + run: + shell: bash jobs: PR_test_build: - runs-on: ubuntu-20.04 - env: - STORE_PASS: test@cake_wallet - KEY_PASS: test@cake_wallet - PR_NUMBER: ${{ github.event.number }} + runs-on: linux-amd64 + container: + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + env: + STORE_PASS: test@cake_wallet + KEY_PASS: test@cake_wallet + MONEROC_CACHE_DIR_ROOT: /opt/generic_cache + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + ANDROID_AVD_HOME: /root/.android/avd + volumes: + - /opt/cw_cache_android/root/.cache:/root/.cache + - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd + - /opt/cw_cache_android/root/.ccache:/root/.ccache + - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache + - /opt/cw_cache_android/root/.gradle/:/root/.gradle + - /opt/cw_cache_android/root/.android/:/root/.android + - /opt/cw_cache_android/root/go/pkg:/root/go/pkg + - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache + - /dev/kvm:/dev/kvm + strategy: + matrix: + api-level: [29] 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 + - name: Fix github actions messing up $HOME... + run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' + - uses: actions/checkout@v4 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 + ref: ${{ github.event.pull_request.head.sha }} + - name: configure git 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.19.6" - channel: stable - - - name: Install package dependencies + git config --global --add safe.directory '*' + git config --global user.email "ci@cakewallet.com" + git config --global user.name "CakeWallet CI" + - name: Get the full commit message 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 + FULL_MESSAGE="$(git log -1 --pretty=%B)" + echo "message<> $GITHUB_ENV + echo "$FULL_MESSAGE" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - 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 + if [[ "x${{ secrets.SALT }}" == "x" ]]; + then + echo "const salt = '954f787f12622067f7e548d9450c3832';" > lib/.secrets.g.dart + else + echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart + fi + if [[ "x${{ secrets.KEY_CHAIN_SALT }}" == "x" ]]; + then + echo "const keychainSalt = '2d2beba777dbf7dff7013b7a';" >> lib/.secrets.g.dart + else + echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.KEY }}" == "x" ]]; + then + echo "const key = '638e98820ec10a2945e968435c9397a3';" >> lib/.secrets.g.dart + else + echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.WALLET_SALT }}" == "x" ]]; + then + echo "const walletSalt = '8f7f1b70';" >> lib/.secrets.g.dart + else + echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.SHORT_KEY }}" == "x" ]]; + then + echo "const shortKey = '653f270c2c152bc7ec864afe';" >> lib/.secrets.g.dart + else + echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.BACKUP_SALT }}" == "x" ]]; + then + echo "const backupSalt = 'bf630d24ff0b6f60';" >> lib/.secrets.g.dart + else + echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.BACKUP_KEY_CHAIN_SALT }}" == "x" ]]; + then + echo "const backupKeychainSalt = 'bf630d24ff0b6f60';" >> lib/.secrets.g.dart + else + echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + fi + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -163,73 +113,205 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart - echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart - echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_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 + # for tests + 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 + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart + + - name: prepare monero_c and cache + run: | + export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }') + echo MONEROC_HASH=$MONEROC_HASH >> /etc/environment + mkdir -p "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c" + pushd scripts + ln -s "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c" + ./prepare_moneroc.sh + popd + pushd scripts/monero_c + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" || true + + rm -rf "$PWD/contrib/depends/built" "$PWD/monero/contrib/depends/built" "$PWD/wownero/contrib/depends/built" + rm -rf "$PWD/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources" + mkdir -p contrib/depends || true + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" "$PWD/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" "$PWD/monero/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" "$PWD/wownero/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" "$PWD/contrib/depends/sources" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources" + popd + + - name: Generate KeyStore + run: | + pushd /opt/generic_cache + if [[ ! -f key.jks ]]; + then + 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 + else + echo "$PWD/key.jks exist, not generating" + fi + popd + cp /opt/generic_cache/key.jks android/app + + - name: Execute Build and Setup Commands + run: | + pushd scripts/android + source ./app_env.sh cakewallet + ./app_config.sh + popd + + - name: Build monero_c + run: | + pushd scripts/android/ + source ./app_env.sh cakewallet + ./build_monero_all.sh + popd + + - name: Install Flutter dependencies + run: | + flutter pub get + + - name: Build mwebd + run: | + set -x -e + export MWEBD_HASH=$(cat scripts/android/build_mwebd.sh | grep 'git reset --hard' | xargs | awk '{ print $4 }') + echo MWEBD_HASH=$MWEBD_HASH >> /etc/environment + pushd scripts/android + gomobile init; + ./build_mwebd.sh --dont-install + popd + + - name: Build Decred + run: | + set -x -e + pushd scripts/android + ./build_decred.sh + popd + + - name: Build generated code + run: | + flutter --version + flutter clean + rm -rf .dart_tool + rm pubspec.lock + flutter pub get + ./model_generator.sh async + + - name: Generate key properties + run: | + dart run tool/generate_android_key_properties.dart keyAlias=testKey storeFile=key.jks storePassword=$STORE_PASS keyPassword=$KEY_PASS + + - name: Generate localization + run: | + dart run tool/generate_localization.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 + sanitized_branch_name=${BRANCH_NAME#origin/} # Remove 'origin/' prefix if it exists + sanitized_branch_name=${sanitized_branch_name:0:16} # Take only the first 16 characters + sanitized_branch_name=$(echo "$sanitized_branch_name" | tr '[:upper:]' '[:lower:]') # Convert to lowercase + sanitized_branch_name=$(echo "$sanitized_branch_name" | sed 's/[^a-z0-9]//g') # Remove all special characters + + echo -e "id=com.cakewallet.test_${sanitized_branch_name}\nname=${BRANCH_NAME}" > android/app.properties - name: Build run: | - cd /opt/android/cake_wallet - flutter build apk --release --split-per-abi - - # - name: Push to App Center - # run: | - # echo 'Installing App Center CLI tools' - # npm install -g appcenter-cli - # echo "Publishing test to App Center" - # appcenter distribute release \ - # --group "Testers" \ - # --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ - # --release-notes ${{ env.BRANCH_NAME }} \ - # --app Cake-Labs/Cake-Wallet \ - # --token ${{ secrets.APP_CENTER_TOKEN }} \ - # --quiet + flutter build apk --dart-define=hasDevOptions=true --release --split-per-abi - name: Rename apk file run: | - cd /opt/android/cake_wallet/build/app/outputs/flutter-apk + cd build/app/outputs/flutter-apk mkdir test-apk - cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk + cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk + cp app-x86_64-release.apk test-apk/${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: Find APK file + id: find_apk + run: | + set -x + apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) + echo "APK_FILE=$apk_file" >> $GITHUB_ENV - - name: Send Test APK + - name: Upload artifact to slack + if: ${{ !contains(env.message, 'skip slack') }} 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 + path: ${{ env.APK_FILE }} channel: ${{ secrets.SLACK_APK_CHANNEL }} - title: "${{ env.BRANCH_NAME }}.apk" - filename: ${{ env.BRANCH_NAME }}.apk - initial_comment: ${{ github.event.head_commit.message }} + initial_comment: ${{ env.message }} + + - name: cleanup + run: rm -rf build/app/outputs/flutter-apk/test-apk/ + + - name: Upload Artifact to github + uses: actions/upload-artifact@v4 + with: + path: ${{ github.workspace }}/build/app/outputs/flutter-apk + name: "android apk" diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index f00584345..f057b19e5 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -1,141 +1,98 @@ -name: PR Test Build linux +name: Cake Wallet Linux -on: - pull_request: - branches: [main] - workflow_dispatch: - inputs: - branch: - description: "Branch name to build" - required: true - default: "main" +on: [pull_request] +defaults: + run: + shell: bash jobs: PR_test_build: - runs-on: ubuntu-20.04 - env: - STORE_PASS: test@cake_wallet - KEY_PASS: test@cake_wallet - PR_NUMBER: ${{ github.event.number }} + runs-on: linux-amd64 + container: + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + env: + STORE_PASS: test@cake_wallet + KEY_PASS: test@cake_wallet + MONEROC_CACHE_DIR_ROOT: /opt/generic_cache + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + DESKTOP_FORCE_MOBILE: Y + volumes: + - /opt/cw_cache_linux/root/.cache:/root/.cache + - /opt/cw_cache_linux/root/.ccache:/root/.ccache + - /opt/cw_cache_linux/root/.pub-cache/:/root/.pub-cache + - /opt/cw_cache_linux/root/go/pkg:/root/go/pkg + - /opt/cw_cache_linux/opt/generic_cache:/opt/generic_cache 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_ENVg - - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - name: Fix github actions messing up $HOME... + run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' + - uses: actions/checkout@v4 with: - java-version: "17.x" - - name: Configure placeholder git details + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: configure git 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.19.6" - channel: stable - - - name: Install package dependencies + git config --global --add safe.directory '*' + git config --global user.email "ci@cakewallet.com" + git config --global user.name "CakeWallet CI" + - name: Get the full commit message run: | - sudo apt update - sudo apt-get install -y curl unzip automake build-essential file pkg-config git python-is-python3 libtool libtinfo5 cmake clang - - - name: Install desktop dependencies - run: | - sudo apt update - sudo apt install -y ninja-build libgtk-3-dev gperf - - 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 scripts && ./gen_android_manifest.sh && cd .. - cd cake_wallet/scripts/android/ - source ./app_env.sh cakewallet - ./app_config.sh - cd ../../.. - cd cake_wallet/scripts/linux/ - source ./app_env.sh cakewallet - ./app_config.sh - cd ../../.. - - - 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: linux_${{ 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/linux/ - 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 - # build mwebd: - cd /opt/android/cake_wallet/scripts/android/ - ./build_mwebd.sh --dont-install - - - 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 - + FULL_MESSAGE="$(git log -1 --pretty=%B)" + echo "message<> $GITHUB_ENV + echo "$FULL_MESSAGE" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - 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 + if [[ "x${{ secrets.SALT }}" == "x" ]]; + then + echo "const salt = '954f787f12622067f7e548d9450c3832';" > lib/.secrets.g.dart + else + echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart + fi + if [[ "x${{ secrets.KEY_CHAIN_SALT }}" == "x" ]]; + then + echo "const keychainSalt = '2d2beba777dbf7dff7013b7a';" >> lib/.secrets.g.dart + else + echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.KEY }}" == "x" ]]; + then + echo "const key = '638e98820ec10a2945e968435c9397a3';" >> lib/.secrets.g.dart + else + echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.WALLET_SALT }}" == "x" ]]; + then + echo "const walletSalt = '8f7f1b70';" >> lib/.secrets.g.dart + else + echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.SHORT_KEY }}" == "x" ]]; + then + echo "const shortKey = '653f270c2c152bc7ec864afe';" >> lib/.secrets.g.dart + else + echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.BACKUP_SALT }}" == "x" ]]; + then + echo "const backupSalt = 'bf630d24ff0b6f60';" >> lib/.secrets.g.dart + else + echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart + fi + if [[ "x${{ secrets.BACKUP_KEY_CHAIN_SALT }}" == "x" ]]; + then + echo "const backupKeychainSalt = 'bf630d24ff0b6f60';" >> lib/.secrets.g.dart + else + echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart + fi + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -149,60 +106,212 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart - echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart - echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_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 + # tests + 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 + # end of test secrets + echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart + echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart + echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - - name: Rename app + - name: prepare monero_c and cache run: | - echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }') + echo MONEROC_HASH=$MONEROC_HASH >> /etc/environment + mkdir -p "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c" + pushd scripts + ln -s "$MONEROC_CACHE_DIR_ROOT/moneroc-$MONEROC_HASH/monero_c" + ./prepare_moneroc.sh + popd + pushd scripts/monero_c + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" || true + mkdir -p "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" || true - - name: Build + rm -rf "$PWD/contrib/depends/built" "$PWD/monero/contrib/depends/built" "$PWD/wownero/contrib/depends/built" + rm -rf "$PWD/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources" + mkdir -p contrib/depends || true + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/built" "$PWD/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/built" "$PWD/monero/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/built" "$PWD/wownero/contrib/depends/built" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/contrib/depends/sources" "$PWD/contrib/depends/sources" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/monero/contrib/depends/sources" "$PWD/monero/contrib/depends/sources" + ln -sf "$MONEROC_CACHE_DIR_ROOT/_cache/wownero/contrib/depends/sources" "$PWD/wownero/contrib/depends/sources" + popd + + - name: Execute Build and Setup Commands run: | - cd /opt/android/cake_wallet - flutter build linux --release + pushd scripts/linux + source ./app_env.sh cakewallet + ./app_config.sh + popd - - name: Prepare release zip file + - name: Build monero_c run: | - cd /opt/android/cake_wallet/build/linux/x64/release - zip -r ${{env.BRANCH_NAME}}.zip bundle + pushd scripts/linux/ + source ./app_env.sh cakewallet + ./build_monero_all.sh + popd - - name: Upload Artifact - uses: kittaakos/upload-artifact-as-is@v0 + - name: Install Flutter dependencies + run: | + flutter pub get + + - name: Build generated code + run: | + ./model_generator.sh async + + - name: Generate localization + run: | + dart run tool/generate_localization.dart + + - name: Build linux + run: | + flutter build linux --dart-define=hasDevOptions=true --release + + - name: Compress release + run: | + pushd build/linux/x64/release + zip -r cakewallet_linux.zip bundle + popd + + - name: Upload Artifact to github + uses: actions/upload-artifact@v4 with: - path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip + path: ${{ github.workspace }}/build/linux/x64/release/cakewallet_linux.zip + name: cakewallet_linux -# Just as an artifact would be enough -# - 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/linux/x64/release/${{env.BRANCH_NAME}}.zip -# channel: ${{ secrets.SLACK_APK_CHANNEL }} -# title: "${{ env.BRANCH_NAME }}_linux.zip" -# filename: ${{ env.BRANCH_NAME }}_linux.zip -# initial_comment: ${{ github.event.head_commit.message }} + - name: Prepare virtual desktop + if: ${{ contains(env.message, 'run tests') }} + run: | + nohup Xvfb :99 -screen 0 720x1280x16 & + echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV + dbus-daemon --system --fork + nohup NetworkManager & + nohup ffmpeg -framerate 60 -video_size 720x1280 -f x11grab -i :99 -c:v libx264 -c:a aac /opt/screen_grab.mkv & + + # Note for people adding tests: + # - Tests are ran on Linux, with some things being mocked out. + # - Screen recording is being provided for the entire length of the test, you can download it in github articats. + # - Screen recordeding is encrypted, look at step "Stop screen recording, encrypt and upload", and add your key if you want + # Reason for encryption is the fact that we restore the wallet from seed, and we don't want to leak that, while there + # isn't much in those wallets anyway, we still wouldn't like to leak it to anyone who is able to access github. + + - name: Test [confirm_seeds_flow_test] + if: ${{ contains(env.message, 'run tests') }} + timeout-minutes: 20 + run: | + xmessage -timeout 30 "confirm_seeds_flow_test" & + rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet + exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/confirm_seeds_flow_test.dart + - name: Test [create_wallet_flow_test] + if: ${{ contains(env.message, 'run tests') }} + timeout-minutes: 20 + run: | + xmessage -timeout 30 "create_wallet_flow_test" & + rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet + exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/create_wallet_flow_test.dart + - name: Test [exchange_flow_test] + if: ${{ contains(env.message, 'run tests') }} + timeout-minutes: 20 + run: | + xmessage -timeout 30 "exchange_flow_test" & + rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet + exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/exchange_flow_test.dart + - name: Test [restore_wallet_through_seeds_flow_test] + if: ${{ contains(env.message, 'run tests') }} + timeout-minutes: 20 + run: | + xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" & + rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet + exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart + - name: Test [cw_monero] + timeout-minutes: 2 + run: cd cw_monero && flutter test + - name: Stop screen recording, encrypt and upload + if: always() + run: | + if [[ ! -f "/opt/screen_grab.mkv" ]]; + then + exit 0; + fi + killall ffmpeg + sleep 5 + killall -9 ffmpeg || true + sleep 5 + # Feel free to add your own public key if you wish + gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 6B3199AD9B3D23B8 # konstantin@cakewallet.com + gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 35C8DBAFB8D9ACAC # cyjan@mrcyjanek.net + gpg --trust-model always --encrypt --output /opt/screen_grab.mkv.gpg \ + --recipient 6B3199AD9B3D23B8 \ + --recipient 35C8DBAFB8D9ACAC \ + /opt/screen_grab.mkv + rm /opt/screen_grab.mkv + mv /opt/screen_grab.mkv.gpg ./screen_grab.mkv.gpg + - name: Upload Artifact to github + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + path: ${{ github.workspace }}/screen_grab.mkv.gpg + name: tests_screen_grab diff --git a/.gitignore b/.gitignore index 8336ca512..84a7ecdcd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ .fvm/ +.fvmrc # IntelliJ related *.iml @@ -126,7 +127,7 @@ cw_shared_external/ios/External/ cw_haven/ios/External/ cw_haven/android/.externalNativeBuild/ cw_haven/android/.cxx/ - +cw_zano/ios/External/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart @@ -137,10 +138,31 @@ lib/polygon/polygon.dart lib/solana/solana.dart lib/tron/tron.dart lib/wownero/wownero.dart +lib/zano/zano.dart +lib/decred/decred.dart + +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png +ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png -ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png -ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png ios/Runner/Info.plist android/app/src/main/res/mipmap-* android/app/src/main/res/drawable/ic_launcher.png @@ -170,9 +192,38 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig +macos/Runner.xcodeproj/project.pbxproj + + +integration_test/playground.dart # Monero.dart (Monero_C) scripts/monero_c # iOS generated framework bin ios/MoneroWallet.framework/MoneroWallet ios/WowneroWallet.framework/WowneroWallet +ios/ZanoWallet.framework/ZanoWallet +*_libwallet2_api_c.dylib + +.flatpak-builder +cake_wallet.flatpak +flatpak-build/ + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/ephemeral/ +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/ephemeral/ +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..151b7af20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,190 @@ +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + +# Heavily inspired by cirrusci images +# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile +# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34/Dockerfile +# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/34-ndk/Dockerfile +# https://github.com/cirruslabs/docker-images-flutter/blob/master/sdk/Dockerfile + +FROM docker.io/debian:12 + +LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet + +# Set necessary environment variables +# Set Go version to latest known-working version +ENV GOLANG_VERSION=1.24.1 + +# Pin Flutter version to latest known-working version +ENV FLUTTER_VERSION=3.27.0 + +# Pin Android Studio, platform, and build tools versions to latest known-working version +# Comes from https://developer.android.com/studio/#command-tools +ENV ANDROID_SDK_TOOLS_VERSION=13114758 +# Comes from https://developer.android.com/studio/releases/build-tools +ENV ANDROID_PLATFORM_VERSION=35 +ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 + +# If we ever need to migrate the home directory... +RUN sed -i 's|^root:[^:]*:[^:]*:[^:]*:[^:]*:/root:|root:x:0:0:root:/root:|' /etc/passwd +# mkdir -p /root && rm -rf /root && cp -a /root /root +ENV HOME=/root +ENV ANDROID_HOME=/opt/android-sdk-linux \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANGUAGE=en_US:en + +# Set Android SDK paths +ENV ANDROID_SDK_ROOT=$ANDROID_HOME \ + PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator + +# Upgrade base image +RUN apt-get update \ + && apt-get upgrade -y + +# Install all build dependencies +RUN set -o xtrace \ + && cd /opt \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + # Core dependencies + bc build-essential curl default-jdk git jq lcov libglu1-mesa libpulse0 libsqlite3-dev libstdc++6 locales openssh-client ruby-bundler ruby-full software-properties-common sudo unzip wget zip \ + # for x86 emulators + libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3-dev libsqlite3-dev libxtst6 libxss1 lftp sqlite3 xxd \ + # Linux desktop dependencies + clang cmake libgtk-3-dev ninja-build pkg-config \ + # monero_c dependencies + autoconf automake build-essential ccache gperf libtool llvm \ + # extra stuff for KVM + bridge-utils libvirt-clients libvirt-daemon-system qemu-kvm udev \ + # Linux test dependencies + ffmpeg network-manager x11-utils xvfb psmisc \ + # aarch64-linux-gnu dependencies + g++-aarch64-linux-gnu gcc-aarch64-linux-gnu \ + # x86_64-linux-gnu dependencies + g++-x86-64-linux-gnu gcc-x86-64-linux-gnu \ + # flatpak dependencies + flatpak flatpak-builder binutils elfutils patch unzip xz-utils zstd \ + && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ + && locale-gen \ + && update-locale LANG=en_US.UTF-8 + +ENV FLATPAK_RUNTIME_VERSION=24.08 +RUN flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo \ + && flatpak install -y flathub org.freedesktop.Platform//${FLATPAK_RUNTIME_VERSION} \ + && flatpak install -y flathub org.freedesktop.Sdk//${FLATPAK_RUNTIME_VERSION} + +# Install nodejs for Github Actions +RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Install Go +ENV PATH=${PATH}:/usr/local/go/bin:${HOME}/go/bin +ENV GOROOT=/usr/local/go +ENV GOPATH=${HOME}/go +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then \ + wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz -O go.tar.gz; \ + elif [ "$ARCH" = "aarch64" ]; then \ + wget https://go.dev/dl/go${GOLANG_VERSION}.linux-arm64.tar.gz -O go.tar.gz; \ + else \ + echo "Unsupported architecture: $ARCH"; exit 1; \ + fi && \ + rm -rf /usr/local/go && \ + tar -C /usr/local -xzf go.tar.gz && \ + rm go.tar.gz && \ + go install golang.org/x/mobile/cmd/gomobile@latest && \ + gomobile init + +RUN git config --global user.email "czarek@cakewallet.com" \ + && git config --global user.name "CakeWallet CI" + + +# Install Android SDK commandline tools and emulator +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ + && wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip -O android-sdk-tools.zip \ + && mkdir -p ${ANDROID_HOME}/cmdline-tools/ \ + && unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \ + && mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ + && chown -R root:root $ANDROID_HOME \ + && rm android-sdk-tools.zip \ + && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ + && yes | sdkmanager --licenses \ + && wget -O /usr/bin/android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/master/community-cookbooks/android-sdk/files/default/android-wait-for-emulator \ + && chmod +x /usr/bin/android-wait-for-emulator \ + && sdkmanager platform-tools \ + && mkdir -p ${HOME}/.android \ + && touch ${HOME}/.android/repositories.cfg \ + + +# Handle emulator not being available on linux/arm64 (https://issuetracker.google.com/issues/227219818) +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ + && sdkmanager emulator + +# Pre-install extra Android SDK dependencies in order to not have to download them for each build +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ + && yes | sdkmanager \ + "platforms;android-$ANDROID_PLATFORM_VERSION" \ + "build-tools;$ANDROID_BUILD_TOOLS_VERSION" \ + "platforms;android-33" \ + "platforms;android-34" \ + "platforms;android-35" \ + "build-tools;33.0.2" \ + "build-tools;33.0.1" \ + "build-tools;33.0.0" \ + "build-tools;35.0.0" + +# Install extra NDK dependency for sp_scanner +ENV ANDROID_NDK_VERSION=27.2.12479018 +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ + && yes | sdkmanager "ndk;$ANDROID_NDK_VERSION" \ + "ndk;27.0.12077973" + +# Install dependencies for tests +# Comes from https://github.com/ReactiveCircus/android-emulator-runner +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" != "x86_64" ]; then exit 0; fi \ + && yes | sdkmanager \ + "system-images;android-29;default;x86_64" \ + "system-images;android-31;default;x86_64" \ + "platforms;android-29" \ + "platforms;android-31" + +# Fake the KVM status so the Android emulator doesn't complain (that much) +RUN (addgroup kvm || true) && \ + adduser root kvm && \ + mkdir -p /etc/udev/rules.d/ && \ + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | tee /etc/udev/rules.d/99-kvm4all.rules + +# Install rustup, rust toolchains, and cargo-ndk +ENV PATH=${HOME}/.cargo/bin:${PATH} +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ + cargo install cargo-ndk && \ + for toolchain in stable nightly; \ + do \ + for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu; \ + do \ + rustup target add --toolchain $toolchain $target; \ + done \ + done + +# Download and install Flutter +ENV HOME=${HOME} +ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} +ENV FLUTTER_ROOT=$FLUTTER_HOME +ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin + +RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} && \ + cd ${FLUTTER_HOME} && \ + git fetch -a + +RUN yes | flutter doctor --android-licenses \ + && flutter doctor \ + && chown -R root:root ${FLUTTER_HOME} + +# Download and pre-cache necessary Flutter artifacts to speed up builds +RUN flutter precache diff --git a/LICENSE.md b/LICENSE.md index 4268b9710..09bb6208b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2023 Cake Labs LLC +Copyright (c) 2018-2025 Cake Labs LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PRIVACY.md b/PRIVACY.md index 76cfcc4d3..a5c8eddfb 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -5,7 +5,7 @@ Last modified: January 24, 2024 Introduction ============ - Cake Labs LLC ("Cake Labs", "Company", or "We") respect your privacy and are committed to protecting it through our compliance with this policy. + Cake Labs LLC ("Cake Labs", "Company", or "We") respects your privacy and are committed to protecting it through our compliance with this policy. This policy describes the types of information we may collect from you or that you may provide when you use the App (our "App") and our practices for collecting, using, maintaining, protecting, and disclosing that information. @@ -13,7 +13,7 @@ Introduction - On this App. - In email, text, and other electronic messages between you and this App. It does not apply to information collected by: - - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or + - Us offline or through any other means, including on any other App operated by the Company or any third party (including our affiliates and subsidiaries); or - Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App. Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates. diff --git a/README.md b/README.md index 1c28f92a2..ea796dbf2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Cake Wallet -Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. +[Cake Wallet](https://cakewallet.com) is an open-source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. Cake Wallet includes support for several cryptocurrencies, including: * Monero (XMR) @@ -26,10 +26,13 @@ Cake Wallet includes support for several cryptocurrencies, including: * Ethereum (ETH) * Litecoin (LTC) * Bitcoin Cash (BCH) -* Polygon (MATIC) +* Polygon (POL) * Solana (SOL) +* Tron (TRX) * Nano (XNO) -* Haven (XHV) +* Zano (ZANO) +* Decred (DCR) +* Wownero (WOW) ## Features @@ -44,7 +47,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Create several wallets * Select your own custom nodes/servers * Address book -* Backup to external location or iCloud +* Backup to an external location or iCloud * Send to OpenAlias, Unstoppable Domains, Yats, and FIO Crypto Handles * Set desired network fee level * Store local transaction notes @@ -81,10 +84,6 @@ Cake Wallet includes support for several cryptocurrencies, including: * Automatically generate new addresses * Specify multiple recipients for batch sending -### Haven Specific Features - -* Send, receive, and store XHV and all xAssets like xUSD, xEUR, xAG, etc. - # Monero.com by Cake Wallet for Android and iOS ## Open Source Monero-Only Wallet @@ -99,6 +98,17 @@ Cake Wallet includes support for several cryptocurrencies, including: * F-Droid: https://fdroid.cakelabs.com * APK: https://github.com/cake-tech/cake_wallet/releases +### APK Verification + +APK releases on GitHub, Accrescent, and F-Droid use the same key. They can easily be verified using [apksigner](https://developer.android.com/tools/apksigner#options-verify) or [AppVerifier](https://github.com/soupslurpr/AppVerifier). + +See below for Cake Wallet's SHA-256 signing certificate hash: + +``` +com.cakewallet.cake_wallet +C5:40:53:AB:0F:10:D9:54:17:62:A3:DA:76:65:AE:3D:BA:5E:7C:74:3A:B4:F1:08:A5:34:9D:62:AC:10:6E:F5 +``` + # Support We have 24/7 free support. Please contact support@cakewallet.com @@ -161,7 +171,7 @@ The only parts to be translated, if needed, are the values m and s after the var 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. -5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. +5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make it transparent. Or you can use another program like Photoshop. 6. Add the new language code to `tool/utils/translation/translation_constants.dart` diff --git a/analysis_options.yaml b/analysis_options.yaml index 2063b5a8c..bd35233ba 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,5 +1,6 @@ include: package:lints/recommended.yaml + analyzer: exclude: [ build/**, @@ -21,6 +22,7 @@ analyzer: lib/solana/cw_solana.dart, lib/tron/cw_tron.dart, lib/wownero/cw_wownero.dart, + lib/zano/cw_zano.dart, ] language: strict-casts: true @@ -83,4 +85,4 @@ linter: # - unawaited_futures # - unnecessary_getters_setters # - unrelated_type_equality_checks -# - valid_regexps \ No newline at end of file +# - valid_regexps diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f5427531..4a8045bb3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,9 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -37,12 +35,23 @@ if (appPropertiesFile.exists()) { } android { - compileSdkVersion 34 + compileSdkVersion 35 + buildToolsVersion "35.0.0" lintOptions { disable 'InvalidPackage' } + compileOptions { + coreLibraryDesugaringEnabled true + + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + + namespace "com.cakewallet.cake_wallet" + defaultConfig { applicationId appProperties['id'] minSdkVersion 24 @@ -72,15 +81,14 @@ android { buildTypes { release { signingConfig signingConfigs.release - - shrinkResources false - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + signingConfig signingConfigs.release + } } - ndkVersion "25.1.8937393" + ndkVersion "27.0.12077973" } flutter { @@ -91,4 +99,10 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' +} +configurations { + implementation.exclude module:'proto-google-common-protos' + implementation.exclude module:'protolite-well-known-types' + implementation.exclude module:'protobuf-javalite' } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index d24d7f10a..a733bae9e 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -5,4 +5,98 @@ -keep class io.flutter.view.** { *; } -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } --dontwarn io.flutter.embedding.** \ No newline at end of file +-dontwarn io.flutter.embedding.** +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication + +# start reown +-dontwarn com.github.luben.zstd.BufferPool +-dontwarn com.github.luben.zstd.ZstdInputStream +-dontwarn com.github.luben.zstd.ZstdOutputStream +-dontwarn com.google.api.client.http.GenericUrl +-dontwarn com.google.api.client.http.HttpHeaders +-dontwarn com.google.api.client.http.HttpRequest +-dontwarn com.google.api.client.http.HttpRequestFactory +-dontwarn com.google.api.client.http.HttpResponse +-dontwarn com.google.api.client.http.HttpTransport +-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder +-dontwarn com.google.api.client.http.javanet.NetHttpTransport +-dontwarn java.awt.Color +-dontwarn java.awt.Dimension +-dontwarn java.awt.Graphics2D +-dontwarn java.awt.Graphics +-dontwarn java.awt.Image +-dontwarn java.awt.Point +-dontwarn java.awt.Polygon +-dontwarn java.awt.Shape +-dontwarn java.awt.color.ColorSpace +-dontwarn java.awt.geom.AffineTransform +-dontwarn java.awt.image.BufferedImage +-dontwarn java.awt.image.ColorModel +-dontwarn java.awt.image.ComponentColorModel +-dontwarn java.awt.image.ComponentSampleModel +-dontwarn java.awt.image.DataBuffer +-dontwarn java.awt.image.DataBufferByte +-dontwarn java.awt.image.DataBufferInt +-dontwarn java.awt.image.DataBufferUShort +-dontwarn java.awt.image.ImageObserver +-dontwarn java.awt.image.MultiPixelPackedSampleModel +-dontwarn java.awt.image.Raster +-dontwarn java.awt.image.RenderedImage +-dontwarn java.awt.image.SampleModel +-dontwarn java.awt.image.SinglePixelPackedSampleModel +-dontwarn java.awt.image.WritableRaster +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.FeatureDescriptor +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.Introspector +-dontwarn java.beans.PropertyDescriptor +-dontwarn java.lang.reflect.InaccessibleObjectException +-dontwarn javax.imageio.IIOImage +-dontwarn javax.imageio.ImageIO +-dontwarn javax.imageio.ImageWriteParam +-dontwarn javax.imageio.ImageWriter +-dontwarn javax.imageio.metadata.IIOMetadata +-dontwarn javax.imageio.stream.ImageOutputStream +-dontwarn javax.swing.JComponent +-dontwarn javax.swing.JFileChooser +-dontwarn javax.swing.JFrame +-dontwarn javax.swing.JPanel +-dontwarn javax.swing.ProgressMonitor +-dontwarn javax.swing.SwingUtilities +-dontwarn org.brotli.dec.BrotliInputStream +-dontwarn org.joda.time.Instant +-dontwarn org.objectweb.asm.AnnotationVisitor +-dontwarn org.objectweb.asm.Attribute +-dontwarn org.objectweb.asm.ClassReader +-dontwarn org.objectweb.asm.ClassVisitor +-dontwarn org.objectweb.asm.FieldVisitor +-dontwarn org.objectweb.asm.Label +-dontwarn org.objectweb.asm.MethodVisitor +-dontwarn org.objectweb.asm.Type +-dontwarn org.tukaani.xz.ARMOptions +-dontwarn org.tukaani.xz.ARMThumbOptions +-dontwarn org.tukaani.xz.DeltaOptions +-dontwarn org.tukaani.xz.FilterOptions +-dontwarn org.tukaani.xz.FinishableOutputStream +-dontwarn org.tukaani.xz.FinishableWrapperOutputStream +-dontwarn org.tukaani.xz.IA64Options +-dontwarn org.tukaani.xz.LZMA2InputStream +-dontwarn org.tukaani.xz.LZMA2Options +-dontwarn org.tukaani.xz.LZMAInputStream +-dontwarn org.tukaani.xz.LZMAOutputStream +-dontwarn org.tukaani.xz.MemoryLimitException +-dontwarn org.tukaani.xz.PowerPCOptions +-dontwarn org.tukaani.xz.SPARCOptions +-dontwarn org.tukaani.xz.SingleXZInputStream +-dontwarn org.tukaani.xz.UnsupportedOptionsException +-dontwarn org.tukaani.xz.X86Options +-dontwarn org.tukaani.xz.XZ +-dontwarn org.tukaani.xz.XZInputStream +-dontwarn org.tukaani.xz.XZOutputStream +-dontwarn us.hebi.matlab.mat.ejml.Mat5Ejml +-dontwarn us.hebi.matlab.mat.format.Mat5 +-dontwarn us.hebi.matlab.mat.format.Mat5File +-dontwarn us.hebi.matlab.mat.types.Array +-dontwarn us.hebi.matlab.mat.types.MatFile$Entry +-dontwarn us.hebi.matlab.mat.types.MatFile +# end reown \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index dc767a55d..f880684a6 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index b03c8a925..8283a7c8c 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -1,35 +1,34 @@ - + - - - - - + + - - + - - - + + + - - - + + + + + - + + + + - + + + + + + + + + + + + + @@ -102,10 +111,15 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle index 7ddb75179..d42aa24b4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,21 +1,8 @@ -buildscript { - ext.kotlin_version = '1.8.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath 'com.google.gms:google-services:4.3.8' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() mavenCentral() + maven { url "https://jitpack.io" } } } diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..d130e538a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 733c691d3..0cc4e42b9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 5a2f14fb1..468f9b55f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,15 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.7.1" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false + id "com.google.gms.google-services" version "4.3.8" apply false } + +include ":app" \ No newline at end of file diff --git a/assets/bitcoin_cash_electrum_server_list.yml b/assets/bitcoin_cash_electrum_server_list.yml index d76668169..948e5f3dc 100644 --- a/assets/bitcoin_cash_electrum_server_list.yml +++ b/assets/bitcoin_cash_electrum_server_list.yml @@ -1,3 +1,10 @@ - uri: bitcoincash.stackwallet.com:50002 - is_default: true \ No newline at end of file + is_default: true + useSSL: true +- + uri: bch.aftrek.org:50002 + useSSL: true +- + uri: node.minisatoshi.cash:50002 + useSSL: true diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 8b734a7bb..83da6a0b2 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -1,8 +1,9 @@ -- - uri: electrum.cakewallet.com:50002 - useSSL: true - uri: btc-electrum.cakewallet.com:50002 + useSSL: true isDefault: true - uri: electrs.cakewallet.com:50001 +- + uri: fulcrum.sethforprivacy.com:50002 + useSSL: true diff --git a/assets/decred_node_list.yml b/assets/decred_node_list.yml new file mode 100644 index 000000000..cb171e701 --- /dev/null +++ b/assets/decred_node_list.yml @@ -0,0 +1,6 @@ +- + uri: default-spv-nodes + is_default: true +- + uri: dcrd.sethforprivacy.com:9108 + useSSL: true \ No newline at end of file diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml index 125085d88..ed425c3c7 100644 --- a/assets/ethereum_server_list.yml +++ b/assets/ethereum_server_list.yml @@ -1,10 +1,14 @@ - - uri: ethereum.publicnode.com + uri: ethereum-rpc.publicnode.com + useSSL: true + isDefault: true - uri: eth.llamarpc.com - uri: rpc.flashbots.net - uri: eth-mainnet.public.blastapi.io +- + uri: eth.nownodes.io - uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/faq/faq_pl.json b/assets/faq/faq_pl.json index a38d79068..b41841cd8 100644 --- a/assets/faq/faq_pl.json +++ b/assets/faq/faq_pl.json @@ -13,7 +13,7 @@ }, { "question" : "Co oznaczają słowa „seed” i „keys”?", - "answer" : "Twoje klucze kodują prywatne informacje w twoim portfelu i pozwalają wydać monety i zobaczyć przychodzące transakcje.\nTwoje ziarno to tylko wersja twojego klucza prywatnego napisana w sposób, który łatwiej Ci zapisać. Wasze nasiona i klucze są w rzeczywistości takie same, tylko w różnych formach!\nNigdy nie dawaj nikomu swojego ziarna ani kluczy. Twoje fundusze zostaną skradzione, jeśli wydasz swoje nasiona lub klucze. Zapisz jednak swoje ziarno i przechowuj je w bezpiecznym miejscu (pozwoli to przywrócić portfel, jeśli zgubisz telefon).\n" + "answer" : "Twoje klucze i fraza seed zawierają prywatne informacje o twoim portfelu i pozwalają wysyłać kryptowalutę oraz zobaczyć przychodzące transakcje.\nFraza „seed” to wersja twojego klucza prywatnego napisana w sposób, który łatwiej Ci zapisać. Wasze frazy seed i klucze są w rzeczywistości takie same, tylko w różnych formach zapisu!\nNigdy nie dawaj nikomu swojej frazy seed ani swoich kluczy. Twoje fundusze zostaną skradzione, jeśli upublicznisz frazę seed lub klucze. Zapisz jednak swoją frazę seed i przechowuj ją w bezpiecznym miejscu (pozwoli to przywrócić portfel, jeśli zgubisz telefon).\n" }, { "question" : "Ile portfeli mogę utworzyć?", @@ -24,11 +24,11 @@ "answer" : "Stuknij menu •••, wybierz „Portfele”, a następnie „Przywróć portfel”. Następnie wprowadź dane początkowe (lub klucze) i opcjonalnie wprowadź datę przed pierwszą transakcją w portfelu (przyspieszy to proces synchronizacji .) Może być konieczne pozostawienie aplikacji otwartej przez 15-30 minut, aby całkowicie przywrócić portfel.\n" }, { - "question" : "Co mogę zrobić, jeśli stracę nasiona?", - "answer" : "Jeśli zapomniałeś o nasieniu, prawdopodobnie gdzieś je zapisałeś. Sprawdź swoje notatki i rozejrzyj się po komputerze. Jeśli nie możesz go nigdzie znaleźć, być może utworzono kopię zapasową Cake Wallet (w takim przypadku będziesz mógł przywrócić dane z tej kopii zapasowej). Jeśli żadna z tych czynności nie działa, niestety nic nie możemy zrobić.\n" + "question" : "Co mogę zrobić, jeśli zapomniałem frazę seed?", + "answer" : "Jeśli zapomniałeś swoją frazę seed, prawdopodobnie gdzieś je zapisałeś. Sprawdź swoje notatki i rozejrzyj się po komputerze. Jeśli nie możesz go nigdzie znaleźć, być może utworzono kopię zapasową Cake Wallet (w takim przypadku będziesz mógł przywrócić dane z tej kopii zapasowej). Jeśli żadna z tych czynności nie działa, niestety nic nie możemy zrobić.\n" }, { - "question" : "Czy zbierasz jakieś informacje o moim portfelu?", + "question" : "Czy zbieracie jakieś informacje o moim portfelu?", "answer" : "Portfel Cake NIE gromadzi ani nie rejestruje żadnych informacji o Twoim portfelu. Dbamy o Twoją prywatność.\n" }, { @@ -37,7 +37,7 @@ }, { "question" : "Co to są „podadresy” i jak z nich korzystać?", - "answer" : "Podadres jest w zasadzie unikalnym adresem, który można wygenerować w dowolnym momencie. Monety wysłane do niego nadal będą pojawiać się w głównym portfelu, ale osoba wysyłająca monety nie może podać Twojego głównego adresu. Podadresy zawsze zaczynają się od „8”.\nMożesz utworzyć nowy podadres na ekranie Odbieranie, dotykając „+” obok przycisku Podadresy. Wprowadź nazwę podadresu i dotknij „Dodaj”. Następnie dotknij nazwy podadresu, gdy chcesz go użyć!\nJeśli jesteś paranoikiem, prawdopodobnie za każdym razem, gdy otrzymasz Monero, powinieneś utworzyć nowy podadres.\n" + "answer" : "Podadres jest w unikalnym adresem, który można wygenerować w dowolnym momencie. Monety wysłane do niego nadal będą pojawiać się w głównym portfelu, ale osoba wysyłająca monety nie zna Twojego głównego adresu. Podadresy zawsze zaczynają się od „8”.\nMożesz utworzyć nowy podadres na ekranie Odbieranie, dotykając „+” obok przycisku Podadresy. Wprowadź nazwę podadresu i dotknij „Dodaj”. Następnie dotknij nazwy podadresu, gdy chcesz go użyć!\nJeśli jesteś paranoikiem, prawdopodobnie za każdym razem, gdy otrzymasz Monero, powinieneś utworzyć nowy podadres.\n" }, { "question" : "Co to jest ID transakcji?", diff --git a/assets/images/2.0x/decred.png b/assets/images/2.0x/decred.png new file mode 100644 index 000000000..2f4919cec Binary files /dev/null and b/assets/images/2.0x/decred.png differ diff --git a/assets/images/2.0x/decred_menu.png b/assets/images/2.0x/decred_menu.png new file mode 100644 index 000000000..4a41efef1 Binary files /dev/null and b/assets/images/2.0x/decred_menu.png differ diff --git a/assets/images/2fa.png b/assets/images/2fa.png new file mode 100644 index 000000000..36c99beab Binary files /dev/null and b/assets/images/2fa.png differ diff --git a/assets/images/2fa_warning_dark.svg b/assets/images/2fa_warning_dark.svg new file mode 100644 index 000000000..c9fcad341 --- /dev/null +++ b/assets/images/2fa_warning_dark.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/2fa_warning_light.svg b/assets/images/2fa_warning_light.svg new file mode 100644 index 000000000..087d8e99b --- /dev/null +++ b/assets/images/2fa_warning_light.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/3.0x/decred.png b/assets/images/3.0x/decred.png new file mode 100644 index 000000000..b2c9ac818 Binary files /dev/null and b/assets/images/3.0x/decred.png differ diff --git a/assets/images/3.0x/decred_menu.png b/assets/images/3.0x/decred_menu.png new file mode 100644 index 000000000..e55b3fb5c Binary files /dev/null and b/assets/images/3.0x/decred_menu.png differ diff --git a/assets/images/apple_pay_logo.png b/assets/images/apple_pay_logo.png new file mode 100644 index 000000000..346007e3b Binary files /dev/null and b/assets/images/apple_pay_logo.png differ diff --git a/assets/images/apple_pay_round_dark.svg b/assets/images/apple_pay_round_dark.svg new file mode 100644 index 000000000..82443bfb4 --- /dev/null +++ b/assets/images/apple_pay_round_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/apple_pay_round_light.svg b/assets/images/apple_pay_round_light.svg new file mode 100644 index 000000000..2beb1248f --- /dev/null +++ b/assets/images/apple_pay_round_light.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank.png b/assets/images/bank.png new file mode 100644 index 000000000..9dc68147a Binary files /dev/null and b/assets/images/bank.png differ diff --git a/assets/images/bank_dark.svg b/assets/images/bank_dark.svg new file mode 100644 index 000000000..670120796 --- /dev/null +++ b/assets/images/bank_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank_light.svg b/assets/images/bank_light.svg new file mode 100644 index 000000000..804716289 --- /dev/null +++ b/assets/images/bank_light.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/birthday_cake.png b/assets/images/birthday_cake.png index 84b084fba..293cd10f6 100644 Binary files a/assets/images/birthday_cake.png and b/assets/images/birthday_cake.png differ diff --git a/assets/images/btc_lock_dark.png b/assets/images/btc_lock_dark.png new file mode 100644 index 000000000..f5b3d7e27 Binary files /dev/null and b/assets/images/btc_lock_dark.png differ diff --git a/assets/images/btc_lock_light.png b/assets/images/btc_lock_light.png new file mode 100644 index 000000000..4320d96e3 Binary files /dev/null and b/assets/images/btc_lock_light.png differ diff --git a/assets/images/buy.png b/assets/images/buy.png index ff4549d5a..32c116e6b 100644 Binary files a/assets/images/buy.png and b/assets/images/buy.png differ diff --git a/assets/images/buy_sell.png b/assets/images/buy_sell.png new file mode 100644 index 000000000..0fbffe56f Binary files /dev/null and b/assets/images/buy_sell.png differ diff --git a/assets/images/cake_logo.png b/assets/images/cake_logo.png index 8a85bf225..abbb4e62b 100644 Binary files a/assets/images/cake_logo.png and b/assets/images/cake_logo.png differ diff --git a/assets/images/cake_logo_dark.svg b/assets/images/cake_logo_dark.svg new file mode 100644 index 000000000..095077443 --- /dev/null +++ b/assets/images/cake_logo_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/cake_logo_light.svg b/assets/images/cake_logo_light.svg new file mode 100644 index 000000000..767e205d1 --- /dev/null +++ b/assets/images/cake_logo_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/cakewallet_android_icon.png b/assets/images/cakewallet_android_icon.png old mode 100755 new mode 100644 index 59cc69414..7f15c62f5 Binary files a/assets/images/cakewallet_android_icon.png and b/assets/images/cakewallet_android_icon.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 00d924171..345888d26 100644 --- a/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/cakewallet_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - - + + + \ No newline at end of file diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png index 10d0a1a82..89c9b0571 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 5b0fde827..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 9c16f0a27..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_background.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 000000000..19669488f Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_background.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_foreground.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..1411a5da5 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_monochrome.png b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..1411a5da5 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png index 8c59ec33e..8f1d1c28b 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 5d25e42e7..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 021fe65de..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_background.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 000000000..75025cfd5 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_background.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_foreground.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..e8c47adb3 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_monochrome.png b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..e8c47adb3 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png index 10c3acd7f..f775a8fac 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index c4b66dc58..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index b440b154d..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_background.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 000000000..9784f16c8 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_foreground.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..6ba8eb301 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_monochrome.png b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..6ba8eb301 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png index 813a3678d..31458fa02 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 75dc0219d..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 90afb19e8..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_background.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..04ef206c8 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_foreground.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..cc93d633b Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_monochrome.png b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..cc93d633b Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png index 671422b96..158afbbf9 100644 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png deleted file mode 100644 index 46b1e2cb1..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png deleted file mode 100644 index 0a2025220..000000000 Binary files a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and /dev/null differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_background.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..66a5487a2 Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_foreground.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..0ecd56e8c Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_monochrome.png b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..0ecd56e8c Binary files /dev/null and b/assets/images/cakewallet_android_icon/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/assets/images/cakewallet_app_logo.png b/assets/images/cakewallet_app_logo.png deleted file mode 100644 index 59cc69414..000000000 Binary files a/assets/images/cakewallet_app_logo.png and /dev/null differ diff --git a/assets/images/cakewallet_icon_1024.png b/assets/images/cakewallet_icon_1024.png index 64682cd1d..35cf42245 100644 Binary files a/assets/images/cakewallet_icon_1024.png and b/assets/images/cakewallet_icon_1024.png differ diff --git a/assets/images/cakewallet_icon_120.png b/assets/images/cakewallet_icon_120.png index 1a2c1b99c..6e45f6423 100644 Binary files a/assets/images/cakewallet_icon_120.png and b/assets/images/cakewallet_icon_120.png differ diff --git a/assets/images/cakewallet_icon_180.png b/assets/images/cakewallet_icon_180.png index ff69a866a..da585ca42 100644 Binary files a/assets/images/cakewallet_icon_180.png and b/assets/images/cakewallet_icon_180.png differ diff --git a/assets/images/cakewallet_logo.png b/assets/images/cakewallet_logo.png index bf6896ad2..c465fa26c 100644 Binary files a/assets/images/cakewallet_logo.png and b/assets/images/cakewallet_logo.png differ diff --git a/assets/images/card.svg b/assets/images/card.svg new file mode 100644 index 000000000..95530cdc9 --- /dev/null +++ b/assets/images/card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/card_dark.svg b/assets/images/card_dark.svg new file mode 100644 index 000000000..2e5bcf986 --- /dev/null +++ b/assets/images/card_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/images/cards.png b/assets/images/cards.png new file mode 100644 index 000000000..b263bc742 Binary files /dev/null and b/assets/images/cards.png differ diff --git a/assets/images/cards.svg b/assets/images/cards.svg deleted file mode 100644 index 699f9d311..000000000 --- a/assets/images/cards.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - diff --git a/assets/images/chainflip.png b/assets/images/chainflip.png new file mode 100644 index 000000000..e588e6361 Binary files /dev/null and b/assets/images/chainflip.png differ diff --git a/assets/images/contact.png b/assets/images/contact.png new file mode 100644 index 000000000..5cf96694b Binary files /dev/null and b/assets/images/contact.png differ diff --git a/assets/images/contact_icon.svg b/assets/images/contact_icon.svg new file mode 100644 index 000000000..6dbfcd5f4 --- /dev/null +++ b/assets/images/contact_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/dcr_icon.png b/assets/images/dcr_icon.png index 609873611..757cd0388 100644 Binary files a/assets/images/dcr_icon.png and b/assets/images/dcr_icon.png differ diff --git a/assets/images/decred.png b/assets/images/decred.png new file mode 100644 index 000000000..0b12f2ef0 Binary files /dev/null and b/assets/images/decred.png differ diff --git a/assets/images/decred_icon.png b/assets/images/decred_icon.png new file mode 100644 index 000000000..9391abc3d Binary files /dev/null and b/assets/images/decred_icon.png differ diff --git a/assets/images/decred_menu.png b/assets/images/decred_menu.png new file mode 100644 index 000000000..5c67923c5 Binary files /dev/null and b/assets/images/decred_menu.png differ diff --git a/assets/images/deuro_icon.png b/assets/images/deuro_icon.png new file mode 100644 index 000000000..4dc068ff8 Binary files /dev/null and b/assets/images/deuro_icon.png differ diff --git a/assets/images/discord.png b/assets/images/discord.png new file mode 100644 index 000000000..23fe37a36 Binary files /dev/null and b/assets/images/discord.png differ diff --git a/assets/images/discourse.png b/assets/images/discourse.png new file mode 100644 index 000000000..b8bab2c5d Binary files /dev/null and b/assets/images/discourse.png differ diff --git a/assets/images/dollar_coin.svg b/assets/images/dollar_coin.svg new file mode 100644 index 000000000..22218f332 --- /dev/null +++ b/assets/images/dollar_coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/flags/abw.png b/assets/images/flags/abw.png new file mode 100644 index 000000000..d049d3a43 Binary files /dev/null and b/assets/images/flags/abw.png differ diff --git a/assets/images/flags/afg.png b/assets/images/flags/afg.png new file mode 100644 index 000000000..f2ea25144 Binary files /dev/null and b/assets/images/flags/afg.png differ diff --git a/assets/images/flags/ago.png b/assets/images/flags/ago.png new file mode 100644 index 000000000..b04d7dfa6 Binary files /dev/null and b/assets/images/flags/ago.png differ diff --git a/assets/images/flags/aia.png b/assets/images/flags/aia.png new file mode 100644 index 000000000..193f0ff41 Binary files /dev/null and b/assets/images/flags/aia.png differ diff --git a/assets/images/flags/and.png b/assets/images/flags/and.png new file mode 100644 index 000000000..fc5d9a89b Binary files /dev/null and b/assets/images/flags/and.png differ diff --git a/assets/images/flags/arm.png b/assets/images/flags/arm.png index 7bdabd4ee..0e4c356e2 100644 Binary files a/assets/images/flags/arm.png and b/assets/images/flags/arm.png differ diff --git a/assets/images/flags/asm.png b/assets/images/flags/asm.png new file mode 100644 index 000000000..fd3818eda Binary files /dev/null and b/assets/images/flags/asm.png differ diff --git a/assets/images/flags/atf.png b/assets/images/flags/atf.png new file mode 100644 index 000000000..af77e45d5 Binary files /dev/null and b/assets/images/flags/atf.png differ diff --git a/assets/images/flags/atg.png b/assets/images/flags/atg.png new file mode 100644 index 000000000..d9a3d9f9e Binary files /dev/null and b/assets/images/flags/atg.png differ diff --git a/assets/images/flags/aut.png b/assets/images/flags/aut.png new file mode 100644 index 000000000..1be1ff483 Binary files /dev/null and b/assets/images/flags/aut.png differ diff --git a/assets/images/flags/aze.png b/assets/images/flags/aze.png new file mode 100644 index 000000000..834b1e696 Binary files /dev/null and b/assets/images/flags/aze.png differ diff --git a/assets/images/flags/bel.png b/assets/images/flags/bel.png new file mode 100644 index 000000000..1c06c5fa7 Binary files /dev/null and b/assets/images/flags/bel.png differ diff --git a/assets/images/flags/bes.png b/assets/images/flags/bes.png new file mode 100644 index 000000000..b00bfb1f5 Binary files /dev/null and b/assets/images/flags/bes.png differ diff --git a/assets/images/flags/bhr.png b/assets/images/flags/bhr.png new file mode 100644 index 000000000..135c254cb Binary files /dev/null and b/assets/images/flags/bhr.png differ diff --git a/assets/images/flags/blz.png b/assets/images/flags/blz.png new file mode 100644 index 000000000..06b23f161 Binary files /dev/null and b/assets/images/flags/blz.png differ diff --git a/assets/images/flags/bmu.png b/assets/images/flags/bmu.png new file mode 100644 index 000000000..6e253a8e6 Binary files /dev/null and b/assets/images/flags/bmu.png differ diff --git a/assets/images/flags/bol.png b/assets/images/flags/bol.png new file mode 100644 index 000000000..4996ddbcc Binary files /dev/null and b/assets/images/flags/bol.png differ diff --git a/assets/images/flags/bra.png b/assets/images/flags/bra.png index ee0b0e8c6..6c4ba8968 100644 Binary files a/assets/images/flags/bra.png and b/assets/images/flags/bra.png differ diff --git a/assets/images/flags/brn.png b/assets/images/flags/brn.png new file mode 100644 index 000000000..bd1d1cc9a Binary files /dev/null and b/assets/images/flags/brn.png differ diff --git a/assets/images/flags/btn.png b/assets/images/flags/btn.png new file mode 100644 index 000000000..962e5e5bb Binary files /dev/null and b/assets/images/flags/btn.png differ diff --git a/assets/images/flags/bvt.png b/assets/images/flags/bvt.png new file mode 100644 index 000000000..4acde7fbf Binary files /dev/null and b/assets/images/flags/bvt.png differ diff --git a/assets/images/flags/bwa.png b/assets/images/flags/bwa.png new file mode 100644 index 000000000..5b7eff92a Binary files /dev/null and b/assets/images/flags/bwa.png differ diff --git a/assets/images/flags/cck.png b/assets/images/flags/cck.png new file mode 100644 index 000000000..d255ab91a Binary files /dev/null and b/assets/images/flags/cck.png differ diff --git a/assets/images/flags/cmr.png b/assets/images/flags/cmr.png new file mode 100644 index 000000000..2bc6ad13c Binary files /dev/null and b/assets/images/flags/cmr.png differ diff --git a/assets/images/flags/cok.png b/assets/images/flags/cok.png new file mode 100644 index 000000000..49386516d Binary files /dev/null and b/assets/images/flags/cok.png differ diff --git a/assets/images/flags/cpv.png b/assets/images/flags/cpv.png new file mode 100644 index 000000000..0683d931f Binary files /dev/null and b/assets/images/flags/cpv.png differ diff --git a/assets/images/flags/cri.png b/assets/images/flags/cri.png new file mode 100644 index 000000000..029bbfc49 Binary files /dev/null and b/assets/images/flags/cri.png differ diff --git a/assets/images/flags/cuw.png b/assets/images/flags/cuw.png new file mode 100644 index 000000000..92a36b728 Binary files /dev/null and b/assets/images/flags/cuw.png differ diff --git a/assets/images/flags/cxr.png b/assets/images/flags/cxr.png new file mode 100644 index 000000000..e644a49ea Binary files /dev/null and b/assets/images/flags/cxr.png differ diff --git a/assets/images/flags/cyp.png b/assets/images/flags/cyp.png new file mode 100644 index 000000000..ba5246809 Binary files /dev/null and b/assets/images/flags/cyp.png differ diff --git a/assets/images/flags/dji.png b/assets/images/flags/dji.png new file mode 100644 index 000000000..185c5322b Binary files /dev/null and b/assets/images/flags/dji.png differ diff --git a/assets/images/flags/dma.png b/assets/images/flags/dma.png new file mode 100644 index 000000000..7f61af95e Binary files /dev/null and b/assets/images/flags/dma.png differ diff --git a/assets/images/flags/dza.png b/assets/images/flags/dza.png new file mode 100644 index 000000000..342284223 Binary files /dev/null and b/assets/images/flags/dza.png differ diff --git a/assets/images/flags/ecu.png b/assets/images/flags/ecu.png new file mode 100644 index 000000000..efb3acb43 Binary files /dev/null and b/assets/images/flags/ecu.png differ diff --git a/assets/images/flags/est.png b/assets/images/flags/est.png new file mode 100644 index 000000000..c2e417b93 Binary files /dev/null and b/assets/images/flags/est.png differ diff --git a/assets/images/flags/eth.png b/assets/images/flags/eth.png new file mode 100644 index 000000000..5b4970a67 Binary files /dev/null and b/assets/images/flags/eth.png differ diff --git a/assets/images/flags/fin.png b/assets/images/flags/fin.png new file mode 100644 index 000000000..b6bc2f9a9 Binary files /dev/null and b/assets/images/flags/fin.png differ diff --git a/assets/images/flags/fji.png b/assets/images/flags/fji.png new file mode 100644 index 000000000..4700ad579 Binary files /dev/null and b/assets/images/flags/fji.png differ diff --git a/assets/images/flags/flk.png b/assets/images/flags/flk.png new file mode 100644 index 000000000..66ff172c2 Binary files /dev/null and b/assets/images/flags/flk.png differ diff --git a/assets/images/flags/fro.png b/assets/images/flags/fro.png new file mode 100644 index 000000000..2c3ed5f6b Binary files /dev/null and b/assets/images/flags/fro.png differ diff --git a/assets/images/flags/fsm.png b/assets/images/flags/fsm.png new file mode 100644 index 000000000..b8aedd34e Binary files /dev/null and b/assets/images/flags/fsm.png differ diff --git a/assets/images/flags/gab.png b/assets/images/flags/gab.png new file mode 100644 index 000000000..c8e1cbd1f Binary files /dev/null and b/assets/images/flags/gab.png differ diff --git a/assets/images/flags/geo.png b/assets/images/flags/geo.png new file mode 100644 index 000000000..46c83a589 Binary files /dev/null and b/assets/images/flags/geo.png differ diff --git a/assets/images/flags/ggi.png b/assets/images/flags/ggi.png new file mode 100644 index 000000000..fbc403f16 Binary files /dev/null and b/assets/images/flags/ggi.png differ diff --git a/assets/images/flags/ggy.png b/assets/images/flags/ggy.png new file mode 100644 index 000000000..a882b4a59 Binary files /dev/null and b/assets/images/flags/ggy.png differ diff --git a/assets/images/flags/glp.png b/assets/images/flags/glp.png new file mode 100644 index 000000000..8bd0a69bf Binary files /dev/null and b/assets/images/flags/glp.png differ diff --git a/assets/images/flags/gmb.png b/assets/images/flags/gmb.png new file mode 100644 index 000000000..fa641ca1a Binary files /dev/null and b/assets/images/flags/gmb.png differ diff --git a/assets/images/flags/grc.png b/assets/images/flags/grc.png new file mode 100644 index 000000000..d7b37b0c7 Binary files /dev/null and b/assets/images/flags/grc.png differ diff --git a/assets/images/flags/grd.png b/assets/images/flags/grd.png new file mode 100644 index 000000000..7138a28d7 Binary files /dev/null and b/assets/images/flags/grd.png differ diff --git a/assets/images/flags/grl.png b/assets/images/flags/grl.png new file mode 100644 index 000000000..53e45988b Binary files /dev/null and b/assets/images/flags/grl.png differ diff --git a/assets/images/flags/guf.png b/assets/images/flags/guf.png new file mode 100644 index 000000000..07a2d5070 Binary files /dev/null and b/assets/images/flags/guf.png differ diff --git a/assets/images/flags/gum.png b/assets/images/flags/gum.png new file mode 100644 index 000000000..828c5f3d9 Binary files /dev/null and b/assets/images/flags/gum.png differ diff --git a/assets/images/flags/guy.png b/assets/images/flags/guy.png new file mode 100644 index 000000000..5845c6db9 Binary files /dev/null and b/assets/images/flags/guy.png differ diff --git a/assets/images/flags/hau.png b/assets/images/flags/hau.png index 7583b5daf..2bfb0a71c 100644 Binary files a/assets/images/flags/hau.png and b/assets/images/flags/hau.png differ diff --git a/assets/images/flags/hmd.png b/assets/images/flags/hmd.png new file mode 100644 index 000000000..8c2931c4e Binary files /dev/null and b/assets/images/flags/hmd.png differ diff --git a/assets/images/flags/ind.png b/assets/images/flags/ind.png index ef721a2aa..45d4a7109 100644 Binary files a/assets/images/flags/ind.png and b/assets/images/flags/ind.png differ diff --git a/assets/images/flags/iot.png b/assets/images/flags/iot.png new file mode 100644 index 000000000..2863320f5 Binary files /dev/null and b/assets/images/flags/iot.png differ diff --git a/assets/images/flags/irl.png b/assets/images/flags/irl.png new file mode 100644 index 000000000..2126054d3 Binary files /dev/null and b/assets/images/flags/irl.png differ diff --git a/assets/images/flags/jam.png b/assets/images/flags/jam.png new file mode 100644 index 000000000..97bce2de3 Binary files /dev/null and b/assets/images/flags/jam.png differ diff --git a/assets/images/flags/jey.png b/assets/images/flags/jey.png new file mode 100644 index 000000000..e144d060e Binary files /dev/null and b/assets/images/flags/jey.png differ diff --git a/assets/images/flags/jor.png b/assets/images/flags/jor.png new file mode 100644 index 000000000..6e5d2bbb8 Binary files /dev/null and b/assets/images/flags/jor.png differ diff --git a/assets/images/flags/kaz.png b/assets/images/flags/kaz.png new file mode 100644 index 000000000..db52cf078 Binary files /dev/null and b/assets/images/flags/kaz.png differ diff --git a/assets/images/flags/ken.png b/assets/images/flags/ken.png new file mode 100644 index 000000000..2570c185d Binary files /dev/null and b/assets/images/flags/ken.png differ diff --git a/assets/images/flags/kir.png b/assets/images/flags/kir.png new file mode 100644 index 000000000..c8b69d702 Binary files /dev/null and b/assets/images/flags/kir.png differ diff --git a/assets/images/flags/kwt.png b/assets/images/flags/kwt.png new file mode 100644 index 000000000..f21563b5f Binary files /dev/null and b/assets/images/flags/kwt.png differ diff --git a/assets/images/flags/lbn.png b/assets/images/flags/lbn.png new file mode 100644 index 000000000..2a9ae1dd0 Binary files /dev/null and b/assets/images/flags/lbn.png differ diff --git a/assets/images/flags/lie.png b/assets/images/flags/lie.png new file mode 100644 index 000000000..0b8c77442 Binary files /dev/null and b/assets/images/flags/lie.png differ diff --git a/assets/images/flags/lka.png b/assets/images/flags/lka.png new file mode 100644 index 000000000..2b6492daa Binary files /dev/null and b/assets/images/flags/lka.png differ diff --git a/assets/images/flags/ltu.png b/assets/images/flags/ltu.png new file mode 100644 index 000000000..dec6babea Binary files /dev/null and b/assets/images/flags/ltu.png differ diff --git a/assets/images/flags/lux.png b/assets/images/flags/lux.png new file mode 100644 index 000000000..9cc1c65b5 Binary files /dev/null and b/assets/images/flags/lux.png differ diff --git a/assets/images/flags/lva.png b/assets/images/flags/lva.png new file mode 100644 index 000000000..b3312700d Binary files /dev/null and b/assets/images/flags/lva.png differ diff --git a/assets/images/flags/mco.png b/assets/images/flags/mco.png new file mode 100644 index 000000000..6c12bd624 Binary files /dev/null and b/assets/images/flags/mco.png differ diff --git a/assets/images/flags/mlt.png b/assets/images/flags/mlt.png new file mode 100644 index 000000000..12809815f Binary files /dev/null and b/assets/images/flags/mlt.png differ diff --git a/assets/images/flags/mnp.png b/assets/images/flags/mnp.png new file mode 100644 index 000000000..3e6c538e4 Binary files /dev/null and b/assets/images/flags/mnp.png differ diff --git a/assets/images/flags/mrt.png b/assets/images/flags/mrt.png new file mode 100644 index 000000000..0fd8d757e Binary files /dev/null and b/assets/images/flags/mrt.png differ diff --git a/assets/images/flags/msr.png b/assets/images/flags/msr.png new file mode 100644 index 000000000..2d2af3aef Binary files /dev/null and b/assets/images/flags/msr.png differ diff --git a/assets/images/flags/mtq.png b/assets/images/flags/mtq.png new file mode 100644 index 000000000..1897c94e7 Binary files /dev/null and b/assets/images/flags/mtq.png differ diff --git a/assets/images/flags/mwi.png b/assets/images/flags/mwi.png new file mode 100644 index 000000000..7ddfbb17b Binary files /dev/null and b/assets/images/flags/mwi.png differ diff --git a/assets/images/flags/myt.png b/assets/images/flags/myt.png new file mode 100644 index 000000000..c149a2a79 Binary files /dev/null and b/assets/images/flags/myt.png differ diff --git a/assets/images/flags/ner.png b/assets/images/flags/ner.png new file mode 100644 index 000000000..87bb8211f Binary files /dev/null and b/assets/images/flags/ner.png differ diff --git a/assets/images/flags/nfk.png b/assets/images/flags/nfk.png new file mode 100644 index 000000000..6e68aee4f Binary files /dev/null and b/assets/images/flags/nfk.png differ diff --git a/assets/images/flags/niu.png b/assets/images/flags/niu.png new file mode 100644 index 000000000..acb36780d Binary files /dev/null and b/assets/images/flags/niu.png differ diff --git a/assets/images/flags/omn.png b/assets/images/flags/omn.png new file mode 100644 index 000000000..6b6cd8b3b Binary files /dev/null and b/assets/images/flags/omn.png differ diff --git a/assets/images/flags/per.png b/assets/images/flags/per.png new file mode 100644 index 000000000..490a26441 Binary files /dev/null and b/assets/images/flags/per.png differ diff --git a/assets/images/flags/plw.png b/assets/images/flags/plw.png new file mode 100644 index 000000000..6f6ff993a Binary files /dev/null and b/assets/images/flags/plw.png differ diff --git a/assets/images/flags/pri.png b/assets/images/flags/pri.png new file mode 100644 index 000000000..cb0c54cd6 Binary files /dev/null and b/assets/images/flags/pri.png differ diff --git a/assets/images/flags/pyf.png b/assets/images/flags/pyf.png new file mode 100644 index 000000000..66a5da6b8 Binary files /dev/null and b/assets/images/flags/pyf.png differ diff --git a/assets/images/flags/qat.png b/assets/images/flags/qat.png new file mode 100644 index 000000000..1e8461e91 Binary files /dev/null and b/assets/images/flags/qat.png differ diff --git a/assets/images/flags/slb.png b/assets/images/flags/slb.png new file mode 100644 index 000000000..d63061a0b Binary files /dev/null and b/assets/images/flags/slb.png differ diff --git a/assets/images/flags/slv.png b/assets/images/flags/slv.png new file mode 100644 index 000000000..e597e45b3 Binary files /dev/null and b/assets/images/flags/slv.png differ diff --git a/assets/images/flags/svk.png b/assets/images/flags/svk.png new file mode 100644 index 000000000..06bed756e Binary files /dev/null and b/assets/images/flags/svk.png differ diff --git a/assets/images/flags/svn.png b/assets/images/flags/svn.png new file mode 100644 index 000000000..a791163bd Binary files /dev/null and b/assets/images/flags/svn.png differ diff --git a/assets/images/flags/tkm.png b/assets/images/flags/tkm.png new file mode 100644 index 000000000..0c3ff8755 Binary files /dev/null and b/assets/images/flags/tkm.png differ diff --git a/assets/images/flags/ton.png b/assets/images/flags/ton.png new file mode 100644 index 000000000..84cf20ef5 Binary files /dev/null and b/assets/images/flags/ton.png differ diff --git a/assets/images/flags/tuv.png b/assets/images/flags/tuv.png new file mode 100644 index 000000000..15478f191 Binary files /dev/null and b/assets/images/flags/tuv.png differ diff --git a/assets/images/flags/ukr.png b/assets/images/flags/ukr.png index 61071e338..c4fe57fcc 100644 Binary files a/assets/images/flags/ukr.png and b/assets/images/flags/ukr.png differ diff --git a/assets/images/flags/ury.png b/assets/images/flags/ury.png new file mode 100644 index 000000000..c41e2780a Binary files /dev/null and b/assets/images/flags/ury.png differ diff --git a/assets/images/flags/vat.png b/assets/images/flags/vat.png new file mode 100644 index 000000000..d6c99cc1f Binary files /dev/null and b/assets/images/flags/vat.png differ diff --git a/assets/images/flags/vir.png b/assets/images/flags/vir.png new file mode 100644 index 000000000..a57f924b2 Binary files /dev/null and b/assets/images/flags/vir.png differ diff --git a/assets/images/flags/vut.png b/assets/images/flags/vut.png new file mode 100644 index 000000000..3c4d6e429 Binary files /dev/null and b/assets/images/flags/vut.png differ diff --git a/assets/images/flip_icon.png b/assets/images/flip_icon.png new file mode 100644 index 000000000..e588e6361 Binary files /dev/null and b/assets/images/flip_icon.png differ diff --git a/assets/images/google_pay_icon.png b/assets/images/google_pay_icon.png new file mode 100644 index 000000000..a3ca38311 Binary files /dev/null and b/assets/images/google_pay_icon.png differ diff --git a/assets/images/hardware_wallet/ledger_flex.png b/assets/images/hardware_wallet/ledger_flex.png new file mode 100644 index 000000000..fa39f241f Binary files /dev/null and b/assets/images/hardware_wallet/ledger_flex.png differ diff --git a/assets/images/hardware_wallet/ledger_nano_s.png b/assets/images/hardware_wallet/ledger_nano_s.png new file mode 100644 index 000000000..02777aeb6 Binary files /dev/null and b/assets/images/hardware_wallet/ledger_nano_s.png differ diff --git a/assets/images/hardware_wallet/ledger_nano_x.png b/assets/images/hardware_wallet/ledger_nano_x.png new file mode 100644 index 000000000..e9328a5ef Binary files /dev/null and b/assets/images/hardware_wallet/ledger_nano_x.png differ diff --git a/assets/images/hardware_wallet/ledger_stax.png b/assets/images/hardware_wallet/ledger_stax.png new file mode 100644 index 000000000..06c9c848e Binary files /dev/null and b/assets/images/hardware_wallet/ledger_stax.png differ diff --git a/assets/images/hero/cw_welcome_dark.svg b/assets/images/hero/cw_welcome_dark.svg new file mode 100644 index 000000000..5479cb1ee --- /dev/null +++ b/assets/images/hero/cw_welcome_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/hero/cw_welcome_light.svg b/assets/images/hero/cw_welcome_light.svg new file mode 100644 index 000000000..ece7d1f84 --- /dev/null +++ b/assets/images/hero/cw_welcome_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/history.svg b/assets/images/history.svg new file mode 100644 index 000000000..f308ab7e3 --- /dev/null +++ b/assets/images/history.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/home_screen_setting_icon.svg b/assets/images/home_screen_setting_icon.svg new file mode 100644 index 000000000..7b3aa7b4c --- /dev/null +++ b/assets/images/home_screen_setting_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x.png new file mode 100644 index 000000000..3fd15f3ce Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x~ipad.png new file mode 100644 index 000000000..3fd15f3ce Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@2x~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@3x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@3x.png new file mode 100644 index 000000000..b6ff994f6 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20@3x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20~ipad.png new file mode 100644 index 000000000..4be1a2317 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-20~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29.png new file mode 100644 index 000000000..219f2c6be Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x.png new file mode 100644 index 000000000..ae94bb0ac Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x~ipad.png new file mode 100644 index 000000000..ae94bb0ac Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@2x~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@3x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@3x.png new file mode 100644 index 000000000..0ef9d3bbf Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29@3x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29~ipad.png new file mode 100644 index 000000000..219f2c6be Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-29~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x.png new file mode 100644 index 000000000..9fdb32376 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x~ipad.png new file mode 100644 index 000000000..9fdb32376 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@2x~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@3x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@3x.png new file mode 100644 index 000000000..485f8b37b Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40@3x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40~ipad.png new file mode 100644 index 000000000..3fd15f3ce Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-40~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@2x~car.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@2x~car.png new file mode 100644 index 000000000..485f8b37b Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@2x~car.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@3x~car.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@3x~car.png new file mode 100644 index 000000000..50148e6dc Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-60@3x~car.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-83.5@2x~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-83.5@2x~ipad.png new file mode 100644 index 000000000..8f290ada2 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon-83.5@2x~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x.png new file mode 100644 index 000000000..485f8b37b Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x~ipad.png new file mode 100644 index 000000000..b11d7332f Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@2x~ipad.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@3x.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@3x.png new file mode 100644 index 000000000..50148e6dc Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon@3x.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ios-marketing.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ios-marketing.png new file mode 100644 index 000000000..1fce95553 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ios-marketing.png differ diff --git a/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ipad.png b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ipad.png new file mode 100644 index 000000000..7d4a82186 Binary files /dev/null and b/assets/images/ios_icons/cakewallet_ios_icons/AppIcon~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x.png new file mode 100644 index 000000000..7ea540caf Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x~ipad.png new file mode 100644 index 000000000..7ea540caf Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@2x~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-20@3x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@3x.png new file mode 100644 index 000000000..6ac773754 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-20@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-20~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-20~ipad.png new file mode 100644 index 000000000..57864a9b3 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-20~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-29.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-29.png new file mode 100644 index 000000000..27f817dfc Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-29.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x.png new file mode 100644 index 000000000..0455b4409 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x~ipad.png new file mode 100644 index 000000000..0455b4409 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@2x~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-29@3x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@3x.png new file mode 100644 index 000000000..1b8a73481 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-29@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-29~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-29~ipad.png new file mode 100644 index 000000000..27f817dfc Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-29~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x.png new file mode 100644 index 000000000..963612d0c Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x~ipad.png new file mode 100644 index 000000000..963612d0c Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@2x~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-40@3x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@3x.png new file mode 100644 index 000000000..b6da404cb Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-40@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-40~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-40~ipad.png new file mode 100644 index 000000000..7ea540caf Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-40~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-60@2x~car.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-60@2x~car.png new file mode 100644 index 000000000..b6da404cb Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-60@2x~car.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-60@3x~car.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-60@3x~car.png new file mode 100644 index 000000000..37f7651a5 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-60@3x~car.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon-83.5@2x~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon-83.5@2x~ipad.png new file mode 100644 index 000000000..21aa12463 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon-83.5@2x~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon@2x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon@2x.png new file mode 100644 index 000000000..b6da404cb Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon@2x~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon@2x~ipad.png new file mode 100644 index 000000000..b6b63a61e Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon@2x~ipad.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon@3x.png b/assets/images/ios_icons/monero_ios_icons/AppIcon@3x.png new file mode 100644 index 000000000..37f7651a5 Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon~ios-marketing.png b/assets/images/ios_icons/monero_ios_icons/AppIcon~ios-marketing.png new file mode 100644 index 000000000..0c977110a Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon~ios-marketing.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/AppIcon~ipad.png b/assets/images/ios_icons/monero_ios_icons/AppIcon~ipad.png new file mode 100644 index 000000000..849c5612a Binary files /dev/null and b/assets/images/ios_icons/monero_ios_icons/AppIcon~ipad.png differ diff --git a/assets/images/kryptonim_dark.png b/assets/images/kryptonim_dark.png new file mode 100644 index 000000000..646d550ba Binary files /dev/null and b/assets/images/kryptonim_dark.png differ diff --git a/assets/images/kryptonim_light.png b/assets/images/kryptonim_light.png new file mode 100644 index 000000000..85e64a3f2 Binary files /dev/null and b/assets/images/kryptonim_light.png differ diff --git a/assets/images/ledger_nano.png b/assets/images/ledger_nano.png deleted file mode 100644 index bb61ba175..000000000 Binary files a/assets/images/ledger_nano.png and /dev/null differ diff --git a/assets/images/letsexchange_icon.svg b/assets/images/letsexchange_icon.svg new file mode 100644 index 000000000..104b43a6b --- /dev/null +++ b/assets/images/letsexchange_icon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png index 73101354a..c465fa26c 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png index 9ceee3c5e..e79db4d99 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png index ef46cd805..2d030845b 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png index 6547a1b1b..eef607c2c 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png index e436872e8..a09896830 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png index 157b00493..a98c5faf8 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png differ diff --git a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png index a46ed4535..d8141a250 100644 Binary files a/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png and b/assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png differ diff --git a/assets/images/meld_logo.svg b/assets/images/meld_logo.svg new file mode 100644 index 000000000..1d9211d64 --- /dev/null +++ b/assets/images/meld_logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/menu.svg b/assets/images/menu.svg new file mode 100644 index 000000000..0a4cc3784 --- /dev/null +++ b/assets/images/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml index 00d924171..c8bd4b26c 100644 --- a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png new file mode 100644 index 000000000..af8126ea9 Binary files /dev/null and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png new file mode 100644 index 000000000..0778b841f Binary files /dev/null and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png new file mode 100644 index 000000000..318914c59 Binary files /dev/null and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png new file mode 100644 index 000000000..b1165abe8 Binary files /dev/null and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png new file mode 100644 index 000000000..ea11a01d3 Binary files /dev/null and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png differ diff --git a/assets/images/nanogpt.png b/assets/images/nanogpt.png new file mode 100644 index 000000000..958400452 Binary files /dev/null and b/assets/images/nanogpt.png differ diff --git a/assets/images/notif.svg b/assets/images/notif.svg new file mode 100644 index 000000000..b1ff5b4fa --- /dev/null +++ b/assets/images/notif.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/notification_icon.svg b/assets/images/notification_icon.svg index 099039e67..360d0b4e6 100644 --- a/assets/images/notification_icon.svg +++ b/assets/images/notification_icon.svg @@ -1,69 +1,3 @@ - - - -image/svg+xml \ No newline at end of file + + + diff --git a/assets/images/passphrase_dark.png b/assets/images/passphrase_dark.png new file mode 100644 index 000000000..f72d1e1a1 Binary files /dev/null and b/assets/images/passphrase_dark.png differ diff --git a/assets/images/passphrase_key.svg b/assets/images/passphrase_key.svg new file mode 100644 index 000000000..c577dc30a --- /dev/null +++ b/assets/images/passphrase_key.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/passphrase_light.png b/assets/images/passphrase_light.png new file mode 100644 index 000000000..f86f68156 Binary files /dev/null and b/assets/images/passphrase_light.png differ diff --git a/assets/images/payjoin.png b/assets/images/payjoin.png new file mode 100644 index 000000000..1ba3dccdb Binary files /dev/null and b/assets/images/payjoin.png differ diff --git a/assets/images/qr-cake.png b/assets/images/qr-cake.png new file mode 100644 index 000000000..7c54dedb0 Binary files /dev/null and b/assets/images/qr-cake.png differ diff --git a/assets/images/receive.png b/assets/images/receive.png new file mode 100644 index 000000000..180a4e5b3 Binary files /dev/null and b/assets/images/receive.png differ diff --git a/assets/images/revolut.png b/assets/images/revolut.png new file mode 100644 index 000000000..bbe342592 Binary files /dev/null and b/assets/images/revolut.png differ diff --git a/assets/images/seed_verified.png b/assets/images/seed_verified.png new file mode 100644 index 000000000..e03706193 Binary files /dev/null and b/assets/images/seed_verified.png differ diff --git a/assets/images/seed_verified_dark.png b/assets/images/seed_verified_dark.png new file mode 100644 index 000000000..cbefa5d8c Binary files /dev/null and b/assets/images/seed_verified_dark.png differ diff --git a/assets/images/seed_verified_light.png b/assets/images/seed_verified_light.png new file mode 100644 index 000000000..a51eddcd7 Binary files /dev/null and b/assets/images/seed_verified_light.png differ diff --git a/assets/images/seed_warning_dark.svg b/assets/images/seed_warning_dark.svg new file mode 100644 index 000000000..0a47254ff --- /dev/null +++ b/assets/images/seed_warning_dark.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/seed_warning_light.svg b/assets/images/seed_warning_light.svg new file mode 100644 index 000000000..cab27ec31 --- /dev/null +++ b/assets/images/seed_warning_light.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/send.png b/assets/images/send.png deleted file mode 100644 index aef504999..000000000 Binary files a/assets/images/send.png and /dev/null differ diff --git a/assets/images/send2.png b/assets/images/send2.png new file mode 100644 index 000000000..85fc570a8 Binary files /dev/null and b/assets/images/send2.png differ diff --git a/assets/images/skrill.svg b/assets/images/skrill.svg new file mode 100644 index 000000000..b264b57eb --- /dev/null +++ b/assets/images/skrill.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/swap.png b/assets/images/swap.png new file mode 100644 index 000000000..fe3fc0893 Binary files /dev/null and b/assets/images/swap.png differ diff --git a/assets/images/quantex.png b/assets/images/swap_trade.png similarity index 100% rename from assets/images/quantex.png rename to assets/images/swap_trade.png diff --git a/assets/images/ton_icon.png b/assets/images/ton_icon.png new file mode 100644 index 000000000..90f9968cc Binary files /dev/null and b/assets/images/ton_icon.png differ diff --git a/assets/images/tor_logo.svg b/assets/images/tor_logo.svg new file mode 100644 index 000000000..ebd00324d --- /dev/null +++ b/assets/images/tor_logo.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/usd_round_dark.svg b/assets/images/usd_round_dark.svg new file mode 100644 index 000000000..f329dd617 --- /dev/null +++ b/assets/images/usd_round_dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/usd_round_light.svg b/assets/images/usd_round_light.svg new file mode 100644 index 000000000..f5965c597 --- /dev/null +++ b/assets/images/usd_round_light.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/images/usdtbsc_icon.png b/assets/images/usdtbsc_icon.png new file mode 100644 index 000000000..9f2cda237 Binary files /dev/null and b/assets/images/usdtbsc_icon.png differ diff --git a/assets/images/wallet_group_confirmed_dark.png b/assets/images/wallet_group_confirmed_dark.png new file mode 100644 index 000000000..a047cb29c Binary files /dev/null and b/assets/images/wallet_group_confirmed_dark.png differ diff --git a/assets/images/wallet_group_confirmed_light.png b/assets/images/wallet_group_confirmed_light.png new file mode 100644 index 000000000..851d32300 Binary files /dev/null and b/assets/images/wallet_group_confirmed_light.png differ diff --git a/assets/images/wallet_group_empty_dark.png b/assets/images/wallet_group_empty_dark.png new file mode 100644 index 000000000..e613d876e Binary files /dev/null and b/assets/images/wallet_group_empty_dark.png differ diff --git a/assets/images/wallet_group_empty_light.png b/assets/images/wallet_group_empty_light.png new file mode 100644 index 000000000..f795648ae Binary files /dev/null and b/assets/images/wallet_group_empty_light.png differ diff --git a/assets/images/wallet_group_options_dark.png b/assets/images/wallet_group_options_dark.png new file mode 100644 index 000000000..479aac57c Binary files /dev/null and b/assets/images/wallet_group_options_dark.png differ diff --git a/assets/images/wallet_group_options_light.png b/assets/images/wallet_group_options_light.png new file mode 100644 index 000000000..308930520 Binary files /dev/null and b/assets/images/wallet_group_options_light.png differ diff --git a/assets/images/wallet_name.png b/assets/images/wallet_name.png deleted file mode 100644 index f586682bd..000000000 Binary files a/assets/images/wallet_name.png and /dev/null differ diff --git a/assets/images/wallet_name_light.png b/assets/images/wallet_name_light.png deleted file mode 100644 index 0199c1b30..000000000 Binary files a/assets/images/wallet_name_light.png and /dev/null differ diff --git a/assets/images/wallet_new.png b/assets/images/wallet_new.png new file mode 100644 index 000000000..47c43bfca Binary files /dev/null and b/assets/images/wallet_new.png differ diff --git a/assets/images/wallet_type.png b/assets/images/wallet_type.png deleted file mode 100644 index 4e0eba8b5..000000000 Binary files a/assets/images/wallet_type.png and /dev/null differ diff --git a/assets/images/wallet_type_light.png b/assets/images/wallet_type_light.png deleted file mode 100644 index e36c0d3aa..000000000 Binary files a/assets/images/wallet_type_light.png and /dev/null differ diff --git a/assets/images/wallet_type_wallet_dark.png b/assets/images/wallet_type_wallet_dark.png new file mode 100644 index 000000000..b840f5547 Binary files /dev/null and b/assets/images/wallet_type_wallet_dark.png differ diff --git a/assets/images/wallet_type_wallet_light.png b/assets/images/wallet_type_wallet_light.png new file mode 100644 index 000000000..ee759a109 Binary files /dev/null and b/assets/images/wallet_type_wallet_light.png differ diff --git a/assets/images/wallets.png b/assets/images/wallets.png new file mode 100644 index 000000000..62ea20039 Binary files /dev/null and b/assets/images/wallets.png differ diff --git a/assets/images/welcome.png b/assets/images/welcome.png deleted file mode 100644 index f1132d253..000000000 Binary files a/assets/images/welcome.png and /dev/null differ diff --git a/assets/images/welcome_dark_theme.svg b/assets/images/welcome_dark_theme.svg new file mode 100644 index 000000000..8f60c931a --- /dev/null +++ b/assets/images/welcome_dark_theme.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/welcome_light.png b/assets/images/welcome_light.png deleted file mode 100644 index 6feff85d1..000000000 Binary files a/assets/images/welcome_light.png and /dev/null differ diff --git a/assets/images/welcome_light_theme.svg b/assets/images/welcome_light_theme.svg new file mode 100644 index 000000000..178b2853d --- /dev/null +++ b/assets/images/welcome_light_theme.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/welcome_wallet_dark.png b/assets/images/welcome_wallet_dark.png new file mode 100644 index 000000000..771a600d3 Binary files /dev/null and b/assets/images/welcome_wallet_dark.png differ diff --git a/assets/images/welcome_wallet_light.png b/assets/images/welcome_wallet_light.png new file mode 100644 index 000000000..2a738be0b Binary files /dev/null and b/assets/images/welcome_wallet_light.png differ diff --git a/assets/images/wyre-icon.png b/assets/images/wyre-icon.png deleted file mode 100644 index a2810948e..000000000 Binary files a/assets/images/wyre-icon.png and /dev/null differ diff --git a/assets/images/wyre.png b/assets/images/wyre.png deleted file mode 100644 index a16bbdc8b..000000000 Binary files a/assets/images/wyre.png and /dev/null differ diff --git a/assets/images/xoswap.svg b/assets/images/xoswap.svg new file mode 100644 index 000000000..ef83c58b1 --- /dev/null +++ b/assets/images/xoswap.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/yat_crypto.png b/assets/images/yat_crypto.png deleted file mode 100644 index fbd5d2483..000000000 Binary files a/assets/images/yat_crypto.png and /dev/null differ diff --git a/assets/images/zano_icon.png b/assets/images/zano_icon.png new file mode 100644 index 000000000..fd48fd6a5 Binary files /dev/null and b/assets/images/zano_icon.png differ diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml index be550177e..cda931b5e 100644 --- a/assets/nano_node_list.yml +++ b/assets/nano_node_list.yml @@ -1,9 +1,9 @@ - uri: nano.nownodes.io useSSL: true - is_default: true - uri: rpc.nano.to + is_default: true useSSL: true - uri: node.nautilus.io diff --git a/assets/node_list.yml b/assets/node_list.yml index d04a9a2e8..917dadfd9 100644 --- a/assets/node_list.yml +++ b/assets/node_list.yml @@ -1,6 +1,8 @@ - uri: xmr-node.cakewallet.com:18081 is_default: true + trusted: true + useSSL: true - uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081 is_default: false @@ -11,9 +13,3 @@ - uri: nodes.hashvault.pro:18081 is_default: false -- - uri: node.c3pool.com:18081 - is_default: false -- - uri: node.community.rino.io:18081 - is_default: false diff --git a/assets/polygon_node_list.yml b/assets/polygon_node_list.yml index 34504269d..3b2cdcdc3 100644 --- a/assets/polygon_node_list.yml +++ b/assets/polygon_node_list.yml @@ -1,6 +1,10 @@ - uri: polygon-rpc.com - - uri: polygon-bor.publicnode.com + uri: polygon-bor-rpc.publicnode.com + useSSL: true + isDefault: true - - uri: polygon.llamarpc.com \ No newline at end of file + uri: polygon.llamarpc.com +- + uri: matic.nownodes.io \ No newline at end of file diff --git a/assets/solana_node_list.yml b/assets/solana_node_list.yml index e3ff9138e..3ba74d980 100644 --- a/assets/solana_node_list.yml +++ b/assets/solana_node_list.yml @@ -1,7 +1,13 @@ - uri: rpc.ankr.com - is_default: true useSSL: true - uri: api.mainnet-beta.solana.com:443 - useSSL: true \ No newline at end of file + useSSL: true +- + uri: solana-rpc.publicnode.com:443 + useSSL: true +- + uri: solana-mainnet.core.chainstack.com + useSSL: true + is_default: true \ No newline at end of file diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index fec1485ac..faf57258a 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,2 +1,4 @@ -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Add built-in Tor support (experimental) +Ledger improvements +UI/UX improvements +Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 4e8a79fc1..c49b895e3 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,9 @@ -Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page) -Improve fee calculation for Bitcoin to protect against overpaying or underpaying -Enhance auto-address generation for Monero -Bug fixes and enhancements \ No newline at end of file +Add built-in Tor support (experimental) +Add dEuro investments +Solana fixes/enhancements +Polygon fixes/enhancements +WalletConnect improvements +Ledger improvements +Payjoin improvements +UI/UX improvements +Bug fixes \ No newline at end of file diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index f9fd91179..1e34de712 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -4,9 +4,8 @@ useSSL: true - uri: api.trongrid.io - is_default: false + is_default: true useSSL: true - uri: trx.nownodes.io - is_default: true useSSL: true \ No newline at end of file diff --git a/assets/zano_node_list.yml b/assets/zano_node_list.yml new file mode 100644 index 000000000..f7b874fcb --- /dev/null +++ b/assets/zano_node_list.yml @@ -0,0 +1,7 @@ +- + uri: 37.27.100.59:10500 + useSSL: false +- + uri: zano.cakewallet.com:11211 + is_default: true + useSSL: false \ No newline at end of file diff --git a/build-guide-linux.md b/build-guide-linux.md deleted file mode 100644 index e0158945b..000000000 --- a/build-guide-linux.md +++ /dev/null @@ -1,176 +0,0 @@ -# Building CakeWallet for Linux - -## Requirements and Setup - -The following are the system requirements to build CakeWallet for your Linux device. - -``` -Ubuntu >= 16.04 -Flutter 3.10.x -``` - -## Building CakeWallet on Linux - -These steps will help you configure and execute a build of CakeWallet from its source code. - -### 1. Installing Package Dependencies - -CakeWallet requires some packages to be install on your build system. You may easily install them on your build system with the following command: - -`$ sudo apt install build-essential cmake pkg-config git curl autoconf libtool` - -> [!WARNING] -> -> ### Check gcc version -> -> It is needed to use gcc 10 or 9 to successfully link dependencies with flutter.\ -> To check what gcc version you are using: -> -> ```bash -> $ gcc --version -> $ g++ --version -> ``` -> -> If you are using gcc version newer than 10, then you need to downgrade to version 10.4.0: -> -> ```bash -> $ sudo apt install gcc-10 g++-10 -> $ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10 -> $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10 -> ``` - -> [!NOTE] -> -> Alternatively, you can use the [nix-shell](https://nixos.org/) with the `gcc10.nix` file\ -> present on `scripts/linux` like so: -> ```bash -> $ nix-shell gcc10.nix -> ``` -> This will get you in a nix environment with all the required dependencies that you can use to build the software from,\ -> and it works in any linux distro. - -### 2. Installing Flutter - -Need to install flutter. For this please check section [How to install flutter on Linux](https://docs.flutter.dev/get-started/install/linux). - -### 3. Verify Installations - -Verify that the Flutter have been correctly installed on your system with the following command: - -`$ flutter doctor` - -The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. - -``` -Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) -``` - -### 4. Acquiring the CakeWallet Source Code - -Download CakeWallet source code - -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch linux/password-direct-input` - -Proceed into the source code before proceeding with the next steps: - -`$ cd cake_wallet/scripts/linux/` - -To configure some project properties run: - -`$ ./cakewallet.sh` - -Build the Monero libraries and their dependencies: - -`$ ./build_all.sh` - -Now the dependencies need to be copied into the CakeWallet project with this command: - -`$ ./setup.sh` - -It is now time to change back to the base directory of the CakeWallet source code: - -`$ cd ../../` - -Install Flutter package dependencies with this command: - -`$ flutter pub get` - -> #### If you will get an error like: -> -> ``` -> The plugin `cw_shared_external` requires your app to be migrated to the Android embedding v2. Follow the steps on the migration doc above and re-run -> this command. -> ``` -> -> Then need to config Android project settings. For this open `scripts/android` (`$ cd scripts/android`) directory and run followed commands: -> -> ``` -> $ source ./app_env.sh cakewallet -> $ ./app_config.sh -> $ cd ../.. -> ``` -> -> Then re-configure Linux project again. For this open `scripts/linux` (`$cd scripts/linux`) directory and run: -> `$ ./cakewallet.sh` -> and back to project root directory: -> `$ cd ../..` -> and fetch dependecies again -> `$ flutter pub get` - -Your CakeWallet binary will be built with some specific keys for iterate with 3rd party services. You may generate these secret keys placeholders with the following command: - -`$ flutter packages pub run tool/generate_new_secrets.dart` - -We will generate mobx models for the project. - -`$ ./model_generator.sh` - -Then we need to generate localization files. - -`$ flutter packages pub run tool/generate_localization.dart` - -### 5. Build! - -`$ flutter build linux --release` - -Path to executable file will be: - -`build/linux/x64/release/bundle/cake_wallet` - -> ### Troubleshooting -> -> If you got an error while building the application with `$ flutter build linux --release` command, add `-v` argument to the command (`$ flutter build linux -v --release`) to get details.\ -> If you got in flutter build logs: undefined reference to `hid_free_enumeration`, or another error with undefined reference to `hid_*`, then rebuild monero lib without hidapi lib. Check does exists `libhidapi-dev` in your scope and remove it from your scope for build without it. - -# Flatpak - -For package the built application into flatpak you need fistly to install `flatpak` and `flatpak-builder`: - -`$ sudo apt install flatpak flatpak-builder` - -Then need to [add flathub](https://flatpak.org/setup/Ubuntu) (or just `$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`). Then need to install freedesktop runtime and sdk: - -`$ flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08` - -To build with using of `flatpak-build` directory run next: - -`$ flatpak-builder --force-clean flatpak-build com.cakewallet.CakeWallet.yml` - -And then export bundle: - -`$ flatpak build-export export flatpak-build` - -`$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet` - -Result file: `cake_wallet.flatpak` should be generated in current directory. - -For install generated flatpak file use: - -`$ flatpak --user install cake_wallet.flatpak` - -For run the installed application run: - -`$ flatpak run com.cakewallet.CakeWallet` - -Copyright (c) 2023 Cake Technologies LLC. diff --git a/com.cakewallet.CakeWallet.yml b/com.cakewallet.CakeWallet.yml index 83efa1388..6a19c3dda 100644 --- a/com.cakewallet.CakeWallet.yml +++ b/com.cakewallet.CakeWallet.yml @@ -1,6 +1,6 @@ app-id: com.cakewallet.CakeWallet runtime: org.freedesktop.Platform -runtime-version: '22.08' +runtime-version: '24.08' sdk: org.freedesktop.Sdk command: cake_wallet separate-locales: false @@ -15,8 +15,6 @@ finish-args: modules: - name: cake_wallet buildsystem: simple - only-arches: - - x86_64 build-commands: - "cp -R bundle /app/cake_wallet" - "chmod +x /app/cake_wallet/cake_wallet" diff --git a/configure_cake_wallet.sh b/configure_cake_wallet.sh index 90ce1c446..a083ec7ff 100755 --- a/configure_cake_wallet.sh +++ b/configure_cake_wallet.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -x -e IOS="ios" ANDROID="android" MACOS="macos" @@ -36,6 +36,6 @@ fi source ./app_env.sh cakewallet ./app_config.sh cd ../.. && flutter pub get -flutter packages pub run tool/generate_localization.dart -./model_generator.sh +dart run tool/generate_localization.dart +#./model_generator.sh #cd macos && pod install diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index 73bc101c4..0d985b237 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -2,22 +2,37 @@ import 'package:bitcoin_base/bitcoin_base.dart'; String addressFromOutputScript(Script script, BasedUtxoNetwork network) { try { - switch (script.getAddressType()) { - case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wpkh: - return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkhInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wsh: - return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2tr: - return P2trAddress.fromScriptPubkey(script: script).toAddress(network); - default: - } + return addressFromScript(script, network).toAddress(network); } catch (_) {} return ''; } + +BitcoinBaseAddress addressFromScript(Script script, + [BasedUtxoNetwork network = BitcoinNetwork.mainnet]) { + final addressType = script.getAddressType(); + if (addressType == null) { + throw ArgumentError("Invalid script"); + } + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); + case P2shAddressType.p2pkhInP2sh: + case P2shAddressType.p2pkInP2sh: + return P2shAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); + case SegwitAddresType.p2tr: + return P2trAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); + } + + throw ArgumentError("Invalid script"); +} diff --git a/cw_bitcoin/lib/address_to_output_script.dart b/cw_bitcoin/lib/address_to_output_script.dart deleted file mode 100644 index 892f7a0d6..000000000 --- a/cw_bitcoin/lib/address_to_output_script.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin; - -List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { - try { - if (network == bitcoin.BitcoinCashNetwork.mainnet) { - return bitcoin.BitcoinCashAddress(address).baseAddress.toScriptPubKey().toBytes(); - } - return bitcoin.addressToOutputScript(address: address, network: network); - } catch (err) { - print(err); - return Uint8List(0); - } -} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index bf36e6fb9..1509f913a 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:mobx/mobx.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/script_hash.dart' as sh; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( @@ -17,7 +17,7 @@ abstract class BaseBitcoinAddressRecord { }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed; + _isUsed = Observable(isUsed); @override bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; @@ -28,7 +28,7 @@ abstract class BaseBitcoinAddressRecord { int _txCount; int _balance; String _name; - bool _isUsed; + final Observable _isUsed; BasedUtxoNetwork? network; int get txCount => _txCount; @@ -41,9 +41,9 @@ abstract class BaseBitcoinAddressRecord { set balance(int value) => _balance = value; - bool get isUsed => _isUsed; + bool get isUsed => _isUsed.value; - void setAsUsed() => _isUsed = true; + void setAsUsed() => _isUsed.value = true; void setNewName(String label) => _name = label; int get hashCode => address.hashCode; @@ -65,8 +65,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { required super.type, String? scriptHash, required super.network, - }) : scriptHash = - scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); + }) : scriptHash = scriptHash ?? + (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; @@ -92,7 +92,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String getScriptHash(BasedUtxoNetwork network) { if (scriptHash != null) return scriptHash!; - scriptHash = sh.scriptHash(address, network: network); + scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); return scriptHash!; } diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index de339175d..c8715b239 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -5,30 +5,32 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:cw_core/utils/print_verbose.dart'; class BitcoinHardwareWalletService { - BitcoinHardwareWalletService(this.ledger, this.device); + BitcoinHardwareWalletService(this.ledgerConnection); - final Ledger ledger; - final LedgerDevice device; + final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { - final bitcoinLedgerApp = BitcoinLedgerApp(ledger); + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); - final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device); - print(masterFp); + final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); final accounts = []; final indexRange = List.generate(limit, (i) => i + index); for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); + final xpub = + await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); Bip32Slip10Secp256k1 hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8e72bc7b0..07083e111 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -30,7 +30,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const allLitecoin = [ BitcoinReceivePageOption.p2wpkh, - BitcoinReceivePageOption.mweb + BitcoinReceivePageOption.mweb, ]; BitcoinAddressType toType() { diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index bda7c39ae..7d6894e14 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,11 +1,19 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.outputs, - {required this.priority, this.feeRate}); + BitcoinTransactionCredentials( + this.outputs, { + required this.priority, + this.feeRate, + this.coinTypeToSpendFrom = UnspentCoinType.any, + this.payjoinUri, + }); final List outputs; final BitcoinTransactionPriority? priority; final int? feeRate; + final UnspentCoinType coinTypeToSpendFrom; + final String? payjoinUri; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 30f04667a..9231022f6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -3,23 +3,34 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_core/encryption_file_utils.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/psbt_transaction_builder.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; +import 'package:cw_bitcoin/payjoin/storage.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/psbt/signer.dart'; +import 'package:cw_bitcoin/psbt/transaction_builder.dart'; +import 'package:cw_bitcoin/psbt/v0_deserialize.dart'; +import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/payjoin_session.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; -import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:ledger_bitcoin/psbt.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; part 'bitcoin_wallet.g.dart'; @@ -31,6 +42,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, Uint8List? seedBytes, String? mnemonic, @@ -61,8 +73,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: - networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, + currency: networkParam == BitcoinNetwork.testnet + ? CryptoCurrency.tbtc + : CryptoCurrency.btc, alwaysScan: alwaysScan, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) @@ -70,29 +83,37 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String derivationPath = walletInfo.derivationInfo!.derivationPath!; // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); - walletAddresses = BitcoinWalletAddresses( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), - network: networkParam ?? network, - masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - ); + + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); + walletAddresses = BitcoinWalletAddresses(walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + mainHd: hd, + sideHd: accountHD.childKey(Bip32KeyIndex(1)), + network: networkParam ?? network, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, + payjoinManager: payjoinManager); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = + this.isEnabledAutoGenerateSubaddress; }); } + @override + bool get hasRescan => true; + static Future create({ required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, @@ -115,9 +136,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -134,6 +157,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, + payjoinBox: payjoinBox, ); } @@ -141,6 +165,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String name, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required String password, required EncryptionFileUtils encryptionFileUtils, required bool alwaysScan, @@ -185,8 +210,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= + snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= + snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; final mnemonic = keysData.mnemonic; @@ -195,7 +222,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -208,35 +236,81 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } return BitcoinWallet( - mnemonic: mnemonic, - xpub: keysData.xPub, - password: password, - passphrase: passphrase, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses, - initialSilentAddresses: snp?.silentAddresses, - initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, - initialBalance: snp?.balance, - encryptionFileUtils: encryptionFileUtils, - seedBytes: seedBytes, - initialRegularAddressIndex: snp?.regularAddressIndex, - initialChangeAddressIndex: snp?.changeAddressIndex, - addressPageType: snp?.addressPageType, - networkParam: network, - alwaysScan: alwaysScan, - ); + mnemonic: mnemonic, + xpub: keysData.xPub, + password: password, + passphrase: passphrase, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp?.addresses, + initialSilentAddresses: snp?.silentAddresses, + initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, + initialBalance: snp?.balance, + encryptionFileUtils: encryptionFileUtils, + seedBytes: seedBytes, + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, + networkParam: network, + alwaysScan: alwaysScan, + payjoinBox: payjoinBox); } - Ledger? _ledger; - LedgerDevice? _ledgerDevice; + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; - void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { - _ledger = setLedger; - _ledgerDevice = setLedgerDevice; - _bitcoinLedgerApp = - BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _bitcoinLedgerApp = BitcoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @override + Future close({bool shouldCleanup = false}) async { + payjoinManager.cleanupSessions(); + super.close(shouldCleanup: shouldCleanup); + } + + late final PayjoinManager payjoinManager; + + bool get isPayjoinAvailable => unspentCoinsInfo.values + .where((element) => + element.walletId == id && element.isSending && !element.isFrozen) + .isNotEmpty; + + Future buildPsbt({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + required Uint8List masterFingerprint, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final psbtReadyInputs = []; + for (final utxo in utxos) { + final rawTx = + await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = + publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + psbtReadyInputs.add(PSBTReadyUtxoWithAddress( + utxo: utxo.utxo, + rawTx: rawTx, + ownerDetails: utxo.ownerDetails, + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + ownerMasterFingerprint: masterFingerprint, + ownerPublicKey: publicKeyAndDerivationPath.publicKey, + )); + } + + return PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) + .psbt; } @override @@ -251,42 +325,125 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!); + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); - final psbtReadyInputs = []; - for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final psbt = await buildPsbt( + outputs: outputs, + fee: fee, + network: network, + utxos: utxos, + publicKeys: publicKeys, + masterFingerprint: masterFingerprint, + memo: memo, + enableRBF: enableRBF, + inputOrdering: inputOrdering, + outputOrdering: outputOrdering, + ); - psbtReadyInputs.add(PSBTReadyUtxoWithAddress( - utxo: utxo.utxo, - rawTx: rawTx, - ownerDetails: utxo.ownerDetails, - ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, - ownerMasterFingerprint: masterFingerprint, - ownerPublicKey: publicKeyAndDerivationPath.publicKey, - )); - } - - final psbt = - PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); - - final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } + @override + Future createTransaction(Object credentials) async { + credentials = credentials as BitcoinTransactionCredentials; + + final tx = (await super.createTransaction(credentials)) + as PendingBitcoinTransaction; + + final payjoinUri = credentials.payjoinUri; + if (payjoinUri == null) return tx; + + final transaction = await buildPsbt( + utxos: tx.utxos, + outputs: tx.outputs + .map((e) => BitcoinOutput( + address: addressFromScript(e.scriptPubKey), + value: e.amount, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(), + fee: BigInt.from(tx.fee), + network: network, + memo: credentials.outputs.first.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + publicKeys: tx.publicKeys!, + masterFingerprint: Uint8List(0)); + + final originalPsbt = await signPsbt( + base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + + tx.commitOverride = () async { + final sender = await payjoinManager.initSender( + payjoinUri, originalPsbt, int.parse(tx.feeRate)); + payjoinManager.spawnNewSender( + sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); + }; + + return tx; + } + + List getUtxoWithPrivateKeys() => unspentCoins + .where((e) => (e.isSending && !e.isFrozen)) + .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this)) + .toList(); + + Future commitPsbt(String finalizedPsbt) { + final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); + + final btcTx = + BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + + return PendingBitcoinTransaction( + btcTx, + type, + electrumClient: electrumClient, + amount: 0, + fee: 0, + feeRate: "", + network: network, + hasChange: true, + ).commit(); + } + + Future signPsbt( + String preProcessedPsbt, List utxos) async { + final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); + + await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { + return utxo.utxo.isP2tr() + ? key.signTapRoot( + txDigest, + sighash: sighash, + tweak: utxo.utxo.isSilentPayment != true, + ) + : key.signInput(txDigest, sigHash: sighash); + }, (txId, vout) async { + final txHex = await electrumClient.getTransactionHex(hash: txId); + final output = BtcTransaction.fromRaw(txHex).outputs[vout]; + return TaprootAmountScriptPair(output.amount, output.scriptPubKey); + }); + + psbt.finalizeV0(); + return base64Encode(psbt.asPsbtV0()); + } + @override Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses + .firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; - final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = + accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!, + final signature = await _bitcoinLedgerApp!.signMessage( message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 697719894..d84d958be 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,9 +1,13 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; +import 'package:payjoin_flutter/receive.dart' as payjoin; part 'bitcoin_wallet_addresses.g.dart'; @@ -15,6 +19,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, + required this.payjoinManager, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, @@ -23,9 +29,19 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.masterHd, }) : super(walletInfo); + final PayjoinManager payjoinManager; + + payjoin.Receiver? currentPayjoinReceiver; + + @observable + String? payjoinEndpoint = null; + @override String getAddress( - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { + {required int index, + required Bip32Slip10Secp256k1 hd, + BitcoinAddressType? addressType, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) { if (addressType == P2pkhAddressType.p2pkh) return generateP2PKHAddress(hd: hd, index: index, network: network); @@ -40,4 +56,33 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return generateP2WPKHAddress(hd: hd, index: index, network: network); } + + @action + Future initPayjoin() async { + try { + await payjoinManager.initPayjoin(); + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + + payjoinManager.resumeSessions(); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } + } + + @action + Future newPayjoinReceiver() async { + try { + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + + payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 5c276390a..177d61e87 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -10,12 +10,15 @@ class BitcoinNewWalletCredentials extends WalletCredentials { DerivationType? derivationType, String? derivationPath, String? passphrase, + this.mnemonic, }) : super( name: name, walletInfo: walletInfo, password: password, passphrase: passphrase, ); + + final String? mnemonic; } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 9f67f7807..317b25bcd 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -5,6 +5,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_service.dart'; @@ -21,10 +22,12 @@ class BitcoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, + this.payjoinSessionSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final Box payjoinSessionSource; final bool alwaysScan; final bool isDirect; @@ -41,7 +44,7 @@ class BitcoinWalletService extends WalletService< case DerivationType.bip39: final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - mnemonic = await MnemonicBip39.generate(strength: strength); + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); break; case DerivationType.electrum: default: @@ -55,6 +58,7 @@ class BitcoinWalletService extends WalletService< passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -79,6 +83,7 @@ class BitcoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -92,6 +97,7 @@ class BitcoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -106,6 +112,15 @@ class BitcoinWalletService extends WalletService< final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); + + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); + + if (keysToDelete.isNotEmpty) { + await unspentCoinsInfoSource.deleteAll(keysToDelete); + } } @override @@ -117,6 +132,7 @@ class BitcoinWalletService extends WalletService< name: currentName, walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -138,7 +154,6 @@ class BitcoinWalletService extends WalletService< credentials.walletInfo?.network = network.value; credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; - final wallet = await BitcoinWallet( password: credentials.password!, xpub: credentials.hwAccountData.xpub, @@ -146,6 +161,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + payjoinBox: payjoinSessionSource, ); await wallet.save(); await wallet.init(); @@ -173,6 +189,7 @@ class BitcoinWalletService extends WalletService< mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index b3aa19bd7..2ddd30df6 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -4,7 +4,9 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_bitcoin/script_hash.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -42,12 +44,13 @@ class ElectrumClient { static const aliveTimerDuration = Duration(seconds: 4); bool get isConnected => _isConnected; - Socket? socket; + ProxySocket? socket; void Function(ConnectionStatus)? onConnectionStatusChange; int _id; final Map _tasks; Map get tasks => _tasks; final Map _errors; + ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; bool _isConnected; Timer? _aliveTimer; String unterminatedString; @@ -57,40 +60,46 @@ class ElectrumClient { Future connectToUri(Uri uri, {bool? useSSL}) async { this.uri = uri; - this.useSSL = useSSL; - await connect(host: uri.host, port: uri.port, useSSL: useSSL); + if (useSSL != null) { + this.useSSL = useSSL; + } + await connect(host: uri.host, port: uri.port); } - Future connect({required String host, required int port, bool? useSSL}) async { + Future connect({required String host, required int port}) async { _setConnectionStatus(ConnectionStatus.connecting); try { await socket?.close(); - socket = null; } catch (_) {} + socket = null; + final ssl = !(useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))); try { - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); + socket = await ProxyWrapper().getSocksSocket(ssl, host, port, connectionTimeout: connectionTimeout); + } catch (e) { + printV("connect: $e"); + if (e is HandshakeException) { + useSSL = !(useSSL ?? false); } - } catch (_) { - _setConnectionStatus(ConnectionStatus.failed); + + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } if (socket == null) { - _setConnectionStatus(ConnectionStatus.failed); + if (_connectionStatus != ConnectionStatus.connecting) { + _setConnectionStatus(ConnectionStatus.failed); + } + return; } - _setConnectionStatus(ConnectionStatus.connected); + // use ping to determine actual connection status since we could've just not timed out yet: + // _setConnectionStatus(ConnectionStatus.connected); socket!.listen( (Uint8List event) { try { @@ -103,23 +112,26 @@ class ElectrumClient { _parseResponse(message); } } catch (e) { - print(e.toString()); + printV("socket.listen: $e"); } }, onError: (Object error) { final errorMsg = error.toString(); - print(errorMsg); + printV(errorMsg); unterminatedString = ''; + socket = null; }, onDone: () { + printV("SOCKET CLOSED!!!!!"); unterminatedString = ''; try { - if (host == socket?.address.host) { - socket?.destroy(); + if (host == socket?.address.host || socket == null) { _setConnectionStatus(ConnectionStatus.disconnected); + socket?.destroy(); + socket = null; } - } catch(e) { - print(e.toString()); + } catch (e) { + printV("onDone: $e"); } }, cancelOnError: true, @@ -164,7 +176,7 @@ class ElectrumClient { unterminatedString = ''; } } catch (e) { - print(e.toString()); + printV("parse $e"); } } @@ -177,7 +189,7 @@ class ElectrumClient { try { await callWithTimeout(method: 'server.ping'); _setConnectionStatus(ConnectionStatus.connected); - } on RequestFailedTimeoutException catch (_) { + } catch (_) { _setConnectionStatus(ConnectionStatus.disconnected); } } @@ -217,40 +229,21 @@ class ElectrumClient { return []; }); - Future>> getListUnspentWithAddress( - String address, BasedUtxoNetwork network) => - call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, network: network)]).then((dynamic result) { - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - val['address'] = address; - return val; - } + Future>?> getListUnspent(String scriptHash) async { + final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]); - return {}; - }).toList(); + if (result is List) { + return result.map((dynamic val) { + if (val is Map) { + return val; } - return []; - }); + return {}; + }).toList(); + } - Future>> 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) { - return val; - } - - return {}; - }).toList(); - } - - return []; - }); + return null; + } Future>> getMempool(String scriptHash) => call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash]) @@ -272,16 +265,12 @@ class ElectrumClient { try { final result = await callWithTimeout( method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000); - if (result is Map) { - return result; - } + return result; } on RequestFailedTimeoutException catch (_) { return {}; } catch (e) { - print("getTransaction: ${e.toString()}"); return {}; } - return {}; } Future> getTransactionVerbose({required String hash}) => @@ -326,9 +315,8 @@ class ElectrumClient { await call(method: 'blockchain.block.get_header', params: [height]) as Map; BehaviorSubject? tweaksSubscribe({required int height, required int count}) { - _id += 1; return subscribe( - id: 'blockchain.tweaks.subscribe:${height + count}', + id: 'blockchain.tweaks.subscribe', method: 'blockchain.tweaks.subscribe', params: [height, count, false], ); @@ -410,7 +398,7 @@ class ElectrumClient { } on RequestFailedTimeoutException catch (_) { return null; } catch (e) { - print("getCurrentBlockChainTip: ${e.toString()}"); + printV("getCurrentBlockChainTip: ${e.toString()}"); return null; } } @@ -441,7 +429,7 @@ class ElectrumClient { return subscription; } catch (e) { - print(e.toString()); + printV("subscribe $e"); return null; } } @@ -480,7 +468,8 @@ class ElectrumClient { return completer.future; } catch (e) { - print(e.toString()); + printV("callWithTimeout $e"); + rethrow; } } @@ -545,7 +534,14 @@ class ElectrumClient { void _setConnectionStatus(ConnectionStatus status) { onConnectionStatusChange?.call(status); + _connectionStatus = status; _isConnected = status == ConnectionStatus.connected; + if (!_isConnected) { + try { + socket?.destroy(); + } catch (_) {} + socket = null; + } } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 15d6843d8..aeb06f1f0 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -7,7 +7,14 @@ class ElectrumBalance extends Balance { required this.confirmed, required this.unconfirmed, required this.frozen, - }) : super(confirmed, unconfirmed); + this.secondConfirmed = 0, + this.secondUnconfirmed = 0, + }) : super( + confirmed, + unconfirmed, + secondAvailable: secondConfirmed, + secondAdditional: secondUnconfirmed, + ); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { @@ -17,17 +24,22 @@ class ElectrumBalance extends Balance { final decoded = json.decode(jsonSource) as Map; return ElectrumBalance( - confirmed: decoded['confirmed'] as int? ?? 0, - unconfirmed: decoded['unconfirmed'] as int? ?? 0, - frozen: decoded['frozen'] as int? ?? 0); + confirmed: decoded['confirmed'] as int? ?? 0, + unconfirmed: decoded['unconfirmed'] as int? ?? 0, + frozen: decoded['frozen'] as int? ?? 0, + secondConfirmed: decoded['secondConfirmed'] as int? ?? 0, + secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0, + ); } int confirmed; int unconfirmed; final int frozen; + int secondConfirmed = 0; + int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => bitcoinAmountToString(amount: ((confirmed + unconfirmed) - frozen) ); @override String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @@ -38,6 +50,21 @@ class ElectrumBalance extends Balance { return frozenFormatted == '0.0' ? '' : frozenFormatted; } - String toJSON() => - json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); + @override + String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + + @override + String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + + @override + String get formattedFullAvailableBalance => + bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen); + + String toJSON() => json.encode({ + 'confirmed': confirmed, + 'unconfirmed': unconfirmed, + 'frozen': frozen, + 'secondConfirmed': secondConfirmed, + 'secondUnconfirmed': secondUnconfirmed, + }); } diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 806f813dd..d096d0e7b 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -5,6 +5,7 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/utils/file.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; @@ -30,7 +31,10 @@ abstract class ElectrumTransactionHistoryBase String _password; int _height; - Future init() async => await _load(); + Future init() async { + clear(); + await _load(); + } @override void addOne(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction; @@ -51,7 +55,7 @@ abstract class ElectrumTransactionHistoryBase final data = json.encode({'height': _height, 'transactions': txjson}); await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e) { - print('Error while save bitcoin transaction history: ${e.toString()}'); + printV('Error while save bitcoin transaction history: ${e.toString()}'); } } @@ -76,14 +80,19 @@ abstract class ElectrumTransactionHistoryBase final val = entry.value; if (val is Map) { - final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); - _update(tx); + // removing transactions with invalid date + if (val['date'] == 1168650000) { + transactions.remove(entry.key); + } else { + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); + _update(tx); + } } }); _height = content['height'] as int; } catch (e) { - print(e); + printV(e); } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ebd90ff2b..7a8b3b951 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -23,20 +23,26 @@ class ElectrumTransactionBundle { class ElectrumTransactionInfo extends TransactionInfo { List? unspents; + bool isReceivedSilentPayment; - ElectrumTransactionInfo(this.type, - {required String id, - int? height, - required int amount, - int? fee, - List? inputAddresses, - List? outputAddresses, - required TransactionDirection direction, - required bool isPending, - required DateTime date, - required int confirmations, - String? to, - this.unspents}) { + ElectrumTransactionInfo( + this.type, { + required String id, + int? height, + required int amount, + int? fee, + List? inputAddresses, + List? outputAddresses, + required TransactionDirection direction, + required bool isPending, + bool isReplaced = false, + required DateTime date, + required int confirmations, + String? to, + this.unspents, + this.isReceivedSilentPayment = false, + Map? additionalInfo, + }) { this.id = id; this.height = height; this.amount = amount; @@ -46,8 +52,10 @@ class ElectrumTransactionInfo extends TransactionInfo { this.direction = direction; this.date = date; this.isPending = isPending; + this.isReplaced = isReplaced; this.confirmations = confirmations; this.to = to; + this.additionalInfo = additionalInfo ?? {}; } factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, @@ -94,6 +102,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: id, height: height, isPending: false, + isReplaced: false, fee: fee, direction: direction, amount: amount, @@ -169,6 +178,7 @@ class ElectrumTransactionInfo extends TransactionInfo { id: bundle.originalTransaction.txId(), height: height, isPending: bundle.confirmations == 0, + isReplaced: false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, fee: fee, @@ -192,6 +202,7 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, + isReplaced: data['isReplaced'] as bool? ?? false, confirmations: data['confirmations'] as int, inputAddresses: inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(), @@ -202,6 +213,8 @@ class ElectrumTransactionInfo extends TransactionInfo { .map((unspent) => BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), + isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, + additionalInfo: data['additionalInfo'] as Map?, ); } @@ -233,9 +246,11 @@ class ElectrumTransactionInfo extends TransactionInfo { direction: direction, date: date, isPending: isPending, + isReplaced: isReplaced ?? false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, - confirmations: info.confirmations); + confirmations: info.confirmations, + additionalInfo: additionalInfo); } Map toJson() { @@ -246,16 +261,19 @@ class ElectrumTransactionInfo extends TransactionInfo { m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; + m['isReplaced'] = isReplaced; m['confirmations'] = confirmations; m['fee'] = fee; m['to'] = to; m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; + m['isReceivedSilentPayment'] = isReceivedSilentPayment; + m['additionalInfo'] = additionalInfo; return m; } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7733562fb..bb9cea1bc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,14 +4,17 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_core/encryption_file_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; @@ -23,11 +26,11 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -39,21 +42,19 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -const int TWEAKS_COUNT = 25; - abstract class ElectrumWalletBase extends WalletBase with Store, WalletKeysFile { @@ -119,7 +120,8 @@ abstract class ElectrumWalletBase switch (currency) { case CryptoCurrency.btc: case CryptoCurrency.ltc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( + case CryptoCurrency.tbtc: + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)).derivePath( _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)) as Bip32Slip10Secp256k1; case CryptoCurrency.bch: @@ -129,7 +131,7 @@ abstract class ElectrumWalletBase } } - return Bip32Slip10Secp256k1.fromExtendedKey(xpub!); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => @@ -138,6 +140,15 @@ abstract class ElectrumWalletBase static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; + static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { + switch (network) { + case LitecoinNetwork.mainnet: + return Bip44Conf.litecoinMainNet.altKeyNetVer; + default: + return null; + } + } + bool? alwaysScan; final Bip32Slip10Secp256k1 accountHD; @@ -170,17 +181,20 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + Set get addressesSet => walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((addr) => addr.address) + .toSet(); List get scriptHashes => walletAddresses.addressesByReceiveType - .where((addr) => addressTypeFromStr(addr.address, network) is! MwebAddress) - .map((addr) => scriptHash(addr.address, network: network)) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) - .where((addr) => addressTypeFromStr(addr.address, network) is! MwebAddress) - .map((addr) => scriptHash(addr.address, network: network)) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => addr.getScriptHash(network)) .toList(); String get xpub => accountHD.publicKey.toExtended; @@ -221,7 +235,7 @@ abstract class ElectrumWalletBase silentPaymentsScanningActive = active; if (active) { - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); final tip = await getUpdatedChainTip(); @@ -231,7 +245,7 @@ abstract class ElectrumWalletBase } if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + _setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip); } } else { alwaysScan = false; @@ -246,23 +260,23 @@ abstract class ElectrumWalletBase } } - int? _currentChainTip; + int? currentChainTip; Future getCurrentChainTip() async { - if (_currentChainTip != null) { - return _currentChainTip!; + if ((currentChainTip ?? 0) > 0) { + return currentChainTip!; } - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; + currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; - return _currentChainTip!; + return currentChainTip!; } Future getUpdatedChainTip() async { final newTip = await electrumClient.getCurrentBlockChainTip(); - if (newTip != null && newTip > (_currentChainTip ?? 0)) { - _currentChainTip = newTip; + if (newTip != null && newTip > (currentChainTip ?? 0)) { + currentChainTip = newTip; } - return _currentChainTip ?? 0; + return currentChainTip ?? 0; } @override @@ -293,6 +307,7 @@ abstract class ElectrumWalletBase Future init() async { await walletAddresses.init(); await transactionHistory.init(); + await cleanUpDuplicateUnspentCoins(); await save(); _autoSaveTimer = @@ -300,12 +315,8 @@ abstract class ElectrumWalletBase } @action - Future _setListeners( - int height, { - int? chainTipParam, - bool? doSingleScan, - bool? usingSupportedNode, - }) async { + Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + if (this is! BitcoinWallet) return; final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -313,7 +324,7 @@ abstract class ElectrumWalletBase return; } - syncStatus = StartingScanSyncStatus(); + syncStatus = AttemptingScanSyncStatus(); if (_isolate != null) { final runningIsolate = await _isolate!; @@ -342,7 +353,7 @@ abstract class ElectrumWalletBase isSingleScan: doSingleScan ?? false, )); - _receiveStream?.cancel(); + await _receiveStream?.cancel(); _receiveStream = receivePort.listen((var message) async { if (message is Map) { for (final map in message.entries) { @@ -406,7 +417,13 @@ abstract class ElectrumWalletBase nodeSupportsSilentPayments = false; } - syncStatus = message.syncStatus; + if (message.syncStatus is SyncingSyncStatus) { + var status = message.syncStatus as SyncingSyncStatus; + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); + } else { + syncStatus = message.syncStatus; + } + await walletInfo.updateRestoreHeight(message.height); } }); @@ -462,29 +479,38 @@ abstract class ElectrumWalletBase if (alwaysScan == true) { _setListeners(walletInfo.restoreHeight); } else { + if (syncStatus is LostConnectionSyncStatus) return; syncStatus = SyncedSyncStatus(); } } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); + printV(stacktrace); + printV("startSync $e"); syncStatus = FailedSyncStatus(); } } @action Future updateFeeRates() async { - if (await checkIfMempoolAPIIsEnabled()) { + if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = - await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); - - final result = json.decode(response.body) as Map; - final slowFee = result['economyFee']?.toInt() ?? 0; - final mediumFee = result['hourFee']?.toInt() ?? 0; - final fastFee = result['fastestFee']?.toInt() ?? 0; + final response = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 15)); + final result = json.decode(response.body) as Map; + final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; + int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; + int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; + if (slowFee == mediumFee) { + mediumFee++; + } + while (fastFee <= mediumFee) { + fastFee++; + } _feeRates = [slowFee, mediumFee, fastFee]; return; - } catch (_) {} + } catch (e) { + printV(e); + } } final feeRates = await electrumClient.feeRates(network: network); @@ -553,6 +579,8 @@ abstract class ElectrumWalletBase Future connectToNode({required Node node}) async { this.node = node; + if (syncStatus is ConnectingSyncStatus) return; + try { syncStatus = ConnectingSyncStatus(); @@ -562,8 +590,9 @@ abstract class ElectrumWalletBase electrumClient.onConnectionStatusChange = _onConnectionStatusChange; await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + printV(stacktrace); + printV("connectToNode $e"); syncStatus = FailedSyncStatus(); } } @@ -574,9 +603,10 @@ abstract class ElectrumWalletBase UtxoDetails _createUTXOS({ required bool sendAll, - required int credentialsAmount, required bool paysToSilentPayment, + int credentialsAmount = 0, int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -587,9 +617,25 @@ abstract class ElectrumWalletBase bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final availableInputs = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + var availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); + // sort the unconfirmed coins so that mweb coins are last: + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1); + for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; @@ -604,7 +650,7 @@ abstract class ElectrumWalletBase allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; - final address = addressTypeFromStr(utx.address, network); + final address = RegexUtils.addressTypeFromStr(utx.address, network); ECPrivate? privkey; bool? isSilentPayment = false; @@ -692,13 +738,13 @@ abstract class ElectrumWalletBase List outputs, int feeRate, { String? memo, - int credentialsAmount = 0, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, - credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); int fee = await calcFee( @@ -722,23 +768,11 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); } - if (amount <= 0) { - throw BitcoinTransactionWrongBalanceException(); - } - // Attempting to send less than the dust limit if (_isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); } - if (credentialsAmount > 0) { - final amountLeftForFee = amount - credentialsAmount; - if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) { - amount -= amountLeftForFee; - fee += amountLeftForFee; - } - } - if (outputs.length == 1) { outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } @@ -760,17 +794,25 @@ abstract class ElectrumWalletBase Future estimateTxForAmount( int credentialsAmount, List outputs, + List updatedOutputs, int feeRate, { int? inputsCount, String? memo, bool? useUnconfirmed, bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { + // Attempting to send less than the dust limit + if (_isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; @@ -786,10 +828,12 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -797,111 +841,125 @@ abstract class ElectrumWalletBase } final changeAddress = await walletAddresses.getChangeAddress( - outputs: outputs, - utxoDetails: utxoDetails, + inputs: utxoDetails.availableInputs, + outputs: updatedOutputs, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); - final address = addressTypeFromStr(changeAddress, network); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, )); + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${changeAddress.isHidden ? "1" : "0"}" + "/${changeAddress.index}"; + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); int fee = await calcFee( utxos: utxoDetails.utxos, - outputs: outputs, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: temp, network: network, memo: memo, feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, ); + updatedOutputs.clear(); + updatedOutputs.addAll(temp); + if (fee == 0) { throw BitcoinTransactionNoFeeException(); } int amount = credentialsAmount; - final lastOutput = outputs.last; + final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - print(amountLeftForChangeAndFee); - - if (!_isBelowDust(amountLeftForChange)) { - // Here, lastOutput already is change, return the amount left without the fee to the user's address. - outputs[outputs.length - 1] = - BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange)); - } else { - // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change + if (_isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + updatedOutputs.removeLast(); outputs.removeLast(); - // Still has inputs to spend before failing - if (!spendingAllCoins) { - return estimateTxForAmount( - credentialsAmount, - outputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - ); + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } } - final estimatedSendAll = await estimateSendAllTx( - outputs, - feeRate, + // if the amount left for change is less than dust, but not less than 0 + // then add it to the fees + fee += amountLeftForChange; + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } else { + // Here, lastOutput already is change, return the amount left without the fee to the user's address. + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, ); - if (estimatedSendAll.amount == credentialsAmount) { - return estimatedSendAll; - } - - // Estimate to user how much is needed to send to cover the fee - final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1; - throw BitcoinTransactionNoDustOnChangeException( - bitcoinAmountToString(amount: maxAmountWithReturningChange), - bitcoinAmountToString(amount: estimatedSendAll.amount), + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } - - // Attempting to send less than the dust limit - if (_isBelowDust(amount)) { - throw BitcoinTransactionNoDustException(); - } - - final totalAmount = amount + fee; - - if (totalAmount > balance[currency]!.confirmed) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (totalAmount > utxoDetails.allInputsAmount) { - if (spendingAllCoins) { - throw BitcoinTransactionWrongBalanceException(); - } else { - outputs.removeLast(); - return estimateTxForAmount( - credentialsAmount, - outputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - hasSilentPayment: hasSilentPayment, - ); - } - } - - return EstimatedTxResult( - utxos: utxoDetails.utxos, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - publicKeys: utxoDetails.publicKeys, - fee: fee, - amount: amount, - hasChange: true, - isSendAll: false, - memo: memo, - spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, - ); } Future calcFee({ @@ -938,11 +996,15 @@ abstract class ElectrumWalletBase @override Future createTransaction(Object credentials) async { try { + // start by updating unspent coins + await updateAllUnspents(); + final outputs = []; final transactionCredentials = credentials as BitcoinTransactionCredentials; final hasMultiDestination = transactionCredentials.outputs.length > 1; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; bool hasSilentPayment = false; @@ -962,18 +1024,27 @@ abstract class ElectrumWalletBase credentialsAmount += outputAmount; - final address = - addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network); + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, network); + final isSilentPayment = address is SilentPaymentAddress; - if (address is SilentPaymentAddress) { + if (isSilentPayment) { hasSilentPayment = true; } if (sendAll) { // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent - outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(0), + isSilentPayment: isSilentPayment, + )); } else { - outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(outputAmount), + isSilentPayment: isSilentPayment, + )); } } @@ -982,28 +1053,39 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; + final updatedOutputs = outputs + .map((e) => BitcoinOutput( + address: e.address, + value: e.value, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(); + if (sendAll) { estimatedTx = await estimateSendAllTx( - outputs, + updatedOutputs, feeRateInt, memo: memo, - credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( credentialsAmount, outputs, + updatedOutputs, feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), network: network, @@ -1026,6 +1108,7 @@ abstract class ElectrumWalletBase )..addListener((transaction) async { transactionHistory.addOne(transaction); await updateBalance(); + await updateAllUnspents(); }); } @@ -1033,7 +1116,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -1043,7 +1126,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: updatedOutputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -1092,18 +1175,18 @@ abstract class ElectrumWalletBase } }); - return PendingBitcoinTransaction( - transaction, - type, - electrumClient: electrumClient, - amount: estimatedTx.amount, - fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), - network: network, - hasChange: estimatedTx.hasChange, - isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: hasTaprootInputs, - )..addListener((transaction) async { + return PendingBitcoinTransaction(transaction, type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys) + ..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { @@ -1113,15 +1196,19 @@ abstract class ElectrumWalletBase }); } - unspentCoins.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); await updateBalance(); + await updateAllUnspents(); }); } catch (e) { throw e; } } + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); + Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, @@ -1150,6 +1237,8 @@ abstract class ElectrumWalletBase 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), + 'alwaysScan': alwaysScan, }); int feeRate(TransactionPriority priority) { @@ -1261,18 +1350,13 @@ abstract class ElectrumWalletBase @action @override - Future rescan({ - required int height, - int? chainTip, - ScanData? scanData, - bool? doSingleScan, - }) async { + Future rescan({required int height, bool? doSingleScan}) async { silentPaymentsScanningActive = true; _setListeners(height, doSingleScan: doSingleScan); } @override - Future close() async { + Future close({bool shouldCleanup = false}) async { try { await _receiveStream?.cancel(); await electrumClient.close(); @@ -1285,6 +1369,10 @@ abstract class ElectrumWalletBase Future updateAllUnspents() async { List updatedUnspentCoins = []; + final previousUnspentCoins = List.from(unspentCoins.where((utxo) => + utxo.bitcoinAddressRecord.type != SegwitAddresType.mweb && + utxo.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)); + if (hasSilentPaymentsScanning) { // Update unspents stored from scanned silent payment transactions transactionHistory.transactions.values.forEach((tx) { @@ -1294,21 +1382,77 @@ abstract class ElectrumWalletBase }); } - await Future.wait(walletAddresses.allAddresses.map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating + walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .forEach((addr) { + if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + }); - unspentCoins = updatedUnspentCoins; + final addressFutures = walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((address) => fetchUnspent(address)) + .toList(); - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + final results = await Future.wait(addressFutures); + final failedCount = results.where((result) => result == null).length; + + if (failedCount == 0) { + for (final result in results) { + updatedUnspentCoins.addAll(result!); + } + unspentCoins = updatedUnspentCoins; + } else { + unspentCoins = handleFailedUtxoFetch( + failedCount: failedCount, + previousUnspentCoins: previousUnspentCoins, + updatedUnspentCoins: updatedUnspentCoins, + results: results, + ); + } + + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((element) => element.walletId == id); + + if (currentWalletUnspentCoins.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; } await updateCoins(unspentCoins); await _refreshUnspentCoinsInfo(); } + List handleFailedUtxoFetch({ + required int failedCount, + required List previousUnspentCoins, + required List updatedUnspentCoins, + required List?> results, + }) { + if (failedCount == results.length) { + printV("All UTXOs failed to fetch, falling back to previous UTXOs"); + return previousUnspentCoins; + } + + final successfulUtxos = []; + for (final result in results) { + if (result != null) { + successfulUtxos.addAll(result); + } + } + + if (failedCount > 0 && successfulUtxos.isEmpty) { + printV("Some UTXOs failed, but no successful UTXOs, falling back to previous UTXOs"); + return previousUnspentCoins; + } + + if (failedCount > 0) { + printV("Some UTXOs failed, updating with successful UTXOs"); + updatedUnspentCoins.addAll(successfulUtxos); + } + + return updatedUnspentCoins; + } + Future updateCoins(List newUnspentCoins) async { if (newUnspentCoins.isEmpty) { return; @@ -1328,6 +1472,7 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) coin.bitcoinAddressRecord.balance += coinInfo.value; } else { @@ -1339,21 +1484,17 @@ abstract class ElectrumWalletBase @action Future updateUnspentsForAddress(BitcoinAddressRecord address) async { final newUnspentCoins = await fetchUnspent(address); - await updateCoins(newUnspentCoins); + await updateCoins(newUnspentCoins ?? []); } @action - Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; + Future?> fetchUnspent(BitcoinAddressRecord address) async { List updatedUnspentCoins = []; - try { - unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); - } catch (e, s) { - print(e); - print(s); - return []; - } + final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + + // Failed to fetch unspents + if (unspents == null) return null; await Future.wait(unspents.map((unspent) async { try { @@ -1371,55 +1512,80 @@ abstract class ElectrumWalletBase @action Future addCoinInfo(BitcoinUnspent coin) async { - final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.bitcoinAddressRecord.address, - value: coin.value, - vout: coin.vout, - isChange: coin.isChange, - isSilentPayment: coin is BitcoinSilentPaymentsUnspent, - ); + // Check if the coin is already in the unspentCoinsInfo for the wallet + final existingCoinInfo = unspentCoinsInfo.values + .firstWhereOrNull((element) => element.walletId == walletInfo.id && element == coin); - await unspentCoinsInfo.add(newInfo); + if (existingCoinInfo == null) { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.bitcoinAddressRecord.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + isSilentPayment: coin is BitcoinSilentPaymentsUnspent, + ); + + await unspentCoinsInfo.add(newInfo); + } } Future _refreshUnspentCoinsInfo() async { try { - final List keys = []; + final List 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) { await unspentCoinsInfo.deleteAll(keys); } } catch (e) { - print(e.toString()); + printV("refreshUnspentCoinsInfo $e"); } } - Future canReplaceByFee(ElectrumTransactionInfo tx) async { + Future cleanUpDuplicateUnspentCoins() async { + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((element) => element.walletId == id); + final Map uniqueUnspentCoins = {}; + final List duplicateKeys = []; + + for (final unspentCoin in currentWalletUnspentCoins) { + final key = '${unspentCoin.hash}:${unspentCoin.vout}'; + if (!uniqueUnspentCoins.containsKey(key)) { + uniqueUnspentCoins[key] = unspentCoin; + } else { + duplicateKeys.add(unspentCoin.key); + } + } + + if (duplicateKeys.isNotEmpty) await unspentCoinsInfo.deleteAll(duplicateKeys); + } + + int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); + + Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { final bundle = await getTransactionExpanded(hash: tx.txHash); _updateInputsAndOutputs(tx, bundle); - if (bundle.confirmations > 0) return false; - return bundle.originalTransaction.canReplaceByFee; + if (bundle.confirmations > 0) return null; + return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null; } catch (e) { - return false; + return null; } } @@ -1427,14 +1593,22 @@ abstract class ElectrumWalletBase final bundle = await getTransactionExpanded(hash: txId); final outputs = bundle.originalTransaction.outputs; - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final ownAddresses = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); - // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( - (element) => element.address == addressFromOutputScript(output.scriptPubKey, network))); + final receiverAmount = outputs + .where((output) => + !ownAddresses.contains(addressFromOutputScript(output.scriptPubKey, network))) + .fold(0, (sum, output) => sum + output.amount.toInt()); - var allInputsAmount = 0; + if (receiverAmount == 0) { + throw Exception("Receiver output not found."); + } + final availableInputs = unspentCoins.where((utxo) => utxo.isSending && !utxo.isFrozen).toList(); + int totalBalance = availableInputs.fold( + 0, (previousValue, element) => previousValue + element.value.toInt()); + + int allInputsAmount = 0; for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; @@ -1445,12 +1619,10 @@ abstract class ElectrumWalletBase int totalOutAmount = bundle.originalTransaction.outputs .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); - var currentFee = allInputsAmount - totalOutAmount; int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; - - return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0; + return totalBalance - receiverAmount - remainingFee >= _dustAmount; } Future replaceByFee(String hash, int newFee) async { @@ -1458,12 +1630,13 @@ abstract class ElectrumWalletBase final bundle = await getTransactionExpanded(hash: hash); final utxos = []; + final outputs = []; List privateKeys = []; var allInputsAmount = 0; String? memo; - // Add inputs + // Add original inputs for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; @@ -1474,8 +1647,7 @@ abstract class ElectrumWalletBase final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); - - final btcAddress = addressTypeFromStr(addressRecord.address, network); + final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final privkey = generateECPrivate( hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, index: addressRecord.index, @@ -1497,10 +1669,8 @@ abstract class ElectrumWalletBase ); } - // Create a list of available outputs - final outputs = []; + // Add original outputs for (final out in bundle.originalTransaction.outputs) { - // Check if the script contains OP_RETURN final script = out.scriptPubKey.script; if (script.contains('OP_RETURN') && memo == null) { final index = script.indexOf('OP_RETURN'); @@ -1516,7 +1686,7 @@ abstract class ElectrumWalletBase } final address = addressFromOutputScript(out.scriptPubKey, network); - final btcAddress = addressTypeFromStr(address, network); + final btcAddress = RegexUtils.addressTypeFromStr(address, network); outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); } @@ -1530,17 +1700,95 @@ abstract class ElectrumWalletBase throw Exception("New fee must be higher than the current fee."); } - // Deduct Remaining Fee from Main Outputs + // Deduct fee from change outputs first, if possible if (remainingFee > 0) { + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); for (int i = outputs.length - 1; i >= 0; i--) { - int outputAmount = outputs[i].value.toInt(); + final output = outputs[i]; + final isChange = changeAddresses + .any((element) => element.address == output.address.toAddress(network)); + + if (isChange) { + int outputAmount = output.value.toInt(); + if (outputAmount > _dustAmount) { + int deduction = (outputAmount - _dustAmount >= remainingFee) + ? remainingFee + : outputAmount - _dustAmount; + outputs[i] = BitcoinOutput( + address: output.address, value: BigInt.from(outputAmount - deduction)); + remainingFee -= deduction; + + if (remainingFee <= 0) break; + } + } + } + } + + // If still not enough, add UTXOs until the fee is covered + if (remainingFee > 0) { + final unusedUtxos = unspentCoins + .where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0) + .toList(); + + for (final utxo in unusedUtxos) { + final address = RegexUtils.addressTypeFromStr(utxo.address, network); + final privkey = generateECPrivate( + hd: utxo.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utxo.bitcoinAddressRecord.index, + network: network, + ); + privateKeys.add(privkey); + + utxos.add(UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utxo.hash, + value: BigInt.from(utxo.value), + vout: utxo.vout, + scriptType: _getScriptType(address)), + ownerDetails: + UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), + )); + + allInputsAmount += utxo.value; + remainingFee -= utxo.value; + + if (remainingFee < 0) { + final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses + .any((addr) => addr.address == output.address.toAddress(network))); + if (changeOutput != null) { + final newValue = changeOutput.value.toInt() + (-remainingFee); + outputs[outputs.indexOf(changeOutput)] = + BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue)); + } else { + final changeAddress = await walletAddresses.getChangeAddress(); + outputs.add(BitcoinOutput( + address: RegexUtils.addressTypeFromStr(changeAddress.address, network), + value: BigInt.from(-remainingFee))); + } + + remainingFee = 0; + break; + } + + if (remainingFee <= 0) break; + } + } + + // Deduct from the receiver's output if remaining fee is still greater than 0 + if (remainingFee > 0) { + for (int i = 0; i < outputs.length; i++) { + final output = outputs[i]; + int outputAmount = output.value.toInt(); if (outputAmount > _dustAmount) { int deduction = (outputAmount - _dustAmount >= remainingFee) ? remainingFee : outputAmount - _dustAmount; + outputs[i] = BitcoinOutput( - address: outputs[i].address, value: BigInt.from(outputAmount - deduction)); + address: output.address, value: BigInt.from(outputAmount - deduction)); remainingFee -= deduction; if (remainingFee <= 0) break; @@ -1579,7 +1827,6 @@ abstract class ElectrumWalletBase final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final key = privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); - if (key == null) { throw Exception("Cannot find private key"); } @@ -1601,8 +1848,16 @@ abstract class ElectrumWalletBase hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { + transactionHistory.transactions.values.forEach((tx) { + if (tx.id == hash) { + tx.isReplaced = true; + tx.isPending = false; + transactionHistory.addOne(tx); + } + }); transactionHistory.addOne(transaction); await updateBalance(); + await updateAllUnspents(); }); } catch (e) { throw e; @@ -1612,8 +1867,6 @@ abstract class ElectrumWalletBase Future getTransactionExpanded( {required String hash, int? height}) async { String transactionHex; - // TODO: time is not always available, and calculating it from height is not always accurate. - // Add settings to choose API provider and use and http server instead of electrum for this. int? time; int? confirmations; @@ -1621,6 +1874,28 @@ abstract class ElectrumWalletBase if (verboseTransaction.isEmpty) { transactionHex = await electrumClient.getTransactionHex(hash: hash); + + if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { + try { + final blockHash = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) + .timeout(Duration(seconds: 15)); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) + .timeout(Duration(seconds: 15)); + + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } catch (_) {} + } } else { transactionHex = verboseTransaction['hex'] as String; time = verboseTransaction['time'] as int?; @@ -1628,7 +1903,7 @@ abstract class ElectrumWalletBase } if (height != null) { - if (time == null) { + if (time == null && height > 0) { time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } @@ -1685,6 +1960,11 @@ abstract class ElectrumWalletBase } } + bool isMine(Script script) { + final derivedAddress = addressFromOutputScript(script, network); + return addressesSet.contains(derivedAddress); + } + @override Future> fetchTransactions() async { try { @@ -1698,6 +1978,7 @@ abstract class ElectrumWalletBase .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { await Future.wait(LITECOIN_ADDRESS_TYPES + .where((type) => type != SegwitAddresType.mweb) .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } @@ -1720,7 +2001,7 @@ abstract class ElectrumWalletBase return historiesWithDetails; } catch (e) { - print(e.toString()); + printV("fetchTransactions $e"); return {}; } } @@ -1732,7 +2013,8 @@ abstract class ElectrumWalletBase final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); @@ -1774,6 +2056,8 @@ abstract class ElectrumWalletBase Future> _fetchAddressHistory( BitcoinAddressRecord addressRecord, int? currentHeight) async { + String txid = ""; + try { final Map historiesWithDetails = {}; @@ -1783,7 +2067,7 @@ abstract class ElectrumWalletBase addressRecord.setAsUsed(); await Future.wait(history.map((transaction) async { - final txid = transaction['tx_hash'] as String; + txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; final storedTx = transactionHistory.transactions[txid]; @@ -1791,7 +2075,9 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } storedTx.isPending = storedTx.confirmations == 0; } @@ -1804,6 +2090,20 @@ abstract class ElectrumWalletBase // Got a new transaction fetched, add it to the transaction history // instead of waiting all to finish, and next time it will be faster + + if (this is LitecoinWallet) { + // if we have a peg out transaction with the same value + // that matches this received transaction, mark it as being from a peg out: + for (final tx2 in transactionHistory.transactions.values) { + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); + // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other + if (tx2.additionalInfo["isPegOut"] == true && + tx2.amount == tx.amount && + heightDiff <= 5) { + tx.additionalInfo["fromPegOut"] = true; + } + } + } transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -1814,45 +2114,76 @@ abstract class ElectrumWalletBase } return historiesWithDetails; - } catch (e) { - print(e.toString()); + } catch (e, stacktrace) { + _onError?.call(FlutterErrorDetails( + exception: "$txid - $e", + stack: stacktrace, + library: this.runtimeType.toString(), + )); return {}; } } Future updateTransactions() async { + printV("updateTransactions() called!"); try { if (_isTransactionUpdating) { return; } - await getCurrentChainTip(); + currentChainTip = await getUpdatedChainTip(); - transactionHistory.transactions.values.forEach((tx) async { - if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) { - tx.confirmations = await getCurrentChainTip() - tx.height! + 1; + bool updated = false; + transactionHistory.transactions.values.forEach((tx) { + if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) { + var confirmations = currentChainTip! - tx.height! + 1; + if (confirmations < 0) { + // if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe: + confirmations = 0; + } + if (confirmations != tx.confirmations) { + updated = true; + tx.confirmations = confirmations; + transactionHistory.addOne(tx); + } } }); + if (updated) { + await transactionHistory.save(); + } + _isTransactionUpdating = true; await fetchTransactions(); walletAddresses.updateReceiveAddresses(); _isTransactionUpdating = false; } catch (e, stacktrace) { - print(stacktrace); - print(e); + printV(stacktrace); + printV(e); _isTransactionUpdating = false; } } Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + (address) => + !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && + address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { final sh = address.getScriptHash(network); - await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { + try { + await _scripthashesUpdateSubject[sh]?.close(); + } catch (e) { + printV("failed to close: $e"); + } + } + try { + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); + } catch (e) { + printV("failed scripthashUpdate: $e"); + } _scripthashesUpdateSubject[sh]?.listen((event) async { try { await updateUnspentsForAddress(address); @@ -1861,25 +2192,27 @@ abstract class ElectrumWalletBase await _fetchAddressHistory(address, await getCurrentChainTip()); } catch (e, s) { - print(e.toString()); + printV("sub error: $e"); _onError?.call(FlutterErrorDetails( exception: e, stack: s, library: this.runtimeType.toString(), )); } + }, onError: (e, s) { + printV("sub_listen error: $e $s"); }); })); } Future fetchBalances() async { final addresses = walletAddresses.allAddresses - .where((address) => addressTypeFromStr(address.address, network) is! MwebAddress) + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) .toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = scriptHash(addressRecord.address, network: network); + final sh = addressRecord.getScriptHash(network); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -1903,8 +2236,30 @@ abstract class ElectrumWalletBase }); } + unspentCoinsInfo.values.forEach((info) { + unspentCoins.forEach((element) { + if (element.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) return; + + if (element.hash == info.hash && + element.vout == info.vout && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value) { + if (info.isFrozen) { + totalFrozen += element.value; + } + } + }); + }); + final balances = await Future.wait(balanceFutures); + if (balances.isNotEmpty && balances.first['confirmed'] == null) { + // if we got null balance responses from the server, set our connection status to lost and return our last known balance: + printV("got null balance responses from the server, setting connection status to lost"); + syncStatus = LostConnectionSyncStatus(); + return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + } + for (var i = 0; i < balances.length; i++) { final addressRecord = addresses[i]; final balance = balances[i]; @@ -1920,10 +2275,14 @@ abstract class ElectrumWalletBase } return ElectrumBalance( - confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen); + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, + ); } Future updateBalance() async { + printV("updateBalance() called!"); balance[currency] = await fetchBalances(); await save(); } @@ -1976,7 +2335,7 @@ abstract class ElectrumWalletBase List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); @@ -2006,10 +2365,10 @@ abstract class ElectrumWalletBase Future _setInitialHeight() async { if (_chainTipUpdateSubject != null) return; - _currentChainTip = await getUpdatedChainTip(); + currentChainTip = await getUpdatedChainTip(); - if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(_currentChainTip!); + if ((currentChainTip == null || currentChainTip! == 0) && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); } _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); @@ -2018,7 +2377,7 @@ abstract class ElectrumWalletBase final height = int.tryParse(event['height'].toString()); if (height != null) { - _currentChainTip = height; + currentChainTip = height; if (alwaysScan == true && syncStatus is SyncedSyncStatus) { _setListeners(walletInfo.restoreHeight); @@ -2037,25 +2396,33 @@ abstract class ElectrumWalletBase if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus || syncStatus is ConnectingSyncStatus) { - syncStatus = AttemptingSyncStatus(); - startSync(); + syncStatus = ConnectedSyncStatus(); } break; case ConnectionStatus.disconnected: - syncStatus = NotConnectedSyncStatus(); + if (syncStatus is! NotConnectedSyncStatus && + syncStatus is! ConnectingSyncStatus && + syncStatus is! SyncronizingSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - syncStatus = LostConnectionSyncStatus(); + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: - syncStatus = ConnectingSyncStatus(); + if (syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectingSyncStatus(); + } break; default: } } void _syncStatusReaction(SyncStatus syncStatus) async { + printV("SYNC_STATUS_CHANGE: ${syncStatus}"); if (syncStatus is SyncingSyncStatus) { return; } @@ -2069,7 +2436,8 @@ abstract class ElectrumWalletBase _isTryingToConnect = true; Timer(Duration(seconds: 5), () { - if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { + if (this.syncStatus is NotConnectedSyncStatus || + this.syncStatus is LostConnectionSyncStatus) { this.electrumClient.connectToUri( node!.uri, useSSL: node!.useSSL ?? false, @@ -2134,6 +2502,12 @@ abstract class ElectrumWalletBase transactionHistory.addOne(tx); } } + + @override + String formatCryptoAmount(String amount) { + final amountInt = int.parse(amount); + return bitcoinAmountToString(amount: amountInt); + } } class ScanNode { @@ -2200,21 +2574,22 @@ Future startRefresh(ScanData scanData) async { BehaviorSubject? tweaksSubscription = null; - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - - // Initial status UI update, send how many blocks left to scan - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - final electrumClient = scanData.electrumClient; await electrumClient.connectToUri( scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), useSSL: scanData.node?.useSSL ?? false, ); + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + if (tweaksSubscription == null) { - final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; final receiver = Receiver( scanData.silentAddress.b_scan.toHex(), scanData.silentAddress.B_spend.toHex(), @@ -2223,16 +2598,45 @@ Future startRefresh(ScanData scanData) async { scanData.labelIndexes.length, ); - tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); - tweaksSubscription?.listen((t) async { - final tweaks = t as Map; + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - if (tweaks["message"] != null) { + tweaksSubscription = await electrumClient.tweaksSubscribe( + height: syncHeight, + count: initialCount, + ); + + Future listenFn(t) async { + final tweaks = t as Map; + final msg = tweaks["message"]; + // success or error msg + final noData = msg != null; + + if (noData) { // re-subscribe to continue receiving messages, starting from the next unscanned height - electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count); + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + tweaksSubscription?.close(); + + final nextTweaksSubscription = electrumClient.tweaksSubscribe( + height: nextHeight, + count: nextCount, + ); + nextTweaksSubscription?.listen(listenFn); + } + return; } + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + final blockHeight = tweaks.keys.first; final tweakHeight = int.parse(blockHeight); @@ -2267,11 +2671,13 @@ Future startRefresh(ScanData scanData) async { fee: 0, direction: TransactionDirection.incoming, isPending: false, + isReplaced: false, date: scanData.network == BitcoinNetwork.mainnet ? getDateByBitcoinHeight(tweakHeight) : DateTime.now(), confirmations: scanData.chainTip - tweakHeight + 1, unspents: [], + isReceivedSilentPayment: true, ); addToWallet.forEach((label, value) { @@ -2326,16 +2732,6 @@ Future startRefresh(ScanData scanData) async { } catch (_) {} syncHeight = tweakHeight; - scanData.sendPort.send( - SyncResponse( - syncHeight, - SyncingSyncStatus.fromHeightValues( - scanData.chainTip, - initialSyncHeight, - syncHeight, - ), - ), - ); if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { if (tweakHeight >= scanData.chainTip) @@ -2351,7 +2747,9 @@ Future startRefresh(ScanData scanData) async { await tweaksSubscription!.close(); await electrumClient.close(); } - }); + } + + tweaksSubscription?.listen(listenFn); } if (tweaksSubscription == null) { @@ -2381,6 +2779,8 @@ class EstimatedTxResult { final int fee; final int amount; final bool spendsSilentPayment; + + // final bool sendsToSilentPayment; final bool hasChange; final bool isSendAll; final String? memo; @@ -2394,35 +2794,6 @@ class PublicKeyWithDerivationPath { final String publicKey; } -BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { - // print("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - // print(network); - if (network is BitcoinCashNetwork) { - if (!address.startsWith("bitcoincash:") && - (address.startsWith("q") || address.startsWith("p"))) { - address = "bitcoincash:$address"; - } - - return BitcoinCashAddress(address).baseAddress; - } - - if (P2pkhAddress.regex.hasMatch(address)) { - return P2pkhAddress.fromAddress(address: address, network: network); - } else if (P2shAddress.regex.hasMatch(address)) { - return P2shAddress.fromAddress(address: address, network: network); - } else if (P2wshAddress.regex.hasMatch(address)) { - return P2wshAddress.fromAddress(address: address, network: network); - } else if (P2trAddress.regex.hasMatch(address)) { - return P2trAddress.fromAddress(address: address, network: network); - } else if (MwebAddress.regex.hasMatch(address)) { - return MwebAddress.fromAddress(address: address, network: network); - } else if (SilentPaymentAddress.regex.hasMatch(address)) { - return SilentPaymentAddress.fromAddress(address); - } else { - return P2wpkhAddress.fromAddress(address: address, network: network); - } -} - BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { if (type is P2pkhAddress) { return P2pkhAddressType.p2pkh; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 0653200ee..614a06a3b 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,7 +1,12 @@ +import 'dart:io' show Platform; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -34,11 +39,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.mainHd, required this.sideHd, required this.network, + required this.isHardwareWallet, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, List? initialSilentAddresses, int initialSilentAddressIndex = 0, + List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), @@ -59,6 +66,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), currentSilentAddressIndex = initialSilentAddressIndex, + mwebAddresses = + ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( @@ -97,13 +106,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _addresses; - late ObservableList addressesByReceiveType; + final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it + final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; + final bool isHardwareWallet; @observable SilentPaymentOwner? silentAddress; @@ -131,27 +144,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } - String receiveAddress; + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); + final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); - final typeMatchingReceiveAddresses = - receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed); - - if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || - typeMatchingReceiveAddresses.isEmpty) { - receiveAddress = generateNewAddress().address; - } else { - final previousAddressMatchesType = - previousAddressRecord != null && previousAddressRecord!.type == addressPageType; - - if (previousAddressMatchesType && - typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { - receiveAddress = previousAddressRecord!.address; - } else { - receiveAddress = typeMatchingReceiveAddresses.first.address; + if (!isEnabledAutoGenerateSubaddress) { + if (previousAddressRecord != null && + previousAddressRecord!.type == addressPageType) { + return previousAddressRecord!.address; } + + if (typeMatchingAddresses.isNotEmpty) { + return typeMatchingAddresses.first.address; + } + + return generateNewAddress().address; } - return receiveAddress; + if (typeMatchingAddresses.isEmpty || typeMatchingReceiveAddresses.isEmpty) { + return generateNewAddress().address; + } + + final prev = previousAddressRecord; + if (prev != null && prev.type == addressPageType && !prev.isUsed) { + return prev.address; + } + + return typeMatchingReceiveAddresses.first.address; } @observable @@ -159,6 +177,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } if (addressPageType == SilentPaymentsAddresType.p2sp) { final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); @@ -170,12 +191,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } return; } + try { + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); - final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); - - previousAddressRecord = addressRecord; - receiveAddresses.remove(addressRecord); - receiveAddresses.insert(0, addressRecord); + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); + } catch (e) { + printV("ElectrumWalletAddressBase: set address ($addr): $e"); + } } @override @@ -223,14 +249,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (walletInfo.type == WalletType.bitcoinCash) { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - // await _generateInitialAddresses(); - // await _generateInitialAddresses(type: SegwitAddresType.mweb); + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { + await _generateInitialAddresses(type: SegwitAddresType.mweb); + } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + if (!isHardwareWallet) { + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + } } updateAddressesByMatch(); @@ -249,7 +279,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress( + {List? inputs, + List? outputs, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -264,7 +297,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } updateChangeAddresses(); - final address = changeAddresses[currentChangeAddressIndex].address; + final address = changeAddresses[currentChangeAddressIndex]; currentChangeAddressIndex += 1; return address; } @@ -308,7 +341,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ); silentAddresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } @@ -324,8 +357,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { type: addressPageType, network: network, ); - _addresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () { + _addresses.add(address); + updateAddressesByMatch(); + }); return address; } @@ -460,7 +495,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await saveAddressesInBox(); } catch (e) { - print(e.toString()); + printV("updateAddresses $e"); } } @@ -477,6 +512,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); if (foundAddress != null) { foundAddress!.setNewName(label); @@ -607,8 +647,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateAddresses() { _addresses.forEach((element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } if (!element.isHidden && element.address != await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { @@ -634,7 +687,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => !addr.isHidden && !addr.isUsed && addr.type == type; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index fa58be238..990719089 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -23,6 +23,8 @@ class ElectrumWalletSnapshot { required this.addressPageType, required this.silentAddresses, required this.silentAddressIndex, + required this.mwebAddresses, + required this.alwaysScan, this.passphrase, this.derivationType, this.derivationPath, @@ -44,6 +46,9 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; + List mwebAddresses; + bool alwaysScan; + ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; @@ -51,15 +56,16 @@ class ElectrumWalletSnapshot { DerivationType? derivationType; String? derivationPath; - static Future load( - EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { + static Future load(EncryptionFileUtils encryptionFileUtils, String name, + WalletType type, String password, BasedUtxoNetwork network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String?; final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; + + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) @@ -71,6 +77,14 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) .toList(); + final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; + final mwebAddresses = mwebAddressTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .toList(); + + final alwaysScan = data['alwaysScan'] as bool? ?? false; + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; @@ -113,6 +127,8 @@ class ElectrumWalletSnapshot { derivationPath: derivationPath, silentAddresses: silentAddresses, silentAddressIndex: silentAddressIndex, + mwebAddresses: mwebAddresses, + alwaysScan: alwaysScan, ); } } diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 3307bfeed..9bdb66eef 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -3,6 +3,11 @@ import 'package:cw_core/exceptions.dart'; class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc); + + @override + String toString() { + return "BitcoinTransactionWrongBalanceException: $amount, $currency"; + } } class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} @@ -13,10 +18,20 @@ class BitcoinTransactionNoDustException extends TransactionNoDustException {} class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChangeException { BitcoinTransactionNoDustOnChangeException(super.max, super.min); + + @override + String toString() { + return "BitcoinTransactionNoDustOnChangeException: max: $max, min: $min"; + } } class BitcoinTransactionCommitFailed extends TransactionCommitFailed { BitcoinTransactionCommitFailed({super.errorMessage}); + + @override + String toString() { + return errorMessage??"unknown error"; + } } class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {} @@ -30,4 +45,6 @@ class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailed class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} +class BitcoinTransactionCommitFailedLessThanMin extends TransactionCommitFailedLessThanMin {} + class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart new file mode 100644 index 000000000..62840933c --- /dev/null +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; + +class LitecoinHardwareWalletService { + LitecoinHardwareWalletService(this.ledgerConnection); + + final LedgerConnection ledgerConnection; + + Future> getAvailableAccounts( + {int index = 0, int limit = 5}) async { + final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); + + await litecoinLedgerApp.getVersion(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer; + + for (final i in indexRange) { + final derivationPath = "m/84'/2'/$i'"; + final xpub = await litecoinLedgerApp.getXPubKey( + accountsDerivationPath: derivationPath, + xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) + .childKey(Bip32KeyIndex(0)); + + final address = generateP2WPKHAddress( + hd: hd, index: 0, network: LitecoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index e2010c520..08c56c600 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,11 +1,16 @@ import 'dart:async'; import 'dart:convert'; + import 'package:convert/convert.dart' as convert; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -34,13 +39,17 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; +import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/ledger_litecoin.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'litecoin_wallet.g.dart'; @@ -48,15 +57,17 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ - required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required Uint8List seedBytes, required EncryptionFileUtils encryptionFileUtils, + Uint8List? seedBytes, + String? mnemonic, + String? xpub, String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -65,6 +76,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) : super( mnemonic: mnemonic, password: password, + passphrase: passphrase, + xpub: xpub, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, network: LitecoinNetwork.mainnet, @@ -73,34 +86,80 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, + alwaysScan: alwaysScan, ) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; - mwebEnabled = alwaysScan ?? false; + if (seedBytes != null) { + mwebHd = + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; + mwebEnabled = alwaysScan ?? false; + } else { + mwebHd = null; + mwebEnabled = false; + } walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialMwebAddresses: initialMwebAddresses, mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + reaction((_) => mwebSyncStatus, (status) async { + if (mwebSyncStatus is FailedSyncStatus) { + // we failed to connect to mweb, check if we are connected to the litecoin node: + late int nodeHeight; + try { + nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; + } catch (_) { + nodeHeight = 0; + } + + if (nodeHeight == 0) { + // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us + } else { + // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); + } + } else if (mwebSyncStatus is SyncingSyncStatus) { + syncStatus = mwebSyncStatus; + } else if (mwebSyncStatus is SyncronizingSyncStatus) { + if (syncStatus is! SyncronizingSyncStatus) { + syncStatus = mwebSyncStatus; + } + } else if (mwebSyncStatus is SyncedSyncStatus) { + if (syncStatus is! SyncedSyncStatus) { + syncStatus = mwebSyncStatus; + } + } + }); } - late final Bip32Slip10Secp256k1 mwebHd; + late final Bip32Slip10Secp256k1? mwebHd; late final Box mwebUtxosBox; Timer? _syncTimer; Timer? _feeRatesTimer; + Timer? _processingTimer; StreamSubscription? _utxoStream; - late RpcClient _stub; late bool mwebEnabled; + bool processingUtxos = false; - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; - List get spendSecret => mwebHd.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; + @observable + SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + + @override + bool get hasRescan => true; + + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; static Future create( {required String mnemonic, @@ -111,6 +170,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { @@ -134,6 +194,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialMwebAddresses: initialMwebAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, @@ -208,101 +269,184 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } return LitecoinWallet( - mnemonic: keysData.mnemonic!, + mnemonic: keysData.mnemonic, + xpub: keysData.xPub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, + initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, - seedBytes: seedBytes!, + seedBytes: seedBytes, passphrase: passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, - alwaysScan: alwaysScan, + alwaysScan: snp?.alwaysScan, ); } + Future waitForMwebAddresses() async { + printV("waitForMwebAddresses() called!"); + // ensure that we have the full 1000 mweb addresses generated before continuing: + // should no longer be needed, but leaving here just in case + await (walletAddresses as LitecoinWalletAddresses).ensureMwebAddressUpToIndexExists(1020); + } + + @action + @override + Future connectToNode({required Node node}) async { + await super.connectToNode(node: node); + + final prefs = await SharedPreferences.getInstance(); + final mwebNodeUri = prefs.getString("mwebNodeUri") ?? "ltc-electrum.cakewallet.com:9333"; + await CwMweb.setNodeUriOverride(mwebNodeUri); + } + @action @override Future startSync() async { - print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); - syncStatus = SyncronizingSyncStatus(); - await subscribeForUpdates(); - await updateTransactions(); - await updateFeeRates(); - - _feeRatesTimer?.cancel(); - _feeRatesTimer = - Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - + printV("startSync() called!"); + printV("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); if (!mwebEnabled) { try { - await updateAllUnspents(); - await updateBalance(); - syncStatus = SyncedSyncStatus(); - } catch (e, s) { - print(e); - print(s); - syncStatus = FailedSyncStatus(); - } + // in case we're switching from a litecoin wallet that had mweb enabled + CwMweb.stop(); + } catch (_) {} + super.startSync(); return; } - await getStub(); - await updateUnspent(); - await updateBalance(); + if (mwebSyncStatus is SyncronizingSyncStatus) { + return; + } + + printV("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); + _syncTimer?.cancel(); + try { + mwebSyncStatus = SyncronizingSyncStatus(); + try { + await subscribeForUpdates(); + } catch (e) { + printV("failed to subscribe for updates: $e"); + } + updateFeeRates(); + _feeRatesTimer?.cancel(); + _feeRatesTimer = + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + + printV("START SYNC FUNCS"); + await waitForMwebAddresses(); + await processMwebUtxos(); + await updateTransactions(); + await updateUnspent(); + await updateBalance(); + } catch (e) { + printV("failed to start mweb sync: $e"); + syncStatus = FailedSyncStatus(); + return; + } _syncTimer?.cancel(); - // delay the timer by a second so we don't overrride the restoreheight if one is set - Timer(const Duration(seconds: 1), () async { - _syncTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async { - if (syncStatus is FailedSyncStatus) return; - final nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; - final resp = await _stub.status(StatusRequest()); + _syncTimer = Timer.periodic(const Duration(milliseconds: 3000), (timer) async { + if (mwebSyncStatus is FailedSyncStatus) { + _syncTimer?.cancel(); + return; + } + final nodeHeight = + await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + + if (nodeHeight == 0) { + // we aren't connected to the ltc node yet + if (mwebSyncStatus is! NotConnectedSyncStatus) { + mwebSyncStatus = FailedSyncStatus(error: "litecoin node isn't connected"); + } + return; + } + + // update the current chain tip so that confirmation calculations are accurate: + currentChainTip = nodeHeight; + + final resp = await CwMweb.status(StatusRequest()); + + try { if (resp.blockHeaderHeight < nodeHeight) { int h = resp.blockHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebHeaderHeight < nodeHeight) { int h = resp.mwebHeaderHeight; - syncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); + mwebSyncStatus = SyncingSyncStatus(nodeHeight - h, h / nodeHeight); } else if (resp.mwebUtxosHeight < nodeHeight) { - syncStatus = SyncingSyncStatus(1, 0.999); + mwebSyncStatus = SyncingSyncStatus(1, 0.999); } else { - // prevent unnecessary reaction triggers: - if (syncStatus is! SyncedSyncStatus) { - syncStatus = SyncedSyncStatus(); - } - + bool confirmationsUpdated = false; if (resp.mwebUtxosHeight > walletInfo.restoreHeight) { await walletInfo.updateRestoreHeight(resp.mwebUtxosHeight); await checkMwebUtxosSpent(); // update the confirmations for each transaction: - for (final transaction in transactionHistory.transactions.values) { - if (transaction.isPending) continue; - int txHeight = transaction.height ?? resp.mwebUtxosHeight; - final confirmations = (resp.mwebUtxosHeight - txHeight) + 1; - if (transaction.confirmations == confirmations) continue; - transaction.confirmations = confirmations; - transactionHistory.addOne(transaction); + for (final tx in transactionHistory.transactions.values) { + if (tx.height == null || tx.height == 0) { + // update with first confirmation on next block since it hasn't been confirmed yet: + tx.height = resp.mwebUtxosHeight; + continue; + } + + final confirmations = (resp.mwebUtxosHeight - tx.height!) + 1; + + // if the confirmations haven't changed, skip updating: + if (tx.confirmations == confirmations) continue; + + // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin): + if (confirmations >= 2 && + tx.direction == TransactionDirection.outgoing && + tx.unspents != null) { + for (var coin in tx.unspents!) { + final utxo = mwebUtxosBox.get(coin.address); + if (utxo != null) { + printV("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + await mwebUtxosBox.delete(coin.address); + } + } + } + + tx.confirmations = confirmations; + tx.isPending = false; + transactionHistory.addOne(tx); + confirmationsUpdated = true; + } + if (confirmationsUpdated) { + await transactionHistory.save(); + await updateTransactions(); } - await transactionHistory.save(); } + + // prevent unnecessary reaction triggers: + if (mwebSyncStatus is! SyncedSyncStatus) { + // mwebd is synced, but we could still be processing incoming utxos: + if (!processingUtxos) { + mwebSyncStatus = SyncedSyncStatus(); + } + } + return; } - }); + } catch (e) { + printV("error syncing: $e"); + mwebSyncStatus = FailedSyncStatus(error: e.toString()); + } }); - // this runs in the background and processes new utxos as they come in: - processMwebUtxos(); } @action @override Future stopSync() async { + printV("stopSync() called!"); _syncTimer?.cancel(); _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); await CwMweb.stop(); + printV("stopped syncing!"); } Future initMwebUtxosBox() async { @@ -320,7 +464,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final oldBox = await CakeHive.openBox(oldBoxName); mwebUtxosBox = await CakeHive.openBox(newBoxName); for (final key in oldBox.keys) { - await mwebUtxosBox.put(key, oldBox.get(key)!); + final value = oldBox.get(key); + await oldBox.delete(key); + await mwebUtxosBox.put(key, value!); } oldBox.deleteFromDisk(); @@ -337,7 +483,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool? usingElectrs, }) async { _syncTimer?.cancel(); - int oldHeight = walletInfo.restoreHeight; await walletInfo.updateRestoreHeight(height); // go through mwebUtxos and clear any that are above the new restore height: @@ -374,8 +519,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { await initMwebUtxosBox(); } - Future handleIncoming(MwebUtxo utxo, RpcClient stub) async { - final status = await stub.status(StatusRequest()); + Future handleIncoming(MwebUtxo utxo) async { + printV("handleIncoming() called!"); + final status = await CwMweb.status(StatusRequest()); var date = DateTime.now(); var confirmations = 0; if (utxo.height > 0) { @@ -398,18 +544,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { confirmations: confirmations, inputAddresses: [], outputAddresses: [utxo.outputId], + isReplaced: false, ); + } else { + if (tx.confirmations != confirmations || tx.height != utxo.height) { + tx.height = utxo.height; + tx.confirmations = confirmations; + tx.isPending = utxo.height == 0; + } } bool isNew = transactionHistory.transactions[tx.id] == null; - // don't update the confirmations if the tx is updated by electrum: - if (tx.confirmations == 0 || utxo.height != 0) { - tx.height = utxo.height; - tx.isPending = utxo.height == 0; - tx.confirmations = confirmations; - } - if (!(tx.outputAddresses?.contains(utxo.address) ?? false)) { tx.outputAddresses?.add(utxo.address); isNew = true; @@ -419,16 +565,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final addressRecord = walletAddresses.allAddresses .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); if (addressRecord == null) { - print("we don't have this address in the wallet! ${utxo.address}"); + printV("we don't have this address in the wallet! ${utxo.address}"); return; } - // if our address isn't in the inputs, update the txCount: - final inputAddresses = tx.inputAddresses ?? []; - if (!inputAddresses.contains(utxo.address)) { - addressRecord.txCount++; - } - + // update the txCount: + addressRecord.txCount++; addressRecord.balance += utxo.value.toInt(); addressRecord.setAsUsed(); } @@ -444,91 +586,137 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future processMwebUtxos() async { + printV("processMwebUtxos() called!"); if (!mwebEnabled) { return; } int restoreHeight = walletInfo.restoreHeight; - print("SCANNING FROM HEIGHT: $restoreHeight"); + printV("SCANNING FROM HEIGHT: $restoreHeight"); final req = UtxosRequest(scanSecret: scanSecret, fromHeight: restoreHeight); - // process old utxos: - // for (final utxo in mwebUtxosBox.values) { - // if (utxo.address.isEmpty) { - // continue; - // } - - // // if (walletInfo.restoreHeight > utxo.height) { - // // continue; - // // } - // // await handleIncoming(utxo, _stub); - - // if (utxo.height > walletInfo.restoreHeight) { - // await walletInfo.updateRestoreHeight(utxo.height); - // } - // } - // process new utxos as they come in: - _utxoStream?.cancel(); - _utxoStream = _stub.utxos(req).listen((Utxo sUtxo) async { - final utxo = MwebUtxo( - address: sUtxo.address, - blockTime: sUtxo.blockTime, - height: sUtxo.height, - outputId: sUtxo.outputId, - value: sUtxo.value.toInt(), - ); + await _utxoStream?.cancel(); + ResponseStream? responseStream = await CwMweb.utxos(req); + if (responseStream == null) { + throw Exception("failed to get utxos stream!"); + } + _utxoStream = responseStream.listen( + (Utxo sUtxo) async { + // we're processing utxos, so our balance could still be inaccurate: + if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SyncronizingSyncStatus(); + processingUtxos = true; + _processingTimer?.cancel(); + _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + processingUtxos = false; + timer.cancel(); + }); + } - // if (mwebUtxosBox.containsKey(utxo.outputId)) { - // // we've already stored this utxo, skip it: - // return; - // } + final utxo = MwebUtxo( + address: sUtxo.address, + blockTime: sUtxo.blockTime, + height: sUtxo.height, + outputId: sUtxo.outputId, + value: sUtxo.value.toInt(), + ); - // if (utxo.address.isEmpty) { - // await updateUnspent(); - // await updateBalance(); - // initDone = true; - // } + if (mwebUtxosBox.containsKey(utxo.outputId)) { + // we've already stored this utxo, skip it: + // but do update the utxo height if it's somehow different: + final existingUtxo = mwebUtxosBox.get(utxo.outputId); + if (existingUtxo!.height != utxo.height) { + printV( + "updating utxo height for $utxo.outputId: ${existingUtxo.height} -> ${utxo.height}"); + existingUtxo.height = utxo.height; + await mwebUtxosBox.put(utxo.outputId, existingUtxo); + } + return; + } - await updateUnspent(); - await updateBalance(); + await updateUnspent(); + await updateBalance(); - final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; + final mwebAddrs = (walletAddresses as LitecoinWalletAddresses).mwebAddrs; - // don't process utxos with addresses that are not in the mwebAddrs list: - if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { - return; - } + // don't process utxos with addresses that are not in the mwebAddrs list: + if (utxo.address.isNotEmpty && !mwebAddrs.contains(utxo.address)) { + return; + } - await mwebUtxosBox.put(utxo.outputId, utxo); + await mwebUtxosBox.put(utxo.outputId, utxo); - await handleIncoming(utxo, _stub); - }); + await handleIncoming(utxo); + }, + onError: (error) { + printV("error in utxo stream: $error"); + mwebSyncStatus = FailedSyncStatus(error: error.toString()); + }, + cancelOnError: true, + ); + } + + Future deleteSpentUtxos() async { + printV("deleteSpentUtxos() called!"); + final chainHeight = await electrumClient.getCurrentBlockChainTip(); + final status = await CwMweb.status(StatusRequest()); + if (chainHeight == null || status.blockHeaderHeight != chainHeight) return; + if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced + + // delete any spent utxos with >= 2 confirmations: + final spentOutputIds = mwebUtxosBox.values + .where((utxo) => utxo.spent && (chainHeight - utxo.height) >= 2) + .map((utxo) => utxo.outputId) + .toList(); + + if (spentOutputIds.isEmpty) return; + + final resp = await CwMweb.spent(SpentRequest(outputId: spentOutputIds)); + final spent = resp.outputId; + if (spent.isEmpty) return; + + for (final outputId in spent) { + await mwebUtxosBox.delete(outputId); + } } Future checkMwebUtxosSpent() async { + printV("checkMwebUtxosSpent() called!"); if (!mwebEnabled) { return; } - while ((await Future.wait(transactionHistory.transactions.values - .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending) - .map(checkPendingTransaction))) - .any((x) => x)); - final outputIds = - mwebUtxosBox.values.where((utxo) => utxo.height > 0).map((utxo) => utxo.outputId).toList(); + final pendingOutgoingTransactions = transactionHistory.transactions.values + .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending); - final resp = await _stub.spent(SpentRequest(outputId: outputIds)); + // check if any of the pending outgoing transactions are now confirmed: + bool updatedAny = false; + for (final tx in pendingOutgoingTransactions) { + updatedAny = await isConfirmed(tx) || updatedAny; + } + + await deleteSpentUtxos(); + + // get output ids of all the mweb utxos that have > 0 height: + final outputIds = mwebUtxosBox.values + .where((utxo) => utxo.height > 0 && !utxo.spent) + .map((utxo) => utxo.outputId) + .toList(); + + final resp = await CwMweb.spent(SpentRequest(outputId: outputIds)); final spent = resp.outputId; if (spent.isEmpty) return; - final status = await _stub.status(StatusRequest()); + + final status = await CwMweb.status(StatusRequest()); final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.blockHeaderHeight != height) return; - if (status.mwebUtxosHeight != height) return; + if (status.mwebUtxosHeight != height) return; // we aren't synced int amount = 0; Set inputAddresses = {}; var output = convert.AccumulatorSink(); var input = sha256.startChunkedConversion(output); + for (final outputId in spent) { final utxo = mwebUtxosBox.get(outputId); await mwebUtxosBox.delete(outputId); @@ -543,6 +731,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { inputAddresses.add(utxo.address); input.add(hex.decode(outputId)); } + if (inputAddresses.isEmpty) return; input.close(); var digest = output.events.single; @@ -558,16 +747,29 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { confirmations: 1, inputAddresses: inputAddresses.toList(), outputAddresses: [], + isReplaced: false, ); - print("BEING ADDED HERE@@@@@@@@@@@@@@@@@@@@@@@2"); transactionHistory.addOne(tx); await transactionHistory.save(); + + if (updatedAny) { + await updateBalance(); + } } - Future checkPendingTransaction(ElectrumTransactionInfo tx) async { + // checks if a pending transaction is now confirmed, and updates the tx info accordingly: + Future isConfirmed(ElectrumTransactionInfo tx) async { if (!mwebEnabled) return false; if (!tx.isPending) return false; + + final isMwebTx = (tx.inputAddresses?.any((addr) => addr.contains("mweb")) ?? false) || + (tx.outputAddresses?.any((addr) => addr.contains("mweb")) ?? false); + + if (!isMwebTx) { + return false; + } + final outputId = [], target = {}; final isHash = RegExp(r'^[a-f0-9]{64}$').hasMatch; final spendingOutputIds = tx.inputAddresses?.where(isHash) ?? []; @@ -575,6 +777,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputId.addAll(spendingOutputIds); outputId.addAll(payingToOutputIds); target.addAll(spendingOutputIds); + for (final outputId in payingToOutputIds) { final spendingTx = transactionHistory.transactions.values .firstWhereOrNull((tx) => tx.inputAddresses?.contains(outputId) ?? false); @@ -582,13 +785,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { target.add(outputId); } } + if (outputId.isEmpty) { return false; } - final resp = await _stub.spent(SpentRequest(outputId: outputId)); - if (!setEquals(resp.outputId.toSet(), target)) return false; - final status = await _stub.status(StatusRequest()); - if (!tx.isPending) return false; + + final resp = await CwMweb.spent(SpentRequest(outputId: outputId)); + if (!setEquals(resp.outputId.toSet(), target)) { + return false; + } + + final status = await CwMweb.status(StatusRequest()); tx.height = status.mwebUtxosHeight; tx.confirmations = 1; tx.isPending = false; @@ -597,6 +804,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } Future updateUnspent() async { + printV("updateUnspent() called!"); await checkMwebUtxosSpent(); await updateAllUnspents(); } @@ -604,13 +812,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future updateAllUnspents() async { - // get ltc unspents: - await super.updateAllUnspents(); - if (!mwebEnabled) { + await super.updateAllUnspents(); return; } - await getStub(); // add the mweb unspents to the list: List mwebUnspentCoins = []; @@ -619,7 +824,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebUtxosBox.keys.forEach((dynamic oId) { final String outputId = oId as String; final utxo = mwebUtxosBox.get(outputId); - if (utxo == null) { + if (utxo == null || utxo.spent) { return; } if (utxo.address.isEmpty) { @@ -630,7 +835,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { .firstWhereOrNull((addressRecord) => addressRecord.address == utxo.address); if (addressRecord == null) { - print("utxo contains an address that is not in the wallet: ${utxo.address}"); + printV("utxo contains an address that is not in the wallet: ${utxo.address}"); return; } final unspent = BitcoinUnspent( @@ -644,6 +849,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } mwebUnspentCoins.add(unspent); }); + + // copy coin control attributes to mwebCoins: + await updateCoins(mwebUnspentCoins); + // get regular ltc unspents (this resets unspentCoins): + await super.updateAllUnspents(); + // add the mwebCoins: unspentCoins.addAll(mwebUnspentCoins); } @@ -653,31 +864,35 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { return balance; } - await getStub(); + + // update unspent balances: + await updateUnspent(); int confirmed = balance.confirmed; int unconfirmed = balance.unconfirmed; + int confirmedMweb = 0; + int unconfirmedMweb = 0; try { mwebUtxosBox.values.forEach((utxo) { - if (utxo.height > 0) { - confirmed += utxo.value.toInt(); - } else { - unconfirmed += utxo.value.toInt(); + bool isConfirmed = utxo.height > 0; + + printV( + "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}"); + + if (isConfirmed) { + confirmedMweb += utxo.value.toInt(); + } + + if (isConfirmed && utxo.spent) { + unconfirmedMweb -= utxo.value.toInt(); + } + + if (!isConfirmed && !utxo.spent) { + unconfirmedMweb += utxo.value.toInt(); } }); } catch (_) {} - // update unspent balances: - - // reset coin balances and txCount to 0: - // unspentCoins.forEach((coin) { - // if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - // coin.bitcoinAddressRecord.balance = 0; - // coin.bitcoinAddressRecord.txCount = 0; - // }); - - await updateUnspent(); - for (var addressRecord in walletAddresses.allAddresses) { addressRecord.balance = 0; addressRecord.txCount = 0; @@ -707,7 +922,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // update the txCount for each address using the tx history, since we can't rely on mwebd // to have an accurate count, we should just keep it in sync with what we know from the tx history: for (final tx in transactionHistory.transactions.values) { - // if (tx.isPending) continue; if (tx.inputAddresses == null || tx.outputAddresses == null) { continue; } @@ -722,7 +936,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } - return ElectrumBalance(confirmed: confirmed, unconfirmed: unconfirmed, frozen: balance.frozen); + return ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: balance.frozen, + secondConfirmed: confirmedMweb, + secondUnconfirmed: unconfirmedMweb, + ); } @override @@ -751,10 +971,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List? inputPrivKeyInfos, List? vinOutpoints, }) async { - final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); - final paysToMweb = outputs + bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + bool paysToMweb = outputs .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); - if (!spendsMweb && !paysToMweb) { + + bool isRegular = !spendsMweb && !paysToMweb; + bool isMweb = spendsMweb || paysToMweb; + + if (isMweb && !mwebEnabled) { + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); + // TODO: likely the change address is mweb and just not updated + } + + if (isRegular) { return await super.calcFee( utxos: utxos, outputs: outputs, @@ -766,23 +995,39 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - if (!mwebEnabled) { - throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!"); - } - if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { outputs = [ BitcoinScriptOutput( script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) ]; } + // https://github.com/ltcmweb/mwebd?tab=readme-ov-file#fee-estimation final preOutputSum = outputs.fold(BigInt.zero, (acc, output) => acc + output.toOutput.amount); - final fee = utxos.sumOfUtxosValue() - preOutputSum; + var fee = utxos.sumOfUtxosValue() - preOutputSum; + + // determines if the fee is correct: + BigInt _sumOutputAmounts(List outputs) { + BigInt sum = BigInt.zero; + for (final e in outputs) { + sum += e.amount; + } + return sum; + } + + final sum1 = _sumOutputAmounts(outputs.map((e) => e.toOutput).toList()) + fee; + final sum2 = utxos.sumOfUtxosValue(); + if (sum1 != sum2) { + printV("@@@@@ WE HAD TO ADJUST THE FEE! @@@@@@@@"); + final diff = sum2 - sum1; + // add the difference to the fee (abs value): + fee += diff.abs(); + } + final txb = BitcoinTransactionBuilder(utxos: utxos, outputs: outputs, fee: fee, network: network); - final resp = await _stub.create(CreateRequest( + final resp = await CwMweb.create(CreateRequest( rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(), scanSecret: scanSecret, spendSecret: spendSecret, @@ -819,17 +1064,89 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.isMweb = mwebEnabled; if (!mwebEnabled) { + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb)) + .address; return tx; } - await getStub(); + await waitForMwebAddresses(); - final resp = await _stub.create(CreateRequest( + final resp = await CwMweb.create(CreateRequest( rawTx: hex.decode(tx.hex), scanSecret: scanSecret, spendSecret: spendSecret, feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000, )); final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); + + // check if the transaction doesn't contain any mweb inputs or outputs: + final transactionCredentials = credentials as BitcoinTransactionCredentials; + + bool hasMwebInput = false; + bool hasMwebOutput = false; + bool hasRegularOutput = false; + + for (final output in transactionCredentials.outputs) { + final address = output.address.toLowerCase(); + final extractedAddress = output.extractedAddress?.toLowerCase(); + + if (address.contains("mweb")) { + hasMwebOutput = true; + } + if (!address.contains("mweb")) { + hasRegularOutput = true; + } + if (extractedAddress != null && extractedAddress.isNotEmpty) { + if (extractedAddress.contains("mweb")) { + hasMwebOutput = true; + } + if (!extractedAddress.contains("mweb")) { + hasRegularOutput = true; + } + } + } + + // check if mweb inputs are used: + for (final utxo in tx.utxos) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + hasMwebInput = true; + } + } + + // could probably be simplified but left for clarity: + bool isPegIn = !hasMwebInput && hasMwebOutput; + bool isPegOut = hasMwebInput && hasRegularOutput; + bool isRegular = !hasMwebInput && !hasMwebOutput; + bool shouldNotUseMwebChange = isPegIn || isRegular || !hasMwebInput; + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(coinTypeToSpendFrom: shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any)) + .address; + if (isRegular) { + tx.isMweb = false; + return tx; + } + + // check if any of the inputs of this transaction are hog-ex: + // this list is only non-mweb inputs: + tx2.inputs.forEach((txInput) { + bool isHogEx = true; + + final utxo = unspentCoins + .firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex); + + // TODO: detect actual hog-ex inputs + + if (!isHogEx) { + return; + } + + int confirmations = utxo.confirmations ?? 0; + if (confirmations < 6) { + throw Exception( + "A transaction input has less than 6 confirmations, please try again later."); + } + }); + tx.hexOverride = tx2 .copyWith( witnesses: tx2.inputs.asMap().entries.map((e) { @@ -849,14 +1166,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return TxWitnessInput(stack: [key.signInput(digest), key.getPublic().toHex()]); }).toList()) .toHex(); - tx.outputs = resp.outputId; + tx.outputAddresses = resp.outputId; + return tx ..addListener((transaction) async { final addresses = {}; transaction.inputAddresses?.forEach((id) async { final utxo = mwebUtxosBox.get(id); - await mwebUtxosBox.delete(id); + // await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent if (utxo == null) return; + // mark utxo as spent so we add it to the unconfirmed balance (as negative): + utxo.spent = true; + await mwebUtxosBox.put(id, utxo); final addressRecord = walletAddresses.allAddresses .firstWhere((addressRecord) => addressRecord.address == utxo.address); if (!addresses.contains(utxo.address)) { @@ -865,15 +1186,18 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { addressRecord.balance -= utxo.value.toInt(); }); transaction.inputAddresses?.addAll(addresses); - + printV("isPegIn: $isPegIn, isPegOut: $isPegOut"); + transaction.additionalInfo["isPegIn"] = isPegIn; + transaction.additionalInfo["isPegOut"] = isPegOut; transactionHistory.addOne(transaction); await updateUnspent(); await updateBalance(); }); } catch (e, s) { - print(e); - print(s); + printV(e); + printV(s); if (e.toString().contains("commit failed")) { + printV(e); throw Exception("Transaction commit failed (no peers responded), please try again."); } rethrow; @@ -886,31 +1210,38 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future close() async { - await super.close(); - _syncTimer?.cancel(); + Future close({bool shouldCleanup = false}) async { _utxoStream?.cancel(); + _feeRatesTimer?.cancel(); + _syncTimer?.cancel(); + _processingTimer?.cancel(); + if (shouldCleanup) { + try { + await stopSync(); + } catch (_) {} + } + await super.close(shouldCleanup: shouldCleanup); } Future setMwebEnabled(bool enabled) async { - if (mwebEnabled == enabled) { + if (mwebEnabled == enabled && + alwaysScan == enabled && + (walletAddresses as LitecoinWalletAddresses).mwebEnabled == enabled) { return; } + alwaysScan = enabled; mwebEnabled = enabled; (walletAddresses as LitecoinWalletAddresses).mwebEnabled = enabled; - await stopSync(); + await save(); + try { + await stopSync(); + } catch (_) {} await startSync(); } - Future getStub() async { - _stub = await CwMweb.stub(); - return _stub; - } - Future getStatusRequest() async { - await getStub(); - final resp = await _stub.status(StatusRequest()); + final resp = await CwMweb.status(StatusRequest()); return resp; } @@ -1011,7 +1342,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List possibleRecoverIds = [0, 1]; - final baseAddress = addressTypeFromStr(address, network); + final baseAddress = RegexUtils.addressTypeFromStr(address, network); for (int recoveryId in possibleRecoverIds) { final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId); @@ -1036,4 +1367,62 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return false; } + + LedgerConnection? _ledgerConnection; + LitecoinLedgerApp? _litecoinLedgerApp; + + @override + void setLedgerConnection(LedgerConnection connection) { + _ledgerConnection = connection; + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @override + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final readyInputs = []; + for (final utxo in utxos) { + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + readyInputs.add(LedgerTransaction( + rawTx: rawTx, + outputIndex: utxo.utxo.vout, + ownerPublicKey: Uint8List.fromList(hex.decode(publicKeyAndDerivationPath.publicKey)), + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + // sequence: enableRBF ? 0x1 : 0xffffffff, + sequence: 0xffffffff, + )); + } + + String? changePath; + for (final output in outputs) { + final maybeChangePath = publicKeys[(output as BitcoinOutput).address.pubKeyHash()]; + if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; + } + + final rawHex = await _litecoinLedgerApp!.createTransaction( + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value, + Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true); + + return BtcTransaction.fromRaw(rawHex); + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 0e7434029..bbb987766 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,12 +1,16 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data'; -import 'package:bech32/bech32.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:flutter/foundation.dart'; @@ -14,10 +18,6 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -String encodeMwebAddress(List scriptPubKey) { - return bech32.encode(Bech32("ltcmweb1", scriptPubKey), 250); -} - class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { @@ -26,49 +26,99 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required super.mainHd, required super.sideHd, required super.network, + required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, super.initialAddresses, + super.initialMwebAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, }) : super(walletInfo) { - // start generating mweb addresses in the background: - initMwebAddresses(); + for (int i = 0; i < mwebAddresses.length; i++) { + mwebAddrs.add(mwebAddresses[i].address); + } + printV("initialized with ${mwebAddrs.length} mweb addresses"); } - final Bip32Slip10Secp256k1 mwebHd; + final Bip32Slip10Secp256k1? mwebHd; bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; - static Timer? mwebTopUpTimer; + bool generating = false; - List get scanSecret => mwebHd.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => - mwebHd.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; + mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; - Future ensureMwebAddressUpToIndexExists(int index) async { - Uint8List scan = Uint8List.fromList(scanSecret); - Uint8List spend = Uint8List.fromList(spendPubkey); - while (mwebAddrs.length <= (index + 1)) { - final address = await CwMweb.address(scan, spend, mwebAddrs.length); - mwebAddrs.add(address!); - } + @override + Future init() async { + if (!isHardwareWallet) await initMwebAddresses(); + await super.init(); } - Future generateNumAddresses(int num) async { + @computed + @override + List get allAddresses { + return List.from(super.allAddresses)..addAll(mwebAddresses); + } + + Future ensureMwebAddressUpToIndexExists(int index) async { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return null; + } + Uint8List scan = Uint8List.fromList(scanSecret); Uint8List spend = Uint8List.fromList(spendPubkey); - for (int i = 0; i < num; i++) { - final address = await CwMweb.address(scan, spend, mwebAddrs.length); - mwebAddrs.add(address!); - await Future.delayed(Duration.zero); + + if (index < mwebAddresses.length && index < mwebAddrs.length) { + return; + } + + while (generating) { + printV("generating....."); + // this function was called multiple times in multiple places: + await Future.delayed(const Duration(milliseconds: 100)); + } + + printV("Generating MWEB addresses up to index $index"); + generating = true; + try { + while (mwebAddrs.length <= (index + 1)) { + final addresses = + await CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50); + printV("generated up to index ${mwebAddrs.length}"); + // sleep for a bit to avoid making the main thread unresponsive: + await Future.delayed(Duration(milliseconds: 200)); + mwebAddrs.addAll(addresses!); + } + } catch (_) {} + generating = false; + printV("Done generating MWEB addresses len: ${mwebAddrs.length}"); + + // ensure mweb addresses are up to date: + // This is the Case if the Litecoin Wallet is a hardware Wallet + if (mwebHd == null) return; + + if (mwebAddresses.length < mwebAddrs.length) { + List addressRecords = mwebAddrs + .asMap() + .entries + .map((e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + )) + .toList(); + addMwebAddresses(addressRecords); + printV("set ${addressRecords.length} mweb addresses"); } } Future initMwebAddresses() async { - for (int i = 0; i < 4; i++) { - await generateNumAddresses(250); - await Future.delayed(const Duration(milliseconds: 1500)); + if (mwebAddrs.length < 1000) { + await ensureMwebAddressUpToIndexExists(20); + return; } } @@ -98,10 +148,17 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress({List? outputs, UtxoDetails? utxoDetails}) async { + Future getChangeAddress( + {List? inputs, + List? outputs, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async { // use regular change address on peg in, otherwise use mweb for change address: - if (outputs != null && utxoDetails != null) { + if (!mwebEnabled || coinTypeToSpendFrom == UnspentCoinType.nonMweb) { + return super.getChangeAddress(); + } + + if (inputs != null && outputs != null) { // check if this is a PEGIN: bool outputsToMweb = false; bool comesFromMweb = false; @@ -113,22 +170,44 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with outputsToMweb = true; } } - utxoDetails.availableInputs.forEach((element) { + + inputs.forEach((element) { + if (!element.isSending || element.isFrozen) { + return; + } if (element.address.contains("mweb")) { comesFromMweb = true; } }); bool isPegIn = !comesFromMweb && outputsToMweb; - if (isPegIn && mwebEnabled) { + bool isNonMweb = !comesFromMweb && !outputsToMweb; + + // use regular change address if it's not an mweb tx or if it's a peg in: + if (isPegIn || isNonMweb) { return super.getChangeAddress(); } } if (mwebEnabled) { - return mwebAddrs[0]; + await ensureMwebAddressUpToIndexExists(1); + updateChangeAddresses(); + return BitcoinAddressRecord( + mwebAddrs[0], + index: 0, + type: SegwitAddresType.mweb, + network: network, + ); } return super.getChangeAddress(); } + + @override + String get addressForExchange { + // don't use mweb addresses for exchange refund address: + final addresses = receiveAddresses + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); + return addresses.first.address; + } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7d976fa03..89ae384d4 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -20,8 +21,9 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, - BitcoinNewWalletCredentials> { - LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + BitcoinRestoreWalletFromHardware> { + LitecoinWalletService( + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; @@ -34,11 +36,11 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final String mnemonic; - switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + switch (credentials.walletInfo?.derivationInfo?.derivationType) { case DerivationType.bip39: final strength = credentials.seedPhraseLength == 24 ? 256 : 128; - mnemonic = await MnemonicBip39.generate(strength: strength); + mnemonic = credentials.mnemonic ?? await MnemonicBip39.generate(strength: strength); break; case DerivationType.electrum: default: @@ -66,6 +68,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -103,13 +106,34 @@ class LitecoinWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); - // if there are no more litecoin wallets left, delete the neutrino db: + // if there are no more litecoin wallets left, cleanup the neutrino db and other files created by mwebd: if (walletInfoSource.values.where((info) => info.type == WalletType.litecoin).isEmpty) { - final appDir = await getApplicationSupportDirectory(); - File neturinoDb = File('${appDir.path}/neutrino.db'); + final appDirPath = (await getApplicationSupportDirectory()).path; + File neturinoDb = File('$appDirPath/neutrino.db'); + File blockHeaders = File('$appDirPath/block_headers.bin'); + File regFilterHeaders = File('$appDirPath/reg_filter_headers.bin'); + File mwebdLogs = File('$appDirPath/logs/debug.log'); if (neturinoDb.existsSync()) { neturinoDb.deleteSync(); } + if (blockHeaders.existsSync()) { + blockHeaders.deleteSync(); + } + if (regFilterHeaders.existsSync()) { + regFilterHeaders.deleteSync(); + } + if (mwebdLogs.existsSync()) { + mwebdLogs.deleteSync(); + } + } + + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + + final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); + + if (keysToDelete.isNotEmpty) { + await unspentCoinsInfoSource.deleteAll(keysToDelete); } } @@ -137,9 +161,23 @@ class LitecoinWalletService extends WalletService< } @override - Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { - throw UnimplementedError( - "Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? LitecoinNetwork.testnet : LitecoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + credentials.walletInfo?.derivationInfo?.derivationPath = + credentials.hwAccountData.derivationPath; + + final wallet = await LitecoinWallet( + password: credentials.password!, + xpub: credentials.hwAccountData.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + await wallet.save(); + await wallet.init(); + return wallet; } @override diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart new file mode 100644 index 000000000..95a523d89 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -0,0 +1,310 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_persister.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; +import 'package:cw_bitcoin/payjoin/storage.dart'; +import 'package:cw_bitcoin/psbt/signer.dart'; +import 'package:cw_bitcoin/psbt/utils.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:payjoin_flutter/common.dart'; +import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/config.dart' as pj_config; +import 'package:payjoin_flutter/uri.dart' as PayjoinUri; + +class PayjoinManager { + PayjoinManager(this._payjoinStorage, this._wallet); + + final PayjoinStorage _payjoinStorage; + final BitcoinWalletBase _wallet; + final Map _activePollers = {}; + + static const List ohttpRelayUrls = [ + 'https://pj.bobspacebkk.com', + 'https://ohttp.achow101.com', + 'https://ohttp.cakewallet.com', + ]; + + static String randomOhttpRelayUrl() => + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]; + + static const payjoinDirectoryUrl = 'https://payjo.in'; + + Future initPayjoin() => pj_config.PConfig.initializeApp(); + + Future resumeSessions() async { + final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); + + final spawnedSessions = allSessions.map((session) { + if (session.isSenderSession) { + printV("Resuming Payjoin Sender Session ${session.pjUri!}"); + return _spawnSender( + sender: Sender.fromJson(json: session.sender!), + pjUri: session.pjUri!, + ); + } + final receiver = Receiver.fromJson(json: session.receiver!); + printV("Resuming Payjoin Receiver Session ${receiver.id()}"); + return spawnReceiver(receiver: receiver); + }); + + printV("Resumed ${spawnedSessions.length} Payjoin Sessions"); + await Future.wait(spawnedSessions); + } + + Future initSender( + String pjUriString, String originalPsbt, int networkFeesSatPerVb) async { + try { + final pjUri = + (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported(); + final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250); + final senderBuilder = await SenderBuilder.fromPsbtAndUri( + psbtBase64: originalPsbt, + pjUri: pjUri, + ); + final persister = PayjoinSenderPersister.impl(); + final newSender = + await senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final senderToken = await newSender.persist(persister: persister); + + return Sender.load(token: senderToken, persister: persister); + } catch (e) { + throw Exception('Error initializing Payjoin Sender: $e'); + } + } + + Future spawnNewSender({ + required Sender sender, + required String pjUrl, + required BigInt amount, + bool isTestnet = false, + }) async { + final pjUri = Uri.parse(pjUrl).queryParameters['pj']!; + await _payjoinStorage.insertSenderSession( + sender, pjUri, _wallet.id, amount); + + return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri); + } + + Future _spawnSender({ + required Sender sender, + required String pjUri, + bool isTestnet = false, + }) async { + final completer = Completer(); + final receivePort = ReceivePort(); + + receivePort.listen((message) async { + if (message is Map) { + try { + switch (message['type'] as PayjoinSenderRequestTypes) { + case PayjoinSenderRequestTypes.requestPosted: + return; + case PayjoinSenderRequestTypes.psbtToSign: + final proposalPsbt = message['psbt'] as String; + final utxos = _wallet.getUtxoWithPrivateKeys(); + final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos); + final txId = getTxIdFromPsbtV0(finalizedPsbt); + _wallet.commitPsbt(finalizedPsbt); + + _cleanupSession(pjUri); + await _payjoinStorage.markSenderSessionComplete(pjUri, txId); + completer.complete(); + } + } catch (e) { + _cleanupSession(pjUri); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString()); + completer.complete(); + } + } else if (message is PayjoinSessionError) { + _cleanupSession(pjUri); + if (message is UnrecoverableError) { + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message); + completer.complete(); + } else if (message is RecoverableError) { + completer.complete(); + } else { + completer.completeError(message); + } + } + }); + + final isolate = await Isolate.spawn( + PayjoinSenderWorker.run, + [receivePort.sendPort, sender.toJson(), pjUri], + ); + + _activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort); + + return completer.future; + } + + Future getUnusedReceiver(String address, + [bool isTestnet = false]) async { + final session = _payjoinStorage.getUnusedActiveReceiverSession(_wallet.id); + + if (session != null) { + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); + + return Receiver.fromJson(json: session.receiver!); + } + + return initReceiver(address); + } + + Future initReceiver(String address, [bool isTestnet = false]) async { + final ohttpKeys = await PayjoinUri.fetchOhttpKeys( + ohttpRelay: await randomOhttpRelayUrl(), + payjoinDirectory: payjoinDirectoryUrl, + ); + + final newReceiver = await NewReceiver.create( + address: address, + network: isTestnet ? Network.testnet : Network.bitcoin, + directory: payjoinDirectoryUrl, + ohttpKeys: ohttpKeys, + ); + final persister = PayjoinReceiverPersister.impl(); + final receiverToken = await newReceiver.persist(persister: persister); + final receiver = await Receiver.load(persister: persister, token: receiverToken); + + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); + + return receiver; + } + + Future spawnReceiver({ + required Receiver receiver, + bool isTestnet = false, + }) async { + final completer = Completer(); + final receivePort = ReceivePort(); + + SendPort? mainToIsolateSendPort; + List utxos = []; + String rawAmount = '0'; + + receivePort.listen((message) async { + if (message is Map) { + try { + switch (message['type'] as PayjoinReceiverRequestTypes) { + case PayjoinReceiverRequestTypes.processOriginalTx: + final tx = message['tx'] as String; + rawAmount = getOutputAmountFromTx(tx, _wallet); + break; + case PayjoinReceiverRequestTypes.checkIsOwned: + (_wallet.walletAddresses as BitcoinWalletAddresses) + .newPayjoinReceiver(); + _payjoinStorage.markReceiverSessionInProgress(receiver.id()); + + final inputScript = message['input_script'] as Uint8List; + final isOwned = + _wallet.isMine(Script.fromRaw(byteData: inputScript)); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': isOwned, + }); + break; + + case PayjoinReceiverRequestTypes.checkIsReceiverOutput: + final outputScript = message['output_script'] as Uint8List; + final isReceiverOutput = + _wallet.isMine(Script.fromRaw(byteData: outputScript)); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': isReceiverOutput, + }); + break; + + case PayjoinReceiverRequestTypes.getCandidateInputs: + utxos = _wallet.getUtxoWithPrivateKeys(); + if (utxos.isEmpty) { + await _wallet.updateAllUnspents(); + utxos = _wallet.getUtxoWithPrivateKeys(); + } + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': utxos, + }); + break; + + case PayjoinReceiverRequestTypes.processPsbt: + final psbt = message['psbt'] as String; + final signedPsbt = await _wallet.signPsbt(psbt, utxos); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': signedPsbt, + }); + break; + + case PayjoinReceiverRequestTypes.proposalSent: + _cleanupSession(receiver.id()); + final psbt = message['psbt'] as String; + await _payjoinStorage.markReceiverSessionComplete( + receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount); + completer.complete(); + } + } catch (e) { + _cleanupSession(receiver.id()); + await _payjoinStorage.markReceiverSessionUnrecoverable( + receiver.id(), e.toString()); + completer.completeError(e); + } + } else if (message is PayjoinSessionError) { + _cleanupSession(receiver.id()); + if (message is UnrecoverableError) { + await _payjoinStorage.markReceiverSessionUnrecoverable( + receiver.id(), message.message); + completer.complete(); + } else if (message is RecoverableError) { + completer.complete(); + } else { + completer.completeError(message); + } + } else if (message is SendPort) { + mainToIsolateSendPort = message; + } + }); + + final isolate = await Isolate.spawn( + PayjoinReceiverWorker.run, + [receivePort.sendPort, receiver.toJson()], + ); + + _activePollers[receiver.id()] = PayjoinPollerSession(isolate, receivePort); + + return completer.future; + } + + void cleanupSessions() { + final sessionIds = _activePollers.keys.toList(); + for (final sessionId in sessionIds) { + _cleanupSession(sessionId); + } + } + + void _cleanupSession(String sessionId) { + _activePollers[sessionId]?.close(); + _activePollers.remove(sessionId); + } +} + +class PayjoinPollerSession { + final Isolate isolate; + final ReceivePort port; + + PayjoinPollerSession(this.isolate, this.port); + + void close() { + isolate.kill(); + port.close(); + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_persister.dart b/cw_bitcoin/lib/payjoin/payjoin_persister.dart new file mode 100644 index 000000000..4e395e36a --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_persister.dart @@ -0,0 +1,66 @@ +import 'package:payjoin_flutter/src/generated/api/receive.dart'; +import 'package:payjoin_flutter/src/generated/api/send.dart'; + +class PayjoinSenderPersister implements DartSenderPersister { + static DartSenderPersister impl() { + final impl = PayjoinSenderPersister(); + return DartSenderPersister( + save: (sender) => impl.save(sender: sender), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiSender sender}) async { + final token = sender.key(); + _store[token.toBytes().toString()] = sender; + return token; + } + + Future load({required SenderToken token}) async { + final sender = _store[token.toBytes().toString()]; + if (sender == null) { + throw Exception('Sender not found for the provided token.'); + } + return sender; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} + +class PayjoinReceiverPersister implements DartReceiverPersister { + static DartReceiverPersister impl() { + final impl = PayjoinReceiverPersister(); + return DartReceiverPersister( + save: (receiver) => impl.save(receiver: receiver), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiReceiver receiver}) async { + final token = receiver.key(); + _store[token.toBytes().toString()] = receiver; + return token; + } + + Future load({required ReceiverToken token}) async { + final receiver = _store[token.toBytes().toString()]; + if (receiver == null) { + throw Exception('Receiver not found for the provided token.'); + } + return receiver; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart new file mode 100644 index 000000000..c56148de2 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -0,0 +1,222 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; +import 'package:cw_bitcoin/psbt/signer.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:payjoin_flutter/bitcoin_ffi.dart'; +import 'package:payjoin_flutter/common.dart'; +import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:http/http.dart' as very_insecure_http_do_not_use; // for errors + +enum PayjoinReceiverRequestTypes { + processOriginalTx, + proposalSent, + getCandidateInputs, + checkIsOwned, + checkIsReceiverOutput, + processPsbt; +} + +class PayjoinReceiverWorker { + final SendPort sendPort; + final pendingRequests = >{}; + + PayjoinReceiverWorker._(this.sendPort); + static final client = ProxyWrapper().getHttpIOClient(); + static Future run(List args) async { + await pj.core.init(); + + final sendPort = args[0] as SendPort; + final receiverJson = args[1] as String; + + final worker = PayjoinReceiverWorker._(sendPort); + final receivePort = ReceivePort(); + + sendPort.send(receivePort.sendPort); + receivePort.listen(worker.handleMessage); + + try { + final receiver = Receiver.fromJson(json: receiverJson); + + final uncheckedProposal = + await worker.receiveUncheckedProposal(receiver); + + final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast(); + sendPort.send({ + 'type': PayjoinReceiverRequestTypes.processOriginalTx, + 'tx': BytesUtils.toHexString(originalTx), + }); + + final payjoinProposal = await worker.processPayjoinProposal( + uncheckedProposal, + ); + final psbt = await worker.sendFinalProposal(payjoinProposal); + sendPort.send({ + 'type': PayjoinReceiverRequestTypes.proposalSent, + 'psbt': psbt, + }); + } catch (e) { + if (e is HttpException || + (e is very_insecure_http_do_not_use.ClientException && + e.message.contains("Software caused connection abort"))) { + sendPort.send(PayjoinSessionError.recoverable(e.toString())); + } else { + sendPort.send(PayjoinSessionError.unrecoverable(e.toString())); + } + } + } + + void handleMessage(dynamic message) async { + if (message is Map) { + final requestId = message['requestId'] as String?; + if (requestId != null && pendingRequests.containsKey(requestId)) { + pendingRequests[requestId]!.complete(message['result']); + pendingRequests.remove(requestId); + } + } + } + + Future _sendRequest(PayjoinReceiverRequestTypes type, + [Map data = const {}]) async { + final completer = Completer(); + final requestId = DateTime.now().millisecondsSinceEpoch.toString(); + pendingRequests[requestId] = completer; + + sendPort.send({ + ...data, + 'type': type, + 'requestId': requestId, + }); + + return completer.future; + } + + Future receiveUncheckedProposal(Receiver session) async { + while (true) { + printV("Polling for Proposal (${session.id()})"); + final extractReq = await session.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); + final request = extractReq.$1; + + final url = Uri.parse(request.url.asString()); + final httpRequest = await client.post(url, + headers: {'Content-Type': request.contentType}, body: request.body); + + final proposal = await session.processRes( + body: httpRequest.bodyBytes, ctx: extractReq.$2); + if (proposal != null) return proposal; + } + } + + Future sendFinalProposal(PayjoinProposal finalProposal) async { + final req = await finalProposal.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); + final proposalReq = req.$1; + final proposalCtx = req.$2; + + final request = await client.post( + Uri.parse(proposalReq.url.asString()), + headers: {"Content-Type": proposalReq.contentType}, + body: proposalReq.body, + ); + + await finalProposal.processRes( + res: request.bodyBytes, + ohttpContext: proposalCtx, + ); + + return await finalProposal.psbt(); + } + + Future processPayjoinProposal( + UncheckedProposal proposal) async { + await proposal.extractTxToScheduleBroadcast(); + // TODO Handle this. send to the main port on a timer? + + try { + // Receive Check 1: can broadcast + final pj1 = await proposal.assumeInteractiveReceiver(); + + // Receive Check 2: original PSBT has no receiver-owned inputs + final pj2 = await pj1.checkInputsNotOwned( + isOwned: (inputScript) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.checkIsOwned, + {'input_script': inputScript}, + ); + return result as bool; + }, + ); + // Receive Check 3: sender inputs have not been seen before (prevent probing attacks) + final pj3 = await pj2.checkNoInputsSeenBefore(isKnown: (input) => false); + + // Identify receiver outputs + final pj4 = await pj3.identifyReceiverOutputs( + isReceiverOutput: (outputScript) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.checkIsReceiverOutput, + {'output_script': outputScript}, + ); + return result as bool; + }, + ); + final pj5 = await pj4.commitOutputs(); + + final listUnspent = + await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs); + final unspent = listUnspent as List; + if (unspent.isEmpty) throw RecoverableError('No unspent outputs available'); + + final selectedUtxo = await _inputPairFromUtxo(unspent[0]); + final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]); + final pj7 = await pj6.commitInputs(); + + // Finalize proposal + final payjoinProposal = await pj7.finalizeProposal( + processPsbt: (String psbt) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.processPsbt, {'psbt': psbt}); + return result as String; + }, + // TODO set maxFeeRateSatPerVb + maxFeeRateSatPerVb: BigInt.from(10000), + ); + return payjoinProposal; + } catch (e) { + printV('Error occurred while finalizing proposal: $e'); + rethrow; + } + } + + Future _inputPairFromUtxo(UtxoWithPrivateKey utxo) async { + final txout = TxOut( + value: utxo.utxo.value, + scriptPubkey: Uint8List.fromList( + utxo.ownerDetails.address.toScriptPubKey().toBytes()), + ); + + final psbtin = + PsbtInput(witnessUtxo: txout, redeemScript: null, witnessScript: null); + + final previousOutput = + OutPoint(txid: utxo.utxo.txHash, vout: utxo.utxo.vout); + + final txin = TxIn( + previousOutput: previousOutput, + scriptSig: await Script.newInstance(rawOutputScript: []), + witness: [], + sequence: 0, + ); + + return InputPair.newInstance(txin: txin, psbtin: psbtin); + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart new file mode 100644 index 000000000..7e85cc773 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:cw_bitcoin/payjoin/manager.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:payjoin_flutter/common.dart'; +import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:payjoin_flutter/src/generated/api/send/error.dart' as pj_error; +import 'package:payjoin_flutter/uri.dart' as pj_uri; + +enum PayjoinSenderRequestTypes { + requestPosted, + psbtToSign; +} + +class PayjoinSenderWorker { + final SendPort sendPort; + final pendingRequests = >{}; + final String pjUrl; + + PayjoinSenderWorker._(this.sendPort, this.pjUrl); + + static Future run(List args) async { + await pj.core.init(); + + final sendPort = args[0] as SendPort; + final senderJson = args[1] as String; + final pjUrl = args[2] as String; + + final sender = Sender.fromJson(json: senderJson); + final worker = PayjoinSenderWorker._(sendPort, pjUrl); + + try { + final proposalPsbt = await worker.runSender(sender); + sendPort.send({ + 'type': PayjoinSenderRequestTypes.psbtToSign, + 'psbt': proposalPsbt, + }); + } catch (e) { + sendPort.send(e); + } + } + final client = ProxyWrapper().getHttpIOClient(); + + /// Run a payjoin sender (V2 protocol first, fallback to V1). + Future runSender(Sender sender) async { + + try { + return await _runSenderV2(sender); + } catch (e) { + printV(e); + if (e is pj_error.FfiCreateRequestError) { + return await _runSenderV1(sender); + } else if (e is HttpException) { + printV(e); + throw Exception(PayjoinSessionError.recoverable(e.toString())); + } else { + throw Exception(PayjoinSessionError.unrecoverable(e.toString())); + } + } + } + + /// Attempt to send payjoin using the V2 of the protocol. + Future _runSenderV2(Sender sender) async { + try { + final postRequest = await sender.extractV2( + ohttpProxyUrl: + await pj_uri.Url.fromStr(PayjoinManager.randomOhttpRelayUrl()), + ); + + final postResult = await _postRequest(postRequest.$1); + final getContext = + await postRequest.$2.processResponse(response: postResult); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted, "pj": pjUrl}); + + while (true) { + printV('Polling V2 Proposal Request (${pjUrl})'); + + final getRequest = await getContext.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); + final getRes = await _postRequest(getRequest.$1); + final proposalPsbt = await getContext.processResponse( + response: getRes, + ohttpCtx: getRequest.$2, + ); + printV("$proposalPsbt"); + if (proposalPsbt != null) return proposalPsbt; + } + } catch (e) { + rethrow; + } + } + + /// Attempt to send payjoin using the V1 of the protocol. + Future _runSenderV1(Sender sender) async { + try { + final postRequest = await sender.extractV1(); + final response = await _postRequest(postRequest.$1); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); + + return await postRequest.$2.processResponse(response: response); + } catch (e, stack) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e, $stack'); + } + } + + Future> _postRequest(Request req) async { + final httpRequest = await client.post(Uri.parse(req.url.asString()), + headers: {'Content-Type': req.contentType}, body: req.body); + + return httpRequest.bodyBytes; + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart new file mode 100644 index 000000000..06e0a5431 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart @@ -0,0 +1,16 @@ +class PayjoinSessionError { + final String message; + + const PayjoinSessionError._(this.message); + + factory PayjoinSessionError.recoverable(String message) = RecoverableError; + factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError; +} + +class RecoverableError extends PayjoinSessionError { + const RecoverableError(super.message) : super._(); +} + +class UnrecoverableError extends PayjoinSessionError { + const UnrecoverableError(super.message) : super._(); +} diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart new file mode 100644 index 000000000..5fb9d5716 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -0,0 +1,104 @@ +import 'package:cw_core/payjoin_session.dart'; +import 'package:hive/hive.dart'; +import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/send.dart'; + +class PayjoinStorage { + PayjoinStorage(this._payjoinSessionSources); + + final Box _payjoinSessionSources; + + static const String _receiverPrefix = 'pj_recv_'; + static const String _senderPrefix = 'pj_send_'; + + Future insertReceiverSession( + Receiver receiver, + String walletId, + ) => + _payjoinSessionSources.put( + "$_receiverPrefix${receiver.id()}", + PayjoinSession( + walletId: walletId, + receiver: receiver.toJson(), + ), + ); + + PayjoinSession? getUnusedActiveReceiverSession(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + session.status == PayjoinSessionStatus.created.name && + !session.isSenderSession) + .firstOrNull; + + Future markReceiverSessionComplete( + String sessionId, String txId, String amount) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.success.name; + session.txId = txId; + session.rawAmount = amount; + await session.save(); + } + + Future markReceiverSessionUnrecoverable( + String sessionId, String reason) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; + await session.save(); + } + + Future markReceiverSessionInProgress(String sessionId) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.inProgress.name; + session.inProgressSince = DateTime.now(); + await session.save(); + } + + Future insertSenderSession( + Sender sender, + String pjUrl, + String walletId, + BigInt amount, + ) => + _payjoinSessionSources.put( + "$_senderPrefix$pjUrl", + PayjoinSession( + walletId: walletId, + pjUri: pjUrl, + sender: sender.toJson(), + status: PayjoinSessionStatus.inProgress.name, + inProgressSince: DateTime.now(), + rawAmount: amount.toString(), + ), + ); + + Future markSenderSessionComplete(String pjUrl, String txId) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = PayjoinSessionStatus.success.name; + session.txId = txId; + await session.save(); + } + + Future markSenderSessionUnrecoverable(String pjUrl, String reason) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; + await session.save(); + } + + List readAllOpenSessions(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + ![ + PayjoinSessionStatus.success.name, + PayjoinSessionStatus.unrecoverable.name + ].contains(session.status)) + .toList(); +} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index df5826a3c..6930524eb 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,4 @@ +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -24,6 +25,9 @@ class PendingBitcoinTransaction with PendingTransaction { this.isSendAll = false, this.hasTaprootInputs = false, this.isMweb = false, + this.utxos = const [], + this.publicKeys, + this.commitOverride, }) : _listeners = []; final WalletType type; @@ -36,10 +40,14 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + List utxos; bool isMweb; + String? changeAddressOverride; String? idOverride; String? hexOverride; - List? outputs; + List? outputAddresses; + final Map? publicKeys; + Future Function()? commitOverride; @override String get id => idOverride ?? _tx.txId(); @@ -56,6 +64,22 @@ class PendingBitcoinTransaction with PendingTransaction { @override int? get outputCount => _tx.outputs.length; + List get outputs => _tx.outputs; + + bool get hasSilentPayment => _tx.hasSilentPayment; + + PendingChange? get change { + try { + final change = _tx.outputs.firstWhere((out) => out.isChange); + if (changeAddressOverride != null) { + return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + } + return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); + } catch (_) { + return null; + } + } + final List _listeners; Future _commit() async { @@ -86,6 +110,10 @@ class PendingBitcoinTransaction with PendingTransaction { throw BitcoinTransactionCommitFailedBIP68Final(); } + if (error.contains("min fee not met")) { + throw BitcoinTransactionCommitFailedLessThanMin(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); } @@ -95,16 +123,21 @@ class PendingBitcoinTransaction with PendingTransaction { Future _ltcCommit() async { try { - final stub = await CwMweb.stub(); - final resp = await stub.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); + } catch (e) { + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); } } @override Future commit() async { + if (commitOverride != null) { + return commitOverride?.call(); + } + if (isMweb) { await _ltcCommit(); } else { @@ -124,8 +157,14 @@ class PendingBitcoinTransaction with PendingTransaction { direction: TransactionDirection.outgoing, date: DateTime.now(), isPending: true, + isReplaced: false, confirmations: 0, inputAddresses: _tx.inputs.map((input) => input.txId).toList(), - outputAddresses: outputs, + outputAddresses: outputAddresses, fee: fee); + + @override + Future commitUR() { + throw UnimplementedError(); + } } diff --git a/cw_bitcoin/lib/psbt/signer.dart b/cw_bitcoin/lib/psbt/signer.dart new file mode 100644 index 000000000..1d0ceba8b --- /dev/null +++ b/cw_bitcoin/lib/psbt/signer.dart @@ -0,0 +1,263 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:ledger_bitcoin/psbt.dart'; +import 'package:ledger_bitcoin/src/utils/buffer_writer.dart'; + +extension PsbtSigner on PsbtV2 { + Uint8List extractUnsignedTX({bool getSegwit = true}) { + final tx = BufferWriter()..writeUInt32(getGlobalTxVersion()); + + final isSegwit = getInputWitnessUtxo(0) != null; + if (isSegwit && getSegwit) { + tx.writeSlice(Uint8List.fromList([0, 1])); + } + + final inputCount = getGlobalInputCount(); + tx.writeVarInt(inputCount); + + for (var i = 0; i < inputCount; i++) { + tx + ..writeSlice(getInputPreviousTxid(i)) + ..writeUInt32(getInputOutputIndex(i)) + ..writeVarSlice(Uint8List(0)) + ..writeUInt32(getInputSequence(i)); + } + + final outputCount = getGlobalOutputCount(); + tx.writeVarInt(outputCount); + for (var i = 0; i < outputCount; i++) { + tx.writeUInt64(getOutputAmount(i)); + tx.writeVarSlice(getOutputScript(i)); + } + tx.writeUInt32(getGlobalFallbackLocktime() ?? 0); + return tx.buffer(); + } + + Future signWithUTXO( + List utxos, UTXOSignerCallBack signer, + [UTXOGetterCallBack? getTaprootPair]) async { + final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false)); + final tx = BtcTransaction.fromRaw(raw); + + /// when the transaction is taproot and we must use getTaproot transaction + /// digest we need all of inputs amounts and owner script pub keys + List taprootAmounts = []; + List