Merge branch 'main' into CW-635--Show-BTC-LTC-BCH-keys-in-wallet-seeds-keys-page

This commit is contained in:
Serhii 2024-08-29 18:44:35 +03:00
commit af82f7fd82
431 changed files with 11505 additions and 7998 deletions

View file

@ -23,9 +23,10 @@ jobs:
docker-images: true docker-images: true
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: "11.x" distribution: "temurin"
java-version: "17"
- name: Configure placeholder git details - name: Configure placeholder git details
run: | run: |
git config --global user.email "CI@cakewallet.com" git config --global user.email "CI@cakewallet.com"
@ -60,7 +61,7 @@ jobs:
path: | path: |
/opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/cw_haven/android/.cxx
/opt/android/cake_wallet/scripts/monero_c/release /opt/android/cake_wallet/scripts/monero_c/release
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
name: Generate Externals name: Generate Externals

View file

@ -39,9 +39,10 @@ jobs:
docker-images: true docker-images: true
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: "11.x" distribution: "temurin"
java-version: "17"
- name: Configure placeholder git details - name: Configure placeholder git details
run: | run: |
git config --global user.email "CI@cakewallet.com" git config --global user.email "CI@cakewallet.com"
@ -53,7 +54,9 @@ jobs:
channel: stable channel: stable
- name: Install package dependencies - name: Install package dependencies
run: sudo apt-get install -y curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang 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 - name: Execute Build and Setup Commands
run: | run: |
@ -76,7 +79,7 @@ jobs:
path: | path: |
/opt/android/cake_wallet/cw_haven/android/.cxx /opt/android/cake_wallet/cw_haven/android/.cxx
/opt/android/cake_wallet/scripts/monero_c/release /opt/android/cake_wallet/scripts/monero_c/release
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }} key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
name: Generate Externals name: Generate Externals

View file

@ -0,0 +1,187 @@
name: PR Test Build linux
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
branch:
description: "Branch name to build"
required: true
default: "main"
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 }}
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
with:
java-version: "17.x"
- 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 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: Generate localization
run: |
cd /opt/android/cake_wallet
flutter packages pub run tool/generate_localization.dart
- name: Build generated code
run: |
cd /opt/android/cake_wallet
./model_generator.sh
- name: Add secrets
run: |
cd /opt/android/cake_wallet
touch lib/.secrets.g.dart
touch cw_evm/lib/.secrets.g.dart
touch cw_solana/lib/.secrets.g.dart
touch cw_core/lib/.secrets.g.dart
touch cw_nano/lib/.secrets.g.dart
touch cw_tron/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
echo "const walletSalt = '${{ secrets.WALLET_SALT }}';" >> lib/.secrets.g.dart
echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart
echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart
echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart
echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart
echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart
echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart
echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart
echo "const moonPayApiKey = '${{ secrets.MOON_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const moonPaySecretKey = '${{ secrets.MOON_PAY_SECRET_KEY }}';" >> lib/.secrets.g.dart
echo "const sideShiftAffiliateId = '${{ secrets.SIDE_SHIFT_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const simpleSwapApiKey = '${{ secrets.SIMPLE_SWAP_API_KEY }}';" >> lib/.secrets.g.dart
echo "const simpleSwapApiKeyDesktop = '${{ secrets.SIMPLE_SWAP_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart
echo "const onramperApiKey = '${{ secrets.ONRAMPER_API_KEY }}';" >> lib/.secrets.g.dart
echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart
echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart
echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> lib/.secrets.g.dart
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart
echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart
echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
- 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 linux --release
- name: Prepare release zip file
run: |
cd /opt/android/cake_wallet/build/linux/x64/release
zip -r ${{env.BRANCH_NAME}}.zip bundle
- name: Upload Artifact
uses: kittaakos/upload-artifact-as-is@v0
with:
path: /opt/android/cake_wallet/build/linux/x64/release/${{env.BRANCH_NAME}}.zip
# 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 }}

2
.gitignore vendored
View file

@ -160,6 +160,8 @@ macos/Runner/Release.entitlements
macos/Runner/Runner.entitlements macos/Runner/Runner.entitlements
lib/core/secure_storage.dart lib/core/secure_storage.dart
lib/core/secure_storage.dart
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png

View file

@ -18,6 +18,12 @@ migration:
- platform: windows - platform: windows
create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
- platform: macos
create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
- platform: linux
create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
# User provided section # User provided section

View file

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img height="100" src=".github/assets/Logo_CakeWallet.png"> ![logo](.github/assets/Logo_CakeWallet.png)
</div> </div>
@ -161,7 +161,9 @@ 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. 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 digit 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 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.
6. Add the new language code to `tool/utils/translation/translation_constants.dart`
## Add a new fiat currency ## Add a new fiat currency

View file

