diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 84c680dda..47b08c44d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -55,7 +55,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.27.4" + flutter-version: "3.27.0" channel: stable - name: Install package dependencies @@ -153,8 +153,8 @@ jobs: echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -168,6 +168,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -178,7 +179,8 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/no_http_imports.yaml b/.github/workflows/no_http_imports.yaml new file mode 100644 index 000000000..dad6821ac --- /dev/null +++ b/.github/workflows/no_http_imports.yaml @@ -0,0 +1,21 @@ +name: No http imports + +on: [pull_request] + +jobs: + PR_test_build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for http package usage + if: github.event_name == 'pull_request' + run: | + GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) || true)" + [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 + echo "$GIT_GREP_OUT" + echo "There are .dart files which use http imports" + echo "Using http package breaks proxy integration" + echo "Please use ProxyWrapper.getHttpClient() from package:cw_core/utils/proxy_wrapper.dart" + exit 1 + \ No newline at end of file diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 9c3d82bc2..507793bd8 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -15,5 +15,5 @@ jobs: [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 echo "$GIT_GREP_OUT" echo "There are .dart files which use print() statements" - echo "Please use printV from package: cw_core/utils/print_verbose.dart" + echo "Please use printV from package:cw_core/utils/print_verbose.dart" exit 1 diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 8f6139747..f7c226ce4 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -98,8 +98,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -113,6 +113,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -124,7 +125,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart @@ -253,6 +255,11 @@ jobs: - name: Build generated code run: | + flutter --version + flutter clean + rm -rf .dart_tool + rm pubspec.lock + flutter pub get ./model_generator.sh async - name: Generate key properties diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 476a033a0..f057b19e5 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -91,8 +91,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -106,6 +106,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -117,7 +118,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/Dockerfile b/Dockerfile index 84179d645..151b7af20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -15,11 +15,11 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.27.4 +ENV FLUTTER_VERSION=3.27.0 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools -ENV ANDROID_SDK_TOOLS_VERSION=11076708 +ENV ANDROID_SDK_TOOLS_VERSION=13114758 # Comes from https://developer.android.com/studio/releases/build-tools ENV ANDROID_PLATFORM_VERSION=35 ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 @@ -164,9 +164,12 @@ RUN (addgroup kvm || true) && \ ENV PATH=${HOME}/.cargo/bin:${PATH} RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ cargo install cargo-ndk && \ + for toolchain in stable nightly; \ + do \ for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu; \ do \ - rustup target add --toolchain stable $target; \ + rustup target add --toolchain $toolchain $target; \ + done \ done # Download and install Flutter @@ -175,8 +178,11 @@ ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} ENV FLUTTER_ROOT=$FLUTTER_HOME ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin -RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \ - && yes | flutter doctor --android-licenses \ +RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} && \ + cd ${FLUTTER_HOME} && \ + git fetch -a + +RUN yes | flutter doctor --android-licenses \ && flutter doctor \ && chown -R root:root ${FLUTTER_HOME} diff --git a/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/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/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/tor_logo.svg b/assets/images/tor_logo.svg new file mode 100644 index 000000000..ebd00324d --- /dev/null +++ b/assets/images/tor_logo.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/usdtbsc_icon.png b/assets/images/usdtbsc_icon.png new file mode 100644 index 000000000..9f2cda237 Binary files /dev/null and b/assets/images/usdtbsc_icon.png differ diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 1176d3d8c..faf57258a 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,4 @@ -New themes and UI/UX improvements -Ledger flow enhancements +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 b68800bd9..c49b895e3 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,5 +1,9 @@ -New themes and UI/UX improvements -Silent Payments scanning fix -Payjoin enhancements -Ledger flow enhancements +Add built-in Tor support (experimental) +Add dEuro investments +Solana fixes/enhancements +Polygon fixes/enhancements +WalletConnect improvements +Ledger improvements +Payjoin improvements +UI/UX improvements Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index d6e931068..0d985b237 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -17,16 +17,21 @@ BitcoinBaseAddress addressFromScript(Script script, switch (addressType) { case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromScriptPubkey(script: script); + return P2pkhAddress.fromScriptPubkey( + script: script, network: BitcoinNetwork.mainnet); case P2shAddressType.p2pkhInP2sh: case P2shAddressType.p2pkInP2sh: - return P2shAddress.fromScriptPubkey(script: script); - case SegwitAddressType.p2wpkh: - return P2wpkhAddress.fromScriptPubkey(script: script); - case SegwitAddressType.p2wsh: - return P2wshAddress.fromScriptPubkey(script: script); - case SegwitAddressType.p2tr: - return P2trAddress.fromScriptPubkey(script: script); + 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/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 97b3c08f8..1509f913a 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -82,7 +82,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { type: decoded['type'] != null && decoded['type'] != '' ? BitcoinAddressType.values .firstWhere((type) => type.toString() == decoded['type'] as String) - : SegwitAddressType.p2wpkh, + : SegwitAddresType.p2wpkh, scriptHash: decoded['scriptHash'] as String?, network: network, ); diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8331a182d..07083e111 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -36,9 +36,9 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: - return SegwitAddressType.p2tr; + return SegwitAddresType.p2tr; case BitcoinReceivePageOption.p2wsh: - return SegwitAddressType.p2wsh; + return SegwitAddresType.p2wsh; case BitcoinReceivePageOption.p2pkh: return P2pkhAddressType.p2pkh; case BitcoinReceivePageOption.p2sh: @@ -46,20 +46,20 @@ class BitcoinReceivePageOption implements ReceivePageOption { case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; case BitcoinReceivePageOption.mweb: - return SegwitAddressType.mweb; + return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: default: - return SegwitAddressType.p2wpkh; + return SegwitAddresType.p2wpkh; } } factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { switch (type) { - case SegwitAddressType.p2tr: + case SegwitAddresType.p2tr: return BitcoinReceivePageOption.p2tr; - case SegwitAddressType.p2wsh: + case SegwitAddresType.p2wsh: return BitcoinReceivePageOption.p2wsh; - case SegwitAddressType.mweb: + case SegwitAddresType.mweb: return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; @@ -67,7 +67,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2sh; case SilentPaymentsAddresType.p2sp: return BitcoinReceivePageOption.silent_payments; - case SegwitAddressType.p2wpkh: + case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 73a6ad0ea..9231022f6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -73,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) @@ -93,12 +94,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mainHd: hd, sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, isHardwareWallet: walletInfo.isHardwareWallet, payjoinManager: payjoinManager); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = + this.isEnabledAutoGenerateSubaddress; }); } @@ -133,7 +136,8 @@ 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; } @@ -206,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; @@ -216,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: @@ -259,10 +266,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { derivationPath: walletInfo.derivationInfo!.derivationPath!); } + @override + Future close({bool shouldCleanup = false}) async { + payjoinManager.cleanupSessions(); + super.close(shouldCleanup: shouldCleanup); + } + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values - .where((element) => element.walletId == id && element.isSending && !element.isFrozen) + .where((element) => + element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; Future buildPsbt({ @@ -279,8 +293,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }) async { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = + await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = + publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -292,7 +308,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - return PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) + return PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) .psbt; } @@ -331,7 +348,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; - final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; + final tx = (await super.createTransaction(credentials)) + as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null) return tx; @@ -354,12 +372,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { publicKeys: tx.publicKeys!, masterFingerprint: Uint8List(0)); - final originalPsbt = - await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + final originalPsbt = await signPsbt( + base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.commitOverride = () async { - final sender = - await payjoinManager.initSender(payjoinUri, originalPsbt, int.parse(tx.feeRate)); + final sender = await payjoinManager.initSender( + payjoinUri, originalPsbt, int.parse(tx.feeRate)); payjoinManager.spawnNewSender( sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); }; @@ -375,7 +393,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future commitPsbt(String finalizedPsbt) { final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); - final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + final btcTx = + BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); return PendingBitcoinTransaction( btcTx, @@ -389,11 +408,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ).commit(); } - Future signPsbt(String preProcessedPsbt, List utxos) async { + 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 + return utxo.utxo.isP2tr() ? key.signTapRoot( txDigest, sighash: sighash, @@ -414,15 +434,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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(message: ascii.encode(message), signDerivationPath: derivationPath); + 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 dd6dc5fae..d84d958be 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -31,12 +31,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final PayjoinManager payjoinManager; - @observable payjoin.Receiver? currentPayjoinReceiver; - @computed - String? get payjoinEndpoint => - currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint(); + @observable + String? payjoinEndpoint = null; @override String getAddress( @@ -47,10 +45,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (addressType == P2pkhAddressType.p2pkh) return generateP2PKHAddress(hd: hd, index: index, network: network); - if (addressType == SegwitAddressType.p2tr) + if (addressType == SegwitAddresType.p2tr) return generateP2TRAddress(hd: hd, index: index, network: network); - if (addressType == SegwitAddressType.p2wsh) + if (addressType == SegwitAddresType.p2wsh) return generateP2WSHAddress(hd: hd, index: index, network: network); if (addressType == P2shAddressType.p2wpkhInP2sh) @@ -59,16 +57,32 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return generateP2WPKHAddress(hd: hd, index: index, network: network); } + @action Future initPayjoin() async { - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - - payjoinManager.resumeSessions(); + try { + await payjoinManager.initPayjoin(); + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + + payjoinManager.resumeSessions(); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } } + @action Future newPayjoinReceiver() async { - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); + try { + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); - printV("Initializing new Payjoin Receiver"); - payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); + payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!); + } catch (e) { + printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; + } } } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 1f5c369e3..2ddd30df6 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -42,7 +44,7 @@ class ElectrumClient { static const aliveTimerDuration = Duration(seconds: 4); bool get isConnected => _isConnected; - Socket? socket; + ProxySocket? socket; void Function(ConnectionStatus)? onConnectionStatusChange; int _id; final Map _tasks; @@ -72,18 +74,11 @@ class ElectrumClient { } catch (_) {} socket = null; + final ssl = !(useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))); try { - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); - } + socket = await ProxyWrapper().getSocksSocket(ssl, host, port, connectionTimeout: connectionTimeout); } catch (e) { + printV("connect: $e"); if (e is HandshakeException) { useSSL = !(useSSL ?? false); } @@ -105,7 +100,6 @@ class ElectrumClient { // use ping to determine actual connection status since we could've just not timed out yet: // _setConnectionStatus(ConnectionStatus.connected); - socket!.listen( (Uint8List event) { try { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index d7449c011..bb9cea1bc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,9 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/format_amount.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; @@ -17,7 +19,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cw_bitcoin/electrum.dart' as electrum; +import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; @@ -48,7 +50,6 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -68,7 +69,7 @@ abstract class ElectrumWalletBase Uint8List? seedBytes, this.passphrase, List? initialAddresses, - electrum.ElectrumClient? electrumClient, + ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, @@ -95,7 +96,7 @@ abstract class ElectrumWalletBase this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? electrum.ElectrumClient(); + this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -166,7 +167,7 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - late electrum.ElectrumClient electrumClient; + late ElectrumClient electrumClient; Box unspentCoinsInfo; @override @@ -181,7 +182,7 @@ abstract class ElectrumWalletBase SyncStatus syncStatus; Set get addressesSet => walletAddresses.allAddresses - .where((element) => element.type != SegwitAddressType.mweb) + .where((element) => element.type != SegwitAddresType.mweb) .map((addr) => addr.address) .toSet(); @@ -332,14 +333,14 @@ abstract class ElectrumWalletBase final receivePort = ReceivePort(); _isolate = Isolate.spawn( - _handleScanSilentPayments, + startRefresh, ScanData( sendPort: receivePort.sendPort, silentAddress: walletAddresses.silentAddress!, network: network, height: height, chainTip: chainTip, - electrumClient: electrum.ElectrumClient(), + electrumClient: ElectrumClient(), transactionHistoryIds: transactionHistory.transactions.keys.toList(), node: (await getNodeSupportsSilentPayments()) == true ? ScanNode(node!.uri, node!.useSSL) @@ -438,6 +439,7 @@ abstract class ElectrumWalletBase BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), ) : silentAddress.B_spend, + network: network, ); final addressRecord = walletAddresses.silentAddresses @@ -491,10 +493,9 @@ abstract class ElectrumWalletBase Future updateFeeRates() async { if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = await http - .get(Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) - .timeout(Duration(seconds: 5)); - + final response = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 15)); final result = json.decode(response.body) as Map; final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; @@ -562,7 +563,7 @@ abstract class ElectrumWalletBase node!.save(); return node!.supportsSilentPayments!; } - } on electrum.RequestFailedTimeoutException catch (_) { + } on RequestFailedTimeoutException catch (_) { node!.supportsSilentPayments = false; node!.save(); return node!.supportsSilentPayments!; @@ -623,9 +624,9 @@ abstract class ElectrumWalletBase switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return utx.bitcoinAddressRecord.type == SegwitAddressType.mweb; + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: - return utx.bitcoinAddressRecord.type != SegwitAddressType.mweb; + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: return true; } @@ -633,7 +634,7 @@ abstract class ElectrumWalletBase 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 == SegwitAddressType.mweb ? 1 : -1); + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1); for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; @@ -641,7 +642,7 @@ abstract class ElectrumWalletBase if (paysToSilentPayment) { // Check inputs for shared secret derivation - if (utx.bitcoinAddressRecord.type == SegwitAddressType.p2wsh) { + if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { throw BitcoinTransactionSilentPaymentsNotSupported(); } } @@ -676,7 +677,7 @@ abstract class ElectrumWalletBase if (privkey != null) { inputPrivKeyInfos.add(ECPrivateInfo( privkey, - address.type == SegwitAddressType.p2tr, + address.type == SegwitAddresType.p2tr, tweak: !isSilentPayment, )); @@ -1162,7 +1163,7 @@ abstract class ElectrumWalletBase throw Exception(error); } - if (utxo.utxo.isP2tr) { + if (utxo.utxo.isP2tr()) { hasTaprootInputs = true; return key.privkey.signTapRoot( txDigest, @@ -1229,7 +1230,7 @@ abstract class ElectrumWalletBase 'change_address_index': walletAddresses.currentChangeAddressIndexByType, 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), 'address_page_type': walletInfo.addressPageType == null - ? SegwitAddressType.p2wpkh.toString() + ? SegwitAddresType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, @@ -1369,7 +1370,7 @@ abstract class ElectrumWalletBase List updatedUnspentCoins = []; final previousUnspentCoins = List.from(unspentCoins.where((utxo) => - utxo.bitcoinAddressRecord.type != SegwitAddressType.mweb && + utxo.bitcoinAddressRecord.type != SegwitAddresType.mweb && utxo.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)); if (hasSilentPaymentsScanning) { @@ -1383,13 +1384,13 @@ abstract class ElectrumWalletBase // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating walletAddresses.allAddresses - .where((element) => element.type != SegwitAddressType.mweb) + .where((element) => element.type != SegwitAddresType.mweb) .forEach((addr) { if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; }); final addressFutures = walletAddresses.allAddresses - .where((element) => element.type != SegwitAddressType.mweb) + .where((element) => element.type != SegwitAddresType.mweb) .map((address) => fetchUnspent(address)) .toList(); @@ -1830,7 +1831,7 @@ abstract class ElectrumWalletBase throw Exception("Cannot find private key"); } - if (utxo.utxo.isP2tr) { + if (utxo.utxo.isP2tr()) { return key.signTapRoot(txDigest, sighash: sighash); } else { return key.signInput(txDigest, sigHash: sighash); @@ -1876,20 +1877,17 @@ abstract class ElectrumWalletBase if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { try { - final blockHash = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block-height/$height", - ), - ); + final blockHash = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) + .timeout(Duration(seconds: 15)); if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty && jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", - ), - ); + final blockResponse = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) + .timeout(Duration(seconds: 15)); + if (blockResponse.statusCode == 200 && blockResponse.body.isNotEmpty && jsonDecode(blockResponse.body)['timestamp'] != null) { @@ -1980,7 +1978,7 @@ abstract class ElectrumWalletBase .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { await Future.wait(LITECOIN_ADDRESS_TYPES - .where((type) => type != SegwitAddressType.mweb) + .where((type) => type != SegwitAddresType.mweb) .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } @@ -2169,7 +2167,7 @@ abstract class ElectrumWalletBase final unsubscribedScriptHashes = walletAddresses.allAddresses.where( (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && - address.type != SegwitAddressType.mweb, + address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { @@ -2392,9 +2390,9 @@ abstract class ElectrumWalletBase derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @action - void _onConnectionStatusChange(electrum.ConnectionStatus status) { + void _onConnectionStatusChange(ConnectionStatus status) { switch (status) { - case electrum.ConnectionStatus.connected: + case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus || syncStatus is ConnectingSyncStatus) { @@ -2402,19 +2400,19 @@ abstract class ElectrumWalletBase } break; - case electrum.ConnectionStatus.disconnected: + case ConnectionStatus.disconnected: if (syncStatus is! NotConnectedSyncStatus && syncStatus is! ConnectingSyncStatus && syncStatus is! SyncronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; - case electrum.ConnectionStatus.failed: + case ConnectionStatus.failed: if (syncStatus is! LostConnectionSyncStatus) { syncStatus = LostConnectionSyncStatus(); } break; - case electrum.ConnectionStatus.connecting: + case ConnectionStatus.connecting: if (syncStatus is! ConnectingSyncStatus) { syncStatus = ConnectingSyncStatus(); } @@ -2526,7 +2524,7 @@ class ScanData { final ScanNode? node; final BasedUtxoNetwork network; final int chainTip; - final electrum.ElectrumClient electrumClient; + final ElectrumClient electrumClient; final List transactionHistoryIds; final Map labels; final List labelIndexes; @@ -2570,234 +2568,6 @@ class SyncResponse { SyncResponse(this.height, this.syncStatus); } -Future _handleScanSilentPayments(ScanData scanData) async { - try { - // if (scanData.shouldSwitchNodes) { - var scanningClient = await ElectrumProvider.connect( - ElectrumTCPService.connect( - Uri.parse("tcp://electrs.cakewallet.com:50001"), - ), - ); - // } - - int syncHeight = scanData.height; - int initialSyncHeight = syncHeight; - - final receiver = Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - ); - - int getCountToScanPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } - - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } - - // Initial status UI update, send how many blocks in total to scan - scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - - final req = ElectrumTweaksSubscribe( - height: syncHeight, - count: getCountToScanPerRequest(syncHeight), - historicalMode: false, - ); - - var _scanningStream = await scanningClient.subscribe(req); - - void listenFn(Map event, ElectrumTweaksSubscribe req) { - final response = req.onResponse(event); - - if (response == null || _scanningStream == null) { - return; - } - - // is success or error msg - final noData = response.message != null; - - if (noData) { - if (scanData.isSingleScan) { - return; - } - - // re-subscribe to continue receiving messages, starting from the next unscanned height - final nextHeight = syncHeight + 1; - - if (nextHeight <= scanData.chainTip) { - final nextStream = scanningClient.subscribe( - ElectrumTweaksSubscribe( - height: nextHeight, - count: getCountToScanPerRequest(nextHeight), - historicalMode: false, - ), - ); - - if (nextStream != null) { - nextStream.listen((event) => listenFn(event, req)); - } else { - scanData.sendPort.send( - SyncResponse(scanData.height, LostConnectionSyncStatus()), - ); - } - } - - return; - } - - final tweakHeight = response.block; - - if (initialSyncHeight < tweakHeight) initialSyncHeight = tweakHeight; - - // Continuous status UI update, send how many blocks left to scan - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, tweakHeight); - - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - - try { - final blockTweaks = response.blockTweaks; - - for (final txid in blockTweaks.keys) { - final tweakData = blockTweaks[txid]; - final outputPubkeys = tweakData!.outputPubkeys; - final tweak = tweakData.tweak; - - try { - final addToWallet = {}; - - // receivers.forEach((receiver) { - // NOTE: scanOutputs, from sp_scanner package, called from rust here - final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); - - if (scanResult.isEmpty) { - continue; - } - - if (addToWallet[receiver.BSpend] == null) { - addToWallet[receiver.BSpend] = scanResult; - } else { - addToWallet[receiver.BSpend].addAll(scanResult); - } - // }); - - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } - - // initial placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) on the following loop - final txInfo = ElectrumTransactionInfo( - WalletType.bitcoin, - id: txid, - height: tweakHeight, - amount: 0, - fee: 0, - direction: TransactionDirection.incoming, - isReplaced: false, - date: DateTime.fromMillisecondsSinceEpoch( - DateTime.now().millisecondsSinceEpoch * 1000, - ), - confirmations: scanData.chainTip - tweakHeight + 1, - isReceivedSilentPayment: true, - isPending: false, - unspents: [], - ); - - List unspents = []; - - addToWallet.forEach((BSpend, scanResultPerLabel) { - scanResultPerLabel.forEach((label, scanOutput) { - final labelValue = label == "None" ? null : label.toString(); - - (scanOutput as Map).forEach((outputPubkey, tweak) { - final t_k = tweak as String; - - final receivingOutputAddress = ECPublic.fromHex(outputPubkey) - .toTaprootAddress(tweak: false) - .toAddress(scanData.network); - - final matchingOutput = outputPubkeys[outputPubkey]!; - final amount = matchingOutput.amount; - final pos = matchingOutput.vout; - - // final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere( - // (receiver) => receiver.B_spend.toHex() == BSpend.toString(), - // ); - - // final labelIndex = labelValue != null ? scanData.labels[label] : 0; - // final balance = ElectrumBalance(); - // balance.confirmed = amount; - - final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( - receivingOutputAddress, - index: 0, - isHidden: false, - isUsed: true, - network: scanData.network, - silentPaymentTweak: t_k, - type: SegwitAddressType.p2tr, - txCount: 1, - balance: amount, - ); - - final unspent = BitcoinSilentPaymentsUnspent( - receivedAddressRecord, - txid, - amount, - pos, - silentPaymentTweak: t_k, - silentPaymentLabel: labelValue, - ); - - unspents.add(unspent); - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); - }); - }); - - scanData.sendPort.send({txInfo.id: txInfo}); - } catch (e, stacktrace) { - printV(stacktrace); - printV(e.toString()); - } - } - } catch (e, stacktrace) { - printV(stacktrace); - printV(e.toString()); - } - - syncHeight = tweakHeight; - - if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) { - if (tweakHeight >= scanData.chainTip) - scanData.sendPort.send( - SyncResponse(syncHeight, SyncedTipSyncStatus(scanData.chainTip)), - ); - - if (scanData.isSingleScan) { - scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - } - - _scanningStream?.close(); - _scanningStream = null; - return; - } - } - - _scanningStream?.listen((event) => listenFn(event, req)); - } catch (e) { - printV("Error in _handleScanSilentPayments: $e"); - scanData.sendPort.send(SyncResponse(scanData.height, LostConnectionSyncStatus())); - } -} - Future startRefresh(ScanData scanData) async { int syncHeight = scanData.height; int initialSyncHeight = syncHeight; @@ -2810,7 +2580,7 @@ Future startRefresh(ScanData scanData) async { useSSL: scanData.node?.useSSL ?? false, ); - int getCountToScanPerRequest(int syncHeight) { + int getCountPerRequest(int syncHeight) { if (scanData.isSingleScan) { return 1; } @@ -2825,10 +2595,11 @@ Future startRefresh(ScanData scanData) async { scanData.silentAddress.B_spend.toHex(), scanData.network == BitcoinNetwork.testnet, scanData.labelIndexes, + scanData.labelIndexes.length, ); // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountToScanPerRequest(syncHeight); + final initialCount = getCountPerRequest(syncHeight); scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); tweaksSubscription = await electrumClient.tweaksSubscribe( @@ -2839,24 +2610,22 @@ Future startRefresh(ScanData scanData) async { Future listenFn(t) async { final tweaks = t as Map; final msg = tweaks["message"]; - - // is success or error msg + // success or error msg final noData = msg != null; if (noData) { - if (scanData.isSingleScan) { - return; - } - // re-subscribe to continue receiving messages, starting from the next unscanned height final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); - if (nextHeight <= scanData.chainTip) { - final nextStream = electrumClient.tweaksSubscribe( + if (nextCount > 0) { + tweaksSubscription?.close(); + + final nextTweaksSubscription = electrumClient.tweaksSubscribe( height: nextHeight, - count: getCountToScanPerRequest(nextHeight), + count: nextCount, ); - nextStream?.listen(listenFn); + nextTweaksSubscription?.listen(listenFn); } return; @@ -2938,7 +2707,7 @@ Future startRefresh(ScanData scanData) async { isUsed: true, network: scanData.network, silentPaymentTweak: t_k, - type: SegwitAddressType.p2tr, + type: SegwitAddresType.p2tr, txCount: 1, balance: amount!, ); @@ -3031,15 +2800,15 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { } else if (type is P2shAddress) { return P2shAddressType.p2wpkhInP2sh; } else if (type is P2wshAddress) { - return SegwitAddressType.p2wsh; + return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { - return SegwitAddressType.p2tr; + return SegwitAddresType.p2tr; } else if (type is MwebAddress) { - return SegwitAddressType.mweb; + return SegwitAddresType.mweb; } else if (type is SilentPaymentsAddresType) { return SilentPaymentsAddresType.p2sp; } else { - return SegwitAddressType.p2wpkh; + return SegwitAddresType.p2wpkh; } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 9d1ae54aa..614a06a3b 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -17,16 +17,16 @@ part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; const List BITCOIN_ADDRESS_TYPES = [ - SegwitAddressType.p2wpkh, + SegwitAddresType.p2wpkh, P2pkhAddressType.p2pkh, - SegwitAddressType.p2tr, - SegwitAddressType.p2wsh, + SegwitAddresType.p2tr, + SegwitAddresType.p2wsh, P2shAddressType.p2wpkhInP2sh, ]; const List LITECOIN_ADDRESS_TYPES = [ - SegwitAddressType.p2wpkh, - SegwitAddressType.mweb, + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, ]; const List BITCOIN_CASH_ADDRESS_TYPES = [ @@ -62,7 +62,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) - : SegwitAddressType.p2wpkh), + : SegwitAddresType.p2wpkh), silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), currentSilentAddressIndex = initialSilentAddressIndex, @@ -71,12 +71,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { super(walletInfo) { if (masterHd != null) { silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex( - masterHd.derivePath("m/352'/1'/0'/1'/0").privateKey.toHex(), - ), - b_spend: ECPrivate.fromHex( - masterHd.derivePath("m/352'/1'/0'/0'/0").privateKey.toHex(), - ), + b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()), + b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()), + network: network, ); if (silentAddresses.length == 0) { @@ -147,13 +144,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } - final typeMatchingAddresses = - _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); - final typeMatchingReceiveAddresses = - typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); + final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); if (!isEnabledAutoGenerateSubaddress) { - if (previousAddressRecord != null && previousAddressRecord!.type == addressPageType) { + if (previousAddressRecord != null && + previousAddressRecord!.type == addressPageType) { return previousAddressRecord!.address; } @@ -253,17 +249,17 @@ 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(type: SegwitAddressType.p2wpkh); + await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await _generateInitialAddresses(type: SegwitAddressType.mweb); + await _generateInitialAddresses(type: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { await _generateInitialAddresses(); if (!isHardwareWallet) { await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddressType.p2tr); - await _generateInitialAddresses(type: SegwitAddressType.p2wsh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); } } @@ -327,7 +323,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) { final currentSilentAddressIndex = silentAddresses - .where((addressRecord) => addressRecord.type != SegwitAddressType.p2tr) + .where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr) .length - 1; @@ -385,7 +381,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void addBitcoinAddressTypes() { final lastP2wpkh = _addresses .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddressType.p2wpkh)) + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() .last; if (lastP2wpkh.address != address) { @@ -411,7 +407,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } final lastP2tr = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddressType.p2tr)); + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); if (lastP2tr.address != address) { addressesMap[lastP2tr.address] = 'P2TR'; } else { @@ -419,7 +415,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } final lastP2wsh = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddressType.p2wsh)); + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); if (lastP2wsh.address != address) { addressesMap[lastP2wsh.address] = 'P2WSH'; } else { @@ -444,7 +440,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void addLitecoinAddressTypes() { final lastP2wpkh = _addresses .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddressType.p2wpkh)) + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() .last; if (lastP2wpkh.address != address) { @@ -454,7 +450,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } final lastMweb = _addresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddressType.mweb)); + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); if (lastMweb.address != address) { addressesMap[lastMweb.address] = 'MWEB'; } else { @@ -564,14 +560,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressRecord.isHidden && !addressRecord.isUsed && // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type - (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddressType.p2wpkh)); + (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); changeAddresses.addAll(newAddresses); } @action Future discoverAddresses(List addressList, bool isHidden, Future Function(BitcoinAddressRecord) getAddressHistory, - {BitcoinAddressType type = SegwitAddressType.p2wpkh}) async { + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { final newAddresses = await _createNewAddresses(gap, startIndex: addressList.length, isHidden: isHidden, type: type); addAddresses(newAddresses); @@ -585,7 +581,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Future _generateInitialAddresses( - {BitcoinAddressType type = SegwitAddressType.p2wpkh}) async { + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { var countOfReceiveAddresses = 0; var countOfHiddenAddresses = 0; @@ -662,7 +658,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void _validateAddresses() { _addresses.forEach((element) async { - if (element.type == SegwitAddressType.mweb) { + if (element.type == SegwitAddresType.mweb) { // this would add a ton of startup lag for mweb addresses since we have 1000 of them return; } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 3e5f331df..990719089 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -87,8 +87,8 @@ class ElectrumWalletSnapshot { final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - var regularAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; - var changeAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; + var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var silentAddressIndex = 0; final derivationType = DerivationType @@ -97,10 +97,10 @@ class ElectrumWalletSnapshot { try { regularAddressIndexByType = { - SegwitAddressType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') + SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') }; changeAddressIndexByType = { - SegwitAddressType.p2wpkh.toString(): + SegwitAddresType.p2wpkh.toString(): int.parse(data['change_address_index'] as String? ?? '0') }; silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 4e6b2536a..08c56c600 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -16,6 +16,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -970,9 +971,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List? inputPrivKeyInfos, List? vinOutpoints, }) async { - bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddressType.mweb); + bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); bool paysToMweb = outputs - .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddressType.mweb); + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); bool isRegular = !spendsMweb && !paysToMweb; bool isMweb = spendsMweb || paysToMweb; @@ -1063,9 +1064,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx.isMweb = mwebEnabled; if (!mwebEnabled) { - tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb)) - .address; + tx.changeAddressOverride = + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb)) + .address; return tx; } await waitForMwebAddresses(); @@ -1107,7 +1108,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // check if mweb inputs are used: for (final utxo in tx.utxos) { - if (utxo.utxo.scriptType == SegwitAddressType.mweb) { + if (utxo.utxo.scriptType == SegwitAddresType.mweb) { hasMwebInput = true; } } @@ -1118,9 +1119,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool isRegular = !hasMwebInput && !hasMwebOutput; bool shouldNotUseMwebChange = isPegIn || isRegular || !hasMwebInput; tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress( - coinTypeToSpendFrom: - shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any)) + .getChangeAddress(coinTypeToSpendFrom: shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any)) .address; if (isRegular) { tx.isMweb = false; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 5e5a27fe8..bbb987766 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -106,7 +106,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with .map((e) => BitcoinAddressRecord( e.value, index: e.key, - type: SegwitAddressType.mweb, + type: SegwitAddresType.mweb, network: network, )) .toList(); @@ -128,7 +128,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType, }) { - if (addressType == SegwitAddressType.mweb) { + if (addressType == SegwitAddresType.mweb) { return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; } return generateP2WPKHAddress(hd: hd, index: index, network: network); @@ -140,7 +140,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType, }) async { - if (addressType == SegwitAddressType.mweb) { + if (addressType == SegwitAddresType.mweb) { await ensureMwebAddressUpToIndexExists(index); } return getAddress(index: index, hd: hd, addressType: addressType); @@ -195,7 +195,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with return BitcoinAddressRecord( mwebAddrs[0], index: 0, - type: SegwitAddressType.mweb, + type: SegwitAddresType.mweb, network: network, ); } @@ -207,7 +207,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with String get addressForExchange { // don't use mweb addresses for exchange refund address: final addresses = receiveAddresses - .where((element) => element.type == SegwitAddressType.p2wpkh && !element.isUsed); + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } } diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index 9476ce23e..95a523d89 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_persister.dart'; import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; @@ -16,6 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/config.dart' as pj_config; import 'package:payjoin_flutter/uri.dart' as PayjoinUri; class PayjoinManager { @@ -31,11 +33,13 @@ class PayjoinManager { 'https://ohttp.cakewallet.com', ]; - static Future randomOhttpRelayUrl() => - PayjoinUri.Url.fromStr(ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]); + static String randomOhttpRelayUrl() => + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]; static const payjoinDirectoryUrl = 'https://payjo.in'; + Future initPayjoin() => pj_config.PConfig.initializeApp(); + Future resumeSessions() async { final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); @@ -43,13 +47,13 @@ class PayjoinManager { if (session.isSenderSession) { printV("Resuming Payjoin Sender Session ${session.pjUri!}"); return _spawnSender( - sender: Sender.fromJson(session.sender!), + sender: Sender.fromJson(json: session.sender!), pjUri: session.pjUri!, ); } - final receiver = Receiver.fromJson(session.receiver!); + final receiver = Receiver.fromJson(json: session.receiver!); printV("Resuming Payjoin Receiver Session ${receiver.id()}"); - return _spawnReceiver(receiver: receiver); + return spawnReceiver(receiver: receiver); }); printV("Resumed ${spawnedSessions.length} Payjoin Sessions"); @@ -59,13 +63,19 @@ class PayjoinManager { Future initSender( String pjUriString, String originalPsbt, int networkFeesSatPerVb) async { try { - final pjUri = (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported(); + final pjUri = + (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported(); final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250); final senderBuilder = await SenderBuilder.fromPsbtAndUri( psbtBase64: originalPsbt, pjUri: pjUri, ); - return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final persister = PayjoinSenderPersister.impl(); + final newSender = + await senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final senderToken = await newSender.persist(persister: persister); + + return Sender.load(token: senderToken, persister: persister); } catch (e) { throw Exception('Error initializing Payjoin Sender: $e'); } @@ -78,7 +88,8 @@ class PayjoinManager { bool isTestnet = false, }) async { final pjUri = Uri.parse(pjUrl).queryParameters['pj']!; - await _payjoinStorage.insertSenderSession(sender, pjUri, _wallet.id, amount); + await _payjoinStorage.insertSenderSession( + sender, pjUri, _wallet.id, amount); return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri); } @@ -110,15 +121,13 @@ class PayjoinManager { } } catch (e) { _cleanupSession(pjUri); - printV(e); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); - completer.completeError(e); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString()); + completer.complete(); } } else if (message is PayjoinSessionError) { _cleanupSession(pjUri); if (message is UnrecoverableError) { - printV(message.message); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message); completer.complete(); } else if (message is RecoverableError) { completer.complete(); @@ -138,40 +147,41 @@ class PayjoinManager { return completer.future; } - Future initReceiver(String address, [bool isTestnet = false]) async { - try { - final payjoinDirectory = await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); + Future getUnusedReceiver(String address, + [bool isTestnet = false]) async { + final session = _payjoinStorage.getUnusedActiveReceiverSession(_wallet.id); - final ohttpKeys = await PayjoinUri.fetchOhttpKeys( - ohttpRelay: await randomOhttpRelayUrl(), - payjoinDirectory: payjoinDirectory, - ); + if (session != null) { + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); - final receiver = await Receiver.create( - address: address, - network: isTestnet ? Network.testnet : Network.bitcoin, - directory: payjoinDirectory, - ohttpKeys: ohttpKeys, - ohttpRelay: await randomOhttpRelayUrl(), - ); - - await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - - return receiver; - } catch (e) { - throw Exception('Error initializing Payjoin Receiver: $e'); + return Receiver.fromJson(json: session.receiver!); } + + return initReceiver(address); } - Future spawnNewReceiver({ - required Receiver receiver, - bool isTestnet = false, - }) async { + Future initReceiver(String address, [bool isTestnet = false]) async { + final ohttpKeys = await PayjoinUri.fetchOhttpKeys( + ohttpRelay: await randomOhttpRelayUrl(), + payjoinDirectory: payjoinDirectoryUrl, + ); + + final newReceiver = await NewReceiver.create( + address: address, + network: isTestnet ? Network.testnet : Network.bitcoin, + directory: payjoinDirectoryUrl, + ohttpKeys: ohttpKeys, + ); + final persister = PayjoinReceiverPersister.impl(); + final receiverToken = await newReceiver.persist(persister: persister); + final receiver = await Receiver.load(persister: persister, token: receiverToken); + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - return _spawnReceiver(isTestnet: isTestnet, receiver: receiver); + + return receiver; } - Future _spawnReceiver({ + Future spawnReceiver({ required Receiver receiver, bool isTestnet = false, }) async { @@ -191,11 +201,13 @@ class PayjoinManager { rawAmount = getOutputAmountFromTx(tx, _wallet); break; case PayjoinReceiverRequestTypes.checkIsOwned: - (_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver(); + (_wallet.walletAddresses as BitcoinWalletAddresses) + .newPayjoinReceiver(); _payjoinStorage.markReceiverSessionInProgress(receiver.id()); final inputScript = message['input_script'] as Uint8List; - final isOwned = _wallet.isMine(Script.fromRaw(bytes: inputScript)); + final isOwned = + _wallet.isMine(Script.fromRaw(byteData: inputScript)); mainToIsolateSendPort?.send({ 'requestId': message['requestId'], 'result': isOwned, @@ -204,7 +216,8 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.checkIsReceiverOutput: final outputScript = message['output_script'] as Uint8List; - final isReceiverOutput = _wallet.isMine(Script.fromRaw(bytes: outputScript)); + final isReceiverOutput = + _wallet.isMine(Script.fromRaw(byteData: outputScript)); mainToIsolateSendPort?.send({ 'requestId': message['requestId'], 'result': isReceiverOutput, @@ -213,6 +226,10 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.getCandidateInputs: utxos = _wallet.getUtxoWithPrivateKeys(); + if (utxos.isEmpty) { + await _wallet.updateAllUnspents(); + utxos = _wallet.getUtxoWithPrivateKeys(); + } mainToIsolateSendPort?.send({ 'requestId': message['requestId'], 'result': utxos, @@ -237,13 +254,15 @@ class PayjoinManager { } } catch (e) { _cleanupSession(receiver.id()); - await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id(), e.toString()); + 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); + await _payjoinStorage.markReceiverSessionUnrecoverable( + receiver.id(), message.message); completer.complete(); } else if (message is RecoverableError) { completer.complete(); diff --git a/cw_bitcoin/lib/payjoin/payjoin_persister.dart b/cw_bitcoin/lib/payjoin/payjoin_persister.dart new file mode 100644 index 000000000..4e395e36a --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_persister.dart @@ -0,0 +1,66 @@ +import 'package:payjoin_flutter/src/generated/api/receive.dart'; +import 'package:payjoin_flutter/src/generated/api/send.dart'; + +class PayjoinSenderPersister implements DartSenderPersister { + static DartSenderPersister impl() { + final impl = PayjoinSenderPersister(); + return DartSenderPersister( + save: (sender) => impl.save(sender: sender), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiSender sender}) async { + final token = sender.key(); + _store[token.toBytes().toString()] = sender; + return token; + } + + Future load({required SenderToken token}) async { + final sender = _store[token.toBytes().toString()]; + if (sender == null) { + throw Exception('Sender not found for the provided token.'); + } + return sender; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} + +class PayjoinReceiverPersister implements DartReceiverPersister { + static DartReceiverPersister impl() { + final impl = PayjoinReceiverPersister(); + return DartReceiverPersister( + save: (receiver) => impl.save(receiver: receiver), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiReceiver receiver}) async { + final token = receiver.key(); + _store[token.toBytes().toString()] = receiver; + return token; + } + + Future load({required ReceiverToken token}) async { + final receiver = _store[token.toBytes().toString()]; + if (receiver == null) { + throw Exception('Receiver not found for the provided token.'); + } + return receiver; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index a499660b0..c56148de2 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -4,14 +4,16 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/bitcoin_ffi.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:http/http.dart' as very_insecure_http_do_not_use; // for errors enum PayjoinReceiverRequestTypes { processOriginalTx, @@ -27,7 +29,7 @@ class PayjoinReceiverWorker { final pendingRequests = >{}; PayjoinReceiverWorker._(this.sendPort); - + static final client = ProxyWrapper().getHttpIOClient(); static Future run(List args) async { await pj.core.init(); @@ -41,11 +43,10 @@ class PayjoinReceiverWorker { receivePort.listen(worker.handleMessage); try { - final httpClient = http.Client(); - final receiver = Receiver.fromJson(receiverJson); + final receiver = Receiver.fromJson(json: receiverJson); final uncheckedProposal = - await worker.receiveUncheckedProposal(httpClient, receiver); + await worker.receiveUncheckedProposal(receiver); final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast(); sendPort.send({ @@ -56,14 +57,14 @@ class PayjoinReceiverWorker { final payjoinProposal = await worker.processPayjoinProposal( uncheckedProposal, ); - final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal); + final psbt = await worker.sendFinalProposal(payjoinProposal); sendPort.send({ 'type': PayjoinReceiverRequestTypes.proposalSent, 'psbt': psbt, }); } catch (e) { if (e is HttpException || - (e is http.ClientException && + (e is very_insecure_http_do_not_use.ClientException && e.message.contains("Software caused connection abort"))) { sendPort.send(PayjoinSessionError.recoverable(e.toString())); } else { @@ -97,15 +98,16 @@ class PayjoinReceiverWorker { return completer.future; } - Future receiveUncheckedProposal( - http.Client httpClient, Receiver session) async { + Future receiveUncheckedProposal(Receiver session) async { while (true) { printV("Polling for Proposal (${session.id()})"); - final extractReq = await session.extractReq(); + final extractReq = await session.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final request = extractReq.$1; final url = Uri.parse(request.url.asString()); - final httpRequest = await httpClient.post(url, + final httpRequest = await client.post(url, headers: {'Content-Type': request.contentType}, body: request.body); final proposal = await session.processRes( @@ -114,13 +116,14 @@ class PayjoinReceiverWorker { } } - Future sendFinalProposal( - http.Client httpClient, PayjoinProposal finalProposal) async { - final req = await finalProposal.extractV2Req(); + Future sendFinalProposal(PayjoinProposal finalProposal) async { + final req = await finalProposal.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final proposalReq = req.$1; final proposalCtx = req.$2; - final request = await httpClient.post( + final request = await client.post( Uri.parse(proposalReq.url.asString()), headers: {"Content-Type": proposalReq.contentType}, body: proposalReq.body, @@ -171,7 +174,7 @@ class PayjoinReceiverWorker { final listUnspent = await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs); final unspent = listUnspent as List; - if (unspent.isEmpty) throw Exception('No unspent outputs available'); + if (unspent.isEmpty) throw RecoverableError('No unspent outputs available'); final selectedUtxo = await _inputPairFromUtxo(unspent[0]); final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]); @@ -214,6 +217,6 @@ class PayjoinReceiverWorker { sequence: 0, ); - return InputPair.newInstance(txin, psbtin); + return InputPair.newInstance(txin: txin, psbtin: psbtin); } } diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart index f720bac01..7e85cc773 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -5,10 +5,12 @@ import 'dart:isolate'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/send.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:payjoin_flutter/src/generated/api/send/error.dart' as pj_error; +import 'package:payjoin_flutter/uri.dart' as pj_uri; enum PayjoinSenderRequestTypes { requestPosted, @@ -29,7 +31,7 @@ class PayjoinSenderWorker { final senderJson = args[1] as String; final pjUrl = args[2] as String; - final sender = Sender.fromJson(senderJson); + final sender = Sender.fromJson(json: senderJson); final worker = PayjoinSenderWorker._(sendPort, pjUrl); try { @@ -42,19 +44,17 @@ class PayjoinSenderWorker { sendPort.send(e); } } + final client = ProxyWrapper().getHttpIOClient(); /// Run a payjoin sender (V2 protocol first, fallback to V1). Future runSender(Sender sender) async { - final httpClient = http.Client(); try { - return await _runSenderV2(sender, httpClient); + return await _runSenderV2(sender); } catch (e) { printV(e); - if (e is PayjoinException && - // TODO condition on error type instead of message content - e.message?.contains('parse receiver public key') == true) { - return await _runSenderV1(sender, httpClient); + if (e is pj_error.FfiCreateRequestError) { + return await _runSenderV1(sender); } else if (e is HttpException) { printV(e); throw Exception(PayjoinSessionError.recoverable(e.toString())); @@ -65,13 +65,14 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V2 of the protocol. - Future _runSenderV2(Sender sender, http.Client httpClient) async { + Future _runSenderV2(Sender sender) async { try { final postRequest = await sender.extractV2( - ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(), + ohttpProxyUrl: + await pj_uri.Url.fromStr(PayjoinManager.randomOhttpRelayUrl()), ); - final postResult = await _postRequest(httpClient, postRequest.$1); + final postResult = await _postRequest(postRequest.$1); final getContext = await postRequest.$2.processResponse(response: postResult); @@ -83,7 +84,7 @@ class PayjoinSenderWorker { final getRequest = await getContext.extractReq( ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), ); - final getRes = await _postRequest(httpClient, getRequest.$1); + final getRes = await _postRequest(getRequest.$1); final proposalPsbt = await getContext.processResponse( response: getRes, ohttpCtx: getRequest.$2, @@ -97,20 +98,20 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V1 of the protocol. - Future _runSenderV1(Sender sender, http.Client httpClient) async { + Future _runSenderV1(Sender sender) async { try { final postRequest = await sender.extractV1(); - final response = await _postRequest(httpClient, postRequest.$1); + final response = await _postRequest(postRequest.$1); sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); return await postRequest.$2.processResponse(response: response); - } catch (e) { - throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e'); + } catch (e, stack) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e, $stack'); } } - Future> _postRequest(http.Client client, Request req) async { + Future> _postRequest(Request req) async { final httpRequest = await client.post(Uri.parse(req.url.asString()), headers: {'Content-Type': req.contentType}, body: req.body); diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart index 9c1c83253..5fb9d5716 100644 --- a/cw_bitcoin/lib/payjoin/storage.dart +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -23,6 +23,14 @@ class PayjoinStorage { ), ); + PayjoinSession? getUnusedActiveReceiverSession(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + session.status == PayjoinSessionStatus.created.name && + !session.isSenderSession) + .firstOrNull; + Future markReceiverSessionComplete( String sessionId, String txId, String amount) async { final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; @@ -76,10 +84,11 @@ class PayjoinStorage { await session.save(); } - Future markSenderSessionUnrecoverable(String pjUrl) async { + Future markSenderSessionUnrecoverable(String pjUrl, String reason) async { final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; await session.save(); } diff --git a/cw_bitcoin/lib/psbt/signer.dart b/cw_bitcoin/lib/psbt/signer.dart index a56aec37d..1d0ceba8b 100644 --- a/cw_bitcoin/lib/psbt/signer.dart +++ b/cw_bitcoin/lib/psbt/signer.dart @@ -40,7 +40,8 @@ extension PsbtSigner on PsbtV2 { return tx.buffer(); } - Future signWithUTXO(List utxos, UTXOSignerCallBack signer, + Future signWithUTXO( + List utxos, UTXOSignerCallBack signer, [UTXOGetterCallBack? getTaprootPair]) async { final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false)); final tx = BtcTransaction.fromRaw(raw); @@ -50,10 +51,10 @@ extension PsbtSigner on PsbtV2 { List taprootAmounts = []; List