@ -10,7 +10,18 @@ analyzer:
lib/generated/*.dart, lib/generated/*.dart,
cw_monero/ios/External/**, cw_monero/ios/External/**,
cw_shared_external/**, cw_shared_external/**,
shared_external/**] shared_external/**,
lib/bitcoin/cw_bitcoin.dart,
lib/bitcoin_cash/cw_bitcoin_cash.dart,
lib/ethereum/cw_ethereum.dart,
lib/haven/cw_haven.dart,
lib/monero/cw_monero.dart,
lib/nano/cw_nano.dart,
lib/polygon/cw_polygon.dart,
lib/solana/cw_solana.dart,
lib/tron/cw_tron.dart,
lib/wownero/cw_wownero.dart,
]
language: language:
strict-casts: true strict-casts: true
strict-raw-types: true strict-raw-types: true

View file

@ -46,7 +46,7 @@ android {
defaultConfig { defaultConfig {
applicationId appProperties['id'] applicationId appProperties['id']
minSdkVersion 24 minSdkVersion 24
targetSdkVersion 33 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -91,5 +91,4 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.unstoppabledomains:resolution:5.0.0'
} }

View file

@ -20,14 +20,10 @@ import android.net.Uri;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom; import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity { public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
boolean isAppSecure = false; boolean isAppSecure = false;
@Override @Override
@ -53,14 +49,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes); random.nextBytes(bytes);
handler.post(() -> result.success(bytes)); handler.post(() -> result.success(bytes));
break; break;
case "getUnstoppableDomainAddress":
int version = Build.VERSION.SDK_INT;
if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) {
getUnstoppableDomainAddress(call, result);
} else {
handler.post(() -> result.success(""));
}
break;
case "setIsAppSecure": case "setIsAppSecure":
isAppSecure = call.argument("isAppSecure"); isAppSecure = call.argument("isAppSecure");
if (isAppSecure) { if (isAppSecure) {
@ -85,23 +73,6 @@ public class MainActivity extends FlutterFragmentActivity {
} }
} }
private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
DomainResolution resolution = new Resolution();
Handler handler = new Handler(Looper.getMainLooper());
String domain = call.argument("domain");
String ticker = call.argument("ticker");
AsyncTask.execute(() -> {
try {
String address = resolution.getAddress(domain, ticker);
handler.post(() -> result.success(address));
} catch (Exception e) {
System.out.println("Expected Address, but got " + e.getMessage());
handler.post(() -> result.success(""));
}
});
}
private void disableBatteryOptimization() { private void disableBatteryOptimization() {
String packageName = getPackageName(); String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

@ -19,14 +19,10 @@ import android.net.Uri;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom; import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity { public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
@Override @Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
@ -51,14 +47,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes); random.nextBytes(bytes);
handler.post(() -> result.success(bytes)); handler.post(() -> result.success(bytes));
break; break;
case "getUnstoppableDomainAddress":
int version = Build.VERSION.SDK_INT;
if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) {
getUnstoppableDomainAddress(call, result);
} else {
handler.post(() -> result.success(""));
}
break;
case "disableBatteryOptimization": case "disableBatteryOptimization":
disableBatteryOptimization(); disableBatteryOptimization();
handler.post(() -> result.success(null)); handler.post(() -> result.success(null));
@ -75,23 +63,6 @@ public class MainActivity extends FlutterFragmentActivity {
} }
} }
private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
DomainResolution resolution = new Resolution();
Handler handler = new Handler(Looper.getMainLooper());
String domain = call.argument("domain");
String ticker = call.argument("ticker");
AsyncTask.execute(() -> {
try {
String address = resolution.getAddress(domain, ticker);
handler.post(() -> result.success(address));
} catch (Exception e) {
System.out.println("Expected Address, but got " + e.getMessage());
handler.post(() -> result.success(""));
}
});
}
private void disableBatteryOptimization() { private void disableBatteryOptimization() {
String packageName = getPackageName(); String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

@ -19,14 +19,10 @@ import android.net.Uri;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
import java.security.SecureRandom; import java.security.SecureRandom;
public class MainActivity extends FlutterFragmentActivity { public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24;
boolean isAppSecure = false; boolean isAppSecure = false;
@Override @Override
@ -52,14 +48,6 @@ public class MainActivity extends FlutterFragmentActivity {
random.nextBytes(bytes); random.nextBytes(bytes);
handler.post(() -> result.success(bytes)); handler.post(() -> result.success(bytes));
break; break;
case "getUnstoppableDomainAddress":
int version = Build.VERSION.SDK_INT;
if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) {
getUnstoppableDomainAddress(call, result);
} else {
handler.post(() -> result.success(""));
}
break;
case "setIsAppSecure": case "setIsAppSecure":
isAppSecure = call.argument("isAppSecure"); isAppSecure = call.argument("isAppSecure");
if (isAppSecure) { if (isAppSecure) {
@ -84,23 +72,6 @@ public class MainActivity extends FlutterFragmentActivity {
} }
} }
private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
DomainResolution resolution = new Resolution();
Handler handler = new Handler(Looper.getMainLooper());
String domain = call.argument("domain");
String ticker = call.argument("ticker");
AsyncTask.execute(() -> {
try {
String address = resolution.getAddress(domain, ticker);
handler.post(() -> result.success(address));
} catch (Exception e) {
System.out.println("Expected Address, but got " + e.getMessage());
handler.post(() -> result.success(""));
}
});
}
private void disableBatteryOptimization() { private void disableBatteryOptimization() {
String packageName = getPackageName(); String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);

View file

@ -2,7 +2,7 @@ buildscript {
ext.kotlin_version = '1.8.21' ext.kotlin_version = '1.8.21'
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
@ -15,7 +15,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/images/flags/arm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,005 B

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,005 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,023 B

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,013 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 1,005 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,009 B

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,4 +1,19 @@
- -
uri: ltc-electrum.cakewallet.com:50002 uri: ltc-electrum.cakewallet.com:50002
useSSL: true useSSL: true
isDefault: true isDefault: true
-
uri: litecoin.stackwallet.com:20063
useSSL: true
-
uri: electrum-ltc.bysh.me:50002
useSSL: true
-
uri: lightweight.fiatfaucet.com:50002
useSSL: true
-
uri: electrum.ltc.xurious.com:50002
useSSL: true
-
uri: backup.electrum-ltc.org:443
useSSL: true

View file

@ -1,4 +1,7 @@
- -
uri: rpc.ankr.com uri: rpc.ankr.com
is_default: true is_default: true
useSSL: true
-
uri: api.mainnet-beta.solana.com:443
useSSL: true useSSL: true

View file

@ -1,2 +1,2 @@
Monero enhancements Enhance auto-address generation for Monero
Bug fixes Bug fixes and enhancements

View file

@ -1,3 +1,4 @@
Monero enhancements Enable BIP39 by default for wallet creation also on Bitcoin/Litecoin (Electrum seed type is still accessible through advanced settings page)
Improvements for Tron and Nano wallets Improve fee calculation for Bitcoin to protect against overpaying or underpaying
Bug fixes Enhance auto-address generation for Monero
Bug fixes and enhancements

176
build-guide-linux.md Normal file
View file

@ -0,0 +1,176 @@
# 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.

View file

@ -1,38 +0,0 @@
# Building CakeWallet for Windows
## Requirements and Setup
The following are the system requirements to build CakeWallet for your Windows PC.
```
Windows 10 or later (64-bit), x86-64 based
Flutter 3 or above
```
## Building CakeWallet on Windows
These steps will help you configure and execute a build of CakeWallet from its source code.
### 1. Installing Package Dependencies
For build CakeWallet windows application from sources you will be needed to have:
> [Install Flutter]Follow installation guide (https://docs.flutter.dev/get-started/install/windows) and install do not miss to dev tools (install https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) which are required for windows desktop development (need to install Git for Windows and Visual Studio 2022). Then install `Desktop development with C++` packages via GUI Visual Studio 2022, or Visual Studio Build Tools 2022 including: `C++ Build Tools core features`, `C++ 2022 Redistributable Update`, `C++ core desktop features`, `MVC v143 - VS 2022 C++ x64/x86 build tools`, `C++ CMake tools for Windwos`, `Testing tools core features - Build Tools`, `C++ AddressSanitizer`.
> [Install WSL] for building monero dependencies need to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu):
`$ sudo apt update `
`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config`
### 2. Pull CakeWallet source code
You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git by following next command:
`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart`
OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip)
### 3. Build Monero, Monero_c and their dependencies
For use monero in the application need to build Monero wrapper - Monero_C which will be used by monero.dart package. For that need to run shell (bash - typically same named utility should be available after WSL is enabled in your system) with previously installed WSL, then change current directory to the application project directory with your used shell and then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`.
### 4. Configure and build CakeWallet application
To configure the application open directory where you have downloaded or unarchived CakeWallet sources and run `cakewallet.bat`.
Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL.
After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application.

View file

@ -0,0 +1,35 @@
app-id: com.cakewallet.CakeWallet
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: cake_wallet
separate-locales: false
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --socket=pulseaudio
- --share=network
- --filesystem=home
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"
- "mkdir -p /app/bin"
- "ln -s /app/cake_wallet/cake_wallet /app/bin/cake_wallet"
- "mkdir -p /app/share/icons/hicolor/scalable/apps"
- "cp cakewallet_icon_180.png /app/share/icons/hicolor/scalable/apps/com.cakewallet.CakeWallet.png"
- "mkdir -p /app/share/applications"
- "cp com.cakewallet.CakeWallet.desktop /app/share/applications"
sources:
- type: dir
path: build/linux/x64/release
- type: file
path: assets/images/cakewallet_icon_180.png
- type: file
path: linux/com.cakewallet.CakeWallet.desktop

View file

@ -3,12 +3,13 @@
IOS="ios" IOS="ios"
ANDROID="android" ANDROID="android"
MACOS="macos" MACOS="macos"
LINUX="linux"
PLATFORMS=($IOS $ANDROID $MACOS) PLATFORMS=($IOS $ANDROID $MACOS $LINUX)
PLATFORM=$1 PLATFORM=$1
if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then
echo "specify platform: ./configure_cake_wallet.sh ios|android|macos" echo "specify platform: ./configure_cake_wallet.sh ios|android|macos|linux"
exit 1 exit 1
fi fi
@ -27,9 +28,14 @@ if [ "$PLATFORM" == "$ANDROID" ]; then
cd scripts/android cd scripts/android
fi fi
if [ "$PLATFORM" == "$LINUX" ]; then
echo "Configuring for linux"
cd scripts/linux
fi
source ./app_env.sh cakewallet source ./app_env.sh cakewallet
./app_config.sh ./app_config.sh
cd ../.. && flutter pub get cd ../.. && flutter pub get
#flutter packages pub run tool/generate_localization.dart flutter packages pub run tool/generate_localization.dart
./model_generator.sh ./model_generator.sh
#cd macos && pod install #cd macos && pod install

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart';
@ -25,7 +25,8 @@ class BitcoinHardwareWalletService {
for (final i in indexRange) { for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'"; final derivationPath = "m/84'/0'/$i'";
final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath);
HDWallet hd = HDWallet.fromBase58(xpub).derive(0); 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);

View file

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cryptography/cryptography.dart' as cryptography; import 'package:cryptography/cryptography.dart' as cryptography;
import 'package:cw_core/sec_random_native.dart'; import 'package:cw_core/sec_random_native.dart';
@ -59,11 +60,7 @@ void maskBytes(Uint8List bytes, int bits) {
} }
} }
String bufferToBin(Uint8List data) { String bufferToBin(Uint8List data) => data.map((e) => e.toRadixString(2).padLeft(8, '0')).join('');
final q1 = data.map((e) => e.toRadixString(2).padLeft(8, '0'));
final q2 = q1.join('');
return q2;
}
String encode(Uint8List data) { String encode(Uint8List data) {
final dataBitLen = data.length * 8; final dataBitLen = data.length * 8;
@ -112,17 +109,18 @@ Future<bool> checkIfMnemonicIsElectrum2(String mnemonic) async {
Future<String> getMnemonicHash(String mnemonic) async { Future<String> getMnemonicHash(String mnemonic) async {
final hmacSha512 = Hmac(sha512, utf8.encode('Seed version')); final hmacSha512 = Hmac(sha512, utf8.encode('Seed version'));
final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic))); final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic)));
final hx = digest.toString(); return digest.toString();
return hx;
} }
Future<Uint8List> mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { Future<Uint8List> mnemonicToSeedBytes(String mnemonic,
{String prefix = segwit, String passphrase = ''}) async {
final pbkdf2 = final pbkdf2 =
cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512);
final text = normalizeText(mnemonic); final text = normalizeText(mnemonic);
// pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) final passphraseBytes = utf8.encode(normalizeText(passphrase));
final key = await pbkdf2.deriveKey( final key = await pbkdf2.deriveKey(
secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits); secretKey: cryptography.SecretKey(text.codeUnits),
nonce: [...'electrum'.codeUnits, ...passphraseBytes]);
final bytes = await key.extractBytes(); final bytes = await key.extractBytes();
return Uint8List.fromList(bytes); return Uint8List.fromList(bytes);
} }

View file

@ -2,10 +2,11 @@ import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
class Mnemonic { class MnemonicBip39 {
/// Generate bip39 mnemonic /// Generate bip39 mnemonic
static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength); static String generate({int strength = 128}) => bip39.generateMnemonic(strength: strength);
/// Create root seed from mnemonic /// Create root seed from mnemonic
static Uint8List toSeed(String mnemonic) => bip39.mnemonicToSeed(mnemonic); static Uint8List toSeed(String mnemonic, {String? passphrase}) =>
bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
} }

View file

@ -2,10 +2,10 @@ import 'dart:convert';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:convert/convert.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.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/electrum_derivations.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
@ -15,6 +15,7 @@ import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart';
@ -30,6 +31,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
Uint8List? seedBytes, Uint8List? seedBytes,
String? mnemonic, String? mnemonic,
String? xpub, String? xpub,
@ -50,14 +52,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: networkParam == null network: networkParam == null
? bitcoin.bitcoin ? BitcoinNetwork.mainnet
: networkParam == BitcoinNetwork.mainnet : networkParam == BitcoinNetwork.mainnet
? bitcoin.bitcoin ? BitcoinNetwork.mainnet
: bitcoin.testnet, : BitcoinNetwork.testnet,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: seedBytes, seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
currency: currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
@ -75,10 +78,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialSilentAddresses: initialSilentAddresses, initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex, initialSilentAddressIndex: initialSilentAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: accountHD.derive(1), sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: networkParam ?? network, network: networkParam ?? network,
masterHd: masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
seedBytes != null ? bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : null,
); );
autorun((_) { autorun((_) {
@ -91,6 +93,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase, String? passphrase,
String? addressPageType, String? addressPageType,
BasedUtxoNetwork? network, BasedUtxoNetwork? network,
@ -112,7 +115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
break; break;
case DerivationType.electrum: case DerivationType.electrum:
default: default:
seedBytes = await mnemonicToSeedBytes(mnemonic); seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break; break;
} }
return BitcoinWallet( return BitcoinWallet(
@ -125,6 +128,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialSilentAddresses: initialSilentAddresses, initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex, initialSilentAddressIndex: initialSilentAddressIndex,
initialBalance: initialBalance, initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
seedBytes: seedBytes, seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
@ -138,54 +142,87 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
required EncryptionFileUtils encryptionFileUtils,
required bool alwaysScan, required bool alwaysScan,
}) async { }) async {
final network = walletInfo.network != null final network = walletInfo.network != null
? BasedUtxoNetwork.fromName(walletInfo.network!) ? BasedUtxoNetwork.fromName(walletInfo.network!)
: BitcoinNetwork.mainnet; : BitcoinNetwork.mainnet;
final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network);
walletInfo.derivationInfo ??= DerivationInfo( final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
derivationType: snp.derivationType ?? DerivationType.electrum,
derivationPath: snp.derivationPath, ElectrumWalletSnapshot? snp = null;
);
try {
snp = await ElectrumWalletSnapshot.load(
encryptionFileUtils,
name,
walletInfo.type,
password,
network,
);
} catch (e) {
if (!hasKeysFile) rethrow;
}
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
keysData = WalletKeysData(
mnemonic: snp!.mnemonic,
xPub: snp.xpub,
passphrase: snp.passphrase,
);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
walletInfo.derivationInfo ??= DerivationInfo();
// set the default if not present: // set the default if not present:
walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
Uint8List? seedBytes = null; Uint8List? seedBytes = null;
final mnemonic = keysData.mnemonic;
final passphrase = keysData.passphrase;
if (snp.mnemonic != null) { if (mnemonic != null) {
switch (walletInfo.derivationInfo!.derivationType) { switch (walletInfo.derivationInfo!.derivationType) {
case DerivationType.electrum: case DerivationType.electrum:
seedBytes = await mnemonicToSeedBytes(snp.mnemonic!); seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break; break;
case DerivationType.bip39: case DerivationType.bip39:
default: default:
seedBytes = await bip39.mnemonicToSeed( seedBytes = await bip39.mnemonicToSeed(
snp.mnemonic!, mnemonic,
passphrase: snp.passphrase ?? '', passphrase: passphrase ?? '',
); );
break; break;
} }
} }
return BitcoinWallet( return BitcoinWallet(
mnemonic: snp.mnemonic, mnemonic: mnemonic,
xpub: snp.xpub, xpub: keysData.xPub,
password: password, password: password,
passphrase: snp.passphrase, passphrase: passphrase,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp?.addresses,
initialSilentAddresses: snp.silentAddresses, initialSilentAddresses: snp?.silentAddresses,
initialSilentAddressIndex: snp.silentAddressIndex, initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
initialBalance: snp.balance, initialBalance: snp?.balance,
encryptionFileUtils: encryptionFileUtils,
seedBytes: seedBytes, seedBytes: seedBytes,
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp.addressPageType, addressPageType: snp?.addressPageType,
networkParam: network, networkParam: network,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
); );
@ -235,7 +272,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
return BtcTransaction.fromRaw(hex.encode(rawHex)); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
} }
@override @override
@ -249,8 +286,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final accountPath = walletInfo.derivationInfo?.derivationPath; 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! final signature = await _bitcoinLedgerApp!.signMessage(_ledgerDevice!,
.signMessage(_ledgerDevice!, message: ascii.encode(message), signDerivationPath: derivationPath); message: ascii.encode(message), signDerivationPath: derivationPath);
return base64Encode(signature); return base64Encode(signature);
} }

View file

@ -1,5 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -24,7 +24,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
}) : super(walletInfo); }) : super(walletInfo);
@override @override
String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { String getAddress(
{required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) {
if (addressType == P2pkhAddressType.p2pkh) if (addressType == P2pkhAddressType.p2pkh)
return generateP2PKHAddress(hd: hd, index: index, network: network); return generateP2PKHAddress(hd: hd, index: index, network: network);

View file

@ -3,14 +3,18 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
class BitcoinNewWalletCredentials extends WalletCredentials { class BitcoinNewWalletCredentials extends WalletCredentials {
BitcoinNewWalletCredentials( BitcoinNewWalletCredentials({
{required String name, required String name,
WalletInfo? walletInfo, WalletInfo? walletInfo,
DerivationType? derivationType, String? password,
String? derivationPath}) DerivationType? derivationType,
: super( String? derivationPath,
String? passphrase,
}) : super(
name: name, name: name,
walletInfo: walletInfo, walletInfo: walletInfo,
password: password,
passphrase: passphrase,
); );
} }

View file

@ -1,8 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart';
import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
@ -19,11 +21,12 @@ class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> { BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan); BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool alwaysScan; final bool alwaysScan;
final bool isDirect;
@override @override
WalletType getType() => WalletType.bitcoin; WalletType getType() => WalletType.bitcoin;
@ -33,16 +36,32 @@ class BitcoinWalletService extends WalletService<
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value; credentials.walletInfo?.network = network.value;
final String mnemonic;
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
case DerivationType.bip39:
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
mnemonic = await MnemonicBip39.generate(strength: strength);
break;
case DerivationType.electrum:
default:
mnemonic = await generateElectrumMnemonic();
break;
}
final wallet = await BitcoinWalletBase.create( final wallet = await BitcoinWalletBase.create(
mnemonic: await generateElectrumMnemonic(), mnemonic: mnemonic,
password: credentials.password!, password: credentials.password!,
passphrase: credentials.passphrase, passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
network: network, network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -61,6 +80,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
saveBackup(name); saveBackup(name);
@ -73,6 +93,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.init(); await wallet.init();
return wallet; return wallet;
@ -97,6 +118,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: currentWalletInfo, walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
alwaysScan: alwaysScan, alwaysScan: alwaysScan,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
@ -123,6 +145,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network, networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
@ -151,6 +174,7 @@ class BitcoinWalletService extends WalletService<
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
network: network, network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
); );
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();

View file

@ -8,6 +8,8 @@ import 'package:cw_bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
enum ConnectionStatus { connected, disconnected, connecting, failed }
String jsonrpcparams(List<Object> params) { String jsonrpcparams(List<Object> params) {
final _params = params.map((val) => '"${val.toString()}"').join(','); final _params = params.map((val) => '"${val.toString()}"').join(',');
return '[$_params]'; return '[$_params]';
@ -41,7 +43,7 @@ class ElectrumClient {
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
Socket? socket; Socket? socket;
void Function(bool?)? onConnectionStatusChange; void Function(ConnectionStatus)? onConnectionStatusChange;
int _id; int _id;
final Map<String, SocketTask> _tasks; final Map<String, SocketTask> _tasks;
Map<String, SocketTask> get tasks => _tasks; Map<String, SocketTask> get tasks => _tasks;
@ -60,39 +62,65 @@ class ElectrumClient {
} }
Future<void> connect({required String host, required int port, bool? useSSL}) async { Future<void> connect({required String host, required int port, bool? useSSL}) async {
_setConnectionStatus(ConnectionStatus.connecting);
try { try {
await socket?.close(); await socket?.close();
socket = null;
} catch (_) {} } catch (_) {}
if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { try {
socket = await Socket.connect(host, port, timeout: connectionTimeout); if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
} else { socket = await Socket.connect(host, port, timeout: connectionTimeout);
socket = await SecureSocket.connect(host, port, } else {
timeout: connectionTimeout, onBadCertificate: (_) => true); socket = await SecureSocket.connect(
} host,
_setIsConnected(true); port,
timeout: connectionTimeout,
socket!.listen((Uint8List event) { onBadCertificate: (_) => true,
try { );
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
}
_parseResponse(message);
}
} catch (e) {
print(e.toString());
} }
}, onError: (Object error) { } catch (_) {
print(error.toString()); _setConnectionStatus(ConnectionStatus.failed);
unterminatedString = ''; return;
_setIsConnected(false); }
}, onDone: () {
unterminatedString = ''; if (socket == null) {
_setIsConnected(null); _setConnectionStatus(ConnectionStatus.failed);
}); return;
}
_setConnectionStatus(ConnectionStatus.connected);
socket!.listen(
(Uint8List event) {
try {
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
}
_parseResponse(message);
}
} catch (e) {
print(e.toString());
}
},
onError: (Object error) {
final errorMsg = error.toString();
print(errorMsg);
unterminatedString = '';
},
onDone: () {
unterminatedString = '';
if (host == socket?.address.host) {
socket = null;
_setConnectionStatus(ConnectionStatus.disconnected);
}
},
cancelOnError: true,
);
keepAlive(); keepAlive();
} }
@ -144,9 +172,9 @@ class ElectrumClient {
Future<void> ping() async { Future<void> ping() async {
try { try {
await callWithTimeout(method: 'server.ping'); await callWithTimeout(method: 'server.ping');
_setIsConnected(true); _setConnectionStatus(ConnectionStatus.connected);
} on RequestFailedTimeoutException catch (_) { } on RequestFailedTimeoutException catch (_) {
_setIsConnected(null); _setConnectionStatus(ConnectionStatus.disconnected);
} }
} }
@ -236,9 +264,24 @@ class ElectrumClient {
return []; return [];
}); });
Future<Map<String, dynamic>> getTransactionRaw({required String hash}) async => Future<dynamic> getTransaction({required String hash, required bool verbose}) async {
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) try {
.then((dynamic result) { final result = await callWithTimeout(
method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000);
if (result is Map<String, dynamic>) {
return result;
}
} on RequestFailedTimeoutException catch (_) {
return <String, dynamic>{};
} catch (e) {
print("getTransaction: ${e.toString()}");
return <String, dynamic>{};
}
return <String, dynamic>{};
}
Future<Map<String, dynamic>> getTransactionVerbose({required String hash}) =>
getTransaction(hash: hash, verbose: true).then((dynamic result) {
if (result is Map<String, dynamic>) { if (result is Map<String, dynamic>) {
return result; return result;
} }
@ -246,9 +289,8 @@ class ElectrumClient {
return <String, dynamic>{}; return <String, dynamic>{};
}); });
Future<String> getTransactionHex({required String hash}) async => Future<String> getTransactionHex({required String hash}) =>
callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) getTransaction(hash: hash, verbose: false).then((dynamic result) {
.then((dynamic result) {
if (result is String) { if (result is String) {
return result; return result;
} }
@ -336,7 +378,7 @@ class ElectrumClient {
try { try {
final topDoubleString = await estimatefee(p: 1); final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5); final middleDoubleString = await estimatefee(p: 5);
final bottomDoubleString = await estimatefee(p: 100); final bottomDoubleString = await estimatefee(p: 10);
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
@ -353,14 +395,21 @@ class ElectrumClient {
// "height": 520481, // "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// } // }
Future<int?> getCurrentBlockChainTip() =>
callWithTimeout(method: 'blockchain.headers.subscribe').then((result) {
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
return null; Future<int?> getCurrentBlockChainTip() async {
}); try {
final result = await callWithTimeout(method: 'blockchain.headers.subscribe');
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
return null;
} on RequestFailedTimeoutException catch (_) {
return null;
} catch (e) {
print("getCurrentBlockChainTip: ${e.toString()}");
return null;
}
}
BehaviorSubject<Object>? chainTipSubscribe() { BehaviorSubject<Object>? chainTipSubscribe() {
_id += 1; _id += 1;
@ -379,6 +428,9 @@ class ElectrumClient {
BehaviorSubject<T>? subscribe<T>( BehaviorSubject<T>? subscribe<T>(
{required String id, required String method, List<Object> params = const []}) { {required String id, required String method, List<Object> params = const []}) {
try { try {
if (socket == null) {
return null;
}
final subscription = BehaviorSubject<T>(); final subscription = BehaviorSubject<T>();
_regisrySubscription(id, subscription); _regisrySubscription(id, subscription);
socket!.write(jsonrpc(method: method, id: _id, params: params)); socket!.write(jsonrpc(method: method, id: _id, params: params));
@ -392,6 +444,9 @@ class ElectrumClient {
Future<dynamic> call( Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async { {required String method, List<Object> params = const [], Function(int)? idCallback}) async {
if (socket == null) {
return null;
}
final completer = Completer<dynamic>(); final completer = Completer<dynamic>();
_id += 1; _id += 1;
final id = _id; final id = _id;
@ -403,8 +458,11 @@ class ElectrumClient {
} }
Future<dynamic> callWithTimeout( Future<dynamic> callWithTimeout(
{required String method, List<Object> params = const [], int timeout = 4000}) async { {required String method, List<Object> params = const [], int timeout = 5000}) async {
try { try {
if (socket == null) {
return null;
}
final completer = Completer<dynamic>(); final completer = Completer<dynamic>();
_id += 1; _id += 1;
final id = _id; final id = _id;
@ -426,6 +484,7 @@ class ElectrumClient {
_aliveTimer?.cancel(); _aliveTimer?.cancel();
try { try {
await socket?.close(); await socket?.close();
socket = null;
} catch (_) {} } catch (_) {}
onConnectionStatusChange = null; onConnectionStatusChange = null;
} }
@ -474,12 +533,9 @@ class ElectrumClient {
} }
} }
void _setIsConnected(bool? isConnected) { void _setConnectionStatus(ConnectionStatus status) {
if (_isConnected != isConnected) { onConnectionStatusChange?.call(status);
onConnectionStatusChange?.call(isConnected); _isConnected = status == ConnectionStatus.connected;
}
_isConnected = isConnected ?? false;
} }
void _handleResponse(Map<String, dynamic> response) { void _handleResponse(Map<String, dynamic> response) {

View file

@ -109,5 +109,4 @@ Map<DerivationType, List<DerivationInfo>> electrum_derivations = {
], ],
}; };
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;
String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!;

View file

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
@ -6,6 +7,8 @@ import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/utils/file.dart'; import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
part 'electrum_transaction_history.g.dart'; part 'electrum_transaction_history.g.dart';
@ -15,13 +18,15 @@ class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$Electru
abstract class ElectrumTransactionHistoryBase abstract class ElectrumTransactionHistoryBase
extends TransactionHistoryBase<ElectrumTransactionInfo> with Store { extends TransactionHistoryBase<ElectrumTransactionInfo> with Store {
ElectrumTransactionHistoryBase({required this.walletInfo, required String password}) ElectrumTransactionHistoryBase(
{required this.walletInfo, required String password, required this.encryptionFileUtils})
: _password = password, : _password = password,
_height = 0 { _height = 0 {
transactions = ObservableMap<String, ElectrumTransactionInfo>(); transactions = ObservableMap<String, ElectrumTransactionInfo>();
} }
final WalletInfo walletInfo; final WalletInfo walletInfo;
final EncryptionFileUtils encryptionFileUtils;
String _password; String _password;
int _height; int _height;
@ -44,7 +49,7 @@ abstract class ElectrumTransactionHistoryBase
txjson[tx.key] = tx.value.toJson(); txjson[tx.key] = tx.value.toJson();
} }
final data = json.encode({'height': _height, 'transactions': txjson}); final data = json.encode({'height': _height, 'transactions': txjson});
await writeData(path: path, password: _password, data: data); await encryptionFileUtils.write(path: path, password: _password, data: data);
} catch (e) { } catch (e) {
print('Error while save bitcoin transaction history: ${e.toString()}'); print('Error while save bitcoin transaction history: ${e.toString()}');
} }
@ -58,7 +63,7 @@ abstract class ElectrumTransactionHistoryBase
Future<Map<String, dynamic>> _read() async { Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName'; final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password); final content = await encryptionFileUtils.read(path: path, password: _password);
return json.decode(content) as Map<String, dynamic>; return json.decode(content) as Map<String, dynamic>;
} }

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
@ -7,10 +9,12 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart'; import 'package:cw_core/format_amount.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:hex/hex.dart';
class ElectrumTransactionBundle { class ElectrumTransactionBundle {
ElectrumTransactionBundle(this.originalTransaction, ElectrumTransactionBundle(this.originalTransaction,
{required this.ins, required this.confirmations, this.time}); {required this.ins, required this.confirmations, this.time});
final BtcTransaction originalTransaction; final BtcTransaction originalTransaction;
final List<BtcTransaction> ins; final List<BtcTransaction> ins;
final int? time; final int? time;
@ -22,7 +26,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
ElectrumTransactionInfo(this.type, ElectrumTransactionInfo(this.type,
{required String id, {required String id,
required int height, int? height,
required int amount, required int amount,
int? fee, int? fee,
List<String>? inputAddresses, List<String>? inputAddresses,
@ -99,7 +103,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
factory ElectrumTransactionInfo.fromElectrumBundle( factory ElectrumTransactionInfo.fromElectrumBundle(
ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network,
{required Set<String> addresses, required int height}) { {required Set<String> addresses, int? height}) {
final date = bundle.time != null final date = bundle.time != null
? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000)
: DateTime.now(); : DateTime.now();
@ -125,7 +129,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
for (final out in bundle.originalTransaction.outputs) { for (final out in bundle.originalTransaction.outputs) {
totalOutAmount += out.amount.toInt(); totalOutAmount += out.amount.toInt();
final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network));
outputAddresses.add(addressFromOutputScript(out.scriptPubKey, network)); final address = addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN')) {
final index = script.indexOf('OP_RETURN');
if (index + 1 <= script.length) {
try {
final opReturnData = script[index + 1].toString();
final decodedString = utf8.decode(HEX.decode(opReturnData));
outputAddresses.add('OP_RETURN:$decodedString');
} catch (_) {
outputAddresses.add('OP_RETURN:');
}
}
}
if (addressExists) { if (addressExists) {
receivedAmounts.add(out.amount.toInt()); receivedAmounts.add(out.amount.toInt());
@ -235,6 +256,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
} }
String toString() { String toString() {
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; 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)';
} }
} }

View file

@ -5,7 +5,8 @@ import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
@ -22,7 +23,6 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/script_hash.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
@ -34,17 +34,18 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.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/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/get_height_by_date.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
import 'package:sp_scanner/sp_scanner.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'; part 'electrum_wallet.g.dart';
@ -54,12 +55,13 @@ const int TWEAKS_COUNT = 25;
abstract class ElectrumWalletBase abstract class ElectrumWalletBase
extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo> extends WalletBase<ElectrumBalance, ElectrumTransactionHistory, ElectrumTransactionInfo>
with Store { with Store, WalletKeysFile {
ElectrumWalletBase({ ElectrumWalletBase({
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required this.networkType, required this.network,
required this.encryptionFileUtils,
String? xpub, String? xpub,
String? mnemonic, String? mnemonic,
Uint8List? seedBytes, Uint8List? seedBytes,
@ -70,7 +72,7 @@ abstract class ElectrumWalletBase
CryptoCurrency? currency, CryptoCurrency? currency,
this.alwaysScan, this.alwaysScan,
}) : accountHD = }) : accountHD =
getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo),
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
_password = password, _password = password,
_feeRates = <int>[], _feeRates = <int>[],
@ -89,23 +91,24 @@ abstract class ElectrumWalletBase
} }
: {}), : {}),
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
this.network = _getNetwork(networkType, currency), this.isTestnet = !network.isMainnet,
this.isTestnet = networkType == bitcoin.testnet,
this._mnemonic = mnemonic, this._mnemonic = mnemonic,
super(walletInfo) { super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient(); this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); transactionHistory = ElectrumTransactionHistory(
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils,
);
reaction((_) => syncStatus, _syncStatusReaction); reaction((_) => syncStatus, _syncStatusReaction);
sharedPrefs.complete(SharedPreferences.getInstance());
} }
static bitcoin.HDWallet getAccountHDWallet( static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network,
CryptoCurrency? currency, Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) {
bitcoin.NetworkType networkType,
Uint8List? seedBytes,
String? xpub,
DerivationInfo? derivationInfo) {
if (seedBytes == null && xpub == null) { if (seedBytes == null && xpub == null) {
throw Exception( throw Exception(
"To create a Wallet you need either a seed or an xpub. This should not happen"); "To create a Wallet you need either a seed or an xpub. This should not happen");
@ -114,25 +117,32 @@ abstract class ElectrumWalletBase
if (seedBytes != null) { if (seedBytes != null) {
return currency == CryptoCurrency.bch return currency == CryptoCurrency.bch
? bitcoinCashHDWallet(seedBytes) ? bitcoinCashHDWallet(seedBytes)
: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(
.derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)); _hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path))
as Bip32Slip10Secp256k1;
} }
return bitcoin.HDWallet.fromBase58(xpub!); return Bip32Slip10Secp256k1.fromExtendedKey(xpub!);
} }
static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) =>
bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'"); Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1;
static int estimatedTransactionSize(int inputsCount, int outputsCounts) => static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10; inputsCount * 68 + outputsCounts * 34 + 10;
bool? alwaysScan; bool? alwaysScan;
final bitcoin.HDWallet accountHD; final Bip32Slip10Secp256k1 accountHD;
final String? _mnemonic; final String? _mnemonic;
bitcoin.HDWallet get hd => accountHD.derive(0); Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
final EncryptionFileUtils encryptionFileUtils;
@override
final String? passphrase; final String? passphrase;
@override @override
@ -164,16 +174,22 @@ abstract class ElectrumWalletBase
.map((addr) => scriptHash(addr.address, network: network)) .map((addr) => scriptHash(addr.address, network: network))
.toList(); .toList();
String get xpub => accountHD.base58!; String get xpub => accountHD.publicKey.toExtended;
@override @override
String? get seed => _mnemonic; String? get seed => _mnemonic;
bitcoin.NetworkType networkType; @override
WalletKeysData get walletKeysData =>
WalletKeysData(mnemonic: _mnemonic, xPub: xpub, passphrase: passphrase);
@override
String get password => _password;
BasedUtxoNetwork network; BasedUtxoNetwork network;
@override @override
bool? isTestnet; bool isTestnet;
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; bool get hasSilentPaymentsScanning => type == WalletType.bitcoin;
@ -184,25 +200,29 @@ abstract class ElectrumWalletBase
bool _isTryingToConnect = false; bool _isTryingToConnect = false;
Completer<SharedPreferences> sharedPrefs = Completer();
Future<bool> checkIfMempoolAPIIsEnabled() async {
bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true;
return isMempoolAPIEnabled;
}
@action @action
Future<void> setSilentPaymentsScanning(bool active, bool usingElectrs) async { Future<void> setSilentPaymentsScanning(bool active) async {
silentPaymentsScanningActive = active; silentPaymentsScanningActive = active;
if (active) { if (active) {
syncStatus = AttemptingSyncStatus(); syncStatus = StartingScanSyncStatus();
final tip = await getUpdatedChainTip(); final tip = await getUpdatedChainTip();
if (tip == walletInfo.restoreHeight) { if (tip == walletInfo.restoreHeight) {
syncStatus = SyncedTipSyncStatus(tip); syncStatus = SyncedTipSyncStatus(tip);
return;
} }
if (tip > walletInfo.restoreHeight) { if (tip > walletInfo.restoreHeight) {
_setListeners( _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip);
walletInfo.restoreHeight,
chainTipParam: _currentChainTip,
usingElectrs: usingElectrs,
);
} }
} else { } else {
alwaysScan = false; alwaysScan = false;
@ -212,10 +232,7 @@ abstract class ElectrumWalletBase
if (electrumClient.isConnected) { if (electrumClient.isConnected) {
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} else { } else {
if (electrumClient.uri != null) { syncStatus = NotConnectedSyncStatus();
await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL);
startSync();
}
} }
} }
} }
@ -269,7 +286,8 @@ abstract class ElectrumWalletBase
void Function(FlutterErrorDetails)? _onError; void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer; Timer? _autoSaveTimer;
static const int _autoSaveInterval = 30; Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
Future<void> init() async { Future<void> init() async {
await walletAddresses.init(); await walletAddresses.init();
@ -277,7 +295,7 @@ abstract class ElectrumWalletBase
await save(); await save();
_autoSaveTimer = _autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save());
} }
@action @action
@ -285,7 +303,7 @@ abstract class ElectrumWalletBase
int height, { int height, {
int? chainTipParam, int? chainTipParam,
bool? doSingleScan, bool? doSingleScan,
bool? usingElectrs, bool? usingSupportedNode,
}) async { }) async {
final chainTip = chainTipParam ?? await getUpdatedChainTip(); final chainTip = chainTipParam ?? await getUpdatedChainTip();
@ -294,7 +312,7 @@ abstract class ElectrumWalletBase
return; return;
} }
syncStatus = AttemptingSyncStatus(); syncStatus = StartingScanSyncStatus();
if (_isolate != null) { if (_isolate != null) {
final runningIsolate = await _isolate!; final runningIsolate = await _isolate!;
@ -312,7 +330,9 @@ abstract class ElectrumWalletBase
chainTip: chainTip, chainTip: chainTip,
electrumClient: ElectrumClient(), electrumClient: ElectrumClient(),
transactionHistoryIds: transactionHistory.transactions.keys.toList(), transactionHistoryIds: transactionHistory.transactions.keys.toList(),
node: usingElectrs == true ? ScanNode(node!.uri, node!.useSSL) : null, node: (await getNodeSupportsSilentPayments()) == true
? ScanNode(node!.uri, node!.useSSL)
: null,
labels: walletAddresses.labels, labels: walletAddresses.labels,
labelIndexes: walletAddresses.silentAddresses labelIndexes: walletAddresses.silentAddresses
.where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1)
@ -400,7 +420,7 @@ abstract class ElectrumWalletBase
BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)),
) )
: silentAddress.B_spend, : silentAddress.B_spend,
hrp: silentAddress.hrp, network: network,
); );
final addressRecord = walletAddresses.silentAddresses final addressRecord = walletAddresses.silentAddresses
@ -417,6 +437,10 @@ abstract class ElectrumWalletBase
@override @override
Future<void> startSync() async { Future<void> startSync() async {
try { try {
if (syncStatus is SyncronizingSyncStatus) {
return;
}
syncStatus = SyncronizingSyncStatus(); syncStatus = SyncronizingSyncStatus();
if (hasSilentPaymentsScanning) { if (hasSilentPaymentsScanning) {
@ -428,8 +452,10 @@ abstract class ElectrumWalletBase
await updateTransactions(); await updateTransactions();
await updateAllUnspents(); await updateAllUnspents();
await updateBalance(); await updateBalance();
await updateFeeRates();
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); _updateFeeRateTimer ??=
Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
if (alwaysScan == true) { if (alwaysScan == true) {
_setListeners(walletInfo.restoreHeight); _setListeners(walletInfo.restoreHeight);
@ -445,14 +471,81 @@ abstract class ElectrumWalletBase
@action @action
Future<void> updateFeeRates() async { Future<void> updateFeeRates() async {
if (await checkIfMempoolAPIIsEnabled()) {
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<String, num>;
final slowFee = result['economyFee']?.toInt() ?? 0;
final mediumFee = result['hourFee']?.toInt() ?? 0;
final fastFee = result['fastestFee']?.toInt() ?? 0;
_feeRates = [slowFee, mediumFee, fastFee];
return;
} catch (_) {}
}
final feeRates = await electrumClient.feeRates(network: network); final feeRates = await electrumClient.feeRates(network: network);
if (feeRates != [0, 0, 0]) { if (feeRates != [0, 0, 0]) {
_feeRates = feeRates; _feeRates = feeRates;
} else if (isTestnet) {
_feeRates = [1, 1, 1];
} }
} }
Node? node; Node? node;
Future<bool> getNodeIsElectrs() async {
if (node == null) {
return false;
}
final version = await electrumClient.version();
if (version.isNotEmpty) {
final server = version[0];
if (server.toLowerCase().contains('electrs')) {
node!.isElectrs = true;
node!.save();
return node!.isElectrs!;
}
}
node!.isElectrs = false;
node!.save();
return node!.isElectrs!;
}
Future<bool> getNodeSupportsSilentPayments() async {
// As of today (august 2024), only ElectrumRS supports silent payments
if (!(await getNodeIsElectrs())) {
return false;
}
if (node == null) {
return false;
}
try {
final tweaksResponse = await electrumClient.getTweaks(height: 0);
if (tweaksResponse != null) {
node!.supportsSilentPayments = true;
node!.save();
return node!.supportsSilentPayments!;
}
} on RequestFailedTimeoutException catch (_) {
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
} catch (_) {}
node!.supportsSilentPayments = false;
node!.save();
return node!.supportsSilentPayments!;
}
@action @action
@override @override
Future<void> connectToNode({required Node node}) async { Future<void> connectToNode({required Node node}) async {
@ -514,13 +607,6 @@ abstract class ElectrumWalletBase
final hd = final hd =
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
final derivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@ -531,17 +617,31 @@ abstract class ElectrumWalletBase
); );
spendsSilentPayment = true; spendsSilentPayment = true;
isSilentPayment = true; isSilentPayment = true;
} else { } else if (!isHardwareWallet) {
privkey = privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
} }
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
inputPrivKeyInfos.add(ECPrivateInfo( String pubKeyHex;
privkey,
address.type == SegwitAddresType.p2tr, if (privkey != null) {
tweak: !isSilentPayment, inputPrivKeyInfos.add(ECPrivateInfo(
)); privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
pubKeyHex = privkey.getPublic().toHex();
} else {
pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex();
}
final derivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
utxos.add( utxos.add(
UtxoWithAddress( UtxoWithAddress(
@ -553,7 +653,7 @@ abstract class ElectrumWalletBase
isSilentPayment: isSilentPayment, isSilentPayment: isSilentPayment,
), ),
ownerDetails: UtxoAddressDetails( ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(), publicKey: pubKeyHex,
address: address, address: address,
), ),
), ),
@ -940,11 +1040,29 @@ abstract class ElectrumWalletBase
bool hasTaprootInputs = false; bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.inputPrivKeyInfos String error = "Cannot find private key.";
.firstWhereOrNull((element) => element.privkey.getPublic().toHex() == publicKey);
ECPrivateInfo? key;
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
error += "\nNo private keys generated.";
} else {
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == publicKey) {
return true;
} else {
error += "\nExpected: $publicKey";
error += "\nPubkey: $elemPubkey";
return false;
}
});
}
if (key == null) { if (key == null) {
throw Exception("Cannot find private key"); throw Exception(error);
} }
if (utxo.utxo.isP2tr()) { if (utxo.utxo.isP2tr()) {
@ -1085,8 +1203,13 @@ abstract class ElectrumWalletBase
@override @override
Future<void> save() async { Future<void> save() async {
if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
await saveKeysFile(_password, encryptionFileUtils);
saveKeysFile(_password, encryptionFileUtils, true);
}
final path = await makePath(); final path = await makePath();
await write(path: path, password: _password, data: toJSON()); await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
await transactionHistory.save(); await transactionHistory.save();
} }
@ -1126,10 +1249,9 @@ abstract class ElectrumWalletBase
int? chainTip, int? chainTip,
ScanData? scanData, ScanData? scanData,
bool? doSingleScan, bool? doSingleScan,
bool? usingElectrs,
}) async { }) async {
silentPaymentsScanningActive = true; silentPaymentsScanningActive = true;
_setListeners(height, doSingleScan: doSingleScan, usingElectrs: usingElectrs); _setListeners(height, doSingleScan: doSingleScan);
} }
@override @override
@ -1138,10 +1260,9 @@ abstract class ElectrumWalletBase
await electrumClient.close(); await electrumClient.close();
} catch (_) {} } catch (_) {}
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
} }
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@action @action
Future<void> updateAllUnspents() async { Future<void> updateAllUnspents() async {
List<BitcoinUnspent> updatedUnspentCoins = []; List<BitcoinUnspent> updatedUnspentCoins = [];
@ -1229,7 +1350,7 @@ abstract class ElectrumWalletBase
await Future.wait(unspents.map((unspent) async { await Future.wait(unspents.map((unspent) async {
try { try {
final coin = BitcoinUnspent.fromJSON(address, unspent); final coin = BitcoinUnspent.fromJSON(address, unspent);
final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); final tx = await fetchTransactionInfo(hash: coin.hash);
coin.isChange = address.isHidden; coin.isChange = address.isHidden;
coin.confirmations = tx?.confirmations; coin.confirmations = tx?.confirmations;
@ -1283,21 +1404,15 @@ abstract class ElectrumWalletBase
} }
} }
Future<bool> canReplaceByFee(String hash) async { Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); try {
final confirmations = verboseTransaction['confirmations'] as int? ?? 0; final bundle = await getTransactionExpanded(hash: tx.txHash);
final transactionHex = verboseTransaction['hex'] as String?; _updateInputsAndOutputs(tx, bundle);
if (bundle.confirmations > 0) return false;
if (confirmations > 0) return false; return bundle.originalTransaction.canReplaceByFee;
} catch (e) {
if (transactionHex == null) {
return false; return false;
} }
final original = bitcoin.Transaction.fromHex(transactionHex);
return original.ins
.any((element) => element.sequence != null && element.sequence! < 4294967293);
} }
Future<bool> isChangeSufficientForFee(String txId, int newFee) async { Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
@ -1338,6 +1453,7 @@ abstract class ElectrumWalletBase
List<ECPrivate> privateKeys = []; List<ECPrivate> privateKeys = [];
var allInputsAmount = 0; var allInputsAmount = 0;
String? memo;
// Add inputs // Add inputs
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
@ -1373,53 +1489,82 @@ abstract class ElectrumWalletBase
); );
} }
int totalOutAmount = bundle.originalTransaction.outputs // Create a list of available outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
final outputs = <BitcoinOutput>[]; final outputs = <BitcoinOutput>[];
for (final out in bundle.originalTransaction.outputs) {
// Add outputs and deduct the fees from it // Check if the script contains OP_RETURN
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) { final script = out.scriptPubKey.script;
final out = bundle.originalTransaction.outputs[i]; if (script.contains('OP_RETURN') && memo == null) {
final address = addressFromOutputScript(out.scriptPubKey, network); final index = script.indexOf('OP_RETURN');
final btcAddress = addressTypeFromStr(address, network); if (index + 1 <= script.length) {
try {
int newAmount; final opReturnData = script[index + 1].toString();
if (out.amount.toInt() >= remainingFee) { memo = utf8.decode(HEX.decode(opReturnData));
newAmount = out.amount.toInt() - remainingFee; continue;
remainingFee = 0; } catch (_) {
throw Exception('Cannot decode OP_RETURN data');
// if new amount of output is less than dust amount, then don't add this output as well }
if (newAmount <= _dustAmount) {
continue;
} }
} else {
remainingFee -= out.amount.toInt();
continue;
} }
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount))); final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
} }
// Calculate the total amount and fees
int totalOutAmount =
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
int currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
if (remainingFee <= 0) {
throw Exception("New fee must be higher than the current fee.");
}
// Deduct Remaining Fee from Main Outputs
if (remainingFee > 0) {
for (int i = outputs.length - 1; i >= 0; i--) {
int outputAmount = outputs[i].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));
remainingFee -= deduction;
if (remainingFee <= 0) break;
}
}
}
// Final check if the remaining fee couldn't be deducted
if (remainingFee > 0) {
throw Exception("Not enough funds to cover the fee.");
}
// Identify all change outputs
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network)))
.toList();
// look for a change address in the outputs int totalChangeAmount =
final changeOutput = outputs.firstWhereOrNull((output) => changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
// deduct the change amount from the output amount // The final amount that the receiver will receive
if (changeOutput != null) { int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
totalOutAmount -= changeOutput.value.toInt();
}
final txb = BitcoinTransactionBuilder( final txb = BitcoinTransactionBuilder(
utxos: utxos, utxos: utxos,
outputs: outputs, outputs: outputs,
fee: BigInt.from(newFee), fee: BigInt.from(newFee),
network: network, network: network,
memo: memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true, enableRBF: true,
); );
@ -1442,10 +1587,10 @@ abstract class ElectrumWalletBase
transaction, transaction,
type, type,
electrumClient: electrumClient, electrumClient: electrumClient,
amount: totalOutAmount, amount: sendingAmount,
fee: newFee, fee: newFee,
network: network, network: network,
hasChange: changeOutput != null, hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(), feeRate: newFee.toString(),
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
@ -1456,50 +1601,73 @@ abstract class ElectrumWalletBase
} }
} }
Future<ElectrumTransactionBundle> getTransactionExpanded({required String hash}) async { Future<ElectrumTransactionBundle> getTransactionExpanded(
{required String hash, int? height}) async {
String transactionHex; 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? time;
int confirmations = 0; int? confirmations;
if (network == BitcoinNetwork.testnet) {
// Testnet public electrum server does not support verbose transaction fetching final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
if (verboseTransaction.isEmpty) {
transactionHex = await electrumClient.getTransactionHex(hash: hash); transactionHex = await electrumClient.getTransactionHex(hash: hash);
final status = json.decode(
(await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body);
time = status["block_time"] as int?;
final height = status["block_height"] as int? ?? 0;
final tip = await getUpdatedChainTip();
if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0;
} else { } else {
final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
transactionHex = verboseTransaction['hex'] as String; transactionHex = verboseTransaction['hex'] as String;
time = verboseTransaction['time'] as int?; time = verboseTransaction['time'] as int?;
confirmations = verboseTransaction['confirmations'] as int? ?? 0; confirmations = verboseTransaction['confirmations'] as int?;
}
if (height != null) {
if (time == null) {
time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round();
}
if (confirmations == null) {
final tip = await getUpdatedChainTip();
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
} }
final original = BtcTransaction.fromRaw(transactionHex); final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[]; final ins = <BtcTransaction>[];
for (final vin in original.inputs) { for (final vin in original.inputs) {
ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId))); final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId);
final String inputTransactionHex;
if (verboseTransaction.isEmpty) {
inputTransactionHex = await electrumClient.getTransactionHex(hash: hash);
} else {
inputTransactionHex = verboseTransaction['hex'] as String;
}
ins.add(BtcTransaction.fromRaw(inputTransactionHex));
} }
return ElectrumTransactionBundle( return ElectrumTransactionBundle(
original, original,
ins: ins, ins: ins,
time: time, time: time,
confirmations: confirmations, confirmations: confirmations ?? 0,
); );
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo( Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash, required int height, bool? retryOnFailure}) async { {required String hash, int? height, bool? retryOnFailure}) async {
try { try {
return ElectrumTransactionInfo.fromElectrumBundle( return ElectrumTransactionInfo.fromElectrumBundle(
await getTransactionExpanded(hash: hash), walletInfo.type, network, await getTransactionExpanded(hash: hash, height: height),
addresses: addressesSet, height: height); walletInfo.type,
network,
addresses: addressesSet,
height: height,
);
} catch (e) { } catch (e) {
if (e is FormatException && retryOnFailure == true) { if (e is FormatException && retryOnFailure == true) {
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
@ -1650,8 +1818,8 @@ abstract class ElectrumWalletBase
await getCurrentChainTip(); await getCurrentChainTip();
transactionHistory.transactions.values.forEach((tx) async { transactionHistory.transactions.values.forEach((tx) async {
if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height != null && tx.height! > 0) {
tx.confirmations = await getCurrentChainTip() - tx.height + 1; tx.confirmations = await getCurrentChainTip() - tx.height! + 1;
} }
}); });
@ -1767,15 +1935,79 @@ abstract class ElectrumWalletBase
final index = address != null final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null; : null;
final HD = index == null ? hd : hd.derive(index); final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
return base64Encode(HD.signMessage(message)); final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
String messagePrefix = '\x18Bitcoin Signed Message:\n';
final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
final decodedSig = hex.decode(hexEncoded);
return base64Encode(decodedSig);
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
if (address == null) {
return false;
}
List<int> sigDecodedBytes = [];
if (signature.endsWith('=')) {
sigDecodedBytes = base64.decode(signature);
} else {
sigDecodedBytes = hex.decode(signature);
}
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
throw ArgumentException(
"signature must be 64 bytes without recover-id or 65 bytes with recover-id");
}
String messagePrefix = '\x18Bitcoin Signed Message:\n';
final messageHash = QuickCrypto.sha256Hash(
BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix));
List<int> correctSignature =
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
List<int> rBytes = correctSignature.sublist(0, 32);
List<int> sBytes = correctSignature.sublist(32);
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
List<int> possibleRecoverIds = [0, 1];
final baseAddress = addressTypeFromStr(address, network);
for (int recoveryId in possibleRecoverIds) {
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
String? recoveredAddress;
if (baseAddress is P2pkAddress) {
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
} else if (baseAddress is P2pkhAddress) {
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
} else if (baseAddress is P2wshAddress) {
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
} else if (baseAddress is P2wpkhAddress) {
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
}
if (recoveredAddress == address) {
return true;
}
}
return false;
} }
Future<void> _setInitialHeight() async { Future<void> _setInitialHeight() async {
if (_chainTipUpdateSubject != null) return; if (_chainTipUpdateSubject != null) return;
_currentChainTip = await getUpdatedChainTip();
if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) {
await getUpdatedChainTip();
await walletInfo.updateRestoreHeight(_currentChainTip!); await walletInfo.updateRestoreHeight(_currentChainTip!);
} }
@ -1794,44 +2026,40 @@ abstract class ElectrumWalletBase
}); });
} }
static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) {
if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) {
return BitcoinCashNetwork.mainnet;
}
if (networkType == litecoinNetwork) {
return LitecoinNetwork.mainnet;
}
if (networkType == bitcoin.testnet) {
return BitcoinNetwork.testnet;
}
return BitcoinNetwork.mainnet;
}
static String _hardenedDerivationPath(String derivationPath) => static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
@action @action
void _onConnectionStatusChange(bool? isConnected) { void _onConnectionStatusChange(ConnectionStatus status) {
if (syncStatus is SyncingSyncStatus) return; switch (status) {
case ConnectionStatus.connected:
if (syncStatus is NotConnectedSyncStatus ||
syncStatus is LostConnectionSyncStatus ||
syncStatus is ConnectingSyncStatus) {
syncStatus = AttemptingSyncStatus();
startSync();
}
if (isConnected == true && syncStatus is! SyncedSyncStatus) { break;
syncStatus = ConnectedSyncStatus(); case ConnectionStatus.disconnected:
} else if (isConnected == false) { syncStatus = NotConnectedSyncStatus();
syncStatus = LostConnectionSyncStatus(); break;
} else if (isConnected != true && syncStatus is! ConnectingSyncStatus) { case ConnectionStatus.failed:
syncStatus = NotConnectedSyncStatus(); syncStatus = LostConnectionSyncStatus();
break;
case ConnectionStatus.connecting:
syncStatus = ConnectingSyncStatus();
break;
default:
} }
} }
void _syncStatusReaction(SyncStatus syncStatus) async { void _syncStatusReaction(SyncStatus syncStatus) async {
if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) { if (syncStatus is SyncingSyncStatus) {
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; return;
} }
if (syncStatus is NotConnectedSyncStatus) { if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected // Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {}; _scripthashesUpdateSubject = {};
@ -1839,8 +2067,8 @@ abstract class ElectrumWalletBase
_isTryingToConnect = true; _isTryingToConnect = true;
Future.delayed(Duration(seconds: 10), () { Timer(Duration(seconds: 10), () {
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) {
this.electrumClient.connectToUri( this.electrumClient.connectToUri(
node!.uri, node!.uri,
useSSL: node!.useSSL ?? false, useSSL: node!.useSSL ?? false,
@ -1857,6 +2085,54 @@ abstract class ElectrumWalletBase
}); });
} }
} }
void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
if (tx.inputAddresses == null ||
tx.inputAddresses!.isEmpty ||
tx.outputAddresses == null ||
tx.outputAddresses!.isEmpty) {
List<String> inputAddresses = [];
List<String> outputAddresses = [];
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
if (address.isNotEmpty) inputAddresses.add(address);
}
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN')) {
final index = script.indexOf('OP_RETURN');
if (index + 1 <= script.length) {
try {
final opReturnData = script[index + 1].toString();
final decodedString = utf8.decode(HEX.decode(opReturnData));
outputAddresses.add('OP_RETURN:$decodedString');
} catch (_) {
outputAddresses.add('OP_RETURN:');
}
}
}
}
tx.inputAddresses = inputAddresses;
tx.outputAddresses = outputAddresses;
transactionHistory.addOne(tx);
}
}
} }
class ScanNode { class ScanNode {
@ -1951,8 +2227,8 @@ Future<void> startRefresh(ScanData scanData) async {
final tweaks = t as Map<String, dynamic>; final tweaks = t as Map<String, dynamic>;
if (tweaks["message"] != null) { if (tweaks["message"] != null) {
// re-subscribe to continue receiving messages // re-subscribe to continue receiving messages, starting from the next unscanned height
electrumClient.tweaksSubscribe(height: syncHeight, count: count); electrumClient.tweaksSubscribe(height: syncHeight + 1, count: count);
return; return;
} }

View file

@ -1,5 +1,4 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
@ -30,7 +29,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses, List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0, int initialSilentAddressIndex = 0,
bitcoin.HDWallet? masterHd, Bip32Slip10Secp256k1? masterHd,
BitcoinAddressType? initialAddressPageType, BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()), }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType = addressesByReceiveType =
@ -53,9 +52,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
super(walletInfo) { super(walletInfo) {
if (masterHd != null) { if (masterHd != null) {
silentAddress = SilentPaymentOwner.fromPrivateKeys( silentAddress = SilentPaymentOwner.fromPrivateKeys(
b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privKey!), b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()),
b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!), b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()),
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'); network: network,
);
if (silentAddresses.length == 0) { if (silentAddresses.length == 0) {
silentAddresses.add(BitcoinSilentPaymentAddressRecord( silentAddresses.add(BitcoinSilentPaymentAddressRecord(
@ -92,8 +92,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses; final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
final BasedUtxoNetwork network; final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd; final Bip32Slip10Secp256k1 mainHd;
final bitcoin.HDWallet sideHd; final Bip32Slip10Secp256k1 sideHd;
@observable @observable
SilentPaymentOwner? silentAddress; SilentPaymentOwner? silentAddress;
@ -224,6 +224,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch(); updateAddressesByMatch();
updateReceiveAddresses(); updateReceiveAddresses();
updateChangeAddresses(); updateChangeAddresses();
_validateAddresses();
await updateAddressesInBox(); await updateAddressesInBox();
if (currentReceiveAddressIndex >= receiveAddresses.length) { if (currentReceiveAddressIndex >= receiveAddresses.length) {
@ -317,7 +318,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
String getAddress( String getAddress(
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => {required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType}) =>
''; '';
@override @override
@ -458,10 +461,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden, Future<void> discoverAddresses(List<BitcoinAddressRecord> addressList, bool isHidden,
Future<String?> Function(BitcoinAddressRecord) getAddressHistory, Future<String?> Function(BitcoinAddressRecord) getAddressHistory,
{BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async {
if (!isHidden) {
_validateSideHdAddresses(addressList.toList());
}
final newAddresses = await _createNewAddresses(gap, final newAddresses = await _createNewAddresses(gap,
startIndex: addressList.length, isHidden: isHidden, type: type); startIndex: addressList.length, isHidden: isHidden, type: type);
addAddresses(newAddresses); addAddresses(newAddresses);
@ -541,11 +540,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch(); updateAddressesByMatch();
} }
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) { void _validateAddresses() {
addrWithTransactions.forEach((element) { _addresses.forEach((element) {
if (element.address != if (!element.isHidden &&
getAddress(index: element.index, hd: mainHd, addressType: element.type)) element.address !=
getAddress(index: element.index, hd: mainHd, addressType: element.type)) {
element.isHidden = true; element.isHidden = true;
} else if (element.isHidden &&
element.address !=
getAddress(index: element.index, hd: sideHd, addressType: element.type)) {
element.isHidden = false;
}
}); });
} }
@ -561,7 +566,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return _isAddressByType(addressRecord, addressPageType); return _isAddressByType(addressRecord, addressPageType);
} }
bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd;
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
!addr.isHidden && !addr.isUsed && addr.type == type; !addr.isHidden && !addr.isUsed && addr.type == type;

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -32,22 +33,28 @@ class ElectrumWalletSnapshot {
final WalletType type; final WalletType type;
final String? addressPageType; final String? addressPageType;
@deprecated
String? mnemonic; String? mnemonic;
@deprecated
String? xpub; String? xpub;
@deprecated
String? passphrase;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
List<BitcoinSilentPaymentAddressRecord> silentAddresses; List<BitcoinSilentPaymentAddressRecord> silentAddresses;
ElectrumBalance balance; ElectrumBalance balance;
Map<String, int> regularAddressIndex; Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex; Map<String, int> changeAddressIndex;
int silentAddressIndex; int silentAddressIndex;
String? passphrase;
DerivationType? derivationType; DerivationType? derivationType;
String? derivationPath; String? derivationPath;
static Future<ElectrumWalletSnapshot> load( static Future<ElectrumWalletSnapshot> load(
String name, WalletType type, String password, BasedUtxoNetwork network) async { EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async {
final path = await pathForWallet(name: name, type: type); final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password); final jsonSource = await encryptionFileUtils.read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ?? <Object>[]; final addressesTmp = data['addresses'] as List? ?? <Object>[];
final mnemonic = data['mnemonic'] as String?; final mnemonic = data['mnemonic'] as String?;

View file

@ -1,9 +0,0 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
final litecoinNetwork = NetworkType(
messagePrefix: '\x19Litecoin Signed Message:\n',
bech32: 'ltc',
bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4),
pubKeyHash: 0x30,
scriptHash: 0x32,
wif: 0xb0);

View file

@ -1,20 +1,29 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; 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:bip39/bip39.dart' as bip39;
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
import 'package:cw_core/transaction_priority.dart'; 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:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:pointycastle/ecc/api.dart';
import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:pointycastle/ecc/curves/secp256k1.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:bip39/bip39.dart' as bip39;
part 'litecoin_wallet.g.dart'; part 'litecoin_wallet.g.dart';
@ -27,6 +36,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes, required Uint8List seedBytes,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase,
String? addressPageType, String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
@ -37,10 +48,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
networkType: litecoinNetwork, network: LitecoinNetwork.mainnet,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: seedBytes, seedBytes: seedBytes,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
currency: CryptoCurrency.ltc) { currency: CryptoCurrency.ltc) {
walletAddresses = LitecoinWalletAddresses( walletAddresses = LitecoinWalletAddresses(
walletInfo, walletInfo,
@ -48,7 +61,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: accountHD.derive(1), sideHd: accountHD.childKey(Bip32KeyIndex(1)),
network: network, network: network,
); );
autorun((_) { autorun((_) {
@ -61,6 +74,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required EncryptionFileUtils encryptionFileUtils,
String? passphrase, String? passphrase,
String? addressPageType, String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
@ -78,7 +92,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
break; break;
case DerivationType.electrum: case DerivationType.electrum:
default: default:
seedBytes = await mnemonicToSeedBytes(mnemonic); seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break; break;
} }
return LitecoinWallet( return LitecoinWallet(
@ -88,6 +102,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
encryptionFileUtils: encryptionFileUtils,
passphrase: passphrase,
seedBytes: seedBytes, seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
@ -95,25 +111,80 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
); );
} }
static Future<LitecoinWallet> open({ static Future<LitecoinWallet> open(
required String name, {required String name,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password, required String password,
}) async { required EncryptionFileUtils encryptionFileUtils}) async {
final snp = final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet);
ElectrumWalletSnapshot? snp = null;
try {
snp = await ElectrumWalletSnapshot.load(
encryptionFileUtils,
name,
walletInfo.type,
password,
LitecoinNetwork.mainnet,
);
} catch (e) {
if (!hasKeysFile) rethrow;
}
final WalletKeysData keysData;
// Migrate wallet from the old scheme to then new .keys file scheme
if (!hasKeysFile) {
keysData =
WalletKeysData(mnemonic: snp!.mnemonic, xPub: snp.xpub, passphrase: snp.passphrase);
} else {
keysData = await WalletKeysFile.readKeysFile(
name,
walletInfo.type,
password,
encryptionFileUtils,
);
}
walletInfo.derivationInfo ??= DerivationInfo();
// set the default if not present:
walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path;
walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
Uint8List? seedBytes = null;
final mnemonic = keysData.mnemonic;
final passphrase = keysData.passphrase;
if (mnemonic != null) {
switch (walletInfo.derivationInfo?.derivationType) {
case DerivationType.bip39:
seedBytes = await bip39.mnemonicToSeed(
mnemonic,
passphrase: passphrase ?? "",
);
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
break;
}
}
return LitecoinWallet( return LitecoinWallet(
mnemonic: snp.mnemonic!, mnemonic: keysData.mnemonic!,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp?.addresses,
initialBalance: snp.balance, initialBalance: snp?.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic!), seedBytes: seedBytes!,
initialRegularAddressIndex: snp.regularAddressIndex, passphrase: passphrase,
initialChangeAddressIndex: snp.changeAddressIndex, encryptionFileUtils: encryptionFileUtils,
addressPageType: snp.addressPageType, initialRegularAddressIndex: snp?.regularAddressIndex,
initialChangeAddressIndex: snp?.changeAddressIndex,
addressPageType: snp?.addressPageType,
); );
} }
@ -132,4 +203,127 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return 0; return 0;
} }
@override
Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null;
final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index));
final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex());
final privateKey = ECDSAPrivateKey.fromBytes(
priv.toBytes(),
Curves.generatorSecp256k1,
);
final signature =
signLitecoinMessage(utf8.encode(message), privateKey: privateKey, bipPrive: priv.prive);
return base64Encode(signature);
}
List<int> _magicPrefix(List<int> message, List<int> messagePrefix) {
final encodeLength = IntUtils.encodeVarint(message.length);
return [...messagePrefix, ...encodeLength, ...message];
}
List<int> signLitecoinMessage(List<int> message,
{required ECDSAPrivateKey privateKey, required Bip32PrivateKey bipPrive}) {
String messagePrefix = '\x19Litecoin Signed Message:\n';
final messageHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix));
final signingKey = EcdsaSigningKey(privateKey);
ECDSASignature ecdsaSign =
signingKey.signDigestDeterminstic(digest: messageHash, hashFunc: () => SHA256());
final n = Curves.generatorSecp256k1.order! >> 1;
BigInt newS;
if (ecdsaSign.s.compareTo(n) > 0) {
newS = Curves.generatorSecp256k1.order! - ecdsaSign.s;
} else {
newS = ecdsaSign.s;
}
final rawSig = ECDSASignature(ecdsaSign.r, newS);
final rawSigBytes = rawSig.toBytes(BitcoinSignerUtils.baselen);
final pub = bipPrive.publicKey;
final ECDomainParameters curve = ECCurve_secp256k1();
final point = curve.curve.decodePoint(pub.point.toBytes());
final rawSigEc = ECSignature(rawSig.r, rawSig.s);
final recId = SignUtils.findRecoveryId(
SignUtils.getHexString(messageHash, offset: 0, length: messageHash.length),
rawSigEc,
Uint8List.fromList(pub.uncompressed),
);
final v = recId + 27 + (point!.isCompressed ? 4 : 0);
final combined = Uint8List.fromList([v, ...rawSigBytes]);
return combined;
}
List<int> magicMessage(List<int> message, String messagePrefix) {
final prefixBytes = StringUtils.encode(messagePrefix);
final magic = _magicPrefix(message, prefixBytes);
return QuickCrypto.sha256Hash(magic);
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
if (address == null) {
return false;
}
List<int> sigDecodedBytes = [];
if (signature.endsWith('=')) {
sigDecodedBytes = base64.decode(signature);
} else {
sigDecodedBytes = hex.decode(signature);
}
if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) {
throw ArgumentException(
"litecoin signature must be 64 bytes without recover-id or 65 bytes with recover-id");
}
String messagePrefix = '\x19Litecoin Signed Message:\n';
final messageHash = QuickCrypto.sha256Hash(magicMessage(utf8.encode(message), messagePrefix));
List<int> correctSignature =
sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes);
List<int> rBytes = correctSignature.sublist(0, 32);
List<int> sBytes = correctSignature.sublist(32);
final sig = ECDSASignature(BigintUtils.fromBytes(rBytes), BigintUtils.fromBytes(sBytes));
List<int> possibleRecoverIds = [0, 1];
final baseAddress = addressTypeFromStr(address, network);
for (int recoveryId in possibleRecoverIds) {
final pubKey = sig.recoverPublicKey(messageHash, Curves.generatorSecp256k1, recoveryId);
final recoveredPub = ECPublic.fromBytes(pubKey!.toBytes());
String? recoveredAddress;
if (baseAddress is P2pkAddress) {
recoveredAddress = recoveredPub.toP2pkAddress().toAddress(network);
} else if (baseAddress is P2pkhAddress) {
recoveredAddress = recoveredPub.toP2pkhAddress().toAddress(network);
} else if (baseAddress is P2wshAddress) {
recoveredAddress = recoveredPub.toP2wshAddress().toAddress(network);
} else if (baseAddress is P2wpkhAddress) {
recoveredAddress = recoveredPub.toP2wpkhAddress().toAddress(network);
}
if (recoveredAddress == address) {
return true;
}
}
return false;
}
} }

View file

@ -1,5 +1,5 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -22,6 +22,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
@override @override
String getAddress( String getAddress(
{required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => {required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType}) =>
generateP2WPKHAddress(hd: hd, index: index, network: network); generateP2WPKHAddress(hd: hd, index: index, network: network);
} }

View file

@ -1,4 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
@ -16,23 +18,40 @@ import 'package:bip39/bip39.dart' as bip39;
class LitecoinWalletService extends WalletService< class LitecoinWalletService extends WalletService<
BitcoinNewWalletCredentials, BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,BitcoinNewWalletCredentials> { BitcoinRestoreWalletFromWIFCredentials,
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); BitcoinNewWalletCredentials> {
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
final Box<UnspentCoinsInfo> unspentCoinsInfoSource; final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
final bool isDirect;
@override @override
WalletType getType() => WalletType.litecoin; WalletType getType() => WalletType.litecoin;
@override @override
Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { Future<LitecoinWallet> create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async {
final String mnemonic;
switch ( credentials.walletInfo?.derivationInfo?.derivationType) {
case DerivationType.bip39:
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
mnemonic = await MnemonicBip39.generate(strength: strength);
break;
case DerivationType.electrum:
default:
mnemonic = await generateElectrumMnemonic();
break;
}
final wallet = await LitecoinWalletBase.create( final wallet = await LitecoinWalletBase.create(
mnemonic: await generateElectrumMnemonic(), mnemonic: mnemonic,
password: credentials.password!, password: credentials.password!,
passphrase: credentials.passphrase, passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
@ -45,21 +64,29 @@ class LitecoinWalletService extends WalletService<
@override @override
Future<LitecoinWallet> openWallet(String name, String password) async { Future<LitecoinWallet> openWallet(String name, String password) async {
final walletInfo = walletInfoSource.values.firstWhereOrNull( final walletInfo = walletInfoSource.values
(info) => info.id == WalletBase.idFor(name, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
try { try {
final wallet = await LitecoinWalletBase.open( final wallet = await LitecoinWalletBase.open(
password: password, name: name, walletInfo: walletInfo, password: password,
unspentCoinsInfo: unspentCoinsInfoSource); name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init(); await wallet.init();
saveBackup(name); saveBackup(name);
return wallet; return wallet;
} catch (_) { } catch (_) {
await restoreWalletFilesFromBackup(name); await restoreWalletFilesFromBackup(name);
final wallet = await LitecoinWalletBase.open( final wallet = await LitecoinWalletBase.open(
password: password, name: name, walletInfo: walletInfo, password: password,
unspentCoinsInfo: unspentCoinsInfoSource); name: name,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.init(); await wallet.init();
return wallet; return wallet;
} }
@ -67,22 +94,23 @@ class LitecoinWalletService extends WalletService<
@override @override
Future<void> remove(String wallet) async { Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())) File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
.delete(recursive: true); final walletInfo = walletInfoSource.values
final walletInfo = walletInfoSource.values.firstWhereOrNull( .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
(info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key); await walletInfoSource.delete(walletInfo.key);
} }
@override @override
Future<void> rename(String currentName, String password, String newName) async { Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( final currentWalletInfo = walletInfoSource.values
(info) => info.id == WalletBase.idFor(currentName, getType()))!; .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
final currentWallet = await LitecoinWalletBase.open( final currentWallet = await LitecoinWalletBase.open(
password: password, password: password,
name: currentName, name: currentName,
walletInfo: currentWalletInfo, walletInfo: currentWalletInfo,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);
await saveBackup(newName); await saveBackup(newName);
@ -96,27 +124,30 @@ class LitecoinWalletService extends WalletService<
@override @override
Future<LitecoinWallet> restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { Future<LitecoinWallet> restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); throw UnimplementedError(
"Restoring a Litecoin wallet from a hardware wallet is not yet supported!");
} }
@override @override
Future<LitecoinWallet> restoreFromKeys( Future<LitecoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => {bool? isTestnet}) async =>
throw UnimplementedError(); throw UnimplementedError();
@override @override
Future<LitecoinWallet> restoreFromSeed( Future<LitecoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,
BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { {bool? isTestnet}) async {
if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) {
throw LitecoinMnemonicIsIncorrectException(); throw LitecoinMnemonicIsIncorrectException();
} }
final wallet = await LitecoinWalletBase.create( final wallet = await LitecoinWalletBase.create(
password: credentials.password!, password: credentials.password!,
passphrase: credentials.passphrase, passphrase: credentials.passphrase,
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save(); await wallet.save();
await wallet.init(); await wallet.init();
return wallet; return wallet;

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